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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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`.
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.
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.
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.
The requirePublicHost egress paths now dispatch through undici's own
fetch, so the existing global fetch stubs in the AI, GeoIP, moodlog,
send, and ntfy suites no longer intercept them. Delegate undici's fetch
back to the installed global stub so each suite keeps asserting its
request shape and canned responses without exercising the network.
CIMD client resolution swallowed every fetch failure as ssrf_blocked and
/authorize logged a bare invalid event, so an unreachable metadata host
was indistinguishable from a genuine SSRF block and the consent flow
returned invalid_client with no diagnosable cause.
Split the resolution failure into ssrf_blocked (a real private-host
refusal) and fetch_failed (timeout / connect error), carrying the
concrete SafeFetchError kind and cause as a short non-secret detail.
Thread the resolution reason and detail into the authorize.invalid
annotation behind a pinned meta shape, and cache a resolved CIMD
document for up to 24h so the consent flow does not re-fetch on every
hit.
The connect-time SSRF pin returned only the first vetted address and
attached an undici@8 Agent to Node's built-in fetch. Two failures:
- A host whose first DNS record is an unreachable IPv6 (no IPv6 route on
the container / LAN / Tailscale) failed the connect instantly, because
the single-address pin discarded the working IPv4 record behind it and
Happy Eyeballs never ran.
- Node's global fetch is backed by its own pinned undici copy; handing it
an Agent built from the project's undici throws UND_ERR_INVALID_ARG on
a dispatch-handler interface mismatch.
Hand undici the full vetted survivor set and enable autoSelectFamily so
it runs RFC 8305 Happy Eyeballs across the already-vetted IPs (rebinding
stays closed: every dialable IP passed isPublicIp in the same lookup).
Drive the pinned path through undici's own fetch so the dispatcher and
the fetch engine share one undici version.
Add an end-to-end regression that drives a real localhost-bound server
through the pinned path (not a mocked fetch, which hid the coupling),
covering the IPv6-to-IPv4 fallback, streaming, timeout, and abort.
The sleep page rendered an assessment under the duration/architecture charts
and a second one under the sleep-quality scores. The quality assessment mostly
never generated — most accounts carry no wearable sleep score — so it sat at the
foot of the page as a duplicate empty card or a redundant connect-a-provider
prompt.
Drop the quality-block assessment so the duration card is the page's single
assessment; the quality block keeps its score tiles. Remove the now-unused
grounded-note fallback and its deterministic grading helper, and the orphaned
quality-assessment message keys across every locale.
The per-metric assessment card carried the Coach hand-off as a bottom-right
text button, present on only some pages. Pin it to the card header's top-right
as a single icon button (no text label) with an accessible name and a tooltip,
and wire every assessment surface to it: the bespoke slug pages
(weight, BMI, pulse, blood pressure, mood), the generic metric scaffold, the
medication-compliance card, and the biomarker detail page already on the shared
card. Each opens the Coach with a localised, auto-sent opener narrowed to that
metric's snapshot source.
The button self-gates on the Coach triple (provider mounted, operator flag,
per-user opt-out) and the card only mounts it on the populated branch, so it
never advertises a control that cannot work. The auto-sent opener now
interpolates the localised measurement name rather than a raw token.
Replace the hand-maintained MEASUREMENT_LOINC literal and the BUCKETED_TYPES
correlation list with signal-registry projections. Both reads are byte-identical
to the previous tables (guarded by the registry-invariant test), so there is no
behaviour change — only one source of truth for each.
Introduce one declarative table describing each health signal across the
surfaces it touches — DB measurement type, detail page, correlation engine,
Coach snapshot, MCP read tool, and FHIR coding — discriminated on kind
(measurement / biomarker / score). Promote the proto-registry in
metric-status-registry.ts to the canonical definition and add thin adapters
that derive the per-surface tables from it.
The registry coexists with the enum-driven code: the DB enum stays the source
of truth for persistence and the registry sits above it. An invariant test
pins every derived table byte-for-byte to the values the hand-maintained
tables carry today, so cross-surface drift fails CI.
Drive the biomarker resource route against a real Postgres: PUT persists a
new reference window and rejects an inverted one, and DELETE drops the marker
together with its readings while leaving another user's marker and readings
intact.
Add coverage for the new biomarker-assessment seam and the marker detail
surface:
- the generator serves today's cache, re-stamps unchanged readings without an
LLM call, and regenerates only when the latest-reading fingerprint changes
- the read-only endpoint returns the envelope, 422s a missing id, and honours
the auth + insightStatus gates
- the detail page paints the stat strip, the chart placeholder, and the
assessment card
- the biomarker resource route drops the marker with its readings in one
userId-narrowed transaction and rejects an inverted target range
Hold the input gate independent of the worker's forced run so an idle marker
re-stamps cached text instead of regenerating, and list the new route on the
non-Coach gate allowlist.
Give each biomarker a detail page that matches the metric sub-pages: a
numbers-first Min/Max/Median/Mean strip over the numeric readings and a
data-grounded AI assessment card.
The assessment is served by a new read-only endpoint
GET /api/insights/biomarker-assessment?biomarkerId=&locale=, returning the
same envelope as /api/insights/metric-status so InsightStatusCard consumes
it unchanged. A new generator reads the marker's LabResult history and
reuses the shared provider runner, the stale-while-revalidate read-only
path, and the persist helper. Regeneration is gated on a fingerprint of the
latest reading, so the assessment is rebuilt only when a new reading lands;
an idle marker re-stamps the cached text without an LLM call. The
biomarker: scope rides the existing on-demand status queue.
The stat strip and the assessment card both self-gate on numeric data, so a
qualitative-only or brand-new marker paints neither.
Add a control beside the marker actions that opens a focused sheet for
the reference low and high only, so a user can tune the band without the
full marker editor. Persists through the existing biomarker PUT and
invalidates the detail, catalog, and result reads.
The per-marker catalog description used to render on every labs overview
row, both in the list and the card layout. It reads better as a caption
beneath the detail-page heading, mirroring the metric pages' explainer
line. Resolve the catalog slug from the marker name and fall back to the
user's own context note.
Hold the submit handler in a ref updated from an effect and dispatch
the auto-send through a microtask, so the first-turn send no longer
calls setState synchronously inside the effect.
Deleting a biomarker now removes its recorded values in the same
userId-narrowed transaction instead of unlinking them via the SetNull
FK. Removing a marker is a deliberate "drop this and its history"
action, so leaving orphaned, unlinked readings behind was surprising.
Update the delete-confirmation copy in all six locales to state that
the values are removed too.
Thread an autoSend flag through the coach-launch flow so a card's
"ask the coach about this" hand-off dispatches its seeded prompt as
the conversation's first turn instead of only seeding the composer.
The flag rides the launch context, the layout mount, the drawer, and
the shared conversation surface, where a ref-guarded mount effect fires
the send exactly once for a fresh thread.
Localize the previously hardcoded English assessment opener in the
metric status card behind insights.coach.assessmentPrompt with a
{metric} placeholder, and localize the resting-pulse coach prefill.
Both keys land in all six locales.
Convert the per-biomarker reading history from a static reverse-chronological
list to a shadcn Table with clickable sort headers. Reuse the shared
useTableSort hook and SortableHead chrome already backing the measurement and
mood lists; the sample-date column opens descending, value/range/note open
ascending. The sort runs client-side over the in-memory reading feed.
Lift the byte-identical sortBy/sortDir/toggleSort triple the measurement
and mood lists each carried inline into a shared useTableSort hook. Both
lists now compose their page-reset and selection-clear side effects around
the hook's toggle rather than re-implementing the direction math, and the
date columns keep their descending-first default via descColumns.
Surface the generation-failure signal in the daily briefing card. When a
held briefing is shown but the last refresh failed, the footer states it
plainly and offers a retry that runs the existing explicit regenerate —
no warm-on-mount. When there is no briefing to fall back on, the empty
state reads "couldn't generate" with the same retry instead of the
generic "no briefing yet". The hint is suppressed when no AI provider is
configured, where that hint already owns the surface and a retry is
futile. New copy lands in all six locales.