# Changelog ## [1.4.33] — 2026-05-17 — Polish and reliability Quality-leap release between two HealthKit milestones. The headline is a P0 hotfix for the `/api/analytics` 500 that broke the Insights mother page for any account with more than a few thousand measurements: the six per-metric status helpers were spreading the entire numeric history into `Math.min` / `Math.max`, which trips a stack overflow once a single argument list exceeds the V8 spread cap. Both the surfaced symptom and the latent risk in five sibling helpers were folded onto a reduce-based min/max path. Around that hotfix landed a deep polish pass: the Insights mother page now defers three below-the-fold blocks behind `next/dynamic` to trim the cold mount cost, the analytics endpoint gained a `?slice=summaries` slim slice that the per-metric sub-pages now ride, the Coach snapshot builder picked up a 60 s LRU keyed on `(userId, scope)`, the Settings shell consolidated three duplicate copies of the about / notifications / about-dropdown surfaces, the navigation strip dropped a redundant "Home" group label and a colliding "Notifications" sibling, every icon-only button on the medications and admin surfaces gained an accessible name, every Progress bar gained one too, three pages had their heading hierarchy repaired to a sequential `h1 → h2 → h3`, the mobile Coach FAB now hides while a chart tooltip is open so it cannot occlude the read-out, and the auth shell normalised every authenticated route to a single `max-w-screen-xl` container. The release closes nineteen issues from a five-surface audit + a runtime bug-hunt pass, plus six follow-up tracks of polish across the affected surfaces. ### Added - **Slim `/api/analytics?slice=summaries` slice.** Per-metric sub-pages opt into a payload that drops `correlations` + `healthScore` + `medications` blocks, cutting the cold-mount response on those routes by roughly half. The mother page stays on the default thick slice so the correlation row + health score badge still resolve from the same cache key. - **`useScrollResetOnRoute()` hook.** Single source of truth for the route-change scroll-to-top behaviour, replacing per-page `useEffect` duplicates across seven mounted surfaces. - **`` primitive.** Extracted from three near-duplicate copies inside the Settings shell so future section additions inherit the icon / title / description treatment without redeclaring the markup. - **Apple App Site Association handler.** `/.well-known/apple-app-site-association` now answers 200 with `application/json` on every host that fronts the app, advertising the iOS bundle's App ID prefix (`S8WDX4W5KX.dev.healthlog.app`) under `webcredentials.apps` so the passkey ceremony shares cleanly between the web origin and the iOS app. The proxy gained a `/.well-known/` public-prefix entry so the Apple CDN fetch lands on the asset instead of the auth gate. ### Changed - **Insights mother page deferred below-the-fold blocks.** Daily briefing, correlation row, and trends row now resolve through `next/dynamic` with skeleton fallbacks that match the existing loading shape. Above-the-fold hero stays an eager import so the initial paint shows the greeting + health-score badge without a flash. - **Settings section renamed.** "Notifications" became "Notification channels" (German: "Benachrichtigungs-Kanäle") so it no longer collides with the inbox at `/notifications` ("Notification Center" / "Benachrichtigungs-Center"). - **Settings Auswertungen section renamed.** The German label "KI-Auswertungen" became "Auswertungen" in user-facing copy per the long-standing rule that user-facing surfaces drop the model-vendor prefix. - **Insights tab strip regrouped by metric category.** Pills now cluster vitals / cardiovascular / activity / wellbeing so long-strip scrolling lands on a related neighbour. - **About section folded into the user-card dropdown.** The in-shell Settings nav no longer surfaces an "About" entry; the sidebar user-card dropdown now owns the "About HealthLog" link. Route `/settings/about` still resolves for direct links. - **Auth shell container width normalised.** Every authenticated route now renders inside a single `max-w-screen-xl` container, retiring three competing widths that drifted across the Insights / Settings / Dashboard surfaces. - **Card defaults normalised.** Every card now defaults to `p-4 md:p-6` padding so mobile + desktop spacing stay in lock step without per-instance overrides. ### Fixed - **`/api/analytics` 500 (P0).** Stack overflow from `Math.min(...values)` / `Math.max(...values)` spreads on long histories. Hotfixed on the surfaced helper and folded across the six sibling status helpers. - **Spotlight onboarding tour intercepted dashboard clicks.** Overlay z-index + pointer-events combination meant the first post-completion tap landed on the dimmed overlay instead of the tile underneath. - **BP chart Y-axis unit.** Read "Hg" instead of "mmHg". - **Bottom nav viewport overlap.** Hardened against the last-line clip on shorter viewports. - **`/insights/puls` subtitle.** Read "Ruhepuls" instead of "Puls". - **Weight chart duplicate Y-axis ticks.** Tick generator emitted the same value twice on narrow domains. - **Web-vitals beacon self-throttling.** Beacon was sampling at full rate and tripping its own queue cap; sample rate dropped to 10% so RUM telemetry actually reports. - **Medication compliance classifier flushing every dose to `very_late`.** Defensive fallback when the dose-window estimator can't resolve a window. - **`/api/insights/generate` POST gating.** Endpoint now refuses on a disabled assistant master flag instead of returning a 500. - **Mood Log overflow on narrow viewports.** Cards no longer shred their internal grid below 360 CSS px. - **F13 username readability.** Header username now respects contrast tokens on the dark theme. - **F14 mobile bottom-nav padding.** Bottom nav no longer vertically clips its second row on the smallest mobile viewport. - **F17 threshold toggle parity.** Settings threshold toggles now reflect their server-side enabled state on mount instead of always opening on. - **Notifications section redundancy.** Three near-duplicate status surfaces collapsed onto one. - **Insights coach-rail labels.** Promoted to semantic `

