Commit Graph
2070 Commits
Author SHA1 Message Date
Marc-André Bombeck 5a62d04651 fix(import): use the success token for import success ticks
The three import success check-icons hardcoded text-emerald-500, bypassing
the semantic text-success token every other surface uses. The token is
AA-tuned per theme (a darker green in light mode) where emerald-500 is a
single fixed value, so the import ticks sat off-tone in light mode and could
fall short of the AA bar the light tokens are tuned for. Swap to text-success
to match the integration-card family.
2026-06-15 01:21:10 +02:00
Marc-André Bombeck 7a96a934bf refactor(import): derive the CSV example header from the shared column list
The downloadable CSV example pinned its header as an inline string literal
while csv-measurements.ts exported CSV_EXAMPLE_COLUMNS as the documented
single source of truth for that order — so the two could silently drift.
Derive the example header from the constant; the column order now has one
owner and the previously unused export is wired in.
2026-06-15 01:20:49 +02:00
Marc-André Bombeck a27247f93c merge(settings): reminders hub, uniform integration cards, per-integration setup-guide links 2026-06-15 01:00:43 +02:00
Marc-André Bombeck ac69107169 chore(compose): whitelist VAPID and APNs env vars so the documented env path reaches the container 2026-06-15 01:00:43 +02:00
Marc-André Bombeck 78c110a6c8 feat(settings): add a Reminders & Notifications home
Reminders used to be scattered across three places: medication reminders on
each med card, mood / low-stock / coach-nudge inside the Notifications channel
screen, and the Vorsorge reminders on their own page. The new Reminders
section is the single front door. It draws a clean line between the two
concepts — reminder categories (the what/when) and notification channels (the
how/where) — and links to each canonical editor, which keeps its own route and
deep link intact. Consolidation by linking, not a rewrite.

The section sits right after Notifications in the settings nav.
2026-06-15 00:59:47 +02:00
Marc-André Bombeck 001c158203 feat(integrations): add a Setup-guide doc-link to every integration card
Each integration card now carries the same discreet "Setup guide" link to
its runbook under docs.healthlog.dev/integrations/<provider>, so a user who
is mid-setup always knows where to go. The docs host lives in one shared
constant and the affordance renders identically across all six providers
(WHOOP, Withings, Fitbit, Polar, Oura, Nightscout).
2026-06-15 00:59:33 +02:00
Marc-André Bombeck a8ff52e998 merge(admin): generate VAPID keys in-app and self-hoster backup/env docs 2026-06-15 00:59:22 +02:00
Marc-André Bombeck 266109f501 docs(self-hosting): VAPID generate button, env names, backup and whitelist notes
Document the new Generate keys button in notifications.md as the easiest
Web Push setup path, demoting the CLI to an alternative. Note that
clientManaged medication-reminder suppression is an iOS-app-only contract
so a PWA-only operator does not go hunting for a web toggle.

Add the VAPID env-var names and APNS_KEY_B64 to .env.production.example
(the loader and APNs sender read them, but the example omitted them),
both as commented placeholders, with a note that Web Push needs no Apple
account and that these vars are not in the compose whitelist.

Add a Backup and restore section to getting-started.md: a local pg_dump
one-liner and the load-bearing warning that the ENCRYPTION_KEYS /
ENCRYPTION_KEY must be backed up alongside the dump or the encrypted data
is unrecoverable, cross-linking the off-host backup and key-rotation
runbooks. Add troubleshooting entries for the compose env-var whitelist
and pull_policy: always.
2026-06-15 00:58:39 +02:00
Marc-André Bombeck d0ba4b061b feat(notifications): generate VAPID keys from the admin panel
Add a server-side VAPID keypair generator so a self-hoster no longer has
to run a shell command to enable Web Push. A new admin-only route
(POST /api/admin/settings/web-push-vapid/generate) calls web-push's
generateVAPIDKeys(), stores the private key encrypted at rest, seeds a
placeholder mailto: subject the operator can edit, and returns only the
public key. The route is cookie-only via requireAdmin(); a Bearer token
cannot reach it. The private key is never returned and never logged — the
audit detail carries a marker only.

