Commit Graph
3121 Commits
Author SHA1 Message Date
Marc-André Bombeck 5f0ca355f9 fix(mental-health): a11y on the check-in result and option controls
On submit the form (and its submit button) unmounts, dropping focus to the
body — move focus to the result heading so keyboard/AT users land on the new
content. Announce the item-9 crisis-resource card via role="alert" so a
screen-reader user hears the resources. Bump the PHQ/GAD scale and
functional-difficulty option buttons to the 44px touch-target floor.
2026-06-28 18:25:43 +02:00
Marc-André Bombeck be4bc3bd10 fix(mental-health): repair the dead-on-arrival assessment POST
The handler passed the `safeJson` `{ data, error }` wrapper straight into
`safeParse`, so the schema never matched and every POST returned 422 — the
screener was dead on arrival. Destructure the envelope and read an unbounded
body no more: cap the request at 8 KB before validation, matching the sibling
clinical routes.

Add the missing write protections: gate both verbs on the opt-in module
server-side (a valid Bearer token included), wrap the create in idempotency so
a double-tap/retry cannot mint duplicate assessment + score rows, and add a
tight per-user write rate-limit.
2026-06-28 18:25:43 +02:00
Marc-André Bombeck f62ba50ff0 feat(mental-health): gate the PHQ-9/GAD-7 check-in behind an opt-in module
The mental-wellbeing screener surface shipped built but unreachable: no nav
entry, no module key, ungated. Add a `mentalHealth` module key (opt-in,
default OFF — a depression/anxiety self-assessment is at least as sensitive
as mood) to the registry, the module-hub icon map, and the exhaustive module
maps. Give the check-in a nav home at /insights/mental-wellbeing, born-gated
on the module so it appears only once the account opts in, and redirect the
page home on a direct hit when the module is off.

Item answers stay encrypted and off the AI Coach / MCP regardless of the
toggle; this only controls whether the surface is visible and reachable.
2026-06-28 18:25:27 +02:00
Marc-André Bombeck 15e740bc2b i18n(mental-health): ship validated PHQ-9/GAD-7 translations and localize crisis copy
The PHQ-9/GAD-7 item wordings rendered in English for es/fr/it/pl users
(de was already validated). Replace them with the officially validated,
public-domain Pfizer/PRIME-MD localized screener wordings (distributed via
the MAPI Research Trust) rather than ad-hoc translations — re-wording a
validated instrument invalidates the score. Localize the crisis-resource
copy too: translate the descriptive parentheticals while keeping the org
proper-nouns (TelefonSeelsorge, Nummer gegen Kummer, krisenchat,
Find a Helpline) and the US brand lines (988, Crisis Text Line) verbatim.

Add the new mental-wellbeing module label/description and the nav label
across all six locales.
2026-06-28 18:25:16 +02:00
Marc-André Bombeck 4f39058629 Merge branch 'feat/v125-env-location-conservative' into release/v1.25.0 2026-06-28 17:47:35 +02:00
Marc-André Bombeck fb580f4725 Merge branch 'feat/v125-self-description-docs' into release/v1.25.0
# Conflicts:
#	docs/api/mcp-capabilities.md
2026-06-28 17:46:40 +02:00
Marc-André Bombeck 444da8fbb3 Merge branch 'feat/v125-mcp-completeness' into release/v1.25.0 2026-06-28 17:45:03 +02:00
Marc-André Bombeck 0224abc419 docs: bring README and docs current for v1.25
Reflect the v1.25 surface without touching the changelog or version
numbers.

- README highlights now cover the new clinical signals (respiratory
  rate, the opt-in PHQ-9 / GAD-7 mental-wellbeing self-checks with calm
  crisis handling, grip strength, pain score, waist / body measurements,
  longevity labs), the signal registry that wires each one across charts,
  correlations, the coach, FHIR, and the API at once, structured records
  (allergies, family history; immunizations noted out of scope), the
  opt-in environmental-context module, inbound document extraction
  (transcribe, never interpret), and the self-description posture (a
  record-keeping and wellness tool, not a medical device).