` headings so the accessible-name tree resolves consistently. - **Heading hierarchy on three surfaces.** Repaired non-sequential `h1 → h3` jumps so the accessible-name tree resolves with the expected nesting. - **Icon-only buttons on the medications page.** Every icon button gained an accessible name; same sweep on the admin feedback inbox and reminders surface. - **Progress-bar accessibility.** Every `` instance now carries an accessible name so a screen-reader pass reads the metric label rather than a bare percentage. - **Button loader CLS.** Buttons now reserve space for the in-flight loader so the surrounding layout doesn't shift when a request lands. - **Mobile Coach FAB occlusion.** FAB auto-hides while any chart tooltip is open so it cannot cover the read-out. - **Settings mobile section strip.** Scroll-snap on the horizontally scrollable strip so the active pill always lands on the leading edge. - **Passkey breakpoint.** Passkey card no longer clips its internal grid on the tablet breakpoint. - **Settings tile padding parity.** Every settings tile reads `p-4 md:p-6` after the card-default normalisation. - **Sidebar "Home" group label.** Redundant collapsible label dropped from the desktop sidebar; the four nav entries now live at the top level. - **Legal-page narrow column convention.** Pinned in code comments so a future refactor doesn't widen the privacy / imprint / terms pages back out. ### Performance - **Coach snapshot builder LRU cache.** 60 s TTL keyed on `(userId, scope)` keeps repeat Coach drawer opens within a short window off the snapshot-build path. - **Assistant flags memoised per request.** Resolver lookup was re-running per surface mount on the Insights cold path. - **Web-vitals sample rate dropped to 10%.** Stops the beacon from self-throttling under high-traffic load. ### Refactor - **`Math.min` / `Math.max` spread folds.** Six per-metric status helpers now reduce instead of spread, retiring the latent stack overflow path that surfaced as the P0 above. - **Scroll-reset consolidated.** Seven per-page `useEffect` duplicates retired behind `useScrollResetOnRoute()`. - **Settings card header consolidated.** Three near-duplicate header markups retired behind ``. ### Accessibility - **Icon-only buttons named** on the medications page + admin feedback inbox + admin reminders surface. - **Progress bars named** across the app so the accessible-name tree exposes the metric label. - **Heading order repaired** on three surfaces so the accessible-name tree nests correctly. - **Coach rail labels** promoted to semantic `