The admin Web Push VAPID card gains a Generate keys button that fills the
public key and subject fields on success. When a keypair already exists
the route refuses with 409 and the card confirms before retrying with
force, warning that regenerating invalidates existing browser
subscriptions.
2026-06-15 00:58:30 +02:00
Marc-André Bombeck 45ccdcb305 merge(notifications): generic webhook + SMTP email channels and operator delivery health 2026-06-15 00:47:16 +02:00
Marc-André Bombeck c04bdc8f4e feat(notifications): document SMTP env and translate channel strings
Add the webhook + email Settings copy and the admin notification-health
panel labels across all six locales. Add the SMTP_* operator vars to the
env manifest (all-or-none: HOST + PORT + FROM enable the channel,
USER/PASS optional), to .env.production.example, and to the docker-compose
environment whitelist so an operator's .env values actually reach the
container.
2026-06-15 00:46:28 +02:00
Marc-André Bombeck 17dd10c3b9 feat(admin): operator-wide notification health panel
Surface the webhook + email cards on the notifications settings page, and
add an operator-wide delivery-health view the per-self diagnostic could
not give. A single groupBy(channel, result) over the trailing 24h of
push_attempts lets the operator answer "are anyone's pushes failing?"
across all users without DB shell, and a count of auto-disabled channels
(enabled=false with an automated disabledReason) flags a flapping upstream
that silently disabled a channel after five failures. The aggregate is
anonymous — it counts channel x result, never names a user. Admin-only by
construction (cookie auth, never Bearer); the window is clamped 1..168h.
The panel rides the existing admin Reminders section.
2026-06-15 00:46:21 +02:00
Marc-André Bombeck 909c2aa99e feat(notifications): add SMTP email channel
Alerts and reminders over email — the one channel every self-hoster
already has. The SMTP transport (host/port/auth/from) is operator-supplied
via SMTP_* env and loaded once per process like the APNs key; the only
per-user value is the recipient address, so SMTP credentials never enter a
user's encrypted channel blob. Absent SMTP env disables the channel
cleanly: the dispatcher skips it and the Settings card hides itself.

Bodies are plain text — HTML is stripped and routine reminders run through
the emoji-strip path; no markdown library is added. SMTP 5xx maps to a
hard reject (permanent — auto-disable); connection/4xx errors are
transient and ride the backoff schedule. Adds nodemailer (the standard,
well-audited mail transport) and its types.
2026-06-15 00:46:12 +02:00
Marc-André Bombeck 7532bbcc05 feat(notifications): add generic webhook channel
A single WEBHOOK channel covers Gotify, Discord, Slack, Matrix, and Home
Assistant: the user supplies a public URL and an optional shared-secret
header, and the dispatcher POSTs a small plain-text JSON envelope. The
outbound goes through safeFetch with requirePublicHost so the SSRF floor
and the connect-time DNS-rebinding pin apply to the user-supplied host,
exactly as they do for ntfy. Responses classify 404/410/401/403 as hard
rejects (auto-disable) and 5xx/429/timeout as transient (backoff).

Settings card mirrors the ntfy card with a save + test-connection flow;
the header value is never returned by the GET and is preserved across a
save round-trip when left blank.
2026-06-15 00:45:50 +02:00
Marc-André Bombeck aec41e154f feat(notifications): register webhook + email channel types
Add WEBHOOK and EMAIL to the channel set, the dispatcher cascade, the
per-event default policy, the push-attempt ledger union, and the admin
diagnostic config-presence check. Channels are string-typed, so no
migration is needed. The dispatcher routes each new type through its
sender with the same decrypt + parse + classify path the existing
channels use; the cascade orders webhook + email between the self-host
relays and the Web Push fallback.

Add the webhook + email channel config shapes and their settings-form
validation schemas, plus the query-key entries for the two new config
reads.
2026-06-15 00:44:26 +02:00
Marc-André Bombeck 168b69b334 merge(insights): surface stored sleep/recovery/strain metrics and a recovery sub-page 2026-06-15 00:40:27 +02:00
Marc-André Bombeck 1b0c4b2b7d test(insights): pin the device-score data-gating across the new surfaces
Cover the contract that matters for the vendor-native score tiles: a tile or
section appears only when its metric has stored readings. Asserts the empty
tile collapses to nothing, the sparse series swaps to the learning state, the
Sleep quality block renders only the metrics with data, and the Recovery page
shows its calm empty note with no data and groups + cross-links the score once
present.
2026-06-15 00:39:34 +02:00
Marc-André Bombeck 0be1a6b8a7 feat(insights): surface the Oura body-temperature deviation on skin temperature
Oura's overnight body-temperature deviation — a signed delta from the personal
baseline that tracks illness and cycle onset — was stored but never shown. Add
it as a data-gated tile beneath the chart on the metabolic skin-temperature
page, its most coherent home. It mounts only when the Oura series has readings,
so a non-Oura account renders the page unchanged.
2026-06-15 00:39:26 +02:00
Marc-André Bombeck 866f3dc0e2 feat(insights): add a Recovery sub-page for the device-native strain scores
WHOOP and Polar record day strain, workout strain, ANS charge, cardio load, and
the day's whole-cycle average / max heart rate and kilojoule energy expenditure
— all stored end-to-end with no render host. The composite recovery score had
only its anatomy detail at /insights/scores/recovery; the raw signals around it
appeared nowhere.

