From d2331d97563f5f9de443b95bcdc4961a4d804c65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Mon, 11 May 2026 08:30:23 +0200 Subject: [PATCH] chore(release): v1.4.23 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-iOS prep + hygiene. Apple Health backend foundation (schema + batch ingest + APNs scaffolding), OpenAPI generator + warn-only CI gate, per-device refresh-token revocation + device-management endpoints, Coach Apple Health metric slot at PROMPT_VERSION 4.23.0, per-user Coach prefs surface, per-message thumbs feedback loop. Seven W5 hygiene items closed alongside. User-visible delta is small on purpose — the weight is in the contract surface the iOS app gets to compile against on day zero. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 211 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 212 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 535dc5f28..64f9fc0f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,216 @@ # Changelog +## [1.4.23] — 2026-05-11 + +### Added + +- **Apple Health measurement schema + batch-ingest contract.** Seven + new `MeasurementType` values (`HEART_RATE_VARIABILITY`, + `RESTING_HEART_RATE`, `ACTIVE_ENERGY_BURNED`, `FLIGHTS_CLIMBED`, + `WALKING_RUNNING_DISTANCE`, `VO2_MAX`, `BODY_TEMPERATURE`), + `APPLE_HEALTH` joins `MeasurementSource`, and `Measurement` picks + up a nullable `sleepStage` column scoped via CHECK constraint to + `SLEEP_DURATION` rows. Sleep is now persisted in minutes instead + of hours. Migration `0036_apple_health_measurement_types` is + strictly additive — no row mutations, no rename, the legacy + `(user_id, type, measured_at, source)` unique index stays. A new + composite unique index `(user_id, type, source, external_id)` + becomes the Apple Health dedup key. New endpoint + `POST /api/measurements/batch` accepts ≤500 entries per call, + wraps `withIdempotency()`, returns per-entry + `inserted | duplicate | skipped` status with a typed reason field, + 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. +- **APNs scaffolding + dispatcher cascade rewire.** `@parse/node-apn` + joins the senders cascade as channel-type 4 (APNs → Telegram → ntfy + → Web Push, deterministic order). 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) and a partial unique index on `apns_token`. + Migration `0037_apns_device_columns`. `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 when a token already belongs to a different account. + Production without `APNS_KEY_ID` is a no-op rather than a boot + failure; partially-set env triggers a single warning at first + dispatch. +- **OpenAPI 3.1 generator + drift CI gate.** `pnpm openapi:generate` + reads Zod v4 `.meta()` annotations on the existing validation + schemas and emits a byte-stable `docs/api/openapi.yaml` (`zod-openapi` + + `yaml@^2` with `sortMapEntries: true`). The eight iOS-critical + routes are registered now: `auth/login`, passkey verify, + `auth/refresh`, `measurements` GET + POST + batch, `devices` POST, + and the comprehensive insights bundle. A new `pnpm openapi:check` + CI step diffs generated vs committed; warn-only for v1.4.23 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 is + preserved at `docs/api/openapi-v1422-legacy.yaml` so iOS DTO + reference doesn't disappear during the incremental migration. +- **Device-management endpoints for native + web settings.** + `GET /api/auth/me/devices` lists active devices with label, last- + seen timestamp, channels (`web_push` / `apns`), and an `isCurrent` + marker keyed off the session's `deviceId` (not the forgeable + `X-Device-Id` header). `DELETE /api/auth/me/devices/[id]` revokes + a single device in one transaction: refresh tokens, access tokens, + notification channels, push subscriptions, and the `Device` 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. +- **Per-user Coach prefs surface (settings cog returns).** New + `User.coachPrefsJson` column (migration `0038_coach_prefs`) + + `GET / PUT /api/auth/me/coach-prefs`. The settings cog on the + Coach drawer opens a right-edge `` letting users dial in + `tone`, `verbosity`, focus-metrics, and exclude-metrics. The + system prompt prepends a per-user OVERRIDE block; the snapshot + pipeline reads prefs BEFORE measurement queries so excluded + metrics never enter the snapshot in the first place. +- **Per-message Coach thumbs feedback + admin aggregate view.** Each + Coach reply renders a 👍 / 👎 affordance. `RecommendationFeedback` + gains a polymorphic `target_type` discriminator (migration + `0040_recommendation_feedback_target_type`); legacy recommendation + feedback rows backfill to `RECOMMENDATION`, new Coach rows persist + with `COACH_MESSAGE` and the message's `coach_messages.id`. New + endpoint `POST /api/insights/chat/messages/:id/feedback`. A new + admin section `/admin/coach-feedback` renders helpful-rate buckets + by (promptVersion, tone, verbosity) and gets a sidebar entry + + EN/DE i18n bundle. + +### Changed + +- **Refresh-token reuse-detection scopes to the originating device.** + Pre-1.4.23 a replayed refresh token revoked every refresh token + the user owned. v1.4.23 narrows the blast radius to the device + that issued the token (legacy null-deviceId tokens still fall back + to user-wide revoke as a safety hatch). A two-device household no + longer gets logged out of both phones when one replays. +- **Pearson surfacing gate raised from n≥14 to n≥20.** Conservative + patch on the low-df p-value path — trades a small number of + borderline correlation cards for stricter false-positive control. + The rigorous incomplete-beta replacement is queued for v1.4.24. +- **Coach drawer prefill becomes a controlled prop.** The + `` mount-cycle hack is replaced with a + `useResettableValue` hook + pure `nextResettableValue` helper. The + drawer rerenders without remounting; the prefill state lives on + the parent as a fully-controlled input. +- **`/api/analytics` BP-in-target aggregate becomes cursor-paged.** + The unbounded `findMany` lands as `fetchBpSeriesChunked` cursoring + in 5 000-row batches with a new `analytics.bp_in_target.row_count` + wide-event for slow-query attribution. Replay regression test + seeds 6 000 rows across a chunk boundary. +- **Coolify webhook contract documented end-to-end.** GHCR build → + `force=true` Coolify deploy → `/api/version` poll → host-side + retag fallback if the `:latest` digest hasn't moved. The CI step + now emits a `::notice::` line with the deploy timestamp + image + sha so future runs surface the contract without opening the + verbose log. +- **Coach feedback foreign-key targets `coach_messages` directly.** + The plaintext `content` column on `recommendation_feedback` is + retired — feedback rows now reference the canonical encrypted + message row, and the aggregator queries through the FK rather + than reading prose into memory. +- **PROMPT_VERSION 4.22.0 → 4.23.0** with a new GROUND RULE 12 + (EN + DE): treat Apple Health categories (HRV, sleep, resting HR, + steps, active energy, flights, distance, VO2 max, body temp) as + silent when the snapshot doesn't carry them. No apologetic openers + about missing data. `aiInsightResponseSchema.dailyBriefing + .keyFindings[].sourceMetric` and `trendAnnotations` enums extend + to admit the nine additive HealthKit categories. +- **`medication_schedules.days_of_week` column deployed.** Migration + `0039_medication_schedule_days_of_week` adds the nullable column + (NULL = daily). Closes the v1.4.22 schema-drift watchlist item. + +### Fixed + +- **Admin coach-feedback sidebar entry.** The W5 H7 admin section + shipped without a sidebar nav entry, so the page was unreachable + from the chrome. +- **APNs `NotificationChannel` auto-upsert on device registration.** + Registering a fresh device with an `apnsToken` now creates the + matching `NotificationChannel` row in the same transaction + instead of leaving the device row orphaned from the cascade. +- **Partial unique index enforces `apns_token` global uniqueness.** + App-layer guard catches the cross-user-hijack case; the DB-layer + partial unique index (`CREATE UNIQUE INDEX … WHERE apns_token IS + NOT NULL`) is the defence-in-depth backstop. +- **Apple Health source badge renders on mobile measurement card.** + Desktop list rendered the chip; the mobile card variant fell back + to source-less prose. Both surfaces now share the same renderer. +- **Coach prefs sheet skeleton + save toast.** Initial open painted + a flash of unstyled defaults; save action ran silently. The sheet + now shows a skeleton during the prefetch and confirms persistence + via the standard toast. +- **Device revoke cascade wraps refresh + access + channels + Device + row in one transaction.** Pre-fix a mid-flight failure could leave + the device row gone while the refresh tokens stayed live. +- **`isCurrent` device marker keys off the session's `deviceId`, + not `X-Device-Id`.** The header is forgeable; the cookie-bound + session record is not. Regression test in the device-list route. +- **Sentinel parser annotates partial-malformed entries instead of + collapsing the whole block.** `SentinelParseResult.malformedEntries[]` + now carries per-line typed reasons; the chat route splits + `coach.keyvalues.parse_partial` from the full-block + `coach.keyvalues.parse_failed` wide-event for partial recovery. + +### Security + +- **APNs send-side defence-in-depth.** Hex format validated at the + registration boundary; cross-user-hijack guard duplicated at the + APNs-token layer (409 + dedicated audit reason). Send-side payload + redacts the resolved user + device IDs before logging. +- **Coolify webhook URL scrubbed from the deploy runbook.** Webhook + + token live in GH secrets; the runbook references the secret + names rather than the raw URL. +- **Coach prose encryption-at-rest restored.** The plaintext + `content` column on `recommendation_feedback` was dropped after + the feedback rows migrated to a `coach_messages` FK. Cypher text + is now the only on-disk form. + +### Refactor + +- **`revokeDeviceCascade(deviceId)` helper.** The four-way revoke + (refresh + access + channels + device row) lived inline at three + call sites. Helper now owns the transaction wrapper; call sites + drop to one line. +- **`useCoachPrefs()` hook.** Coach drawer + settings sheet shared + the same fetch + mutate + invalidate triplet; the hook collapses + the three call sites into one with shared optimistic-update logic. +- **`buildCoachSnapshot` scope-record argument.** The build pipeline + passed seven booleans across three callees; collapses to a single + `scope` record. Per-stream toggles read off `scope.has(metric)`. +- **OpenAPI registry uses static imports instead of dynamic + require.** The lazy require pattern broke type inference at the + registration site; the static import keeps the generator output + deterministic. + +### Deferred to v1.4.24 + +- Pearson incomplete-beta p-value (W5 H6 carried as the + conservative n≥20 patch). +- Settings-cog vs per-message-controls UX consolidation + (waits on first-week thumbs data). +- OpenAPI drift gate flip from warn-only to hard-fail + (requires registry catch-up first). +- `coach-prefs.test.ts` integration `NextRequest` URL mock + regression — predates v1.4.23, surfaced during the W6 reconcile. +- Sec-MED-1 follow-ups: intra-batch dedup accounting (Sec-LOW-1), + idempotency 422 retry hint (Sec-LOW-2), APNs key-file path + redaction (Sec-LOW-3), refresh-failure audit userId (Sec-LOW-4). + +### Deferred to v1.5 + +- iOS native client (P1: login + dashboard + widget) — server + contracts all locked in v1.4.23. +- Apple Health sync (P2) — schema + batch endpoint shipped; iOS + `HealthKitService` + `SyncCoordinator` wire still pending. +- Coach extended for HRV / Sleep / Resting HR / Steps (P3) — schema + slot + GROUND RULE 12 landed; prompt rules + i18n bundles pending. +- Per-metric APNs alerts (P4) — sender + dispatcher shipped; per- + event opt-out + background pushes pending. + ## [1.4.22] — 2026-05-10 ### Added diff --git a/package.json b/package.json index 333fe04e5..83d753142 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "healthlog", - "version": "1.4.22", + "version": "1.4.23", "description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.", "license": "AGPL-3.0-only", "homepage": "https://healthlog.dev",