feat(illness): Rest Mode — annotate score/recovery/streaks during an active episode + FHIR/doctor-report

An active illness/condition episode annotates, it never penalises: the
measured number is untouched, only the narrative changes.

- rest-mode.ts: one server-authoritative resolver (module-gated, fail-soft)
  for "is the account in Rest Mode + which episodes back it". iOS mirrors the
  resolved context, never recomputes the suppression.
- health-score: /api/analytics attaches a value-free Rest Mode annotation;
  computeHealthScore stays pure (score never adjusted).
- recovery-score: computeRecoveryScore carries the Rest Mode context as of the
  scored day; the persisted blend is unchanged.
- coach-nudge: pause cadence-nudges during an active episode (new
  skippedDuringIllness counter, gate sits before the trigger search).
- achievements: bridgeFrozenStreakGaps freezes (does not break) every
  day-streak across illness days; never invents a streak, a non-illness gap
  still breaks.
- coincident-deviation: reframe a fired flag as illness-explained; vital
  numbers untouched. Surfaced as a neutral line on the insights card.
- coach snapshot: illness history + active illness as load-bearing context so
  the coach knows about Rest Mode (no decrypted note reaches the prompt).
- FHIR: illness episodes map to Condition (generic disease root, label on
  code.text, patient-reported/unconfirmed) + a bounding Encounter.
- doctor-report: module-gated illness section in the aggregator + PDF table
  (retrospective, neutral palette); FHIR Condition section in the bundle.

i18n x6 for the report + reframe copy. Tests: score-annotation-not-
penalisation, streak-freeze, coach-nudge pause, FHIR Condition/Encounter
mapping, doctor-report inclusion, Rest Mode resolver. typecheck/lint/test
green, openapi in sync, knip clean.
This commit is contained in:
Marc-André Bombeck committed 2026-06-16 22:25:50 +02:00
1 parent 8447ad90cc
commit c59cf0b652
37 files changed
+1352 -24

No files matched your search

+16
View File
@@ -18,6 +18,7 @@ import {
} from "@/lib/analytics/bp-in-target-fast-path";
import { computeUserHealthScoreFastPath } from "@/lib/analytics/health-score-fast-path";
import { isModuleEnabled } from "@/lib/modules/gate";
import { resolveRestMode } from "@/lib/illness/rest-mode";
import { buildHealthScoreBpInputs } from "@/lib/analytics/health-score-inputs";
import { deriveBpWindow90 } from "@/lib/analytics/window-confidence";
import { computeCorrelationHypothesesFastPath } from "@/lib/analytics/correlations-fast-path";
@@ -444,6 +445,21 @@ async function buildAnalyticsResponse(user: AuthedUser) {
moodEnabled,
});
// v1.18.1 P4 — 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.
if (healthScore) {
const restMode = await resolveRestMode(user.id);
healthScore.restMode = restMode.active
? {
active: true,
since: restMode.since,
episodeCount: restMode.episodeCount,
}
: null;
}
return {
summaries: results,
bmi,