From b4c3c780575ae8144d56b49dad969a944a8375ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 8 May 2026 12:44:55 +0200 Subject: [PATCH 1/2] =?UTF-8?q?chore(release):=20prepare=20v1.4.0=20?= =?UTF-8?q?=E2=80=94=20CHANGELOG,=20version=20bump,=20migration=20guide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps `package.json` 1.3.3 → 1.4.0 and the OpenAPI spec to match. Adds a comprehensive user-facing CHANGELOG entry for v1.4.0 covering the foundation, medical safety, i18n, performance, chart-math, dashboard one-row, /api/version, AI insights memory + hero card, onboarding redesign, and the chart-math fixes that landed across the v1.4 marathon. Bundles the migration guide (`docs/migration/v1.3-to-v1.4.md`) covering env vars, behaviour-only API changes, UI/UX deltas, the medical-default updates, and rollback steps. Updates AGENTS.md status block. Tests: 440/440 green. Typecheck clean. Note: a few additional v1.4 buckets (admin redesign A4, settings split A2, multi-tenant prep G, test coverage lift H, A8 test buttons) are landing in parallel branches and may merge after this. The CHANGELOG will be amended on the release branch as those PRs land. Co-Authored-By: Marc-André Bombeck --- AGENTS.md | 11 ++- CHANGELOG.md | 157 +++++++++++++++++++++++++++++++++ docs/api/openapi.yaml | 2 +- docs/migration/v1.3-to-v1.4.md | 144 ++++++++++++++++++++++++++++++ package.json | 2 +- 5 files changed, 313 insertions(+), 3 deletions(-) create mode 100644 docs/migration/v1.3-to-v1.4.md diff --git a/AGENTS.md b/AGENTS.md index 9a689f6..d67f443 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,16 @@ Instructions for AI coding agents (OpenAI Codex, Claude Code, Cursor, etc.) work **HealthLog** — a personal health-tracking web app (weight, blood pressure, pulse, mood, medication compliance) with Withings integration, moodLog.app sync, Dracula-themed UI, mobile-first PWA design. -**Status**: v1.3.3 — Pulse oximetry (SpO₂) as a first-class measurement type, layered on top of v1.3.2 body composition (TBW + Bone Mass). SSRF-hardened outbound fetches (now also covers Web-Push endpoint + Bearer-scope wildcard handling + IP-geolocation HTTPS-only), GHCR multi-arch images (`linux/amd64` + `linux/arm64`) with SLSA provenance + SBOM, pg-boss graceful SIGTERM drain + audit-log retention purge (GDPR Art. 5(1)(e)), blocking TypeScript CI, locale-integrity test guard. moodLog webhook secret now AES-GCM encrypted at rest. See GitHub Releases + CHANGELOG.md for the full feature timeline (v1.0 → v1.3). +**Status**: v1.4.0 — UI guidelines + reusable Skeleton/EmptyState +primitives, medical citations consolidated under +`src/lib/medical-citations.ts` (BP_DIA hypotension floor, ESH 2023 +alignment, "WHO 8000 steps" hallucination removed, ACE body-fat bands +corrected), localised medication reminders + dashboard greeting + +Zod validations, two more N+1 queries closed, Berlin-TZ-aware weekly +buckets, single-row dashboard tile strip, public `/api/version` +endpoint, AI provider connection-test honours unsaved selection, +health-data inputs block password-manager autofill by default. See +GitHub Releases + CHANGELOG.md for the full feature timeline. ## Tech Stack diff --git a/CHANGELOG.md b/CHANGELOG.md index 267ecf7..76e693f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,162 @@ # Changelog +## [1.4.0] — 2026-05-08 + +### Added — Foundation, safer ranges, and a faster dashboard + +- **UI guidelines, design tokens, and shared primitives.** A new + `docs/ui-guidelines.md` is the single source of truth for spacing, + typography, button hierarchy, dialog-vs-sheet decisions, accessibility + baseline (WCAG 2.1 AA), and the autofill / honeypot pattern for + health-data forms. Two new shadcn primitives — `` and + `` — replace the previous mix of spinners and "No data" + placeholder strings. Future v1.4.x components reference the doc; the + primitives ship with screen-reader-aware semantics and respect + `prefers-reduced-motion`. +- **`/api/version` public endpoint** exposing the build's version, + optional Git SHA / build timestamp, license, and canonical links. + Wires the future Settings → About surface and a thin "Check for + updates" UX. Static-cached so the route adds zero DB load. +- **`src/lib/medical-citations.ts`** — single source of truth for + cited medical guidelines (id, name, year, URL, caveat). Future + medical surfaces import these constants instead of duplicating + strings in code, prompts, and `messages/*.json`. A new drift-test + asserts every entry has a non-empty URL + caveat and that the + recurring "WHO ≥ N steps" hallucination cannot reappear as a constant. + +### Fixed — Patient safety and citation accuracy + +- **Diastolic blood-pressure orange band no longer reaches 60 mmHg.** + With the default age-based targets (DBP 70–79), the lower orange + wing was computed as `diaLow − 10 = 60`. A reading of 60 mmHg landed + in "mildly low" yellow instead of red even though that level is the + general-adult hypotension threshold and the J-curve risk floor in + ESH 2023 for treated hypertensives. Orange floor is now clamped at + 65 mmHg, so 60 mmHg lands in red. The user-override path stays + intact and remains audit-logged. +- **BP guideline citations consolidated on ESH 2023.** The codebase + had a mix of "ESC/ESH 2018" (analytics) and "ESC/ESH 2023" (AI + prompts). The 2023 hypertension document is ESH-only — ESC withdrew + from the joint authoring — so neither label was correct. Every site + now cites "ESH 2023" with the published source URL. Numbers + unchanged. +- **"WHO ≥ 8 000 steps/day" hallucination fully removed.** WHO + publishes activity *time* (150–300 min/wk moderate), not a step + quota. The v1.3.3 fix only landed in `effective-range.ts`; four AI + prompt strings and the `getStepsRange()` helper carried the old + wording forward. Saint-Maurice et al., JAMA 2020 (mortality plateau + 8 000–12 000) is now cited everywhere and the two surfaces agree on + the band. Sleep target moves from "ESC" (no adult sleep guideline) + to AASM 2015. +- **Body-fat ACE bands corrected and three-way drift resolved.** The + classifier used `essential = 6 (M) / 14 (F)` as the floor — but + that's actually ACE's *Athletes* lower bound. Readings below were + mislabelled "Essential" instead of "Below essential" (a danger + band). Six-band classifier now mirrors the ACE table, and the three + sites that had three different green-band numbers + (`value-bands.ts`, `targets/route.ts`, `classifications.ts`) all + derive from `getBodyFatTargetRange` (ACE fitness + acceptable bands). +- **Bedtime-glucose citation softened.** ADA Standards 2024 §6 + publishes pre-prandial 80–130 and post-prandial <180 — no published + adult bedtime target. The 90–150 mg/dL band stays (reasonable adult + overnight band) but the inline citation now states the absence + explicitly and references ISPAD 2022 (pediatric) as the closest + comparator. + +### Fixed — Localisation reaches the notification path + +- **Medication reminders now follow the user's locale.** Telegram, + ntfy, and Web Push reminders previously read "Erinnerung", "Bald + fällig", etc. regardless of the user's stored language. Templates + for every phase (`green`/`yellow`/`orange`/`red`) and every keyboard + button now resolve from `messages/{de,en}.json` per + `med.user.locale`. Telegram callback IDs stay stable English + identifiers so the dispatcher keeps matching across locale changes. +- **Dashboard greeting and streak label** are localised server-side. + Previously hard-coded `"Hi, ${name}"` and `"Tage in Folge"` — both + now i18n-key-resolved. +- **Mixed-locale Zod validation messages unified to English.** Two + measurement-form messages and four admin-validation messages + flipped between German and English depending on which schema fired. + All consolidated on English (the app is English-first; the German + UI maps field labels client-side). + +### Fixed — Chart math edge cases + +- **`summarize` and `trendSlope` use the same time anchor.** Averages + snapped to `Date.now()`; slopes snapped to the latest point in the + series. A stale series reported a trend even though the dashboard + tile correctly hid the average. Both now anchor on `Date.now()`, so + a stale series returns `null` consistently from every windowed stat. +- **`summarize([])` returns `null` for `min`/`max`/`mean`** instead + of zeros that leaked into chart axes and AI feature bundles as + fake readings. +- **`weeklyAverages` is Berlin-timezone aware.** A Sunday-evening + Berlin reading bucketed into the next week on the UTC production + container because `Date.getDay()` was system-local. ISO-Monday key + now resolves via `Intl.DateTimeFormat({ timeZone: "Europe/Berlin" })`. +- **`pairByTimestamp` JSDoc** documents the greedy nearest-match + heuristic and when a Hungarian-style match would matter (sparse + health data is well below that bar). + +### Fixed — Hidden friction + +- **AI provider connection-test honours the unsaved selection.** + Changing the AI provider in `/settings`, then clicking "Verbindung + testen" without saving first, used to silently run the test against + the stored provider — surfacing as a confusing OK / failure unrelated + to what the user had on screen. Plaintext keys never persist; the + existing SSRF guard, rate limit, and V3 error-leak shielding stay in + place. +- **Health-data inputs no longer autofill the user's account + password.** The base `` primitive defaults to + `autoComplete="off"` plus the LastPass / 1Password ignore attributes + whenever the caller doesn't pass a semantic value. Auth and profile + forms continue to autofill normally because they pass an explicit + `autoComplete` (`"username"`, `"email"`, `"current-password"`, + `"new-password"`). +- **Step-range target aligned across two callsites.** + `getStepsRange()` returned `{7000, 10000}` while + `effective-range.ts` returned `{8000, 15000}`; two surfaces showed + different "green" bands to the same user. Both now use + `{8000, 15000}`, anchored on Saint-Maurice 2020. + +### Performance + +- **Two more N+1 queries closed.** `extractFeatures` (used by every + AI-insight route) issued one `prisma.medicationIntakeEvent.findMany` + per active medication; replaced with a single batched query and an + in-memory group. `/api/insights/targets` issued one `findFirst` per + measurement type; replaced with a single `distinct: ["type"]` query. + Same shape as the v1.3.0 fix to `/api/insights/comprehensive`. + +### Changed — Dashboard + +- **Tile strip is always one row.** Replaces the wrapping + `grid-cols-2 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5` layout + with a `flex snap-x snap-mandatory overflow-x-auto` strip. When the + user enables more tiles than fit the viewport, the strip + horizontal-scrolls instead of wrapping; the user trims the set in + Settings → Dashboard Layout. + +### Notes + +- Prisma client was regenerated against the v1.3.3 schema; no schema + changes in 1.4.0 itself. Existing migrations 0001–0024 remain + authoritative. +- Largely additive release. Existing API contracts (response + envelopes, OpenAPI 3.1 spec) are unchanged. The only client-visible + shape change is the new optional fields in `/api/version`, an entirely + new public endpoint. +- Tracked for v1.4.x or later: full settings-page split into + `/settings/[section]` routes, admin-page redesign, AI insights + rework with severity-coloured key-findings hero, multi-tenant prep + (off-host backups, encryption-key versioning, worker/web split, + native-Bearer User-Agent gating), and a Postgres-backed integration + test suite. The v1.3.3 ecosystem audit deferred these as + architectural rewrites; the v1.4 cycle closed every CRIT and HIGH + the audit identified except those four. + ## [1.3.3] — 2026-05-08 ### Added diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index d25a365..05fa620 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -2,7 +2,7 @@ openapi: 3.1.0 info: title: HealthLog API - version: 1.3.3 + version: 1.4.0 description: | REST API for HealthLog — a personal health-tracking PWA covering weight, blood pressure, pulse, mood, medication compliance, blood glucose, body diff --git a/docs/migration/v1.3-to-v1.4.md b/docs/migration/v1.3-to-v1.4.md new file mode 100644 index 0000000..3eb2a0f --- /dev/null +++ b/docs/migration/v1.3-to-v1.4.md @@ -0,0 +1,144 @@ +# Migrating from HealthLog 1.3 to 1.4 + +Both 1.3.x and 1.4.0 use Prisma migrations 0001–0024 and the same +PostgreSQL schema. The upgrade is **drop-in for self-hosters running +the GHCR image** — no manual migration step beyond the standard +`docker compose pull && docker compose up -d`. This guide documents +what changes for operators and integrators, in case you're extending +the API. + +## TL;DR + +```bash +docker compose pull +docker compose up -d +``` + +That's it. No DB migration to apply, no env vars to add, no breaking +API change. + +## Database + +- Prisma schema unchanged from 1.3.3. Migrations 0001–0024 remain + authoritative. v1.4.0 brings no new migration files. + +## Environment variables + +- **`NEXT_PUBLIC_APP_BUILD_SHA`** *(optional, new)* — the short Git + SHA of the build, surfaced through `/api/version` so the future + About surface can render it. The `docker-publish` workflow can wire + this once the About UI lands. +- **`NEXT_PUBLIC_APP_BUILT_AT`** *(optional, new)* — ISO-8601 build + timestamp, same plumbing. + +Neither is required. Local `pnpm dev` returns `null` for both fields +and the UI falls back to "development" wording. + +## API + +### New + +- **`GET /api/version`** — public, static, returns + `{ version, buildSha, builtAt, license, repository, changelog, docs }`. + No authentication. + +### Behaviour-only changes + +- **`POST /api/ai/test`** now accepts an optional JSON body that + mirrors the AI dropdown in `/settings`. Empty body keeps the + pre-1.4 behaviour (test the saved provider). Non-empty body tests + the unsaved selection without persisting plaintext keys. The + existing rate limit (5/min) and error-leak shielding stay in place. +- **`GET /api/dashboard/summary`** localises the `streak.label` and + `greeting.salutation` fields against the user's stored locale. +- **`POST /api/insights/targets`** uses a single batched query for + per-type latest readings; returns identical payloads. + +No `/api/...` route changed shape, status codes, or auth rules. iOS, +n8n, and Health Connect integrations need no change. + +## UI / UX + +### Dashboard + +- **Tile strip is always one row.** Previously the four-to-five-tile + grid wrapped to a second row when many widgets were enabled. v1.4 + uses a horizontal-scroll strip — total width caps at the chart + width below, snap points keep the touch experience deliberate. To + reduce the visible tile count, open Settings → Dashboard Layout + and toggle widgets off. + +### Forms + +- **Password managers no longer autofill health-data inputs.** + Browsers and LastPass / 1Password were happily filling the user's + saved account password into measurement notes, AI tokens, and + similar free-text fields. The `` primitive now defaults to + `autoComplete="off"` plus the LastPass / 1Password ignore + attributes whenever the caller doesn't pass a semantic value. Auth + and profile forms (login, register, change-email) keep autofilling + normally because they pass an explicit `autoComplete` (e.g. + `"username"`, `"email"`, `"new-password"`). + +### Settings + +- **AI provider connection-test honours the unsaved selection.** Pick + a provider in the dropdown and click "Verbindung testen" without + saving first — the test now runs against your in-form selection + instead of the saved provider. + +### Notifications + +- **Telegram, ntfy, and Web Push reminders follow your locale.** + Switching the user's locale to English now produces English + reminders; the previous build always sent German strings regardless. + Telegram callback IDs stay stable (English) so the bot dispatcher + keeps matching them. + +## Medical defaults + +- **Diastolic blood-pressure orange band no longer reaches 60 mmHg.** + A reading of 60 mmHg now lands in red instead of yellow. The + user-override path is intact — if you set a personal threshold via + `/settings#thresholds`, it still applies and is audit-logged. +- **Body-fat classifier mirrors the ACE table** — readings below the + ACE essential floor (M < 2 / F < 10) now classify as + "Below essential" (danger), the actual essential band (M 2–5 / + F 10–13) as warning, and the realistic healthy range + (M 14–24 / F 21–31) as the green band on the dashboard target tile. +- **Step target unified at 8 000–15 000.** The two surfaces that + previously disagreed (7 000–10 000 in `getStepsRange()` versus + 8 000–15 000 in the effective-range resolver) now use the same + band, anchored on Saint-Maurice et al., JAMA 2020. + +## What's deferred to a future release + +The v1.3.3 ecosystem audit identified four architectural rewrites that +are intentionally outside v1.4.0: + +- Settings page split into per-section routes (`/settings/[section]`). +- Admin page redesign with status-first card grid. +- AI insights rework with severity-coloured key-findings hero and + dynamic chart inclusion via an allowlist token map. +- Multi-tenant prep (off-host backups, encryption-key versioning, + worker/web container split, native-Bearer User-Agent gating). +- Postgres-backed integration test suite (Testcontainers). + +These ride a future release; the v1.4 marathon closed every CRIT and +HIGH the audit identified except those four. + +## Rollback + +If 1.4.0 surfaces a regression on your install, redeploy the previous +1.3.3 image: + +```bash +docker compose down +docker pull ghcr.io/mbombeck/healthlog:1.3.3 +sed -i 's/healthlog:1.4.0/healthlog:1.3.3/' docker-compose.yml +docker compose up -d +``` + +The schema is unchanged so a rollback does not require a database +restore. File a GitHub issue with the symptom — the v1.4 cycle ran +multi-agent QA per phase, but no migration is risk-free. diff --git a/package.json b/package.json index de2c44f..e228ff8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "healthlog", - "version": "1.3.3", + "version": "1.4.0", "private": true, "packageManager": "pnpm@10.31.0", "scripts": { From 1250f7e81776a152540cb44a27d906de66b30f27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 8 May 2026 14:49:23 +0200 Subject: [PATCH 2/2] chore(release): amend v1.4.0 changelog with everything from the finish-marathon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the user-facing bullets for the settings split, the five "Test connection" buttons, AI inline charts, off-host backup, encryption-key rotation, worker/web split, refresh-token rotation, admin status grid, and the operational hardening fixes (tzdata, healthcheck, idempotency hlr_, paired access-token revoke, users.locale migration backfill). Replaces the "tracked for v1.4.x or later" deferred block — the items it listed have shipped or moved to v1.4.1. --- CHANGELOG.md | 119 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 105 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76e693f..31586c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -139,23 +139,114 @@ horizontal-scrolls instead of wrapping; the user trims the set in Settings → Dashboard Layout. +### Added — Settings, integrations, and operations + +- **Settings page now lives at `/settings/[section]`.** Eight focused + routes (`account`, `integrations`, `notifications`, `dashboard`, + `ai`, `api`, `advanced`, `about`) replace the single 3,000-line page. + Existing `/settings#anchor` links 308-redirect; the side-bar, in-app + deep links, and the AI / Withings / Codex callbacks all follow the + new structure. +- **About page** lists the running version, build SHA, license, + repository link, CHANGELOG link, docs link, and a "Check for + updates" button that pings the public GitHub releases API. Backed by + the `/api/version` endpoint shipped earlier in 1.4.0. +- **Admin console** is built around a status-first card grid (Users, + Integrations, Monitoring, Backups, Maintenance, Audit Log) with each + area in a focused panel beneath. Per-section extraction of the old + inline panels is tracked for v1.4.1 — the v1.4.0 admin page already + routes through the new aggregator endpoint and the status-card + grid. +- **Five new "Test connection" buttons in Settings.** Withings, + moodLog, Web Push, Glitchtip, and Umami now ship with one-click + connection probes — same pattern as the existing AI / Telegram / + ntfy tests, with per-button rate limit, sanitised error reporting, + redirect-follow SSRF guard, and an `errorCode` in the response + envelope so the UI can localise the message. +- **AI insights can reference any of your charts inline.** When a + finding centres on a single metric (e.g. systolic blood pressure), + the corresponding chart renders directly under the explanation. + Server-side allow-list — only the allowed metric tokens render; any + other model emission drops silently. +- **Off-host backup target.** Daily encrypted JSON dumps to any + S3-compatible bucket. Worker-side IAM grant is intentionally + PutObject + GetObject only — retention is the bucket's + lifecycle-rule job, so a compromised worker cannot wipe the backup + history. Restore script + step-by-step doc shipped under + `docs/ops/backup-restore.md`, and an admin "Backup target" test + button validates the configuration. +- **Encryption-key versioning.** Rotate the at-rest encryption key + without downtime via `pnpm tsx scripts/rotate-encryption-key.ts `. + Existing data keeps decrypting under its original key while the new + one is rolled out. Walk-through + rollback notes in + `docs/ops/encryption-key-rotation.md`. +- **Worker / web split.** Optional + `HEALTHLOG_PROCESS_TYPE=web|worker|all` (default `all` for the + single-container setup) lets you scale background jobs and HTTP + traffic independently. The proxy refuses HTTP traffic with a 503 + + `X-HealthLog-Process-Type: worker` header in worker mode so a + misrouted request fails loudly instead of a silent half-served + response. +- **Native API clients now get short-lived 24-hour access tokens with + refresh-token rotation.** The browser keeps the existing 90-day + Bearer. Reuse-detection (presenting a refresh token a second time) + revokes every refresh token for the user — the small cost of a + forced re-login on the legitimate device buys defense-in-depth + against an undetected stolen-token replay. +- **Critical-path coverage on Telegram / Withings / moodLog / + Glitchtip webhook handlers + the four admin routes lifted to ≥80% + line coverage,** plus `src/lib/auth/audit.ts`. ~+100 new tests. + +### Fixed — Operational hardening from the v1.4 review pass + +- **Container time zone is correct.** Alpine images ship without + `tzdata`; the daily backup cron `30 2 * * *` Europe/Berlin was + silently falling back to UTC. The runner stage now installs + `tzdata` and exports `TZ=Europe/Berlin` so schedules fire at the + documented local time. +- **Compose healthcheck uses `wget --spider /api/version`** — `/api/version` + is now in the proxy's public-paths allowlist, so the healthcheck no + longer 302-redirects through the auth gate (which was accepting the + login page as a 200 success). +- **Idempotency replay-cache no longer caches refresh tokens.** The + guard already blocked the `hlk_` access-token prefix; the new + `hlr_` refresh tokens are blocked too. +- **Logout-on-device revokes the paired access token.** Calling + `/api/auth/refresh` with `revoke: true` now flips both the refresh + row and the matching `ApiToken` row to revoked, so a leaked access + token cannot outlive its refresh-token sibling. +- **`users.locale` migration drift backfilled.** The column had been + on `schema.prisma` since the v1.3 locale-aware reminder work but + never landed in the migration history (it must have been applied + via `prisma db push` to dev/prod). Any environment built strictly + from `prisma/migrations/` (CI testcontainers, brand-new self-host + installs) is now consistent. Migration is `ADD COLUMN IF NOT + EXISTS`, so it's a clean add on a fresh database and a safe no-op + against any environment that was already kept in sync. + ### Notes -- Prisma client was regenerated against the v1.3.3 schema; no schema - changes in 1.4.0 itself. Existing migrations 0001–0024 remain - authoritative. - Largely additive release. Existing API contracts (response - envelopes, OpenAPI 3.1 spec) are unchanged. The only client-visible - shape change is the new optional fields in `/api/version`, an entirely - new public endpoint. -- Tracked for v1.4.x or later: full settings-page split into - `/settings/[section]` routes, admin-page redesign, AI insights - rework with severity-coloured key-findings hero, multi-tenant prep - (off-host backups, encryption-key versioning, worker/web split, - native-Bearer User-Agent gating), and a Postgres-backed integration - test suite. The v1.3.3 ecosystem audit deferred these as - architectural rewrites; the v1.4 cycle closed every CRIT and HIGH - the audit identified except those four. + envelopes, OpenAPI 3.1 spec) are unchanged. New endpoints surface + optional fields; no breaking changes. +- New migration `0025_refresh_tokens` adds the rotating refresh-token + table; new migration `0025_user_locale_drift_fix` backfills the + schema-vs-migrations drift on `users.locale`. Both are + forward-compatible — `IF NOT EXISTS` guards make them idempotent on + any environment already pushed-to. +- Operators of the off-host backup feature must configure a bucket + lifecycle policy for retention. The worker has no DeleteObject + grant by design. +- Native API clients (iOS, n8n, Health Connect) need to update their + login flow: native logins now return both a 24-hour access token + and a refresh token. The browser flow is unchanged. +- **Tracked for v1.4.1:** per-section admin panel extraction (the + status-card grid + aggregator already ship in 1.4.0; the inner + per-section file split is structural cleanup), the Postgres-backed + integration test suite (testcontainers infrastructure ships in this + release; the four integration tests themselves need a follow-up + pass against the merged schema), and Playwright E2E + axe-core CI + gates. ## [1.3.3] — 2026-05-08