- ai-providers.md gains a "what the Coach is — and isn't" section: the
  non-diagnostic standard and the calm proactive morning check-in
  behaviour.
- mcp-capabilities.md notes that the new measurement signals reach the
  assistant through the existing metric and labs tools via the registry,
  that the mental-wellbeing instruments / records / documents are
  deliberately not exposed over MCP, and ties the grounding contract to
  the project-wide self-description standard.
- getting-started.md lists the environmental module's Open-Meteo egress
  as an opt-in outbound dependency.
2026-06-28 17:43:43 +02:00
Marc-André Bombeck 3bd52a322b feat(self-description): add the non-diagnostic / intended-purpose standard
Introduce one canonical "what this is / is not, not a medical device"
statement plus a layered, calm disclaimer set, and apply it on the
surfaces that earn a context note.

- New shared `MedicalDisclaimer` component renders the copy as plain
  React text (no markdown renderer, per the XSS posture). Variants:
  intendedPurpose, coach, insights, labs, medications, emergency,
  dataPosture.
- New `selfDescription` i18n namespace across all six locales: the
  intended-purpose line, the per-surface notes, the emergency note, and
  an explicit data-posture statement (self-hosted, data stays on your
  instance, encrypted at rest, BYOK / local model, no third-party
  analytics).
- Apply once per surface, layered not plastered: a calm standing line on
  the Coach new-chat hero, one framing line in the Insights layout shell
  (covers the briefing and trends), a labs note (ranges vary, only a
  clinician interprets), a neutral medications note, and the data-posture
  line where the provider key is configured. Mental-wellbeing keeps its
  existing crisis copy; records keep their existing note.
- Extend the Coach system prompt (EN + DE) with a self-description ground
  rule so the Coach answers "what are you / where does my data go"
  honestly and stays non-diagnostic.

App-authored copy was already observational; the few diagnose/treat
tokens are negations, idioms, or framed as the user's own self-reported
statement, so no softening was required.
2026-06-28 17:43:31 +02:00
Marc-André Bombeck b82a9ff07c docs(mcp): document clinical-signal reads and the off-surface set 2026-06-28 17:37:40 +02:00
Marc-André Bombeck 7cd67c8dc8 feat(mcp): expose v1.25 clinical signals as grounded reads
Bring the new physical clinical signals onto the MCP read surface through
the existing rollup-backed reads. Grip strength, pain (0-10 NRS), waist
circumference, and waist-to-height ratio now resolve in get_metric_baseline,
compare_metric, and detect_changepoints, are discoverable through search
(present-only), and hydrate through fetch (metric:<KEY>).

These signals sit off the Coach snapshot by design, so the MCP layer owns
an explicit exposure allowlist and consumes the signal registry only for
grounding (backing measurement type, unit, population band). The PHQ-9 /
GAD-7 mental-health screeners and the environmental signals are absent from
the allowlist, so they never resolve through any read tool. The longevity
lab panel is already reachable through get_labs and the lab resource
template; no change needed there.
2026-06-28 17:37:40 +02:00
Marc-André Bombeck bacd591dee feat(environment): frame overrides as location periods and surface effective-from
Settings → Environment now reads the override list as "location periods": a place
plus a date range that covers a trip OR corrects a stretch of earlier history,
overriding the home for the days it spans. Show the home's effective-from date
and prefill the backfill start with it, so the default span matches what the
server will actually store. Strings updated across all six locales.
2026-06-28 17:36:21 +02:00
Marc-André Bombeck c254135406 feat(environment): resolve past days conservatively, never guessing from home
Rework the per-day location resolution so it can no longer fabricate weather for
days before the home was set. Precedence, first hit wins:

  1. an explicit dated location period (a trip, or a corrected stretch of
     history) covering the day;
  2. DEVICE — a reserved slot for a future client-supplied coarse per-day
     location (ranks between the period and home; not produced server-side yet);
  3. the home location, but only for days on/after its effective-from date;
  4. otherwise the day is skipped.