Add /insights/recovery as the home for them, grouped into recharge, strain &
load, and the day's heart & energy. Each block is data-gated, so an account
without a strap or ring lands on a calm empty note rather than a broken surface.
When a recovery score is present the page cross-links to its anatomy view rather
than duplicating the ring. Server-authoritative throughout.

Give the page a single nav home alongside Workouts in the unified destination
model so it is discoverable on both the sidebar and the mobile More hub.
2026-06-15 00:39:18 +02:00
Marc-André Bombeck e063f79353 feat(sleep): surface the nightly sleep-quality scores on the Sleep page
WHOOP and Oura write sleep efficiency, performance, consistency, need, the
disturbance count, and the Oura sleep score every night; until now none of them
rendered anywhere. Add a Sleep quality block after the rhythm section that
surfaces all six through the device-score tile.

Each tile is data-gated, so a non-wearable account never sees an empty card and
the whole block collapses metric-by-metric down to nothing when no source
writes them. It reads the same analytics summaries slice the page already
fetched for its duration gate, so it costs no extra round-trip.
2026-06-15 00:39:07 +02:00
Marc-André Bombeck 794192c089 feat(insights): add a calm device-score tile for vendor-native metrics
The WHOOP / Oura / Polar score family (sleep performance, day strain, ANS
charge, body-temperature deviation, …) is ingested end-to-end and lit in the
AI chart-token allowlist, but no page renders it. These scores sit outside the
metric-status registry by design — they are vendor-rolled-up values, not
signals the app re-derives — so they carry no assessment card.

DeviceScoreTile is the surface for them: a title, the latest reading, the
trailing-window mean, and a small sparkline, read straight from the stored
analytics summaries slice (server-authoritative, never recomputed). One neutral
card, never a status colour. When the series is still sparse the sparkline gives
way to the shared LearningGate so a two-night-old strap never paints a jagged
two-point line. The connected variant reads its own summary so it can drop onto
a page that already owns an analytics read.

Add the page-level copy in all six locales.
2026-06-15 00:38:59 +02:00
Marc-André Bombeck d1055b4047 merge(coherence): unify nav, layout settings, mood source, learning gates, coach nav home
# Conflicts:
#	src/lib/personal-records/pr-direction.ts
2026-06-15 00:23:54 +02:00
Marc-André Bombeck 347143c4b7 fix(personal-records): map the new derived metric types to a null PR direction
ANS charge, cardio load, the sleep score, and the body-temperature
deviation reached the MeasurementType enum without a branch in
getPRDirection, which left the exhaustive switch non-total — typecheck
fell through to an implicit undefined return and the drift guard failed.

These are derived / homeostatic composites with no goal axis, so they
join the other scores and temperature metrics on the null PR direction.
2026-06-15 00:21:42 +02:00
Marc-André Bombeck cf31211fbc refactor(settings): gather layout personalization under one home
Personalizing how the app is laid out was split across four unrelated
settings sections — Dashboard, Insights, Medications, Mood — with no
shared framing, so the same concept (arrange, reorder, show/hide) read
as four disconnected places, plus the settings nav carried four separate
arrange entries.

