# HealthLog v1.4.23 — release summary ## Release brief v1.4.23 is the foundation release v1.4.x hasn't shipped before: the user-visible delta is small on purpose because the weight is in the contract surface underneath it. Seven new `MeasurementType` values, `APPLE_HEALTH` joining `MeasurementSource`, a sleep-stage enum, an idempotent `POST /api/measurements/batch` endpoint, APNs scaffolding wired into the existing notification cascade, an OpenAPI 3.1 generator with a warn-only drift CI gate, per-device refresh-token reuse-detection, three new device-management routes, a per-user Coach prefs surface behind the settings cog, a per-message Coach helpful/unhelpful loop, and seven hygiene items from the v1.4.22 backlog. None of those land as loud features; together they're the shape of v1.5's contract surface, frozen in code before the iOS app starts compiling against it. The cadence has matured beyond the loud/quiet two-beat into a three-beat: **loud release → polish release → foundation release**. v1.4.20 / v1.4.22 / v1.4.23 was the first time the whole cycle ran end-to-end. ## Live state | Field | Value | | ------------------ | ------------------------------------------------------------------------------- | | URL | `https://healthlog.bombeck.io` | | `/api/version` | `1.4.23` | | Image digest | `sha256:b20c25a49b1835aa66c03a4670c27a3ba0ea9414dab3ee4a45f5cd35d4f353d1` | | Version transition | 2026-05-11T06:40:49Z (host-side retag fallback) | | GH release | https://github.com/MBombeck/HealthLog/releases/tag/v1.4.23 | | Branch model | `develop` → `main` release-merge model, fourth tag through it (v1.4.20 onwards) | ## Smoke (no session) `/api/version` → 200 (returns `1.4.23`). Every gated route (`/`, `/insights`, `/insights/report/2026-W19`, `/admin/api-tokens`, `/settings/integrations`, `/achievements`) returns 307 → `/auth/login`, confirming the proxy gate is alive on the new image. ## What shipped ### Wave 1 — Research One research pass with three streams ran before any code went in: the Apple Health `HKQuantityTypeIdentifier` → `MeasurementType` mapping (with units + canonical aggregation rules), the APNs Node library decision (`@parse/node-apn` versus alternatives — chosen for type-safety + maintenance posture + lazy provider init), and the OpenAPI 3.1 tooling choice (`zod-openapi` + `yaml@^2` — picked for byte-stable output across runs once `sortMapEntries: true` is on). ### Wave 2 — Apple Health foundation Seven additive `MeasurementType` values (`HEART_RATE_VARIABILITY`, `RESTING_HEART_RATE`, `ACTIVE_ENERGY_BURNED`, `FLIGHTS_CLIMBED`, `WALKING_RUNNING_DISTANCE`, `VO2_MAX`, `BODY_TEMPERATURE`). `APPLE_HEALTH` joins `MeasurementSource`. A new `SleepStage` enum + a nullable `Measurement.sleepStage` column scoped by CHECK constraint to `SLEEP_DURATION` rows. Sleep is now persisted in minutes instead of hours. Composite unique index `(user_id, type, source, external_id)` becomes the Apple Health dedup key — the legacy `(user_id, type, measured_at, source)` index stays untouched. Migration `0036_apple_health_measurement_types` is strictly additive: no row mutations, no rename. `POST /api/measurements/batch` accepts ≤500 entries per call, wraps `withIdempotency()`, returns per-entry `inserted | duplicate | skipped` status with a typed reason, and is idempotency-replay safe. Sleep-stage aggregation in `/api/analytics` rolls multi-stage nights into one Berlin-day datapoint with a per-stage breakdown over the trailing 30 days. Apple Health source chip renders on both the desktop list and the mobile card variant. ### Wave 3 — APNs scaffolding `@parse/node-apn@8.1.0` joins the senders cascade as channel-type 4. Cascade order is now an explicit `channelPriority()` sort (APNs → Telegram → ntfy → Web Push, unknowns last) so a Postgres scan-order change can't reorder delivery between deploys. Provider is lazily-initialised per gateway (`sandbox` vs `production`), JWT auto-rotates inside the library. Permanent failures (`Unregistered`, `BadDeviceToken`, `DeviceTokenNotForTopic`) drop the dead `Device` row mirroring the web-push 410 cleanup. `Device` model gains nullable `apnsToken` + `apnsEnvironment` columns with a paired CHECK constraint (either both set or both null) plus a partial unique index on `apns_token` for defence-in-depth. `POST /api/devices` accepts paired `apnsToken` + `apnsEnvironment` with a 422 when one comes without the other and a 409 + `apns_token_owned_by_other_user` audit reason on cross-user re-registration. Production without `APNS_KEY_ID` is a no-op rather than a boot failure; partially-set env triggers a single warning at first dispatch. ### Wave 4 — OpenAPI generator, Coach metric slot, native-auth hardening, Coolify runbook The OpenAPI generator emits `docs/api/openapi.yaml` from Zod `.meta()` annotations. Eight iOS-touched routes are registered now: `auth/login`, passkey verify, `auth/refresh`, `measurements` GET + POST + batch, `devices` POST, and the comprehensive insights bundle. `pnpm openapi:check` diffs generated against committed; CI is warn-only for v1.4.23 (`continue-on-error: true`) so a registry oversight on a non-iOS route doesn't red-bar a PR, flips to hard-fail in v1.4.24. The legacy hand-maintained spec stays at `docs/api/openapi-v1422-legacy.yaml` during the incremental migration. `PROMPT_VERSION` ratchets 4.22.0 → 4.23.0 with a new GROUND RULE 12 (EN + DE): treat Apple Health categories as silent when the snapshot doesn't carry them. The strict insight schema's `sourceMetric` and `trendAnnotations` enums extend to admit nine additive HealthKit categories (HRV, sleep, resting HR, steps, active energy, flights, distance, VO2 max, body temp). The Coach snapshot pipeline queries the new measurement types only when scope toggles them on, so web-only accounts pay zero extra SQL. Refresh-token reuse-detection scopes the blast radius to the originating device — pre-1.4.23 a replay revoked every refresh token the user owned, which is the right call for security but a foot-gun for a two-device household. Legacy null-`deviceId` tokens still fall back to user-wide revoke as a safety hatch. Three new routes round it out: `GET /api/auth/me/devices` lists active devices with label, last-seen, channels, and an `isCurrent` marker keyed off the session's `deviceId` (not the forgeable `X-Device-Id` header). `DELETE /api/auth/me/devices/[id]` revokes one device transactionally: refresh tokens, access tokens, notification channels, push subscriptions, and the row. `DELETE /api/devices/[id]` is the native-friendly mirror the iOS APNs-rotation flow calls. Cross-user attempts return 404 with no enumeration leak. The Coolify auto-deploy runbook is captured end-to-end: `COOLIFY_WEBHOOK` + `COOLIFY_TOKEN` repo secrets, the "Watch image registry for new digests" UI toggle as the load-bearing piece, a `::notice::` line on the workflow step that surfaces the deploy timestamp + sha for triage, and the host-side retag fallback verbatim. The webhook URL stays in repo secrets — never in a tracked file. ### Wave 5 — Hygiene Seven backlog items from `.planning/v1422-backlog.md` shipped as atomic commits. - Sentinel parser malformed-enum hardening (`SentinelParseResult.malformedEntries[]` + per-line typed reasons; the chat route splits `coach.keyvalues.parse_partial` from the full-block `coach.keyvalues.parse_failed`). - Analytics-route unbounded `findMany` replaced with cursor-paged 5 000-row chunks via `fetchBpSeriesChunked`; `analytics.bp_in_target.row_count` wide-event meta added for slow-query attribution; regression test seeds 6 000 rows across a chunk boundary. - `` controlled-prop refactor: `useResettableValue` hook + pure `nextResettableValue` helper land the same UX without weaponising React keys. - Per-user prompt-tuning surface (Coach settings cog returns). `User.coachPrefsJson` migration `0038_coach_prefs`, `GET / PUT /api/auth/me/coach-prefs`, settings cog opens a right-edge ``, system prompt prepends a per-user OVERRIDE, snapshot reads prefs **before** measurement queries so `excludeMetrics` filters before the snapshot lands. - Schema drift on `medication_schedules.days_of_week` resolved — column is referenced by nine source files across four user-visible surfaces; migration `0039_medication_schedule_days_of_week` deploys it (NULL = daily, no backfill needed). - Pearson surfacing gate raised from n≥14 to n≥20 — conservative patch; the rigorous incomplete-beta replacement is queued for v1.4.24. - Coach helpful/unhelpful first-week observation view. Polymorphic `RecommendationFeedback.target_type` migration `0040_recommendation_feedback_target_type`; `POST /api/insights/chat/messages/:id/feedback`; `buildCoachFeedbackBuckets` aggregator slice by (`PROMPT_VERSION`, tone, verbosity); `/admin/coach-feedback` admin page with EN + DE i18n bundle and a sidebar entry. ### Wave 6 — Multi-agent review + reconcile Six reviews ran in parallel (code, security, design/UX, senior-dev, simplify, product-lead). The reconcile pack landed 11 atomic commits across five sessions: one CRITICAL applied, all nine HIGH findings applied, four of five simplify findings applied, three MEDIUM findings applied, all LOW findings triaged into `.planning/v1423-backlog.md`. The headline reconcile work: admin coach-feedback sidebar surface restored, APNs `NotificationChannel` auto-upsert on registration, partial unique index for global `apns_token` uniqueness, Apple Health source badge on the mobile card variant, Coach prefs skeleton + save toast, device revoke wrapped in a single transaction, `isCurrent` marker keyed off the session's `deviceId` rather than the forgeable header, Coach feedback FK migrated to reference `coach_messages` directly (the plaintext `content` column on `recommendation_feedback` is gone — encryption-at-rest is back to being the only on-disk form), the Coolify runbook URL scrubbed from tracked files, and the sentinel partial-malformed annotation work landing in the W5 H1 commit. Test deltas: 2191 → 2236 unit (+45 across the W5/W6 arc), 100 integration (with two pre-existing `coach-prefs.test.ts` `NextRequest` URL mock failures that predate v1.4.23 and are documented in the backlog). ### Wave 7 — Release Pre-release verify (typecheck / lint / openapi:check / 2236 unit / 110 of 112 integration — the two failures are the pre-existing `coach-prefs.test.ts` carryover, not a v1.4.23 regression). `package.json` bumped 1.4.22 → 1.4.23. CHANGELOG entry covering Added / Changed / Fixed / Security / Refactor / Deferred-to-v1.4.24 / Deferred-to-v1.5. `chore(release): v1.4.23` on `develop`. `Release v1.4.23` no-fast-forward merge on `main`. Tag `v1.4.23` pushed. GHCR build green. The Coolify auto-deploy fired with `force=true` via the MCP API call and reported "finished" in 18 seconds — the same `:latest` digest-cache fault that hit v1.4.20 and v1.4.21. The CI workflow logged `COOLIFY_WEBHOOK or COOLIFY_TOKEN secret missing — skipping auto-deploy` (the same maintainer-action gap the v1.4.22 brief flagged for v1.4.23 prep), so the auto-deploy path didn't even attempt to run. The host-side fallback was the recipe captured in `.planning/coolify-auto-deploy-howto.md`: `docker pull ghcr.io/mbombeck/healthlog:1.4.23` on `apps-01` (digest `sha256:b20c25a49b…`), `docker tag …:1.4.23 …:latest`, `docker compose up -d --force-recreate app` in `/data/coolify/applications/pg8wggwogo8c4gc4ks0kk4ss`. Container recreated cleanly, health check turned green inside ~30 seconds, `/api/version` flipped to 1.4.23 at 2026-05-11T06:40:49Z. ## iOS readiness — what v1.4.23 gives the iOS app on day zero Every server-side contract the v1.5 P1 plan assumed is now locked in `main`: - `POST /api/auth/login` with `X-Client-Type: native` returns the bearer + refresh + `deviceId` tuple. - `POST /api/auth/refresh` accepting `hlr_*` and returning a fresh pair, with per-device reuse-detection scoping the replay-blast radius. - `POST /api/devices` accepting `apnsToken` + `apnsEnvironment` with the cross-user-hijack guard duplicated at the APNs-token layer. - `GET /api/auth/me/devices` for the iOS Settings → Devices tab. - `DELETE /api/devices/[id]` for the iOS APNs-rotation cleanup path. - `POST /api/measurements/batch` for the Apple Health sync (P2 itself, but contract is locked in v1.4.23). - `GET /api/measurements?from=&to=&type=` — already shipped in v1.4 for the web app, unchanged. - `docs/api/openapi.yaml` regenerates byte-identically from `src/lib/openapi/routes.ts` and covers all eight P1-touched routes. The iOS DTO codegen has a stable target. Thirteen open iOS-DTO questions were consolidated in `.planning/phase-W4-v1423-report.md` (lines 137-199) and routed across v1.5 phases in `.planning/phase-W6-v1423-product-lead-review.md` section C. P1 owns three (APNs token hex wire format, `apnsEnvironment` mapping at `#if DEBUG` site, `X-Device-Id` header on `/api/auth/me/devices`); P2 owns five (the `HEALTHKIT` → `APPLE_HEALTH` rename on the iOS DTO, `externalId` shape, `sleepStage` numeric codepoint, no pre-conversion on the wire, unknown-identifier park-for-retry behaviour); P3 owns four (multi-device cascade behaviour, `collapseId` shape, device-list channel rendering, refresh + device-deletion race handling); P4 owns one (token rotation cadence). The iOS Swift code lands answers as it lands; nothing on the server side blocks the answer. Server-side files touched in v1.5 P1: zero. The whole P1 wave is iOS Swift work against a stable server contract. ## Quality - Two format-sweep + planning commits on `develop` (`f1e6630` prettier sweep, `766a9ae` W1 research + W5/W6 review packs), one OpenAPI regen (`4183552`), one release-prep (`d2331d9 chore(release): v1.4.23`) - Release-merge `0dc0e16 Release v1.4.23` on `main` - W2/W3/W4/W5 produced 28 atomic commits across `develop` between v1.4.22 tag and the release-prep commit - W6 reconcile produced 11 atomic commits across `develop` (one CRITICAL, all nine HIGH, four simplify, three MEDIUM) - Tests: 2191 → 2236 unit (+45 across the W5/W6 arc), 110 / 112 integration with the two pre-existing `coach-prefs.test.ts` `NextRequest` URL mock failures carried from v1.4.22. Typecheck clean, lint baseline (21 warnings, no new — none were new in this release), `openapi:check` clean. ## Branch model `develop` is the daily target; `main` is release-only and follows tags. v1.4.23 is the fourth tag through the `develop` → `main` release-merge model since it landed in v1.4.20 F1. Future GHCR builds and Coolify auto-deploy fire only on `main`. The Coolify "Watch image registry for new digests" UI toggle is the load-bearing piece — verified on this release, runbook captured in `.planning/coolify-auto-deploy-howto.md`. ## Carry-overs - **Coolify auto-deploy still needs maintainer action.** Third release in a row that landed via the host-side retag fallback because the `COOLIFY_WEBHOOK` + `COOLIFY_TOKEN` repo secrets aren't set (and even when the MCP API fires `force=true`, the `:latest` digest in the local Docker cache pins the previous version). The W4 F8 runbook + the `::notice::` line on the workflow now make the failure mode visible; the durable fix is the two secrets + the Coolify "Watch image registry for new digests" UI toggle. Documented in `.planning/coolify-auto-deploy-howto.md`. - **`coach-prefs.test.ts` integration `NextRequest` URL mock regression** — pre-existing failure that predates v1.4.23 (surfaced during W6 Session A; reproduces with reconcile changes stashed). Two test cases fail; non-blocking for the release. Tracked in `.planning/v1423-backlog.md` under "Test infrastructure" — needs investigation into how `NextRequest`'s URL parsing interacts with the test harness's mocked `cookies()`. - **Pearson incomplete-beta replacement** — v1.4.23 W5 H6 raised `MIN_PAIRED_N` from 14 → 20 as a conservative surfacing-gate fix. The rigorous replacement is the v1.4.24 candidate and should land before correlation auto-discovery widens in v1.5/v1.6. - **OpenAPI drift gate is warn-only for v1.4.23.** Generator covers ~880 lines vs the legacy hand-maintained spec's 5 468. Flip to hard-fail in v1.4.24 and complete registry coverage in parallel; the risk window is "every PR between v1.4.23 and v1.4.24" — pull v1.4.24 forward if the iOS DTO codegen starts compiling against the spec sooner than expected. - **Settings-cog vs per-message-controls debate.** Design pushback during the W6 review raised concern that the dual surface duplicates intent. Defer to v1.4.24 once the first-week thumbs data shows whether per-user prompt prefs drift from per-message ratings. - **The remaining security MEDs + all LOWs** — listed in `.planning/v1423-backlog.md` lines 26-54. Intra-batch dedup accounting, idempotency 422 retry hint on the OpenAPI 422 description, APNs key-file path redaction in wide-event meta, refresh-token failure audit `userId` extension. None are user-facing; all flagged for v1.4.24. ## Strategic next v1.5 P1 (iOS first launch — login + dashboard + widget) is now a ~5-day iOS Swift sprint against locked server contracts. The detailed P1 plan with concrete iOS file paths lives in `.planning/phase-W6-v1423-product-lead-review.md` section C. P2 (Apple Health sync) is ~5 days of iOS work on `HealthKitService` + `SyncCoordinator` against `POST /api/measurements/batch`. P3 (Coach extended for HRV / Sleep / Resting HR / Steps) ratchets `PROMPT_VERSION` 4.23.0 → 5.0.0 and copies the web `` UX to iOS. P4 (per-metric APNs alerts) leans on the W3 scaffolding for the send-side. P5 (web polish + v1.5 release brief) is unchanged from the v1.4.22 plan — the Insights page split now has a concrete sub-tree map because v1.4.23 froze the schema underneath it. P6 (cross-user feedback aggregation cron) becomes "wire the existing aggregator to a daily `pg-boss` schedule + append OMIT/REPHRASE rules to `PROMPT_VERSION` when a bucket's helpful-rate drops below 50%" — the aggregator code already exists in v1.4.23. GitHub release: https://github.com/MBombeck/HealthLog/releases/tag/v1.4.23