7 conflict files resolved by taking the upstream/HEAD side (which carries the W4d/W5/W6c/W3f feature additions). Stashed-side changes that were genuine new features re-land in subsequent commits if any were lost. This sweeps up the loose state the W4e+W5e+W5d combined agent left mid-stash. Resolves develop's working tree for clean push.
18 KiB
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 splitscoach.keyvalues.parse_partialfrom the full-blockcoach.keyvalues.parse_failed). - Analytics-route unbounded
findManyreplaced with cursor-paged 5 000-row chunks viafetchBpSeriesChunked;analytics.bp_in_target.row_countwide-event meta added for slow-query attribution; regression test seeds 6 000 rows across a chunk boundary. <CoachDrawer key={prefill}>controlled-prop refactor:useResettableValuehook + purenextResettableValuehelper land the same UX without weaponising React keys.- Per-user prompt-tuning surface (Coach settings cog returns).
User.coachPrefsJsonmigration0038_coach_prefs,GET / PUT /api/auth/me/coach-prefs, settings cog opens a right-edge<Sheet>, system prompt prepends a per-user OVERRIDE, snapshot reads prefs before measurement queries soexcludeMetricsfilters before the snapshot lands. - Schema drift on
medication_schedules.days_of_weekresolved — column is referenced by nine source files across four user-visible surfaces; migration0039_medication_schedule_days_of_weekdeploys 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_typemigration0040_recommendation_feedback_target_type;POST /api/insights/chat/messages/:id/feedback;buildCoachFeedbackBucketsaggregator slice by (PROMPT_VERSION, tone, verbosity);/admin/coach-feedbackadmin 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/loginwithX-Client-Type: nativereturns the bearer + refresh +deviceIdtuple.POST /api/auth/refreshacceptinghlr_*and returning a fresh pair, with per-device reuse-detection scoping the replay-blast radius.POST /api/devicesacceptingapnsToken+apnsEnvironmentwith the cross-user-hijack guard duplicated at the APNs-token layer.GET /api/auth/me/devicesfor the iOS Settings → Devices tab.DELETE /api/devices/[id]for the iOS APNs-rotation cleanup path.POST /api/measurements/batchfor 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.yamlregenerates byte-identically fromsrc/lib/openapi/routes.tsand 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(f1e6630prettier sweep,766a9aeW1 research + W5/W6 review packs), one OpenAPI regen (4183552), one release-prep (d2331d9 chore(release): v1.4.23) - Release-merge
0dc0e16 Release v1.4.23onmain - W2/W3/W4/W5 produced 28 atomic commits across
developbetween 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.tsNextRequestURL mock failures carried from v1.4.22. Typecheck clean, lint baseline (21 warnings, no new — none were new in this release),openapi:checkclean.
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_TOKENrepo secrets aren't set (and even when the MCP API firesforce=true, the:latestdigest 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.tsintegrationNextRequestURL 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.mdunder "Test infrastructure" — needs investigation into howNextRequest's URL parsing interacts with the test harness's mockedcookies().- Pearson incomplete-beta replacement — v1.4.23 W5 H6 raised
MIN_PAIRED_Nfrom 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.mdlines 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 audituserIdextension. 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 <CoachDrawer> 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