Add one Layout & Personalization hub that presents the four editors as a
consistent set of linked cards and replaces the four scattered nav
entries with a single home. Each editor keeps its own route and mutation
flow — deep links, the page-header cogs, and the hub's links all still
resolve — and a Layout child route now highlights the hub in the nav.
2026-06-15 00:20:16 +02:00
Marc-André Bombeck 2736e23092 merge(oura): real sleep timeline, SpO2, sleep score, temperature deviation, contributors + reconcile
# Conflicts:
#	docs/api/openapi.yaml
#	messages/de.json
#	messages/en.json
#	messages/es.json
#	messages/fr.json
#	messages/it.json
#	messages/pl.json
#	prisma/schema.prisma
#	src/components/measurements/measurement-list-meta.ts
#	src/lib/__tests__/measurement-type-enum-coverage.test.ts
#	src/lib/insights/chart-tokens.ts
#	src/lib/measurements/__tests__/apple-health-mapping.test.ts
#	src/lib/measurements/categories.ts
#	src/lib/personal-records/pr-direction.ts
#	src/lib/validations/measurement.ts
2026-06-15 00:17:51 +02:00
Marc-André Bombeck 412b22e68e merge(polar): pull ANS charge, cardio load, SpO2, distance; fix sleep timeline; invalid_grant + reconcile 2026-06-15 00:13:01 +02:00
Marc-André Bombeck a0662d1b20 fix(oura): audit credential deletes and trim integration vestiges
Deleting a user's stored Oura BYO credentials drops a live connection (a
token minted against the deleted app is orphaned), but the credentials
DELETE route skipped the auditLog + markDisconnected the disconnect route
performs, leaving the integration ledger parked at its last state with no
audit trail. Mirror the disconnect route when a token was present.

Drop two unused classifier type aliases and unexport the internal
invalid-grant helper, and refresh the stale 'full OAuth from env' headers on
the connect route and the env-credentials helper to the v1.17.1 DB-first
framing. Add the missing route tests for the BYO surface this release
touched: credentials GET/PUT/DELETE (422 reject, encrypt-on-write, the
audit/ledger branch) and the callback CSRF / cross-user / nocreds / success
paths.
2026-06-15 00:09:56 +02:00
Marc-André Bombeck c5e91c4cb6 feat(oura): pull the real sleep timeline, SpO2, Sleep Score and more
Widen the Oura sync from a thin slice to the signals the v2 API actually
exposes, all server-authoritative:

  - Real measured sleep timeline. mapSleep now reads the per-stage
    sleep_phase_5_min hypnogram and emits one timed SLEEP_DURATION row per
    contiguous 5-minute run, each stamped at its own end instant from
    bedtime_start. The per-night reader reconstructs the true clock-time
    timeline (measured, not synthesised) instead of the degraded
    all-on-bedtime_end shape. Falls back to the four stage totals when no
    hypnogram is present.
  - daily_spo2 → OXYGEN_SATURATION and daily_sleep → SLEEP_SCORE, filling
    two ladder slots that nothing wrote before.
  - daily_readiness temperature_deviation → BODY_TEMPERATURE_DEVIATION, and
    the eight readiness contributors are parsed (no longer dropped at the
    JSON boundary) for the Insights surface.
  - daily_activity equivalent_walking_distance → WALKING_RUNNING_DISTANCE,
    so an Oura-only day no longer contributes zero distance.

Every sleep row now carries a record-scoped externalId
(sleep:<record-id>:<tag>) so a nap and the main sleep on one day stay
distinct instead of overwriting each other on a shared day key.
2026-06-15 00:09:45 +02:00
Marc-André Bombeck 3cdbab908c feat(measurements): add Sleep Score and body-temperature deviation types
Two additive measurement_type values for the Oura coverage completion:

  - SLEEP_SCORE — the headline 0-100 sleep-quality score (unit score),
    kept apart from the WHOOP sleep sub-scores so two vendors' headline
    numbers never share a bucket.
  - BODY_TEMPERATURE_DEVIATION — a signed nightly body-temperature offset
    from the personal baseline (celsius). It is a deviation, not an
    absolute reading, so it cannot share the body / skin / wrist
    temperature buckets.

Migration 0165 extends the enum (additive, no backfill). Register both in
the validation enum, units, plausibility ranges, category overlay, list-UI
label/icon/colour maps, chart-token allowlist, and the PR-direction switch
(both resolve to no record direction). Both are descriptive signals, not
clinical vitals, so they stay out of the doctor-report PDF. Localise the
two list labels across all six locales and regenerate the OpenAPI contract.
2026-06-15 00:09:33 +02:00
Marc-André Bombeck 87e570abfe fix(polar): lift invalid_grant to reauth and harden the credentials surface
classifyPolarError now lifts a 400 carrying invalid_grant on the token
endpoint from persistent to reauth_required, mirroring Oura via the shared
isOAuthInvalidGrant helper. A stale or replayed authorization code at connect
time now surfaces the reconnect prompt instead of silently parking after 24 h.