The home route stamps `homeSince` whenever the home is set/updated and returns
it. The overview exposes it. Backfill bounds are now optional and default to the
conservative `[homeSince .. today]` span instead of a fixed reach onto the
current home; days before `homeSince` still resolve to a skip in the worker, so
even a wider explicit start cannot back-fill weather for the pre-home past. To
reach further back a user adds an explicit location period for that range.

Add unit coverage for the precedence (including the effective-from boundary and
the skip-not-guess rule) and the default backfill range.
2026-06-28 17:36:14 +02:00
Marc-André Bombeck 6aad4a465e feat(environment): add home effective-from and reserve a DEVICE location source
Add `User.homeSince` (nullable effective-from instant) and a `DEVICE` value to
the location-source enum (migration 0219). `homeSince` records when the home
location became effective so day resolution can attribute a past day to the
home only on/after that instant, rather than to the current home for every past
day. Existing accounts with a home get `homeSince` stamped to the migration
instant; their deep past is filled by explicit location periods instead.

`DEVICE` is reserved for a future client-supplied coarse per-day location: the
enum value and the wire contract accept it now so the resolver can rank it
later without rework. Nothing writes it server-side yet.
2026-06-28 17:35:45 +02:00
Marc-André Bombeck a27a1ee71d Merge feat/v125-inbound-documents into release/v1.25.0
Inbound documents module (migration 0218).

