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.
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.
Remove the in-app water-intake logging feature: the WATER_INTAKE
measurement type, the per-user daily-goal field, the goal-ring card and
its endpoint, and the related query keys, OpenAPI route, and locale
strings. The feature is dropped from this release; the baseline-drift,
breathing-screening, and labs-changes insight cards are unaffected, and
the pre-existing TOTAL_BODY_WATER body-composition type stays intact.
Add three read-only awareness cards to the Insights overview, each drawn
off an existing engine and each honest about empty state — the section
un-mounts rather than surfacing a husk.
- Health status: surfaces what is drifting from the user's own normal by
folding the personal-band deviations (coincident-deviation engine) with
the dated, sustained level shifts (changepoint detector) at
GET /api/insights/health-status. Awareness only, never a diagnosis.
- Breathing screening: summarises the last 30 nights of the per-night
sleep-breathing-disturbance index plus device-flagged events at
GET /api/insights/breathing-screening. Mirrors the device's own
classification and states plainly that it is a screening signal, not a
diagnosis — discuss with a clinician.
- Lab changes: pairs the two most-recent numeric lab panels and reports
each shared analyte's signed delta and reference-band standing at
GET /api/insights/labs-changes. Neutral framing only.
Each reshaping helper is pure and unit-tested for its present/absent
states. Register the three sections in the insights-layout universe so
they merge in as default-visible, wire them into the overview and the
edit-mode title map, add the query-factory keys, document the routes in
the OpenAPI contract, and translate every new string across all six
locales.
Add three grounded read tools, each a thin façade over an existing
server-authoritative engine (no new analytics, userId from the session):
- get_medication_schedule: next-due / overdue per active medication via
the canonical recurrence engine (computeDisplayDue), with as-needed
flagged and no scheduled due time.
- get_integration_status: per-provider sync health (connected, last sync,
reauth-required/failing) from the integration-status ledger, so a user
can see why their data is stale. No secrets or tokens.
- get_preventive_care: the user's already-configured Vorsorge reminders
with their persisted next-due dates and an overdue flag; it surfaces
configured reminders only and invents no screening logic.
Each declares an output schema, read-only annotations, and {present:false}
on absence. Page the search tool across the full assembled result set
(the prior 200-row scan made later items unreachable even through the
cursor) and give each result a per-item deep link where an id exists.
Document the MCP server: an operator guide for enabling the
off-by-default module and connecting Claude.ai / ChatGPT / Claude
Desktop, a capabilities reference for the tool / resource / prompt
catalogue and the grounding + write model, and a skill-author guide for
building connectors against it. Link all three from the docs indexes.
Combine the remote MCP server (stdio + remote /mcp + OAuth 2.1 + read
tools + deep-value reads + a confirmed, audience-bound write surface)
with the v1.23 account-security line (MFA, known/trusted devices,
free-text note encryption).
- schema: keep every v1.23 User relation (MFA, known/trusted devices)
alongside the McpOAuthConnection relation; the MeasurementSource enum
carries the MCP value.
- api-handler: the MCP audience guard (a token scoped to
health:read/health:write is bound to /mcp and refused a REST write)
and the step-up MFA helpers coexist.
- maintenance queues: register the MCP token-cleanup sweep next to the
note-encryption backfill and key-rotation queues; shift it to 04:00
so the two daily discovery ticks do not collide.
- migrations: renumber the MCP migrations to 0206/0207 so they follow
the v1.23 chain (0199-0205).
- MCP mood writes route the note through the v1.23 at-rest cipher.
Add an opt-in "remember this device" at the second-factor step. On opt-in
a random token is minted, set in an httpOnly+Secure+SameSite=Strict cookie,
and only its HMAC hash is stored in a new TrustedDevice table. A later sign-in
from the same browser still requires the password (factor 1); a matching,
unexpired trusted device lets the login skip factor 2 only.
The token is never accepted for step-up — a trusted-device login does not stamp
mfaVerifiedAt — so a stolen device cookie cannot reach a destructive action.
Trusted devices are listed and revocable from the security surface, are revoked
wholesale when the second factor is removed or the user signs out everywhere,
and cascade away on account deletion. A recovery-code login never trusts the
browser. Migration 0204 adds the table (and the instance-wide mfa_required
policy column that the enforcement work reads).
Introduce a dedicated /settings/security section that gathers every
account-protection control in one place: TOTP enrolment (QR plus manual
secret, confirm, recovery codes shown once with copy and download),
WebAuthn security keys as a second factor (register, list with last-used,
rename, step-up-gated removal), recovery-code count and regeneration, and
primary passkey management.
The second-factor WebAuthn path keeps its own credential store, separate
from primary passkeys, and uses non-resident keys so an account can hold
many. Relying-party resolution is shared with the passkey path. A status
endpoint backs the hub; the step-up gate now recognises a registered
security key as an active second factor so a WebAuthn-only account can
still reach the gated actions.
Drop the in-app bug-report surface end to end: the user submission page,
the feedback intake and admin inbox, the optional GitHub-issue
escalation, the operator config, and the dedicated achievement badges.
- delete the /bugreport page and the bugreport / feedback / admin-feedback API routes
- drop the Feedback model, its enums, and the AppSettings GitHub-issue and enable columns (migration 0205)
- remove the admin Feedback section, the Bug Report integration card, and the system-status row
- drop the bug-report nav entry and the app-settings provider that only carried its flag
- remove the bug-hunter achievement badges and their metric
- prune the related i18n keys across every locale and the encrypted-column and key-rotation registries
Add the user-facing account-security surface in Settings:
- Active sessions (closes#64): GET /api/auth/me/sessions lists each browser
login with a coarse device label, masked IP, resolved location, the sliding
last-active stamp, and the current-session marker. DELETE the collection
signs out everywhere else (keeps the current session, revokes other sessions
and native refresh tokens, leaves API tokens alone); DELETE one revokes a
single session, scoped to the owner so a foreign id 404s.
- A sliding last-active stamp on the session, written throttled and
fire-and-forget off the request-resolution path.
- Security activity: GET /api/auth/me/security-activity, the shared read over
the audit log filtered to sign-ins, second-factor changes, password changes,
session revocations, exports, and deletions, with masked IPs.
OpenAPI regenerated for the new endpoints.
Add an opt-in to download the full backup as an encrypted archive instead
of a plaintext JSON file. The user supplies a passphrase over the request;
the server derives a 256-bit key with Argon2id and seals the export bytes
with AES-256-GCM into a self-describing HLX1 archive (magic, version, KDF
params, salt, iv, tag, ciphertext), mirroring the off-host backup envelope.
POST /api/export/encrypted returns the binary archive and is step-up gated
for accounts with a second factor; single-factor accounts use a normal
session. The passphrase is never stored and never logged, and there is no
server-side recovery — the UI states this. A standalone decryptor
(scripts/decrypt-export.ts) opens the archive with the passphrase outside
the app. The plaintext full-backup route now shares one payload builder so
both variants emit the same restore-compatible shape.
When a password is accepted for an account with a confirmed TOTP factor, the
login route no longer mints a session or token. It creates a single-use,
~5-minute MfaChallenge (the opaque ticket is stored only as a hash) and
returns HTTP 200 with data:null, error:null and meta.mfaRequired + mfaTicket +
methods. The client completes the login at POST /api/auth/mfa/verify with the
ticket + a TOTP or recovery code, receiving the exact same token bundle the
password path issues, with the session stamped second-factor-verified.
- The ticket is claimed once, atomically, only after the factor verifies, so
two concurrent submissions can mint at most one session; no session ever
exists before factor two.
- Wrong codes increment the ticket attempt counter and burn it at the cap
(throttle, not an account lock); the anonymous verify surface is also
rate-limited per IP.
- meta.mfaRequired is returned only after a valid password and the
invalid-credentials response is identical regardless of MFA state, so an
attacker without the password cannot enumerate MFA.
- The session/token tail is extracted to login-response.ts and shared by both
the password path and the verify path, keeping the transport contract
identical. OpenAPI registers the verify endpoint, the MFA management
endpoints, and the mfaRequired login variant.
Append an MCP value to MeasurementSource (migration 0196, additive enum
extension) so a row logged through the remote MCP endpoint is distinguishable
from MANUAL/TELEGRAM provenance and gets an isolated idempotency namespace
(userId, type, MCP, externalId). The value joins the read enum so existing
read/response shapes decode the rows; the REST batch and single-POST write
surfaces still reject it (server-resolved only). Regenerate the OpenAPI
contract for the additive enum.
Add three per-user AI settings, all configured in the app (no new env vars),
behind one migration (0198):
- Response timeout (seconds). A per-user upstream timeout threaded onto the
provider call. A local/self-hosted backend can take more than the legacy
60 s default to load a model on the first request, which timed the Coach
out before the first token. Defaults to a generous ~180 s when unset;
validated to 10–600 s.
- True token streaming for the local OpenAI-compatible client. The client now
POSTs with stream:true and parses the SSE delta chunks, emitting tokens as
they arrive, with a per-idle-gap timeout instead of a whole-call ceiling and
a graceful fallback to the buffered path when a server cannot stream. The
Coach route runs the provider call inside the response stream behind a
keepalive heartbeat so a reverse proxy no longer idle-drops the connection
during model load or generation. Grounding, refusal, and sentinel-stripping
still run on the complete reply before any token reaches the client.
- Dedicated document-scan (Lab-OCR) provider. Off by default — the OCR path
uses the main provider chain. When enabled, lab-report processing resolves a
separate provider/model/key, with a hint when the chosen model is not
vision-capable. The new key column is AES-256-GCM at rest and registered in
the encrypted-columns registry and the rotation script.
i18n: EN + DE translations for the new Settings → AI strings (other locales
carry interim copies to keep locale parity green).
Add the medication-class to target-vital map and the adherence-to-vital
storyline so the Coach can surface, association-only, when an adherence dip
lines up with a drift in the vital that medication targets. Never causal,
never a dose-change suggestion; dose questions defer to the prescriber.
Add an in-repo CUSUM / binary-segmentation changepoint detector over DAY
rollups with a high firing bar (persistence plus within-segment-spread
magnitude) so the Coach can name a dated, sustained level shift instead of a
vague "lately", and stays silent when no break clears the bar.
Surface which source recovery is read from when two sources materially
diverge, with the honest reason, instead of silently averaging.
Lay the n-of-1 experiment substrate: a nullable encrypted outcome column on
coach_plans (migration 0197), the review_due / reviewed lifecycle statuses
wired into the encrypted-column registry and rotation script, and a daily
worker that reads the experiment result back in the user's own before/after
numbers — association-only, honest-null, no cheerlead on an adverse trend,
and routing a worsened vital to the clinician. The user-visible read-back
stays behind COACH_EXPERIMENT_VERDICT until its review cases clear.
Extend the outbound dose screen to catch experiment-shaped dose changes
(halve / skip / stop a medication to test it). Add evaluation cases and unit
tests for each new surface.
Bridge the existing transport-agnostic MCP server factory to a Next.js
route at /mcp using the SDK's Web-standard Streamable HTTP transport, so a
remote client can connect with a manually-minted hlk_ Bearer token. The
same tool/resource layer the local stdio transport serves is reused
verbatim — no duplicated tool logic.
The route owns four gates in front of the transport:
- Auth: Authorization: Bearer hlk_… resolved by the same validator the
HTTP edge uses, so revoked/expired/scope semantics never drift. A cookie
is never consulted, so the cookie-only admin boundary stays unreachable
here regardless of token scope.
- Rate limit: the Postgres limiter keyed by the <userId>:<tokenId> binding
(mcp:<binding>), so a single shared credential cannot drain the account.
- Module gate: a new opt-in mcp module, OFF by default. With it off the
endpoint answers 404 and reveals nothing — a self-hoster who never opts
in gets zero new exposure. The Modules settings hub renders the toggle.
- Transport: a fresh stateless server + transport per request.
Read-only by construction (only read tools are registered) and same-origin
by construction (no CORS headers). The proxy lets /mcp skip the page cookie
redirect since it authenticates by header, mirroring the webhook surfaces.
Add the management surface for the reminder memory: list, create, confirm,
done/dismiss, and soft-delete, every query scoped to the session owner with
the same no-IDOR posture as the plans routes (a cross-user or unknown id is
a 0-count no-op, never a leaked existence). The note is the only writable
field; timing is resolved server-side from the closed grammar.
Add the confirm endpoint behind the generalised action card. It switches on
a closed action-type allowlist and builds each entity field-by-field from a
server-resolved spec: a preventive-care reminder whose schedule comes from a
closed interval catalog, or a reminder note. It never auto-applies, and the
allowlist excludes any medication or clinical change.
Regenerate the OpenAPI contract for the new routes.
Thread the rolling early-detection pass and the labs -> outcome pass through the
correlations route and the coach correlations reader, both of which already
build the same daily series. The route response gains an optional `emerging`
block (recent-window pairs not yet established, flagged provisional) and a
`labCorrelations` block; the coach reader gains matching `emerging` and
`labDrivers` fields. Regenerate the OpenAPI contract for the new response shape.
The bottom-right launcher opened a blank new chat on an unread proactive
message instead of the thread the Coach wrote. Return the owning
conversation id from the nudge-status endpoint, have the launcher open
/coach?c=<id> on the unread path, and stamp the same deep link on the push
metadata so the notification lands in the exact conversation rather than
relying on a most-recent heuristic.
Add a `hidden` boolean to the Biomarker model (migration
0195_biomarker_hidden) so a marker the user no longer needs can be
soft-removed instead of deleted: it leaves the active catalog and the
lab-entry pickers but keeps its readings and canonical unit/range.
Thread `hidden` through the DTO, the update schema, the PUT handler
(field-by-field), and the list endpoint (visible markers first). Regen
the OpenAPI contract.
Adds the Coach goal / if-then-plan memory model (propose-then-confirm,
encrypted, migration 0194) and a standing Coach quality harness (graded
golden set + red-team in CI, optional nightly model judge off by default).
Caches the dashboard snapshot per language. No breaking changes.
Hotfix: the Coach and daily briefing failed for ChatGPT-provider accounts —
a provider tool-call rejection surfaced as a 500. Provider failures now
degrade gracefully, the Codex tool-call wire format is corrected, and the
provider error body is captured for diagnosis. Also: the thinking bubble
shows only the animated dots.
Add a CoachPlan model: durable, user-scoped if-then implementation plans
the Coach proposes and the user confirms. The background memory-refresh
pass extracts plans the user agreed to and writes them as "proposed"; only
the user-facing PATCH on /api/coach/plans/[id] activates one. The free-text
cue / action / target are encrypted at rest with the shared Bytes codec;
metric, status and dates stay plain so the injection picker ranks without a
per-row decrypt. Active plans fold into the Coach snapshot's memory block
and survive the prompt-budget drop alongside facts. List, confirm and
soft-delete routes are Coach-gated and narrow the owner from the session.
Key the dashboard-snapshot query cell by locale so a locale switch reads
freshly localised tile copy on its own cell instead of serving the prior
locale's prose until the slot goes stale. The zero-arg key stays
byte-identical, so every prefix-based invalidation keeps reaching the
locale-keyed cells; the offline-banner and preloader match on the prefix.
Migration 0194 (additive CREATE TABLE). Encryption-rotation registry and
script cover the three new Bytes columns.
Surfaces the Coach's existing intelligence as ambient presence: a per-metric
"Coach read", a visible scope handoff and a seeded opener, briefing callbacks,
a tension verdict and return-to-baseline note on the score, two motivational-
interviewing moves, and grounding parity on the local model path. No
migrations, no breaking changes.
Patch release. Pins the action row in the centred dialog so the primary
button stays reachable without scrolling under long forms, browser zoom, or
display scaling. No migrations, no breaking changes.
Thread the request abort signal through runCoachToolLoop into each
per-round completion so a mid-generation client disconnect tears down
the upstream call instead of paying the full token cost. Completes the
no-tools-path wiring to cover the tool-retrieval path as well.
Regenerate the OpenAPI spec for the trimmed OCR-capability reason enum.