The BYO-credentials DELETE now audit-logs and parks the integration ledger at
disconnected for parity with the dedicated disconnect route, and the PUT
audit-logs the credential write. Refresh the connect-route comment that still
described Polar as env-only OAuth; it has resolved BYO-first since v1.17.1.

Add the invalid_grant classification test plus the previously-missing
callback (happy path, CSRF mismatch, cross-user, off-spec token), connect, and
credentials route tests.
2026-06-15 00:09:22 +02:00
Marc-André Bombeck 574b860eb4 feat(polar): complete the AccessLink pull surface
Polar read three collections off the wire and dropped several useful signals
at the mapper. Close the gaps, keeping every value server-authoritative and
canonical-unit:

- Sleep now follows the END-instant convention the canonical night-grouper
  depends on. mapSleep reconstructs an ordered, contiguous CORE/DEEP/REM
  timeline from the parsed sleep_start_time / sleep_end_time, stamping each
  segment at its own end instant with an indexed externalId, plus an IN_BED
  envelope and the score at the sleep end. This restores the hypnogram (the
  stages no longer collapse onto one midnight-UTC instant) and fixes wake-day
  mis-attribution for negative-UTC users. Records without a usable window fall
  back to the prior midnight anchor. Synthetic ordering is flagged
  reconstructed.
- ans_charge maps to ANS_CHARGE (through-as-is; a baseline-relative deviation
  that can run negative).
- distance_from_steps maps to WALKING_RUNNING_DISTANCE (metres), filling the
  Polar distance-ladder slot that previously had no producer.
- New fetchers + mappers for Training Load Pro (cardio_load -> CARDIO_LOAD)
  and the Elixir SpO2 collection (-> OXYGEN_SATURATION), with Polar added to
  the SpO2 source ladder below the established straps.

No strap/profile weight is fetched; egress stays on safeFetch.
2026-06-15 00:07:42 +02:00
Marc-André Bombeck fd39580ee6 feat(measurements): add ANS_CHARGE and CARDIO_LOAD measurement types
Polar Nightly Recharge and Training Load Pro carry two native signals with
no existing home: the HRV-based autonomic-nervous-system charge (distinct
from the 1-6 recovery band that maps to RECOVERY_SCORE) and the device-native
cardio-load strain figure (distinct from WHOOP DAY_STRAIN and the COMPUTED
STRAIN_SCORE). Add both to the canonical enum (migration 0166) and wire them
through the validation enum, canonical units, plausibility ranges, category
cluster, chart-token allowlist, PR-direction resolver, and the measurement-
list label/icon/colour maps, with labels across all six locales. Both are
device-derived composites, so they read no PR direction and stay out of the
clinical vitals PDF, matching the WHOOP-native score classes.

Regenerate the OpenAPI contract for the two new enum members.
2026-06-15 00:07:30 +02:00
Marc-André Bombeck 9bedd6b4f8 feat(nav): give the Coach a single primary nav home
The Coach is reachable from seven scattered entry points — a floating
button, the hero CTA, suggested-prompt chips, every metric empty state,
a per-metric icon, the conversations drawer — but it had no entry in the
nav. Power users found it instantly; a new user could miss the
differentiator entirely behind a 56px floating circle.

Add the Coach to the shared destination model as one labeled, primary
home, consistent with every other destination across desktop and mobile.
The existing entry points stay; this adds the front door they lacked.
2026-06-15 00:06:28 +02:00
Marc-André Bombeck c90459a7bd refactor(nav): drive desktop and mobile from one destination model
The desktop sidebar and the mobile bottom-nav each hand-curated their
own destination list, and the two had drifted: the sidebar listed
Measurements and Mood inline but hid Workouts; the bottom bar buried
Measurements and Mood in More but promoted Workouts. A user who learned
the product on one platform re-learned it on the other.