` headings. ### Internal - **Retired** the now-orphan `settings.kiInsights` locale key after the Auswertungen section rename, the `AssistantDisabledNotice` component, and the dead `settings.placeholder` locale entries left behind by earlier releases. - **Test fixtures** aligned with the renamed Insights / Notifications / Settings surfaces; the import-string scan on the Insights mother page now accepts both eager and `next/dynamic` spellings. ## [1.4.32] — 2026-05-17 — HealthKit Tier 1 wave A First public surface wave for the HealthKit Tier 1 metrics that the iOS contributor brief locked in for v1.5. The headline item is a workouts flow that lands end-to-end on the web: a list page at `/insights/workouts`, a detail page with route preview and summary stats, and a dashboard tile that surfaces the three most recent sessions. Alongside the workouts surface arrives a family of five new metric sub-pages — HRV, resting heart rate, blood oxygen, body temperature, and active energy — each carried by a shared scaffold so adding the next HealthKit metric is a four-line page module. The release also cleans up two latent issues uncovered during the wave-A audit: the workouts list endpoint had a Prisma field-name bug that would have produced a 500 the moment a real client called it, and HRV plus resting heart rate were sitting in the `vitals` insight bucket where they did not belong. ### Added - **Workouts API.** `GET /api/workouts` returns a paginated list shaped to the iOS contract (`distanceM`, `activeEnergyKcal`, `avgHr`, `maxHr`) using the v1.4.30 canonical-row picker; `GET /api/workouts/{id}` returns a single workout with the same field shape. Both endpoints honour `requireAuth()` and respect the existing per-user isolation guarantees. - **`/insights/workouts` list page.** Browsable workout archive with newest-first ordering, type-filter pills, and an Apple-Health-onboarding hint when the list is empty. - **`/insights/workouts/[id]` detail page.** Per-workout view with summary tiles (duration, distance, energy, average and max heart rate), an inline-SVG route preview when GPS samples are present, and a graceful-unavailable notice for per-second HR samples. - **Dashboard recent-workouts tile.** New widget id `recentWorkouts` defaults to visible and surfaces the three most recent sessions on the home page. Self-gates on a non-empty list and renders an Apple-Health-onboarding hint otherwise. - **Five HealthKit metric sub-pages.** New pages at `/insights/hrv`, `/insights/ruhepuls`, `/insights/sauerstoffsaettigung`, `/insights/koerpertemperatur`, and `/insights/aktive-energie`, each rendered through the shared `` scaffold with its own chart-cog popover state slot. Body temperature surfaces the existing manual-entry CTA; the other four intentionally render their empty state without a primary action because the Apple Health / Withings ingest is the only path for those metrics. - **Insights tab-strip pills.** Six new pills (workouts plus the five HealthKit metrics) join the strip and self-gate on data presence, so brand-new accounts still see the seven pre-existing pills only. ### Changed - **HRV + resting HR realigned to the cardiovascular bucket.** Both metrics moved out of `vitals` into `cardiovascular` to match the iOS handoff brief's category table. The insight bucket map, the per-metric status card grouping, and the comprehensive narration all read the new placement; no client contract changes because the bucket only drives in-app grouping. ### Fixed - **Latent 500 on workouts list endpoint.** The list query referenced two non-existent Prisma columns (`distanceMeters` and `energyKcal`); tests mocked Prisma with `as never` so the bug never surfaced in CI. The endpoint now uses the v1.4.30 `pickCanonicalWorkoutRows()` helper and projects the iOS contract field shape end-to-end. ## [1.4.31] — 2026-05-16 — Operator toggles + insights tab-strip + Coolify auto-deploy fix Three orthogonal patches in one release. The biggest item is a per-surface operator toggle matrix that lets the maintainer carve which model-driven surfaces stay visible without removing the provider configuration — Coach, Daily Briefing, per-metric status cards, correlations, and the Health-Score delta explainer each get an admin switch, plus a master kill-switch above them all. Alongside that lands the root-cause fix for the /insights tab-strip blocking on mobile: an abort timeout on the advisor fetch, a `React.memo` on the strip with its `availability` prop memoised in the layout shell, and a lazy-loaded Coach drawer collapse the worst-case tap-block window from the LLM-completion tail down to the bounded parallel-fetch window. The release also closes the long-standing Coolify auto-deploy race: five releases in a row the webhook reported "finished" but Coolify still pulled the prior `:latest` digest because the publish workflow fired the webhook inside GHCR's CDN propagation window; a 90 s sleep before the trigger step lands inside the existing `continue-on-error: true` envelope. ### Added - **Assistant-surface operator toggles.** Six boolean columns on `AppSettings` (`assistantEnabled` + five sub-flags) carve the visibility cut for every model-driven surface. Master forces every sub-flag false in the resolver, so a single flip kills the whole assistant. New admin panel at `/admin/assistant` (slug between `ai-quality` and `coach-feedback`) renders the six toggles; sub-toggles grey out when the master is off. Defaults preserve the v1.4.30 behaviour (every surface visible) so upgrades require no admin action. - **`GET /api/feature-flags`.** Projects the matrix over HTTP for every client — web React tree + iOS native. `requireAuth()` gates the route, `Cache-Control: private, max-age=60` caps the read cost on the hot /insights mount path. - **`PUT /api/admin/settings/assistant-flags`.** Dedicated admin write surface that echoes both the raw column values and the resolved (master-killed) shape. Optimistic UI on the admin panel; the runtime feature-flag cache invalidates on every write so toggled surfaces react within the same operator session. - **`` component.** Small inline notice for callers that resolve a 403 + `errorCode: "assistant.disabled."` from the server. Localised in all six locales. - **`useFeatureFlags()` hook.** Fails open — any network error or absent `` returns the all-on default so the user never loses an affordance to an instrumentation gap. ### Fixed - **Insights tab-strip blocking on mobile.** Three orthogonal client-side fixes per `.planning/research/v15-insights-blocking-bug.md`: - `fetchAdvisor` gets an 8 s `AbortController`; `AbortError` falls through to the existing graceful-null return path so the UI surfaces the regen CTA instead of pinning the query in `isFetching: true` for the LLM tail. - `` is `React.memo`'d, the inner `buildTabs` call is `useMemo`'d on `availability`, and the layout shell memoises the `availability` prop on its three leaf inputs. Together they collapse the "shell re-renders → strip re-renders → 8 pills re-render" cascade on every cache-write of analytics or comprehensive. - `` is now lazy-loaded via `next/dynamic` so the SSE machinery, chat reader, suggested-prompts rail, and settings sheet don't initialise on every cold /insights mount. ### Changed - **Server-side gating of assistant endpoints.** Every LLM-driven endpoint now reads the operator flag set near the top of the handler and throws a typed `AssistantDisabledError` when the relevant surface is off. The api-handler catches the error and returns 403 + `meta.errorCode: "assistant.disabled."`. Older clients without the errorCode see a generic 403; v1.4.31+ clients render the `` empty state. Gated endpoints: `/api/insights/chat` (coach), `/api/insights/generate` (coach), `/api/insights/comprehensive` (coach), `/api/insights/cards` (insightStatus), `/api/insights/correlations` (correlations), and the six `*-status` per-metric routes (insightStatus). ### CI - **Coolify auto-deploy race fix.** `Trigger Coolify deploy` step in `.github/workflows/docker-publish.yml` now sleeps 90 s before firing so GHCR's CDN edges have time to propagate the fresh `:latest` digest. The webhook lands after the edge read catches up, Coolify pulls the new digest, and the running container recreates cleanly. Full root-cause + hypothesis matrix in `.planning/round-coolify-auto-deploy-fix-2026-05-16.md`. - **OpenAPI pre-commit hook.** New `.githooks/pre-commit` runs `pnpm openapi:check` when the staged diff touches Zod schemas or API routes; on drift it regenerates the spec, re-stages the file, and continues the commit. Activated via the new `scripts/install-hooks.sh`. Skips in CI so the existing `security.yml` server-side gate stays authoritative. ### iOS contract Every change is additive on the wire. - `GET /api/feature-flags` is net-new. iOS clients that predate it never call the endpoint and see the all-on default implicitly. - The six `AppSettings.assistant*` columns default to `true` in the migration so existing rows pick the defaults up without data movement. - The 403 + `meta.errorCode: "assistant.disabled."` envelope is additive — pre-v1.4.31 iOS reads the 403 as a generic auth error and degrades gracefully through its existing 403 handler. v1.4.31+ iOS reads the `errorCode` to render the surface-specific empty state. - The locked-contract entry at `.planning/v15-ios-handoff/08-locked-contracts.md` §14 documents the rule that the flag matrix gates BOTH server-routed AND on-device assistant surfaces. Apple Foundation Models on-device Coach + Briefing flows in the v0.6.0 iOS path honour the same flag matrix as the server-routed surfaces. ## [1.4.30.1] — 2026-05-16 — Categories endpoint + conflict-resolution lock A two-item follow-up to v1.4.30. The categorisation overlay shipped yesterday as a TypeScript map; the new `GET /api/measurement-categories` endpoint projects it over HTTP so the iOS-side picker can fetch the canonical shape rather than carry a hard-coded mirror — the unblocker the iOS team flagged as the one missing piece. In parallel the SyncMode conflict-resolution policy that the v0.6.0 standalone-first track depends on lands as §13 of `08-locked-contracts.md`. ### Added - **`GET /api/measurement-categories`.** Thin projection of `src/lib/measurements/categories.ts` over HTTP. Response carries `version: 1`, an ordered `categories` array with `labelKey` translation keys, and the `assignments` map keyed on `MeasurementType`. `requireAuth()` gates the route — any logged-in user passes — and `Cache-Control: public, max-age=600` enables a 10-minute edge cache. Locked per `.planning/RESPONSE-TO-IOS-TEAM-2026-05-16.md` §3 R1. ### Documented - **SyncMode conflict-resolution policy.** `.planning/v15-ios-handoff/08-locked-contracts.md` gains §13 per R9 of the iOS-team response doc. Hard-spec covers bulk-backfill, steady-state bidirectional sync via `(updatedAt, syncVersion)` optimistic concurrency, the 409 write-conflict + 410 Gone delete-conflict envelope shapes, the LWW-by-`updatedAt` resolution with server-wins on millisecond tie, and the `GET /api/sync/state` response. The "What is NOT in this file" stub renumbers to §14. - **iOS contributor brief.** §3 bumps the live-production marker to v1.4.30 and adds the four v1.4.30 endpoints plus the new v1.4.30.1 endpoint to the iOS-consumable list. §5b / §6 reframe the Coach SSE story per R6 — the server endpoint stays live; the iOS native server-Coach drawer is deferred pending MDR Class-IIa pre-review. v1.5.0 iOS ships Apple Foundation Models on-device Daily Briefing + Trend Observations as the primary assistant surface. - **v1.5 strategic plan.** §2 slots Apple Health XML import as v1.4.34 immediately before the web-freeze marker (per R2), detailing the multipart endpoint, the streaming XML parser, the async job model, the per-MeasurementType ingestion stats, the idempotent UPSERT on `externalId`, and the admin endpoint variant. Effort L (~3-4 days). The freeze trigger renumbers from v1.4.33 to v1.4.34; the decision-log row "Apple Health XML import slot" lands; the §4 deferred row and §6 open question #3 close out. ## [1.4.30] — 2026-05-16 — iOS-coordinated foundation (Daily-Stats + SyncMode) Server-side prep for the next iOS TestFlight build. Five surfaces land together so the iOS engineer can pick them up in one cut-over: a locked `externalId` shape for daily-aggregated cumulative HealthKit rows, the SyncMode foundation columns + handshake + bulk-backfill endpoints, a first-class `MoodEntry.note` column that replaces the legacy `tags: ["note:"]` workaround, a cross-source workout dedup helper, and two new MeasurementType enums (`WALKING_STEADINESS`, `AUDIO_EXPOSURE_EVENT`) plus the shared `MEASUREMENT_CATEGORIES` overlay that drives the iOS permission picker and the future Insights nav. ### Added - **Daily-stats `externalId` helper.** `dailyStatsExternalId(hkId, date)` mints `"stats::"` alongside the v1.4.29 `CUMULATIVE_HK_TYPES` set. The shape is locked in `.planning/v15-ios-handoff/08-locked-contracts.md` §12 and `.planning/v15-ios-handoff/06-ios-responsibilities.md`. iOS emits one row per day per cumulative type via `HKStatisticsCollectionQuery`; the unique index collapses re-syncs idempotently, and a future PATCH-on-divergence call updates a late-watch-sync revision without inserting a second row. - **Drain script + admin endpoint** at `scripts/drain-per-sample-cumulative.ts` and `POST /api/admin/drain-per-sample-cumulative` (gated by `requireAdmin()`, default `dryRun: true`). Collapses pre-Option-A per-sample APPLE_HEALTH cumulative rows into one row per day per type. Idempotent — re-running on a fully-collapsed account reports zero buckets touched. - **SyncMode foundation.** Migration 0062 adds `Measurement.sync_version` (Int, default 1), `Measurement.deleted_at` (Timestamp, nullable) for soft-deletes, and `User.last_synced_at` (Timestamp, nullable). The new `GET /api/sync/state` returns the handshake response (lastSyncedAt, server clock, live + tombstoned counters); the call also bumps the checkpoint so iOS reads the OLD value then trusts subsequent writes via the standard read paths. - **Bulk backfill endpoints.** `POST /api/mood-entries/bulk` and `POST /api/medications/intake/bulk` accept up to 500 entries per call with the same response envelope as the measurements + workouts batch. Probe-then-upsert distinguishes inserted vs duplicate on the mood path; idempotency-key collision yields duplicate on the intake path. Both rate-limited at 60/min/user. - **`MoodEntry.note` column** (migration 0063). The bulk + single POST + PUT routes thread the new field through; the existing `tags: ["note:"]` workaround backfills via `scripts/backfill-mood-note-column.ts` (CLI dry-run by default, `--confirm` commits). - **`pickCanonicalWorkoutRows()`** at `src/lib/measurements/pick-canonical-workout-rows.ts`. Cross-source workout dedup symmetric to `pickCanonicalSourceRows()`. Buckets rows by 5-minute startedAt slot + sportType and walks the existing measurement source ladder; metric-aware tunes (route → Apple wins, HR zones → Withings wins) defer to v1.5.x. - **`MEASUREMENT_CATEGORIES` overlay** at `src/lib/measurements/categories.ts`. UI-only category map (vitals / body / activity / sleep / hearing / environment / cardiovascular / metabolic) that drives the iOS HealthKit permission picker, the post-v1.5 web Insights nav, and the Coach evidence shelf chip-grouping. Completeness wall in the test suite catches a new MeasurementType lacking a category assignment. - **Two MeasurementType enums** (migration 0064) — `WALKING_STEADINESS` (iOS 15+ Mobility daily rollup, ×100 scaled from Apple's 0..1 fraction) and `AUDIO_EXPOSURE_EVENT` (iOS 13+ category flag fired when the rolling 7-day average crosses the WHO 80-dBA threshold; environmental + headphone events share the same enum value). The wiring registries (`apple-health-mapping`, `categories`, `pr-direction`, `chart-tokens`, six locale files) pick them up in the same release. ### Changed - **Real-Postgres integration coverage expanded.** Every new endpoint introduced in v1.4.30 rides the v1.4.29 testcontainer fixture: the drain helper, the admin endpoint, the sync-state handshake, the mood-entries bulk upsert, the medication-intake bulk insert + idempotency-key collision, the mood-note column round-trip. Full integration suite at 47 files / 190 specs. - **`HK_QUANTITY_TYPE_DEFERRED` trimmed.** Three identifiers move out of the deferred set into the mapping table: `HKQuantityTypeIdentifierAppleWalkingSteadiness`, `HKCategoryTypeIdentifierEnvironmentalAudioExposureEvent`, `HKCategoryTypeIdentifierHeadphoneAudioExposureEvent`. ### iOS contract Every API change is additive. The new endpoints (`/api/sync/state`, `/api/mood-entries/bulk`, `/api/medications/intake/bulk`, `/api/admin/drain-per-sample-cumulative`) are net-new — no iOS consumer yet. The SyncMode columns carry defaults so existing iOS POSTs round-trip unchanged. The two new MeasurementType enums are net-new values; iOS clients that predate the codegen pass will not encounter them in read paths because no source writes them yet, and the codegen path will pick them up on the next iOS regeneration. **Cutover sequence.** v1.4.30 ships with the helper + drain script + server tolerance for both shapes. The next iOS TestFlight build adopts `HealthKitStatisticsService.swift` and starts posting daily- aggregated rows for the five cumulative types. Operator runs the drain script once after the new TestFlight cuts over; per-sample row pressure on `Measurement` drops 50-200× for cumulative types. ## [1.4.29.1] — 2026-05-16 — Daily-step aggregation hotfix A one-line follow-up to v1.4.29. The dashboard 7-day step chart now renders daily totals (sums in the thousands), not per-sample averages (hundreds), matching the server-side `aggregate=daily` contract that landed earlier today. ### Fixed - **Client-side daily aggregator.** `health-chart.tsx` reduced every HealthKit type with `sum / count` regardless of whether the metric is cumulative or spot. The branch now consults `CUMULATIVE_HK_TYPES` and picks `sum` for steps, active energy, flights climbed, walking + running distance, and time in daylight, while the spot metrics (BP, weight, pulse, BG, body fat, mood, sleep) keep the mean. ### Tests - The existing chart suite (137 specs) continues to pass; the one-line branch is exercised by the v1.4.29 server-side aggregation tests and needs no additional coverage. ## [1.4.29] — 2026-05-16 — Dashboard performance + chart polish A targeted performance + polish patch. The headline is a faster dashboard on data-rich accounts: the pulse chart drops up to ~5 000 raw rows per range tab in favour of one row per day, the duplicate `/api/dashboard/widgets` fetch on every mount collapses to one, and three long-running analytics reads are bounded to the trailing window the tile path actually needs. Two production regressions close — the `aggregate=daily|weekly|monthly` path on `/api/measurements` 500'd for every grain, and the same path averaged cumulative step counts instead of summing. Mobile chrome gets a tighter rhythm: dashboard tiles pin to a single 140-px contract at `` range test asserts the default 30-day window asks for `aggregate=daily` and the `windowOverride="last7days"` mini-chart does not. - `` mobile-tile-height contract pinned via a new snapshot test (`trend-card-tile-height.test.tsx`). ## [1.4.28.1] — 2026-05-16 — Dashboard-save hotfix "Speichern" on the Dashboard works again for every account whose layout was last persisted before v1.4.28. The v1.4.28 retire of the `glp1` widget id left an orphan entry in `dashboardWidgetsJson` for legacy users, which made the save validator reject the payload; the resolver now filters unknown widget ids on read. ### Fixed - **Dashboard save against legacy layouts.** `resolveDashboardLayout` in `src/lib/dashboard-layout.ts` drops widget ids that are no longer in the registry before returning the layout, so the `dashboardWidgetsJson` round-trip (`GET /api/me/dashboard` → user edit → `PUT /api/me/dashboard`) succeeds even when the persisted JSON still names `glp1` or any other retired widget. ### Tests - `src/lib/__tests__/dashboard-layout.test.ts` gains a case for the retired-id filter (19/19 green). ## [1.4.28] — 2026-05-16 Bug-fix and consistency follow-through after the v1.4.27 mobile sweep. The headline is a tighter dashboard and insights surface: six widgets retire from code entirely (GLP-1 tile, dashboard `` mount, GLP-1 detail-page intake history + inventory, the `` block and its regeneration affordance, the weekly-report route), four broken edges close (workout-edit duplicate- timestamp 409, BD-Zielbereich tile rebuilt on the shared `` primitive, `/insights/puls` chart timeout fallback, sticky tab-strip scroll lock), and one consistency contract lands per maintainer directive: every medication-list row, every medication- detail section header, every trend tile, every Coach launch glyph, every chart-height contract reads on one shape. The HealthScore card grows to fill its column, gains an accessible `?` tooltip explaining the delta, and drops the placeholder "Wochenbericht erstellen" button. iOS contracts are intact; the only `/api/*` evolution is additive (the `aggregate=monthly` grain on `/api/measurements` and an internal-only `/api/internal/web-vitals` beacon route). ### Removed - **GLP-1 dashboard tile.** `src/components/dashboard/glp1-tile.tsx` and every reference (`dashboard-layout.ts` entry, the `dashboard.glp1.*` i18n keys, the test fixtures) retire. The tile is gone from `/`; the recovered vertical real estate flows to the trends row + HealthScore column. - **Dashboard `` mount.** The standalone Drug-Level pane retired from `/`. The component itself stays — it serves the `/medications/[id]/history` page exclusively. The `compact` mode and `windowHoursBefore` override drop with the dashboard mount; the history-page recipe paints the chart inside `` on the same heading scale as every other section on the page. - **`` surface.** The "Persönlicher Berater" card at the bottom of `/insights` and its "Insights aktualisieren" regeneration button retire. The Coach drawer is the single assistant entry point. Component, mounts, test fixtures, i18n keys, and the cached-advisor query all retire together. - **Weekly-report route.** `/insights/report/[week]` and the `` mount on the hero retire. No remaining consumer; the Coach drawer covers the equivalent ask. - **GLP-1 detail-page intake history + inventory.** The "Dosis- Historie" disclosure and "Bestand" section retire from the GLP-1 medication detail page. The page now collapses to header, schedule, dose-titration ladder, and side-effects — every section the maintainer flagged as wanted; every section flagged as unwanted is gone. Inventory tracking remains opt-in for a future release. - **Hero "Wochenbericht erstellen" button.** The placeholder affordance retires from the `/insights` action row. The row now carries a single "Coach fragen" button so the HealthScore card on the opposite column has the height to reach the last suggested prompt. ### Fixed - **Workout edit raised a 500 on duplicate timestamps.** The save path on `PUT /api/measurements/[id]` returned a generic 500 when a sport-typed sample (`ACTIVE_ENERGY_BURNED`, `FLIGHTS_CLIMBED`, `WALKING_RUNNING_DISTANCE`, `EXERCISE_TIME`) collided with an existing row on the same `(type, measuredAt)` natural key. Returns 409 with a `measurements.duplicateTimestamp` translation key now; the iOS native client and the web measurement-form both surface the conflict to the user instead of a generic toast. - **BD-Zielbereich tile rendered "1.1." instead of numbers.** The tile's bespoke rendering path read the schedule date through `Intl.DateTimeFormat` and missed the underlying value. Tile rebuilt on the shared `` primitive so the chrome, the state shape, the prop API, and the fallback behaviour match the Weight and BP siblings. The divergence has been the open finding across v1.4.22, v1.4.25, v1.4.26 patches — this release rewrites the tile instead of patching. - **`/insights/puls` hung waiting on the assessment provider.** The pulse status card called the language-model provider with no client-side timeout. A timeout helper (`with-timeout.ts`) caps every status-card provider call at 20 s and returns the rule-based fallback string on timeout; the page renders the chart immediately and the assessment line resolves async. - **Scroll stuck on `/insights` mother-page navigation.** The sticky tab-strip's intersection-observer was fighting the focus-on-mount logic so the page sometimes refused to scroll past the tab row. The observer rebinds on route change and the focus-on-mount effect now waits for the next animation frame so the scroll position is locked in before the observer fires. - **`` paint exceeded the active range window.** The chart fetched every dose-event in history regardless of the selected range. Now bounded to the active range — 30 days / 90 days / all-time — so the wire payload and the recharts render scale linearly with the visible window. - **Mood trend tile painted a residual `rounded-xl` border.** The shadcn `` default paints `rounded-xl` while the trends row needs `rounded-md` to match the `` shape. Mini- mode override adds `rounded-md`; the visual rhythm of the row reads on one envelope. - **Side-effects card overflowed at 320 px.** The "Nebenwirkungen erfassen" CTA chip ran past the section header. The qualifier drops from every locale ("Erfassen" / "Log" / "Consigner" / "Registrar" / "Registra" / "Dodaj"); the section title carries the context. The date column on the entry rows narrows from 88 px to 56 px so the free-text slot recovers 32 px of wrap headroom. - **Medication-list row shape diverged for GLP-1 entries.** The GLP-1 row painted with a brand icon and a middle-dot separator; every other row painted two-line without an icon. Both routes through a shared `` now — line 1 is `{name} {dose}`, line 2 is the class label plus state badges. The GLP-1 outlier shape is gone. - **HealthScore card came up short below the action+prompts column.** The card now opts into `flex h-full flex-col` with the disclaimer footer pinned via `mt-auto`; the hero row switches to `md:items-stretch` on `md+` so the score column reaches the bottom of the suggested-prompt rail. Equal-height contract across the hero strip. - **`/measurements` aggregation truncated long-window queries.** The `take` cap applied before bucketising, so a 365-day daily-grain query returned only the first N raw rows. Aggregation now runs as a Postgres `date_trunc` GROUP BY and the cap applies to the bucketised result; a 1-year window returns up to 365 daily buckets. The all-time chart range resolves to monthly grain (24- bucket ceiling) or weekly when history is under two years. - **Schlaf sub-page missed the per-section assessment slot.** Six of seven insights sub-pages mount `` underneath the chart; Sleep did not. Documented as the intentional skip with a `// no per-section assessment yet` marker so future contributors do not re-flag it. - **Insights-targets locale strings missed on FR / ES / IT / PL.** `measurements.duplicateTimestamp` and `insights.sleep.description` now carry native translations in every locale. ### Changed - **BD-Zielbereich tile aligned to ``.** Tile chrome, state shape, prop API, and fallback states all match Weight and BP. The Z-value rounds to one decimal at the boundary, the legend carries the same micro labels across siblings, the empty state reads the same copy with the same CTA shape. - **Medication-list rows route through ``.** GLP-1 and standard rows both render `{name} {dose}` on line 1 and class label plus state badges on line 2. State badges break onto their own row at 320 px so the two-line shape holds. - **Medications detail-page chrome collapsed to one heading scale.** Every section on `/medications/[id]` mounts through `` (`text-base font-semibold leading-6 tracking-tight`). DrugLevelChart's standalone header migrates to `

` with the same classes. Micro labels lift from `text-[10px]` / `text-[11px]` to `text-xs` across Scheduling, Titration, SideEffects. Three scales survive the page: heading, body (`text-sm`), micro (`text-xs`). - **Coach launch shape consolidated to three primitives.** A single `` mounts once per Insights surface (FAB on `