# Changelog ## [1.4.26] — 2026-05-15 Hotfix release. Adds a public, unauthenticated privacy-policy page at `/privacy` so the iOS native application can register a reachable URL in App Store Connect. The policy enumerates every Apple HealthKit identifier the iOS app reads, lists every active third-party sub- processor with its data-protection policy, restates the EU MDR medical-device boundary that scopes the AI Coach surface, and walks through the GDPR Art. 15-22 / DSGVO data-subject rights with concrete in-app routes the user can hit. The page bypasses the standard auth-shell so an App-Store reviewer or a first-time visitor sees the full document immediately. The conservative-semver pattern still applies: this could have been versioned `1.4.25.1` for symmetry with the iOS hotfix track, but 4-part versions break the strict-semver guard in `/api/version` so we incremented to the next clean patch instead. ### Added - Public privacy policy at `/privacy` with full HealthKit quantity-type enumeration (18 identifiers plus `sleepAnalysis`), Withings measurement-family list, sub-processor table (Anthropic, OpenAI, Withings, Apple, Telegram, GitHub, Cloudflare, Hetzner), Apple privacy-nutrition-label mapping, and a verbatim EU MDR 2017/745 + MDCG 2021-24 medical-device-boundary statement. - `auth.privacyPolicy` translation key in all six locales (English in EN, German in DE, native translations for FR / ES / IT / PL). ### Changed - The unauthenticated login page links out to `/privacy` below the sign-in card so a first-time visitor can review the policy before signing up, matching GDPR Art. 13 pre-signup expectations. - The auth shell now treats `/privacy` as a standalone public page — long-form legal content renders edge-to-edge instead of being squeezed into the centered login-card layout. ## [1.4.25] — 2026-05-14 Largest feature delta in the v1.4.x line. Insights expands from one page into seven dedicated metric routes; GLP-1 medication tracking lands end-to-end across injection picker, dashboard tile, weight-chart markers, therapy timeline, plateau detection, drug-level chart with a Research-Mode acknowledgment gate, EMA-sourced drug knowledge, EMA titration ladder, cadence visualisation, compliance chips, side-effect taxonomy, pen-and-vial inventory with a 30-day in-use clock, and a dedicated doctor-report section; cross-source priority becomes a two-axis resolver with a per-user Settings surface; per-user timezone threads through ten analytics and presentation surfaces; Withings coverage doubles with twelve new measurement types, webhook-driven BP / temperature ingestion, plus Activity (steps + distance + active-energy) and Sleep v2 (stage-level segments) syncs; the onboarding flow is rebuilt as a nested-route wizard with welcome carousel, goals chip-picker, source selection grid, baseline form, and a welcome-back resume banner; Personal Records ship end-to-end with a detection worker, push opt-in, and a metric-trend badge; Health Score gains a per-component provenance accordion; the Coach runs on native first-party prompts across all six locales with a 1800-assertion refusal-probe matrix; the OpenAPI drift gate flips to hard-fail; the GHCR image is multi-arch so Apple Silicon Macs and arm64 clouds pull native. Migrations 0043–0060 are additive and forward-only. `PROMPT_VERSION` 4.24.0 → 4.25.0 with GROUND RULE 9 (Coach refuses GLP-1 dose recommendations) and GROUND RULE 15 (Coach refuses drug-level estimates with MDR + MDCG 2021-24 cites). ### Added - **Insights sub-pages — seven dedicated metric routes with a shared tab strip.** `/insights/blutdruck`, `/insights/gewicht`, `/insights/puls`, `/insights/stimmung`, `/insights/medikamente`, `/insights/bmi` and `/insights/schlaf` each render the metric's full chart, range bands, trend annotations and correlation rows beneath the mother-page hero. Tab strip lifts the metric switcher above each chart for cross-metric scanning; the mother page slims to general status and the Coach hero. - **Sleep sub-page with per-night stacked-bar of sleep stages.** `/insights/schlaf` renders awake / REM / core / deep as stacked columns over the last 7 / 14 / 30 nights, sourced from the `sleepStage` column on `Measurement`. - **Targets (`/insights/zielwerte`) redesigned with a conditional Coach-handoff card.** New `` primitive replaces the v1.4.22 layout. Page-level consistency strip pins meta context above the cards. The Coach-handoff card only renders when a language-model provider is configured; rule-based mode hides it cleanly. Mobile-first three-column grid; per-card cog for visibility toggles. - **GLP-1 medication tracking — full integration across ten surfaces.** New `Medication.treatmentClass` enum (`GLP1`, `STANDARD`); `MedicationDoseChange` history table; `InjectionSite` enum with eight site values. A new body-map picker proposes the next rotation site based on the last fourteen days of injections. Medication-card grows a `glp1` variant (text-rich, no inline chart per directive — chart lives on the dashboard tile and `/insights/medikamente` therapy timeline). Dashboard tile shows next-injection schedule + week-over-week weight delta. Weight chart gains vertical injection markers. Therapy timeline on the insights sub-page plots titration alongside weight trace. Plateau- detection rule in `/api/insights/briefing` flags four-week stalls with referral framing. Doctor-report PDF carries a dedicated GLP-1 section. Migration 0046. - **Doctor-report per-section toggles + mood default-off.** New `User.doctorReportPrefsJson` column (Migration 0045) + `PUT /api/auth/me/doctor-report-prefs`. Each section (mood, achievements, GLP-1, etc.) gets a per-user toggle in Settings → Reports. Mood defaults to off (clinical-sensitivity); empty sections hide rather than render a "no data" stub. - **Cross-source priority resolution — two-axis architecture.** New `User.sourcePriorityJson` column (Migration 0048) stores a per-user resolver: a single-axis ladder by metric type plus an optional per-device-type override. `pickCanonicalSource()` walks the metric axis first; a per-device override (e.g. "BP from Withings BPM Connect, weight from Withings Body+") wins within that ladder. Defaults: cumulative + sleep + HRV + RHR favour Apple Health → Withings → manual; point measurements favour Withings → Apple Health → manual. The `__default__` sentinel is retired in favour of a null bucket. - **Settings → Sources screen for per-user priority configuration.** Drag-and-reorder list per metric, per-device override picker, reset-to-defaults action. Audit-log entry on every write. - **Per-user timezone — Option B threaded through ten surfaces.** New `User.timezone` column (defaults to `Europe/Berlin`). The CSV exporter emits ISO-8601 with offset; formatters honour the user's tz; Profile picker covers all IANA zones; admin sets the default for new accounts; signup detects the browser tz; the doctor-report PDF dates use the user's tz; chart x-axes take a `timezone` prop; Coach snapshot timestamps land in the user's tz; `MoodEntry.tz` records local-day grouping for weekday correlation (Migration 0044); the weight-weekday correlator buckets days in the user's tz. - **Health Score provenance accordion.** Each of the four scoring components (BP, weight, mood, compliance) gets an inline disclosure listing the canonical source (Withings / Apple Health / manual) and `asOf` timestamp behind the score number. `aria-labelledby` panel pairing for screen readers. - **Four additional locales — French, Spanish, Italian, Polish.** First-party translation bundles cover the full string surface with a `` flagging the locale as community-maintained and pointing at the translation-feedback issue template. Coach + insights system prompts ship as native per-locale bodies (no `REPLY LANGUAGE` footer indirection) with the full safety-contract matrix in YAML per locale + structural refusal-probe coverage in CI (see Tests). EN + DE stay Marc-maintained. Locale picker covers all six; signup browser-detect maps `fr|es|it|pl` to the corresponding bundle. - **Coach — native first-party system prompts across six locales.** Coach + insights system prompts move from EN body + `REPLY LANGUAGE` footer to native per-locale bodies; the safety contract matrix lives as YAML per locale (single source of truth across drafting + tests). The refusal-probe matrix in CI catches cross-locale prompt regressions before they reach the dispatch surface. - **VO2 max dashboard trend tile (opt-in).** Secondary-metric pattern — default-invisible, surfaces only when the user has VO2 max data from Apple Health. - **Personal Records end-to-end — schema, detection worker, badge, push opt-in.** Migration 0054 introduces the `PersonalRecord` table + `PersonalRecordDirection` helper; `GET /api/personal-records` is paginated (default twenty-five, max two hundred). A pg-boss worker sweeps MAX / MIN per metric and the workout slots on every batch-ingest + a thirty-minute fallback cron (concurrency five, warmup gate so the very first datapoint per metric does not promote itself). Metric trend tiles render a PR badge when the record landed in the last thirty days, with WCAG-AA contrast in dark mode. A per-user push opt-in toggle (default off) wires into the existing dispatcher cascade. - **Workouts — schema + typed batch-ingest endpoint.** Migration 0053 introduces the `Workout` + `WorkoutRoute` tables; the Zod boundary in `src/lib/validations/workout.ts` covers a twenty-member sport-type union and a `GeoJSON LineString` route column in JSONB. `POST /api/workouts/batch` accepts up to five hundred rows per call, wraps `withIdempotency()`, returns the same `inserted | duplicate | skipped` envelope as the measurements batch, and reconciles inserted vs duplicate counts correctly under contention. Rate-limited and size-capped. - **Onboarding rebuilt as a nested-route wizard.** The legacy v1.4.20 onboarding ships as a six-step wizard at `/onboarding/[step]` powered by a new `` primitive: welcome carousel with value-prop slides, goals chip-picker, source selection 4-card grid (with Apple Health marked coming-soon), baseline form, source-connect step, and a done step. `User.onboardingStep` (Migration 0057, with Migration 0060 backfilling and flipping the column to `NOT NULL DEFAULT 0`) drives resume-state; a welcome-back banner surfaces on the entry page when an in-progress wizard is detected. `POST /api/onboarding/step` advances the wizard with rate-limit + audit, returns 409 on a concurrent advance to close the read-then-write race. The marketing entry-point swaps from the v1.4.20 single-page flow to the new wizard. Five locales carry the shell key surface (six total). - **GLP-1 — EMA drug knowledge layer.** A first-party drug-knowledge module covers five GLP-1 / GIP-GLP-1 drugs with EPAR-sourced half-life, recommended titration step, brand-name table, and a brand-to-id lookup. A drift-guard test pins the values against the EPAR + Psp 4.13099 references on disk so the module cannot silently drift. - **GLP-1 — Research Mode with estimated drug-level chart, MDR acknowledgment dialog, and Settings toggle.** A pure one-compartment pharmacokinetic helper produces qualitative drug-level traces from the user's injection history (steady-state approximation, observational use only). The chart renders on the GLP-1 medication detail page behind a Research-Mode gate: `User.researchModeAck*` columns (Migration 0058) record the acknowledgment version + timestamp; a dialog explains the EU MDR 2017/745 + MDCG 2021-24 context and cites EMA EPAR plus Schneck / Urva 2024 before the user can opt in. `GET / POST / DELETE /api/auth/me/research-mode` carries the acknowledgment lifecycle. A Settings → Advanced toggle exposes the opt-in plus a re-prompt banner whenever the acknowledgment version bumps. - **GLP-1 — side-effect logging with a 21-entry × 5-category taxonomy.** Migration 0059 introduces the `MedicationSideEffect` table; pure helpers produce the taxonomy + a five-point Likert severity scale; a section on the GLP-1 detail page lets the user log entries with category + severity + free-text notes. Category is derived server-side from the entry (no client-supplied category) so the taxonomy cannot drift between client and server. - **GLP-1 — cadence visualisation + compliance chips.** Pure helpers expand a medication's schedule into expected slots, pair each slot with the closest actual intake, and produce a cadence timeline + compliance chip strip on the detail page. `GET /api/medications/[id]/cadence` returns the structured payload; the helpers honour per-user timezone so cross-midnight doses bucket correctly regardless of locale. - **GLP-1 — EMA titration ladder display.** Pure helpers walk the EMA-sourced titration schedule from the knowledge layer; the GLP-1 detail page renders the user's current step + remaining ladder as observational reference, framed as reference-not-advice and bound by GROUND RULE 15. `GET /api/medications/[id]/titration-ladder` returns the structured ladder + current step. - **GLP-1 — pen-and-vial inventory with a 30-day in-use clock.** Migration 0056 introduces the `MedicationInventoryItem` table + a pure state machine (SEALED → IN_USE → EXPIRED) with a 30-day in-use clock from `markAsFirstUseAt`. An inventory card on the medication detail page surfaces the active pen, days remaining, and SEALED stock; intake events decrement the dose count, and a daily 03:00 cron flips expired in-use items in a single `updateMany`. Re-runs the state machine on every PATCH so a back-dated first-use immediately moves a stale pen to EXPIRED. - **Apple Health identifier mapping (server-side).** Ports the identifier table from `k0rventen/apple-health-grafana` and `dogsheep/healthkit-to-sqlite` with MIT and Apache-2.0 attribution recorded in source headers and `NOTICE`. Covers the v1.4.25 ingest surface; iOS-18 long-tail mappings (sleep apnea, GAD-7, paddle/row sports, FHIR clinical) carry inline release-window comments. - **Audio-exposure and time-in-daylight measurement types.** Migration 0052 adds `ENVIRONMENT_AUDIO_EXPOSURE`, `HEADPHONE_AUDIO_EXPOSURE` and `TIME_IN_DAYLIGHT` to the `MeasurementType` enum. Doctor report mentions them when populated; no dedicated chart yet. - **Withings expanded coverage — twelve new measurement types and the BP / temperature webhook subscription.** Migration 0049 adds HRV, body temperature, SpO2 v2, VO2 max, fat-free mass, fat mass, muscle mass, skin temperature, pulse-wave velocity, vascular age and visceral fat as `MeasurementType` values. Withings webhook coverage extended to BP and temperature so the latency goes from ~1 h polling to seconds. OAuth scope upgraded to include `user.activity` and an in-app banner prompts existing users to reconnect once. - **Withings — Activity sync (steps + distance + active-energy).** Calls `getactivity`, ingests one row per day, anchors at noon UTC so positive-offset users bucket cleanly into their local day. Backed by a pg-boss `activity-sync` queue plus a webhook enqueue hook on the new subscription channel. - **Withings — Sleep v2 sync (stage-level segments).** Calls `sleepv2_get` and writes per-night stage segments through the new `sleepStage` composite (Migration 0055 widens the Measurement unique index to include `sleep_stage` with `NULLS NOT DISTINCT`). Backed by a pg-boss `sleep-v2-sync` queue plus webhook enqueue. The sleep sub-page's stacked-bar chart renders the segments directly. - **Withings — webhook subscriptions expanded to activity + sleep v2.** Subscription registration plus webhook delivery routing for the two new event types. - **Admin Login overview — location, provider and CSV polish.** Adds a location column derived from the audit IP; adds a provider column distinguishing passkey, password, API-token and OAuth login paths; the CSV export drops two unused columns and the per-row collapse affordance comes off. - **DELETE `/api/measurements/by-external-ids` for iOS deletion sync.** Idempotent batch delete keyed on `(user, source, externalId)` tuples; rate-limited the same way as the batch ingest endpoint. - **Multi-arch Docker image.** GHCR publish workflow builds `linux/amd64` on `ubuntu-latest` plus `linux/arm64` on `ubuntu-24.04-arm` and merges the manifest. Apple Silicon Macs and arm64 clouds now pull native; the previously-stale README claim is accurate again. - **GLP-1 endpoint hardening.** `POST /api/medications/[id]/glp1` now parses through bounded Zod schemas (`glp1DoseChangePostSchema`, `glp1InventoryPostSchema`) with length-capped notes, finite-number guards on dose value, and bounded `effectiveFrom`. Every write produces an audit-log row and the route inherits the 30-per-minute-per-user rate limit from the rest of the medication surface. - **Translation-feedback issue template.** GitHub issue template for community translation corrections, paired with the maintainership banner copy on FR / ES / IT / PL surfaces. - **Repository polish.** README hero rewrite (what / who / try the demo, thirty-second read). Topics expanded 10 → 18. Branch protection v2 with conversation-resolution. GitHub Discussions enabled. Issue template + PR template carry-over from v1.4.20 preserved. ### Changed - **Insights mother page slimmed; metric depth moved to sub-pages.** `/insights` becomes a navigation hub with hero strip + Daily Briefing + correlations + trends row; per-metric depth lives one click away on the dedicated sub-pages. - **Targets page-shell unified with the rest of Insights.** Single consistency strip + Coach handoff + per-card actions rather than the v1.4.22 sparkline-everywhere layout. - **Coach default-window preference plus chip-order rationalised.** The window picker now persists per-user (last seven / thirty / ninety days); suggested-prompt chips reorder so health-focus chips lead and meta-discovery chips trail. Composer auto-grows on multi-line input. Distinct error UX for daily-limit hit vs provider rate-limit. Microphone affordance retired (was a placeholder). - **Dashboard global comparison-overlay default removed.** Per-chart preference from v1.4.22 is the only knob now; the dashboard-level default is gone. - **`medication_schedules.*` columns mapped to snake_case via Prisma `@map`.** Migration 0047 — cosmetic rename only, convention parity with the rest of the schema. Resolves the v1.4.24 demo-deploy schema-drift finding. - **Top-page padding parity across `AuthShell`, Settings and Admin shells.** Cross-page rhythm matches; the previous one-off paddings on Admin and Settings asymmetric to Insights are gone. - **Settings icon + heading convention uniform across all twenty- three sections.** Every section header pairs an icon with a heading; the v1.4.24-era split (icon-only on some, heading-only on others) is gone. - **`PROMPT_VERSION` 4.24.0 → 4.25.0** carries two new safety ground rules across all six locales: GROUND RULE 9 forbids GLP-1 dose recommendations (Coach falls back to a clinical-referral framing on any dose question); GROUND RULE 15 forbids drug-level estimates and cites EU MDR 2017/745 + MDCG 2021-24 in the refusal copy. Coach + insights system prompts ship as native bodies per locale rather than the legacy `REPLY LANGUAGE` footer. - **Dashboard top-tile polish.** Tile headings collapse to a single line per locale (BP / BF abbreviations land in EN + DE + the four Romance locales); the trend arrow moves inline next to the headline value; the value row baseline-aligns across tiles regardless of font fallback. Regression guard pins the baseline contract. - **OpenAPI drift gate flips to hard-fail.** `pnpm openapi:check` CI step now red-bars a PR on any drift between the Zod registry and `docs/api/openapi.yaml`. The v1.4.23 warn-only window closes; iOS DTO codegen can rely on the spec being authoritative. - **Coolify auto-deploy gate exposed as an explicit maintainer-toggleable repo variable.** `vars.COOLIFY_AUTO_DEPLOY` is now a `true | false` switch in the deploy workflow instead of the implicit secret-presence gate that silently no-op'd in v1.4.21-23. - **Settings → Sources sentinel cleanup.** The `__default__` device-type bucket sentinel is retired in favour of a null bucket at the storage layer; the Settings UI reads cleaner and the reorder helpers (`reorderLadder`) collapse the two `moveSource` + `moveDeviceType` paths into one. - **Section wrapper for medication detail.** A new `` wraps the three medication-detail shells (titration, scheduling, side-effects) plus the inventory disclosure top so cross-section chrome stays consistent and future sections drop in without copy-paste. - **Shared route ownership helper.** Nine medication routes (`titration`, `side-effects`, `inventory`, `cadence`, `intake`, `compliance`, `phase-config`, `api-endpoint`, plus the GLP-1 convenience route) now call `assertMedicationOwnership` from `src/lib/medications/route-guards.ts` rather than open-coding the lookup; rate-limit headers across the same surface align on the option-bag form `apiError(..., { headers: rateLimitHeaders(rl) })`. ### Fixed - **Settings save regression on Zod v4 record semantics.** `z.record(z.string(), …)` switched to `z.partialRecord(…)` on the dashboard-prefs schema so optional keys round-trip cleanly through the PUT. The save-then-blank-load bug from v1.4.24 is gone. - **Comparison-shift baseline regression.** Comparison overlay was subtracting the wrong-period baseline on several charts; baseline lookups now match the caption window. Three regression guards. - **Raw metric-token leaks in generated prose** — `metric:` identifiers surfaced in three surfaces that escaped the v1.4.22 sweep (`` fallback path, briefing `keyFinding` helper, and Coach in-flight bubble). `stripChartTokens()` regex widened to cover lowercase prose remnants; prompt-side GROUND RULES 8 and 13 reaffirm the constraint. PROMPT_VERSION 4.23.0 → 4.24.0 carried the prompt change; 4.25.0 inherits. - **Withings BP and temperature webhook latency.** Webhook subscription now covers BP and temperature endpoints, so the earliest a measurement reaches the user drops from ~1 h to seconds. - **Dev-server crash on Tailwind v4 `color-mix` parser + Next 16 api-handler private-field.** Two unrelated dev-time crashes surfaced together: Tailwind v4 choked on a `color-mix()` invocation in a chart token; Next 16 + Webpack fallback hit a private-field access on a force-static route handler. `color-mix` invocation replaced; api-handler guards the field access. Production unaffected — these surfaced only under the dev compile path. - **Insights duplicate `StatusCard`.** The mother page rendered two copies of the BP status card at certain viewports due to a hero-strip ↔ correlation-row overlap. Card hoisted to the single canonical location. - **Coach-feedback admin header layout shift.** Sticky-header contract mismatch — the section's `
` carried a different top padding than its siblings, so navigating into the section visibly shifted the chrome. Padding parity restored across all admin sections. - **Notification-status-card heading icon parity.** Was the one section without an icon next to its heading; aligns with the cross-section convention now. - **WCAG 2.5.5 touch-target floor across the top bar, section strips and the injection-site picker.** Several pill chips and the injection-picker dots sat below the 44-px floor; all hit the floor now without changing the visual density at typical rendering sizes. - **Sleep-stage chart strokes resolve through Dracula tokens.** The chart was using the legacy `hsl(var(--border))` form which Tailwind v4 rejects; switched to the resolved token. - **"Per night" hardcoded German in four locales.** The sleep-page subtitle suffix was hardcoded `pro Nacht` and rendered as raw German across FR / ES / IT / PL. Translated in all six locales; an i18n drift-guard covers the contract. - **Cross-page top-padding asymmetry.** AuthShell, Settings, Admin all carry the same top padding now (see Changed). - **`berlinIsoWeekday()` timezone hard-code.** The weekday helper hardcoded Europe/Berlin; now takes a `timezone` argument and threads through the weight-weekday correlator. - **Batch-ingest race reconciliation under contention.** Two concurrent batches with overlapping `externalId` sets returned inconsistent `inserted` / `duplicate` counts because the reconciliation pass was logically inverted (no-op when the catch fired). Fixed; replay regression test seeds the race. - **`requireAuth()` blocks narrow-scope iOS Bearer tokens on unscoped routes.** v1.4.24 closed the inverse hole (Bearer with no scope reaching unscoped handlers); this release closes the over-broad version — a token scoped exclusively to `medication:ingest` could not reach handlers that don't declare a `requiredPermission`. Now declared-scope tokens are allowed through provided the route's declared scope (or wildcard) is in the token's set. - **`createMeasurementSchema` dropped `deviceType` on single-entry POST.** The batch route accepted `deviceType` per row; the single-entry POST stripped it on the Zod boundary. Mirror parity restored. - **Personal-records endpoint unpaginated.** `GET /api/personal-records` now clamps `?limit` (default twenty-five, maximum two hundred); the unbounded read is gone. - **Withings activity sync bucketed positive-offset users into the wrong day.** The per-day row was anchored at 23:59:59 UTC, so a user in `Pacific/Auckland` saw activity rows attached to the following local day. Anchored at noon UTC (`T12:00:00.000Z`) with a regression suite covering Berlin / Los Angeles / Tokyo / Auckland. - **Cadence and compliance helpers ignored per-user timezone.** `expandScheduleSlots`, `pairDoses`, `buildCadenceTimeline` and the compliance-chip helper now accept a `timeZone` argument that threads through `Intl.DateTimeFormat`, so cross-midnight doses bucket correctly regardless of user locale. The cadence route resolves through `resolveUserTimeZone(user)`. - **Inventory state-machine ignored back-dated first-use.** Issuing a PATCH that pushed `markAsFirstUseAt` more than thirty days into the past left the item in IN_USE; the state machine now re-runs on every PATCH and the stale item flips to EXPIRED. Regression test pins the contract. - **Inventory expire-stale cron looped per row.** Replaced the row-by-row `prisma.update` loop in `expireStaleInUseItems` with a single `updateMany`; bulk-update contract pinned by test. - **Onboarding step write was read-then-update.** A concurrent advance could double-step the wizard. The update now conditions on `{ id, onboardingStep: current, onboardingCompletedAt: null }` and returns 409 on conflict. - **Workout schema accepted `endedAt <= startedAt`.** `createWorkoutSchema` gained a `.superRefine` rejecting zero-duration and inverted windows; the PR detection worker's MIN-direction slot also guards against `durationSec === 0` so a malformed row cannot promote itself to a personal record. - **Personal Record detection duplicate writes under contention.** Same-millisecond writes into a null workout slot now reconcile to a single row; regression test seeds the race. - **Source-priority parse failures were silent.** Storage parse errors now route through an observer breadcrumb and the resolved blob is frozen at construction so downstream mutation is caught immediately. - **Health-Score `asOf` derived non-deterministically.** The `asOf` timestamp now derives from the input dates instead of the wall clock so repeated computes against the same dataset produce stable timestamps. - **Source-priority audit-log entries missed two write paths.** Doctor-report-prefs and source-priority PUTs were not landing audit-log rows; both write paths emit now. - **Personal-record badge dark-mode contrast.** Lifted to WCAG-AA contrast on the `` surface. - **Range-bar zone backgrounds.** `` mixed Tailwind raw palette tokens with Dracula tokens; all zone backgrounds now resolve through Dracula tokens with the pinned test rewritten. - **Research-Mode acknowledgment dialog footer scrolled below fold.** The footer pins outside the scrolling region on small viewports. - **44-px touch-target floor across the onboarding wizard.** Shell back/skip/next, carousel pager, source-card grid, baseline and goals-chip CTAs all hit the WCAG 2.5.5 floor without changing visual density at typical rendering sizes. - **Workout endpoint reconciled inserted-vs-duplicate counts incorrectly under contention.** Race-recovery path produced inconsistent envelope counts; corrected with a regression test seeding the contention window. - **Legacy Withings `?secret=` webhook form had no counter.** An in-memory `withings.webhook.legacy_form_total` counter surfaces through `ops-stats`; the warning text gained the re-subscription URL for affected accounts. - **Acknowledgment dialog i18n back-button.** Medication history page back-button hard-coded `Zurück`; routed through `t()`. - **Sleep + audio-exposure + comparison-hint i18n keys.** Backfill pass picked up the keys missed by the v1.4.22 sweep across all six locales. - **Build resolution for safety-contract YAML.** The loader now resolves the YAML matrix path from `cwd` rather than `__dirname` so the build does not break when the bundler reshapes the module-relative root. - **GLP-1 drift guard self-skips when the EMA research file is absent.** Local checkouts without the reference file no longer red-bar the test. - **`x-axis` chart ticks resolved in user timezone.** `xAxisTicks` now renders the tick formatter in `user.displayTimezone` instead of hard-coding Europe/Berlin. - **Chart token references switched to `var(--token)` form.** Recharts strokes were still using `hsl(var(--token))`; switched to bare `var(--token)` so Tailwind v4's color-mix parser resolves them. - **`detectGlp1Plateau` branches lacked direct coverage.** Added Prisma-mocked unit tests covering the previously-uncovered branches. ### Security - **Withings webhook secret no longer leaks into structured logs.** `WITHINGS_WEBHOOK_SECRET` was landing as a URL path-segment in `http.path` on every Wide Event the request produced. The logging stack now carries a parameterised `PATH_SECRET_PATHS` registry seeded with the `/api/withings/webhook/[token]` shape, and `WideEventBuilder.setHttp` rewrites path + route segments to `[REDACTED]` before they reach stdout / the in-memory ring buffer / Loki. Existing query-string redaction is unaffected. - **Coach refuses GLP-1 dose recommendations.** GROUND RULE 9 across all six locales — the Coach surfaces a clinical-referral message on any dose adjustment ask, never a number. PROMPT_VERSION 4.25.0 carries the rule. - **Coach refuses drug-level estimates with regulatory cites.** GROUND RULE 15 across all six locales — the Coach refuses any ask for an estimated drug level or pharmacokinetic prediction, cites EU MDR 2017/745 + MDCG 2021-24 in the refusal copy, and routes the user to the Research Mode disclosure flow. Adversarial refusal-probe matrix exercises 20+ paraphrasings per GROUND RULE across six locales in CI on every push, so prompt-injection regressions surface before reaching the dispatch surface. - **Research Mode is gated by an MDR acknowledgment dialog.** The estimated drug-level chart on the GLP-1 detail page renders only after the user opts in through a dialog that cites EU MDR 2017/745 + MDCG 2021-24, EMA EPAR and the Schneck / Urva 2024 pharmacokinetic reference. The acknowledgment version re-prompts the user on bump. - **GLP-1 convenience endpoint hardened.** `POST /api/medications/[id]/glp1` parses through bounded Zod schemas (`glp1DoseChangePostSchema`, `glp1InventoryPostSchema`), enforces length caps on notes, finite-number guards on dose value, and bounded `effectiveFrom`; every write produces an audit-log row and the route inherits the 30-per-minute-per-user rate limit from the rest of the medication surface. - **GLP-1 medication strings sanitised before LLM prompt interpolation.** A malicious or malformed medication name no longer reaches the prompt body verbatim. Sanitiser passes Latin-letter + digit + common punctuation only. - **Batch-ingest rate limit (sixty per minute per user default).** `POST /api/measurements/batch`, `POST /api/workouts/batch` and `DELETE /api/measurements/by-external-ids` carry a token-bucket rate limit returning the standard 429 envelope. - **Audit-log writes on source-priority and doctor-report-prefs PUTs.** Every change to either preference produces an `AuditLog` row for the user's audit-log surface and the admin cross-section view. - **Withings OAuth `user.activity` scope upgrade with explicit user reconnect.** New scope only takes effect after the user re-authorises; the reconnect banner makes the requirement visible. - **Source-priority storage is per-user.** No cross-user lookup paths; one user's priority blob never enters another user's resolver. ### Refactor / Hygiene - **`pickCanonicalSource` becomes a two-axis lookup with single- axis fallback.** Reuses `getDeviceTypeLadder()` so the per-device override can be queried from the canonical-row picker without duplicating ladder logic. - **Health-Score `COMPONENT_ORDER` hoisted + `Intl.DateTimeFormat` memoised.** Provenance accordion calls the formatter once per render rather than four times. - **`SubPageSlug` type + array derived from `SUB_PAGE_METRIC` record.** Single source of truth for the seven sub-page slugs. - **`source-priority` `__default__` sentinel → null bucket.** Settings → Sources stops carrying a fake `__default__` device- type entry; the null bucket reads cleaner at the storage layer. - **`apple-health-mapping` sleep branch collapsed.** Three sleep- stage cases shared the same body; collapsed to one. - **`HK_QUANTITY_TYPE_TO_MEASUREMENT` removed.** The legacy view was a redundant projection of `APPLE_HEALTH_TYPE_MAP`; all callers go through the canonical map now. - **Dead-code cleanup pass.** Drops `` (zero callers since v1.4.20), ``, `` wrapper and three orphan `insightsGeneralStatus` query keys from the v1.4.16-era hero. Removes the orphan `/api/insights/general-status` route (superseded by `` since v1.4.16). - **Coach `` controlled-prop refactor is fully in place** — the v1.4.24 follow-up of `useResettableValue` is now the only call path; the legacy remount hack code is gone. - **380 dead i18n keys dropped.** Runtime probe across the six locales identified 380 keys with zero call sites and zero runtime resolutions; all six locale bundles are smaller by that count. Top namespaces: `settings`, `admin`, `classifications`, `medications`. The remaining ~148 keys are queued for a second pass in v1.4.26. - **Dead system-prompt constants removed.** `BASE_SYSTEM_PROMPT` and `INSIGHTS_SYSTEM_PROMPT` constants had no remaining importers after the native per-locale prompt move; both deleted. - **`useInsightStatus` hook extracted.** The four insights sub-page status queries collapsed into a single hook so the duplicated `useQuery` + Suspense fallback shape lives in one place. - **`` wrapper.** Three medication-detail shells (titration, scheduling, side-effects) and the inventory disclosure top share a single wrapper; future medication detail sections drop in without copy-paste. - **Shared helpers across the medication + onboarding surfaces.** New modules `src/lib/api/read-error.ts`, `src/lib/medications/route-guards.ts`, `src/lib/medications/research-mode-types.ts` and `src/lib/medications/dose-string.ts` consolidate previously duplicated patterns. Three near-identical dose-string parsers collapse to one `parseDoseMg`; four-times-duplicated `readError` collapses to one; nine medication routes call the shared `assertMedicationOwnership`. - **Side-effect taxonomy drift-guard.** `SIDE_EFFECT_CATEGORY_VALUES` and `SIDE_EFFECT_ENTRY_VALUES` now derive from the Prisma enum via `z.nativeEnum`; a drift-guard test pins Prisma enum ↔ taxonomy map ↔ validator triangle so the three cannot drift. - **Side-effect category derived server-side.** The `createSideEffectSchema` no longer accepts a client-supplied `category`; the server derives it from the entry. Removes the defensive 422 path and the cross-tier drift surface. - **Dashboard top-tile baseline alignment.** Inline trend arrow + baseline-aligned value row across all dashboard tiles, with the baseline contract pinned by regression test. - **Range-bar Dracula token swap.** `` zone backgrounds resolve through Dracula tokens with the pinned test rewritten; raw palette references gone. - **44-px touch-target floor across the top-bar + section strips.** WCAG 2.5.5 alignment across the broader navigation surface, complementing the onboarding-specific pass. - **Cat-C typo + naming polish from the W10 review pass.** Minor identifier renames flagged by the dead-code probe; no functional change. - **`safeRequestProp` widened catch.** Narrowed catch broadened to tolerate `undefined` requests after the hygiene-review pass surfaced the regression. - **`pickCanonicalWorkout` helper for cross-source workout dedup.** Source-priority logic centralised for the v1.5 iOS workout-ingest path. - **Insights regenerate button relocated; tab strip lifted.** Tab-strip extraction + relocation of the regenerate button to the top-right, paving the way for the seven sub-page surfaces. ### Tests - 2244 → 3828 passing unit tests across 344 files (+1584; one pre-existing skip carries through). Integration suite 140 → ~170 across 11 files. e2e green on the W2 CI fix (coach-prefs URL mock + Pixel-5 selector hardening) plus the Fix-I hot-fix that re-anchored the dashboard insight-card and the mobile x-axis tick locators. - **Coach refusal-probe matrix.** 1800+ assertions exercise 15 GROUND RULES across six locales with 20+ adversarial paraphrasings each. CI runs the matrix on every push; any prompt-injection or jailbreak regression surfaces before reaching the dispatch surface. Drift-guard tests pin the YAML-per-locale safety-contract matrix in lockstep with the validator-derived enums. - **New unit suites (selection):** GLP-1 plateau-detection text formatter; two-axis source-priority resolution (single-axis fallback, per-device override, mixed-bucket selection); source-priority Settings reducer (reorder, reset, validate); medication-card GLP-1 variant + injection-site-picker (RTL); doctor-report prefs PUT contract; personal-records pagination clamp; `berlinIsoWeekday` timezone-aware suite; sleep-stage chart i18n-suffix drift-guard; Health-Score provenance i18n drift-guard; api-handler private-field crash regression; pure cadence + compliance helpers with TZ assertions across UTC+0, UTC+9, UTC-8; side-effect taxonomy drift-guard; EMA titration ladder helpers; pen-inventory state-machine + 30-day clock helpers; PR detection worker (MAX / MIN per metric + workout slots + warmup gate + null-slot dup regression + zero-duration guard); Withings activity TZ regression across Berlin / Los Angeles / Tokyo / Auckland; Withings sleep v2 segment ingest; log-redaction path-segment rule + `setHttp` rewrite; GLP-1 endpoint Zod + audit + rate-limit; onboarding step concurrent- advance regression; inventory PATCH state-machine back-dated first-use regression; bulk-update contract for the expire-stale cron; brand-name guard + sentinel-preservation + safety-contract parity for the YAML matrix; locale auto-discovery + fallback- chain runtime guard. - **New integration suites:** two-axis canonical-source resolution end-to-end; `requireAuth()` narrow-scope on unscoped route; batch-ingest race reconciliation under contention; workout-batch race + size-cap + ingest end-to-end; PR-detection end-to-end; cross-source priority for Withings Activity + Apple Health; Withings sleep-stage composite ingest; per-user timezone end-to- end (Pacific/Auckland). - **Migrations:** 9 new (0051–0059) + 1 hardening (0060 `onboarding_step_not_null` backfill + NOT NULL flip). All additive and forward-only; migration 0057 comment rewritten to match PG11+ fast-path ADD COLUMN DEFAULT semantics. ### Deferred to v1.4.26 See `.planning/v1426-backlog.md` for the full backlog with effort estimates and source citations. Headline items: - `User.onboardingGoals` column + server-side persistence for the goals chip-picker selection (today the picker holds the choice client-side only; the dashboard-widgets seed feature reads it in v1.4.26). - `advance()` hook extraction for the three onboarding step components — pattern surfaced in the simplifier review, deferred to avoid collision with v1.4.25 touch-target work. - `glp1-pk.ts` unused-export decision (internalise vs wire dashboard chip). - Seven orphan endpoint go / no-go decisions (`/api/admin/ai-settings`, `/api/admin/backup/test`, `/api/admin/status-overview`, `/api/monitoring/{glitchtip,umami}/test`). - ~148 dead i18n keys remaining after the W10 runtime-probe sweep (the v1.4.25 pass removed 380 of the 528 candidates; the remaining ~148 need second-pass call-site verification). - FR / ES / IT / PL prose hand-review by a native speaker for the Coach + insights surface (the structural refusal-probe matrix covers safety; the user-facing prose still benefits from a human pass). - Coach `lastYear` baseline + row-tap-to-prefill polish. - Sleep sub-page stacked-column visual polish (stage colours + legend density). - Mood verbal-labels persistence behind a per-user toggle. - Drug-level chart-side 90-day staleness clock wiring (the acknowledgment dialog already re-prompts on version bump and Coach refuses at GROUND RULE 15; the clock is defence-in-depth). - iOS-18 long-tail HK identifier mappings (sleep apnea, GAD-7 / PHQ-9, running form, paddle / row / ski distances, pregnancy / cycle, FHIR clinical). - VO2 max chart-row card on `/insights/` (dashboard tile shipped; chart card queued alongside the iOS body-composition page). - Lazy-loaded locale JSON bundles (all six locales import synchronously at present, ~675 KB to every client). ### Deferred to v1.5 - **iOS Swift app — P1 through P5.** Login + dashboard + widget (P1); Apple Health sync (P2); Coach extended for HRV / sleep / resting HR / steps (P3); per-metric APNs alerts (P4); workouts + GeoJSON routes (P5). All server contracts locked in v1.4.25. - **Workout ingest API matching the v1.5 iOS contract.** Schema shipped this release; endpoint signature finalised during the iOS sprint. - **Two-Brain Coach refactor.** Statistical findings produced by a deterministic pipeline; LLM owns the narrative layer only. Reduces hallucination surface; unblocks evidence-grounded citations. - **HRV anomaly detection against a rolling baseline.** - **Mindfulness, dietary-water and symptoms-unification ingest types.** - **ECG waveform ingest + FHIR / HKClinicalRecord.** - **Pearson incomplete-beta replacement** for the rigorous surfacing-gate (v1.4.23 carryover). ## [1.4.24] — 2026-05-12 Security + accessibility hardening release. Closes the highest-priority findings from a focused security audit of the v1.4.23 release: a Bearer token that omits a scope no longer authenticates routes which never declared one, the public Umami proxy is rate-limited, the proxy nonce is Edge-runtime portable, a session-expiry delete race can no longer 500 a parallel request, the import-validation envelope now matches the standard error contract, and the runtime Docker image installs Prisma 7.8 alongside pinned worker dependencies. Accessibility passes on the notifications surface, admin shell, chart overlays and notification settings deep-link anchors round out the user-facing work. ### Security - **Bearer auth is fail-closed when a route declares no scope.** `requireAuth()` previously let any non-revoked, non-expired API token through whenever a handler omitted the `requiredPermission` argument. A narrowly-scoped token (e.g. `["medication:ingest"]`) could therefore reach every handler that called `requireAuth()` bare — including account-sensitive routes such as `/api/auth/profile`, `/api/auth/password`, `/api/withings/credentials`, `/api/settings/data`, `/api/export/full-backup` and `/api/tokens`. The Bearer path now throws `HttpError(403)` with audit reason `scope_required` unless the token carries the `["*"]` wildcard (the iOS app login token), preserving the existing session-cookie path and the explicit-scope path (`/api/ingest/medications`). The admin surface is unaffected — `requireAdmin()` was already cookie-only. - **Public Umami analytics proxy now rate-limits by client IP.** `POST /api/send` forwards browser analytics events to the configured Umami origin. The SSRF allow-list and 64 KB body cap were already in place, but the endpoint itself was unbounded. Added a 120-events-per- minute-per-IP gate using the existing `checkRateLimit` infrastructure; abuse returns the standard `429` envelope without invoking the upstream fetch. - **Edge-runtime portable nonce generation in the proxy.** The proxy previously assembled the CSP nonce via `Buffer.from(...)`, which is not guaranteed in the Edge runtime. Switched to `btoa(String.fromCharCode(...new Uint8Array(16)))`. Same 128 bits of entropy, Node-and-Edge compatible without a runtime declaration. - **Session-expiry delete race no longer 500s a parallel request.** `getSession()` reaped expired session rows with `prisma.session .delete()`; two requests arriving at the same expired session would race and the loser would surface a 500. Replaced with `deleteMany` + a catch-all guard, matching the discipline already used in `destroySession()`. ### Fixed - **`POST /api/import` returns the standard `{ data: null, error }` envelope on validation failure.** The handler previously returned the validation message inside `apiSuccess(...)` with status 422, which clients could not distinguish from a successful import. - **Runtime Docker image pins Prisma 7.8 + worker dependencies.** The multi-stage image was installing `prisma@7.4.0`, `@prisma/engines @7.4.0`, unpinned `pg-boss@12`, `@prisma/adapter-pg@7` and `pg@8` into the worker prefix while the app code shipped Prisma 7.8 from the lockfile. Pinned to `prisma@7.8`, `@prisma/engines@7.8`, `pg-boss@12.18`, `@prisma/adapter-pg@7.8` and `pg@8.20` so migration engine and generated client cannot drift across a deploy. - **Notifications page surfaces a load error instead of a silent "no channels" empty state.** `useQuery` failures now render an alert with `role="alert"` and `notifications.loadError` copy (EN/DE), so a transient 500 on `/api/notifications/preferences` stops looking like a misconfiguration. ### Accessibility - **Notification-preference switches carry accessible names.** Each switch in the per-event table now exposes an `aria-label` combining event-type label and channel label; the mobile per-event layout pairs each `` with its `