Extract one ordered destination model both bars consume. The sidebar
renders every entry in order; the bottom bar keeps its 5-slot shape and
derives its More hub from the same list, so the two surfaces tell one
story. Workouts is now first-class on both. A shared active-resolver
prefers the most-specific sibling so a sub-route no longer lights up its
parent.
2026-06-15 00:04:13 +02:00
Marc-André Bombeck 3606610751 merge(medications): silent intake-sync push and Live Activity token for cross-device clearing 2026-06-14 23:59:14 +02:00
Marc-André Bombeck 6d8dd75fff feat(devices): accept a Live Activity push token on registration
Add an optional hex liveActivityPushToken to device registration. The iOS
client registers the current per-Activity ActivityKit push token so the
server can address a Live Activity update or end push on a
medication-intake mutation, and sends null to clear it when no Activity is
running. Following the medicationDelivery override convention, the field
is only touched when present, so an ordinary re-register keeps the prior
value. Regenerate the OpenAPI contract.
2026-06-14 23:58:27 +02:00
Marc-André Bombeck 836fd7405e feat(medications): dispatch the cross-device intake sync on a dose mutation
Wire the silent intake-sync push into both intake writers. The single
route fires once for the affected slot after the row is persisted; the
bulk route collects the distinct (medicationId, scheduledFor) slots whose
state actually changed and fires one push per slot, so a 500-row backfill
wakes a device per affected slot rather than per row. Pending echoes,
duplicates, and no-downgrade no-ops change nothing visible and are
excluded.

Both pass the originating device through the X-Device-Id header so the
device that recorded the dose is skipped. The dispatch is best-effort and
fire-and-forget: the canonical rows are already written, so a push miss
never affects the response.
2026-06-14 23:58:21 +02:00
Marc-André Bombeck a5ec4ef8dd feat(notifications): add silent medication-intake cross-device sync push
Introduce the MEDICATION_INTAKE_SYNC event and the server-side dispatcher
that wakes a user's other iOS devices after a dose is logged on one of
them, so a running Live Activity or Home-Screen widget reconciles against
the canonical server state.

The dispatch is APNs-only by construction: it sends a silent
apns-push-type: background push (content-available, no alert) carrying
the affected medicationId + scheduledFor, and never fans out to Telegram,
ntfy, or Web Push, which have no silent-sync transport. A new raw APNs
send helper carries the background and liveactivity push types without
the alert/sound/badge enrichment the alert path injects. Devices APNs
reports as permanently dead are reaped; the originating device is
excluded from the fan-out via its registered device token.

When a device has a stored Live Activity push token, an additional
liveactivity end push goes to the per-Activity topic so the lock-screen
Activity ends immediately rather than waiting for the next background
wake.
2026-06-14 23:58:11 +02:00
Marc-André Bombeck b1a81bb65e refactor(insights): unify the learning-state gates behind one primitive
Glucose, sleep debt, chronotype, the trajectory forecast, and the
composite-score anatomy each hand-rolled the warm 'still learning your X'
paragraph — five copies with different markup, markers, and no shared
accessibility contract. The calm voice held only because each author
copied the tone by hand.

Add a LearningGate primitive that owns the presentation and behaviour of
a learning state (the muted body, an optional caveat line, the learning
marker, and the polite live region) and repoint all five surfaces to it.
Each keeps its own card chrome and its metric-specific copy; only the
gate's look and behaviour are now shared, so they can no longer drift.
2026-06-14 23:57:15 +02:00
Marc-André Bombeck fd5cc78377 refactor(mood): read the dashboard and analytics mood series from one engine
The dashboard snapshot and the /api/mood/analytics route each carried a
hand-copied rollup read, coverage fallback, and daily summarize of the
same numbers. Two reads of one metric is the seam where the dashboard
tile and the insights mood sparkline can drift apart.

Extract buildMoodDailySeries as the single mood engine and route both
callers through it, so the same mood number reads identically on the
dashboard, the insights surface, and the iOS client.
2026-06-14 23:53:22 +02:00
Marc-André Bombeck 44eed87e89 fix(vorsorge): pair the loading spinner with motion-reduce:animate-none 2026-06-14 23:44:44 +02:00
Marc-André Bombeck 4f039626be merge(labs): structured lab results with reference ranges, doctor-report and FHIR
# Conflicts:
#	docs/api/openapi.yaml
#	messages/de.json
#	messages/en.json
#	messages/es.json
#	messages/fr.json
#	messages/it.json
#	messages/pl.json
#	src/lib/openapi/registry.ts
#	src/lib/openapi/routes/index.ts
#	src/lib/query-keys/index.ts
2026-06-14 23:43:47 +02:00
Marc-André Bombeck 72fb51c257 feat(labs): add the Laborwerte page, navigation, and OpenAPI contract
A dedicated /labs page hosts manual entry and a per-analyte view: the
four essentials (biomarker, value, unit, date) stay visible, the
reference range, panel grouping and note sit under an optional section.
Results group by analyte with a small inline trend once an analyte has
two or more readings. The reference-range badge is deliberately neutral
and calm — an out-of-range value reads as a quiet directional marker, not
an alarming red.