# Conflicts:
#	messages/de.json
#	messages/en.json
#	messages/es.json
#	messages/fr.json
#	messages/it.json
#	messages/pl.json
#	prisma/schema.prisma
#	src/lib/modules/__tests__/gate.test.ts
#	src/lib/query-keys/index.ts
2026-06-28 17:05:50 +02:00
Marc-André Bombeck a1b4f0d00e Merge feat/v125-environmental into release/v1.25.0
Environmental context module (migration 0217).
2026-06-28 17:02:36 +02:00
Marc-André Bombeck 0989ed4f20 Merge fix/v125-coach-nudge into release/v1.25.0
Proactive nudge rework + PUT /api/auth/me/locale.
2026-06-28 17:02:10 +02:00
Marc-André Bombeck 9c57d9e38e Merge feat/v125-ios-serverdep into release/v1.25.0
Labs OCR provenance + delete-scoping.
2026-06-28 17:01:29 +02:00
Marc-André Bombeck 088cad6c64 feat(environment): add opt-in module, settings surface, API, and nightly fetch
Add the opt-in Environment module (default off), its Settings surface (home
location search, travel overrides, backfill, with Open-Meteo attribution), the
gated API routes (overview, home, geocode, travel CRUD, backfill), the nightly
pg-boss fetch job with a lookback window plus on-demand backfill, the query
keys, six-locale strings, and the regenerated OpenAPI. The module performs an
outbound weather fetch tied to a coarse location, so it ships dark and the user
turns it on deliberately; off means no job, no row, no egress.
2026-06-28 16:58:52 +02:00
Marc-André Bombeck cdee3e3efc feat(environment): register env signals and wire weather into correlation discovery
Register the seven environmental-exposure signals through the signal registry
as a new "environment" kind (correlation-eligible; off Coach/MCP/FHIR in v1),
and fold their daily series into the correlation engine as lagged behaviour
channels. Weather/daylight now pair against mood, sleep, and vitals through
the existing FDR / effect-size / shrinkage gates, so a thin series degrades to
absent rather than a spurious link. All env channels share one ENVIRONMENT
family so two same-day fields never lag-correlate against each other.
2026-06-28 16:58:30 +02:00
Marc-André Bombeck 76dc03ae7f feat(environment): add Open-Meteo client, location resolution, and field vocabulary
Add the keyless Open-Meteo client (historical archive + geocoding) routed
through safeFetch, the per-day location-resolution + fetch/upsert service
(travel override wins over home, then degrades silently), and the shared
environmental-exposure field table the registry and correlation engine both
read. The base URL is operator-overridable for a self-hosted instance; the
fetch stays server-side so a private/LAN host is not blocked.
2026-06-28 16:58:22 +02:00
Marc-André Bombeck 0151f86f3f feat(environment): add environmental-context data model (migration 0217)
Add the opt-in environmental-context store: per-day EnvironmentContext
observations (temperature, daylight, sunshine, precipitation, pressure,
humidity, cloud, weather code) keyed by user + day and location, plus the
manual EnvironmentTravelLocation overrides and the coarse home-location
columns on the user. Location is city-level by design; the weather model
needs no finer than ~10-25 km and no GPS track is ever stored.
2026-06-28 16:58:13 +02:00
Marc-André Bombeck 33bf952d9d feat(documents): OpenAPI contract and translations for inbound documents
Document the /api/documents/inbound/* surface in the OpenAPI registry and
regenerate the spec. Add the inbound-documents copy across all six locales,
including the non-diagnostic disclaimer and the module label/description.
2026-06-28 16:57:24 +02:00
Marc-André Bombeck 422778f679 feat(documents): module gate, nav, and review-then-confirm UI
Add the opt-in inboundDocuments module (off by default, like the MCP
endpoint): ingesting a clinical document egresses it to the OCR/vision
provider, so the surface ships dark and the user turns it on from the
Modules hub. The born-gated /documents page carries the prominent
non-diagnostic disclaimer (we read, we don't interpret; nothing is saved
until you approve it; only your clinician can interpret these results),
uploads a document, and walks each extracted fact: source provenance and
confidence are shown, low-confidence facts are blocked from approval until
edited, and the confirm step commits the approved facts.
2026-06-28 16:57:16 +02:00
Marc-André Bombeck 98ca0b9d61 feat(documents): extract structured facts from clinical documents
Ingest an uploaded doctor report / discharge letter through the existing
dedicated OCR/vision provider rails (the v1.22 document-scan provider/key,
reused — no new provider) and transcribe structured facts into staging.

The safety line is extract, never interpret: the prompt frames the document
as untrusted data to reproduce verbatim. Diagnoses map to Condition,
measured values to Observation, medications to MedicationStatement — with
STATED status only. A code survives only when the document writes it and its
system matches the resource (SNOMED/ICD-10 for a diagnosis, LOINC for a
value, RxNorm/ATC for a medication); nothing is inferred, no value is
range-flagged, no condition is linked to a medication. Every fact carries
per-field provenance and a confidence score; a fact below the confidence
floor fails closed — it cannot be approved until the user edits it.

Routes (apiHandler + Zod + session-narrowed userId, opt-in module-gated):
- POST/GET /api/documents/inbound — upload+extract (vision or local-OCR
  text), and list. The raw document is stored encrypted at rest.
- GET/DELETE /api/documents/inbound/[id] — review detail; soft-delete.
- PATCH .../facts/[factId] — correct a staged fact before approval.
- POST .../confirm — the only write path: approved facts flow into the
  labs / conditions / medications stores through their normal create paths;
  rejected facts are discarded; low-confidence facts are refused.
2026-06-28 16:57:06 +02:00
Marc-André Bombeck 5280c183bd feat(documents): add inbound clinical-document data model (migration 0218)
Store an uploaded doctor report / discharge letter and stage extracted
structured facts for review. Two models:

- InboundDocument: the uploaded document, raw bytes encrypted at rest via
  the shared AES-256-GCM Bytes codec (content_encrypted), with queryable
  metadata (kind, mime, size, status, report date).
- ExtractedFact: the review-then-confirm staging row — fact type, status,
  per-field provenance, a confidence score, and a needs-review gate, plus
  the FHIR-staged payload (stated codes/status only) as JSONB.

Wire content_encrypted into the encryption-key rotation registry and the
rotation script so a key drop never strands a stored document.
2026-06-28 16:56:54 +02:00
Marc-André Bombeck e216a9d678 feat(coach): warm, localized proactive nudge with opt-in AI enrichment
Rework the proactive Coach nudge so it reads like one human checking in on
another instead of a terse English alert.

Copy: the title is a name-led greeting that rotates day to day ("Guten
Morgen, …" / "Hey …"; the 05:15 frame makes a morning opener safe) and the
body is one calm observation plus a gentle invite — never an imperative.
Drop the verbatim quoting of the user's decrypted self-context sentence:
the focus is referenced only by presence, abstractly, and is no longer
decrypted at all. Remove the 80-char mid-word clamp; each channel composes
to a length budget instead.

Cadence: suppress the nudge when the user already talked to the Coach in
the last ~24 h. With the existing rolling frequency cap (never two in a
window) and the no-trigger-no-nudge rule, the surface stays anti-nag.

AI enrichment (opt-in via the coach prefs blob, default off): when a
provider is healthy the body is composed through the model under hard
guards — a per-user budget gate, a tight per-call timeout, a per-tick
ceiling so the sequential tick can't stall across the user base, and the
outbound safety screen on the output. Any error, timeout, budget refusal
or unsafe reply falls silently back to the deterministic template, which is
always the default and the fail-closed fallback.

Localized greetings, per-trigger bodies and the focus note across all six
locales; the prefs opt-in is surfaced in the notification settings.
2026-06-28 16:50:49 +02:00
Marc-André Bombeck 54c9b4206f fix(i18n): persist User.locale so background messages match the UI language
The language switcher wrote the choice to localStorage, the
`healthlog-locale` cookie and `<html lang>` only — never to the user row.
Server-side background work (the proactive Coach nudge, the Telegram test
message) has no request cookie to read and falls back to `User.locale`;
with the column left null those messages went out in English regardless of
the language the user reads the app in.

Add `PUT /api/auth/me/locale` (idempotent, validated against the supported
locales) and have the i18n provider mirror the active locale onto the row —
on every switch and once on mount, so the column is backfilled for users
who set their language before this landed. Fire-and-forget: a 401 on a
public page or an offline blip never blocks the UI flip; the cookie stays
the first-paint source of truth.
2026-06-28 16:50:32 +02:00
Marc-André Bombeck 29db83ee5a fix(measurements): scope external-id delete to app-minted HealthKit rows
DELETE /api/measurements/by-external-ids now only tombstones rows the iOS
client minted through its HealthKit ingestion path (source APPLE_HEALTH).
An externalId the app never minted — an integration- or manually-sourced
row that happens to collide on externalUUID — falls outside the predicate
and is a no-op rather than a delete. Defence-in-depth so a foreign
HealthKit sample with a colliding UUID cannot be tombstoned by the
periodic reconciliation. The delete stays user-scoped as before.

Add a route test proving a foreign-origin externalId is not deleted, and
regenerate the OpenAPI spec for the narrowed contract.
2026-06-28 16:30:28 +02:00
Marc-André Bombeck 74986116cd feat(labs): accept optional OCR provenance on lab create
The structured lab-create path (POST /api/labs) now accepts an optional
`source` marker ("MANUAL" | "OCR"). The on-device-OCR ingestion path posts
"OCR" so a reading captured from a phone-camera lab photo records its
provenance; the raw image never touches the server on this path. Omitting
the field reads as "MANUAL", so the existing hand-entry contract is
unchanged. The value is narrowed by a closed Zod enum and assigned
field-by-field, never spread from the body. Mirrors the "OCR" source the
server-side /api/labs/ocr/commit path already stamps.

Regenerate the OpenAPI spec for the widened request body.
2026-06-28 16:30:17 +02:00
Marc-André Bombeck 2563690e4b fix(insights): add chart tokens for v1.25 clinical measurement types
The PHQ-9 / GAD-7 screener totals and the physical clinical measures
(grip strength, pain NRS, waist circumference, waist-to-height) are
continuous numeric series. Register their metric:<TYPE> tokens so the
chart-token drift guard passes and the AI insight prose can reference
them through the generic chart renderer.
2026-06-28 16:15:48 +02:00
Marc-André Bombeck 8b1cccbfce chore(openapi): regenerate spec after v1.25 feature merges 2026-06-28 16:12:53 +02:00
Marc-André Bombeck 65f1358179 Merge branch 'feat/v125-coach-insights-rest' into release/v1.25.0 2026-06-28 16:10:48 +02:00
Marc-André Bombeck ce2bddcc51 Merge branch 'feat/v125-records' into release/v1.25.0
# Conflicts:
#	docs/api/openapi.yaml
#	messages/de.json
#	messages/en.json
#	messages/es.json
#	messages/fr.json
#	messages/it.json
#	messages/pl.json
#	scripts/rotate-encryption-key.ts
#	src/lib/crypto/encrypted-columns.ts
#	src/lib/openapi/routes/index.ts
#	src/lib/query-keys/index.ts
2026-06-28 16:10:23 +02:00
Marc-André Bombeck d5f31b5b5f Merge branch 'feat/v125-clinical-signals' into release/v1.25.0 2026-06-28 16:07:59 +02:00
Marc-André Bombeck c322a68d19 feat(mental-health): screener UI, locale-aware copy, and OpenAPI surface
Add the opt-in PHQ-9 / GAD-7 questionnaire surface at /insights/mental-wellbeing:
instrument picker, 0–3 item scale, server-scored result with the validated
severity band, the verbatim Pfizer/PHQ attribution, the screening-not-diagnosis
disclaimer, and a calm crisis-resource card on a positive item-9. The surface
deliberately does NOT mount the shared Coach-launch card — mental-health item
content stays out of the AI.

- i18n: full mentalHealth block plus the six new measurement-type labels and the
  longevity-marker catalogue names/descriptions across all six locales
  (de/en/es/fr/it/pl). Instrument item wording uses the validated German text and
  the canonical English elsewhere (re-wording invalidates the score).
- OpenAPI: regenerate the contract for /api/mental-health/assessments (response
  excludes raw item answers by construction).
- Exclude the new manual / instrument types from the HealthKit-counterpart guard.
2026-06-28 16:05:51 +02:00
Marc-André Bombeck d570d558fa feat(insights): make sleep debt a configurable source ladder with a computed explainer
Generalise sleep debt onto the existing per-metric source-priority picker
(a new sleepDebt ladder, native-above-computed, default WHOOP > Oura > Polar >
computed) instead of assuming one provider. The rhythm read resolves the active
source off the ladder and carries it on the DTO; when the active source is our
own COMPUTED engine — every user today, since no provider ships a native
sleep-debt balance — the Sleep page explains what the figure means and that it
differs from a wearable's native number. The computed model itself is unchanged
and correct (a short rolling balance of need minus sleep, surplus-credited);
only stale comments that still referenced the old 14-night window are corrected.
2026-06-28 16:04:22 +02:00
Marc-André Bombeck c862b4880f feat(coach): add an opt-out for proactive ambient suggestions
Add a per-user toggle that turns off the proactive ambient example suggestions
(the daily seeded opener on the Coach hero). The flag rides the existing coach
notification-prefs blob (coach.ambientSuggestions, default on) — no schema
change — and a shared hook makes it the single source every ambient suggestion
surface consults, so the seeded opener and its fetch both stand down when off.
2026-06-28 16:04:22 +02:00
Marc-André Bombeck e55058a1ce fix(insights): stop the period review card leaking methodology and raw enums
The weekly/monthly review card rendered an internal methodology line plus the
raw MeasurementType tokens it drew on ('Based on: WEIGHT, BLOOD_PRESSURE_SYS…'),
which read as broken and added no value. Drop the provenance caption entirely so
no enum token is ever user-facing, and hide the card whenever there is no real
generated narrative (no 'preparing' husk), consistent with the hide-when-empty
pattern. The query keeps polling in the background, so the card appears the
moment a narrative lands.
2026-06-28 16:04:22 +02:00
Marc-André Bombeck fba8553192 fix(insights): remove the trajectory continuation tile from metric pages
The short-horizon trajectory tile read as a wall of text with no clear value
on the metric pages. Drop it from the weight page and the shared HealthKit
metric page (the opt-in forecast slot is removed entirely, not left dead), and
delete the now-orphaned tile + fan components. The trajectory engine stays for
the Coach narrative; only the metric-page surfacing is removed.
2026-06-28 16:04:22 +02:00
Marc-André Bombeck da132a75dd feat(coach): wire the composer mic to real dictation and hide it when unusable
The composer mic advertised dictation but errored on tap in environments where
the Web Speech API cannot serve. Gate the control on the API being present AND
a secure context, map the locale to a region-qualified recognition language,
and retire the button for the session on a permission refusal. When dictation
is genuinely unavailable the mic hides rather than showing a control that
errors on every tap.
2026-06-28 16:04:22 +02:00
Marc-André Bombeck 9d22412caf fix(coach): size the example opener to its text and keep it single-line
The ambient example opener was stretched to the full composer width, leaving
a large empty tail that read as broken. Size it to its text instead (capped at
the full width) and guarantee a single line: the question truncates with an
ellipsis on a narrow phone rather than wrapping or forcing a horizontal scroll.
2026-06-28 16:04:22 +02:00
Marc-André Bombeck a37883e3f8 feat(records): surface allergies + family history in Settings
Add a new "Medical history" (Anamnese) Settings section that hosts the two
structured-record managers — allergies and family history — each a
list + add/edit sheet + delete-with-confirm built on the shared query-key
factory and the ResponsiveSheet pattern. It sits between the illness journal
and the preventive-care reminders and is always shown (not module-gated, like
Vorsorge), since these are foundational health-profile records.

The section carries a plain non-diagnostic note ("your own records, not a
medical diagnosis"). Full i18n across all six locales (de/en/es/fr/it/pl).
2026-06-28 16:03:25 +02:00
Marc-André Bombeck 7ec2555d9a feat(records): map allergies + family history to FHIR R4
Emit `AllergyIntolerance` and `FamilyMemberHistory` resources in the
health-record document bundle. Vendor-blind and non-diagnostic, matching the
illness `Condition` stance: the user's own label rides `code.text` (never a
machine-guessed clinical code); only the FHIR-fixed value sets — allergy
category, clinical/verification status, the v3 RoleCode family-member concept —
carry codings. Every resource is marked patient-reported (allergy
`verificationStatus: unconfirmed` plus an explicit "not a clinical diagnosis"
note) so a clinician's viewer never reads it as an adjudicated finding.

The records are loaded owner-scoped in the export route and folded into the
fhir + package bundle formats with their own Composition sections.
2026-06-28 16:03:15 +02:00
Marc-André Bombeck 5052b2cc08 feat(records): CRUD API + OpenAPI for allergies and family history
Add the `/api/allergies` and `/api/family-history` endpoints (list / create /
read / update / soft-delete), each wrapped in `apiHandler` with Zod
`safeParse` + `returnAllZodIssues`. `userId` is always narrowed from the
session/Bearer — never a body field — and the Prisma `data` is built
field-by-field (no mass assignment). Creates wrap in `withIdempotency`. The
free-text reaction/note are encrypted before write and decrypted fail-soft on
read via the server-authoritative DTOs.

Register the query-key factory entries and the OpenAPI route tables (new
`Records` tag); regenerate `docs/api/openapi.yaml`.
2026-06-28 16:03:05 +02:00
Marc-André Bombeck 3ded891641 feat(records): add allergy + family-history data model
Introduce two structured health-record tables, kept as reference records
(not time-series signals):

- `allergies` — AllergyIntolerance-style: substance + category + type +
  severity + status + optional onset. The substance label stays queryable
  plaintext; the free-text reaction and note are AES-256-GCM encrypted
  (`reaction_encrypted` / `notes_encrypted` BYTEA).
- `family_history_entries` — one condition per relative: relationship +
  condition label + optional age-at-onset; the free-text note is encrypted.

Both are owner-scoped and soft-deleted. Migrations 0215/0216 are additive
and idempotent. The three new encrypted columns are registered in
ENCRYPTED_COLUMNS and wired into the key-rotation script, so a key drop can
never silently strand them undecryptable.
2026-06-28 16:02:54 +02:00
Marc-André Bombeck 7575275d7e feat(mental-health): opt-in PHQ-9 / GAD-7 screeners with item-9 crisis signposting
Add the validated mental-health self-assessment surface, beside mood tracking
(never replacing it) and off by default:

- MentalHealthAssessment stores per-item answers in a single AES-256-GCM Bytes
  blob (registered for key rotation); only the server-authoritative total,
  severity band, and item-9 flag are denormalised. The total projects to a
  PHQ9_SCORE / GAD7_SCORE measurement row so the trend rides existing infra
  without ever exposing item content.
- Item-9 safety: any non-zero PHQ-9 self-harm answer surfaces a calm,
  locale-aware crisis-resource set (DE: TelefonSeelsorge 0800 111 0 111 / 112;
  international fallback) and stamps crisisShownAt. No clinical interpretation,
  no AI ingestion of item content, no third-party alerting.
- API + OpenAPI: POST/GET /api/mental-health/assessments; the response excludes
  raw item answers by construction. Required Pfizer/PHQ attribution lives in the
  instrument module, rendered verbatim.

The screener scores are kept off the Coach snapshot and MCP surfaces, and out of
exports by default — item content never reaches the AI pipeline.
2026-06-28 15:47:39 +02:00
Marc-André Bombeck e4875db93e feat(signals): register the v1.25 clinical signals through the registry
Add the clinical-signals wave's new measurement types and wire them through
the signal registry so FHIR, correlation eligibility, ingestion, and the
measurement-list surfaces derive from one definition each:

- MeasurementType: PHQ9_SCORE, GAD7_SCORE, GRIP_STRENGTH, PAIN_NRS,
  WAIST_CIRCUMFERENCE, WAIST_TO_HEIGHT (+ AssessmentInstrument enum and the
  MentalHealthAssessment model that backs the screener scores).
- Registry: the six measurement/score signals plus the longevity lab panel as
  kind:"biomarker" entries; the FHIR golden fixture is regenerated in lockstep.
  Grip-strength and waist-to-height carry a null LOINC (NEEDS-VERIFY) and emit a
  local text concept until a stable code is confirmed.
- FHIR: add the survey observation category for the screener / pain signals.
- Labs: extend the biomarker catalogue with ApoB, Lp(a), fasting insulin, and
  the omega-3 index; add LOINC codes for those plus GGT and hs-CRP; add a
  computed HOMA-IR helper.
- Exhaustive maps updated to keep the enum coverage, category, list-meta, and
  personal-record drift guards green.
2026-06-28 15:47:25 +02:00
Marc-André Bombeck 3a84a07423 Merge branch 'fix/v125-mcp-egress' into release/v1.25.0 2026-06-28 15:19:23 +02:00
Marc-André Bombeck 13959e8da3 Merge branch 'feat/v125-shared-assessment-card' into release/v1.25.0 2026-06-28 15:19:23 +02:00