Adds the labs section toggle to the health-record export panel, the /labs
nav entries, the /api/labs OpenAPI routes, and the labs copy across all
six locales.
2026-06-14 23:39:52 +02:00
Marc-André Bombeck fbecb6b5c8 feat(labs): surface lab results in the doctor report and FHIR export
Add a Lab results section to the report aggregator (latest reading per
analyte over the window), a new labs section toggle (ON by default, the
same stance as BP and weight since the user recorded these to share with
a clinician), a PDF table with a quiet non-colour in/out-of-range marker,
and one laboratory Observation per result in the FHIR bundle carrying the
lab's reported referenceRange. Everything stays server-authoritative.
2026-06-14 23:39:29 +02:00
Marc-André Bombeck 271cc0dcfa feat(labs): add structured lab-result store with /api/labs CRUD
Record discrete biomarker readings (HbA1c, LDL, ferritin, TSH, …) with
their value, unit, reference range and sample date. The store pairs with
the annual blood-panel reminder, which until now had nowhere to record
the result it reminds the user to obtain.

The list/create/update/delete endpoints narrow userId from the session,
build the write data field-by-field, soft-delete via deletedAt, and
encrypt the optional note at rest. A neutral three-state reference-range
classifier (in-range / below / above / unknown) is computed
server-side so every surface renders the same calm verdict.

findLatestLabResultForAnalytes exposes the read point the blood-panel
reminder consults to mark itself satisfied, without coupling the two.
2026-06-14 23:39:16 +02:00
Marc-André Bombeck 4dcaee5ec5 merge(reminders): measurement/Vorsorge reminders with auto-resolve and neutral cards
# Conflicts:
#	docs/api/openapi.yaml
#	src/lib/openapi/routes/index.ts
2026-06-14 23:36:57 +02:00
Marc-André Bombeck 946a9f792a docs(api): document the preventive-care reminder routes
Register the /api/measurement-reminders surface in the OpenAPI table and
regenerate the committed spec so the iOS client consumes the
MeasurementReminderDTO contract from a single source.
2026-06-14 23:33:43 +02:00
Marc-André Bombeck c6045482c3 feat(reminders): preventive-care surface — wann, was, wo
Add the Vorsorge page and section: an upcoming-reminders list sorted by
server-computed next-due, with a create form (measurement-linked or
free-text, cadence, notify hour, location) and a per-card Erledigt and
delete action. Cards stay neutral-coloured regardless of due state — the
status reads through a discreet badge only, never an alarming tint. Copy
ships in all six locales.
2026-06-14 23:33:37 +02:00
Marc-André Bombeck 8058d81590 feat(notifications): MEASUREMENT_REMINDER event type and client-managed gate
Register the MEASUREMENT_REMINDER event type (default on at the channel
layer; the real gate is the per-reminder enabled flag) and add the
notificationPrefs.measurementReminder.clientManaged sub-object plus its
cron-side resolver, mirroring the medication and cycle client-managed
precedent. The event rides the generic APNs category path and stays
non-time-sensitive, so it delivers as a plain banner with no client
change.
2026-06-14 23:33:30 +02:00
Marc-André Bombeck 383963d3b4 feat(reminders): every-15-min dispatcher for preventive-care reminders
Add the measurement-reminder cron, mirroring the mood-reminder shape: a
pure due-predicate (past nextDueAt AND inside the reminder's local
notify-hour) and a tick runner that dispatches through the existing
notification cascade. Auto-resolve runs in the cron, not on the ingest
hot path: a matching reading of the reminder's type advances
lastSatisfiedAt and skips the nudge (BP on the SYS sentinel; free-text
reminders resolve only on a manual satisfy). Dedup is the nextDueAt
advance past now, so no ledger table is needed. The queue is registered in
the all-queues list, scheduled, and bound to its worker; a registration
guard test pins that wiring.
2026-06-14 23:33:22 +02:00