v1.4.23 is a foundation release: small user-visible delta on purpose, because the weight is in the contract surface the iOS app gets to compile against on day zero.
Added
- Apple Health measurement schema + batch-ingest contract. Seven new
MeasurementTypevalues (HEART_RATE_VARIABILITY,RESTING_HEART_RATE,ACTIVE_ENERGY_BURNED,FLIGHTS_CLIMBED,WALKING_RUNNING_DISTANCE,VO2_MAX,BODY_TEMPERATURE).APPLE_HEALTHjoinsMeasurementSource. A newSleepStageenum + nullableMeasurement.sleepStagecolumn scoped via CHECK constraint toSLEEP_DURATIONrows. Sleep is now persisted in minutes. Composite unique index(user_id, type, source, external_id)is the Apple Health dedup key. Migration0036is strictly additive — no row mutations, no rename. New endpointPOST /api/measurements/batchaccepts ≤500 entries per call, wrapswithIdempotency(), returns per-entryinserted | duplicate | skippedstatus, and is idempotency-replay safe. Sleep-stage aggregation in/api/analyticsrolls multi-stage nights into one Berlin-day datapoint. - APNs scaffolding + dispatcher cascade rewire.
@parse/node-apnjoins the senders cascade (APNs → Telegram → ntfy → Web Push, deterministic order). Provider is lazy-initialised per gateway, JWT auto-rotates. Permanent failures drop the deadDevicerow mirroring the web-push 410 cleanup.Devicemodel gains nullableapnsToken+apnsEnvironmentcolumns with a paired CHECK constraint plus a partial unique index for defence-in-depth.POST /api/devicesaccepts the paired fields with a 422 when one comes without the other and a 409 + dedicated audit reason on cross-user re-registration. Production withoutAPNS_KEY_IDis a no-op rather than a boot failure. - OpenAPI 3.1 generator + drift CI gate.
pnpm openapi:generateemits a byte-stabledocs/api/openapi.yamlfrom Zod.meta()annotations. The eight iOS-touched routes are registered.pnpm openapi:checkdiffs generated against committed; CI is warn-only for v1.4.23, flips to hard-fail in v1.4.24. - Device-management endpoints.
GET /api/auth/me/deviceslists active devices with label, last-seen, channels, and anisCurrentmarker keyed off the session'sdeviceId(not the forgeable header).DELETE /api/auth/me/devices/[id]revokes one device transactionally.DELETE /api/devices/[id]is the native-friendly mirror the iOS APNs-rotation flow calls. - Per-user Coach prefs surface (settings cog returns).
User.coachPrefsJsoncolumn +GET / PUT /api/auth/me/coach-prefs. The settings cog opens a right-edge sheet for tone, verbosity, focus / exclude metrics. The snapshot pipeline reads prefs before measurement queries so excluded metrics never enter the snapshot. - Per-message Coach thumbs feedback + admin aggregate view. Each Coach reply renders a 👍 / 👎 affordance;
RecommendationFeedbackgains a polymorphictarget_typediscriminator. NewPOST /api/insights/chat/messages/:id/feedback. Admin section/admin/coach-feedbackrenders helpful-rate buckets by (PROMPT_VERSION, tone, verbosity).
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-
deviceIdtokens still fall back to user-wide revoke as a safety hatch. - Pearson surfacing gate raised from n≥14 to n≥20. Conservative patch on the low-df p-value path; the rigorous incomplete-beta replacement is queued for v1.4.24.
- Coach drawer prefill becomes a controlled prop.
useResettableValuehook + purenextResettableValuehelper replace thekey={prefill}mount-cycle hack. /api/analyticsBP-in-target aggregate becomes cursor-paged. UnboundedfindManyreplaced withfetchBpSeriesChunked(5 000-row chunks); regression test seeds 6 000 rows across a chunk boundary.- Coolify webhook contract documented end-to-end. GHCR build →
force=trueCoolify deploy →/api/versionpoll → host-side retag fallback if the:latestdigest hasn't moved. - Coach feedback foreign-key targets
coach_messagesdirectly. Plaintextcontentcolumn onrecommendation_feedbackis retired. - PROMPT_VERSION 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. Strict schema's
sourceMetricandtrendAnnotationsenums extend to admit nine additive HealthKit categories. medication_schedules.days_of_weekcolumn deployed (migration0039, NULL = daily).
Fixed
- Admin coach-feedback sidebar entry restored.
- APNs
NotificationChannelauto-upserted on device registration. - Partial unique index enforces global
apns_tokenuniqueness as defence-in-depth. - Apple Health source badge renders on the mobile measurement-card variant.
- Coach prefs sheet skeleton + save toast.
- Device-revoke cascade wraps refresh + access + channels + Device row in one transaction.
isCurrentdevice marker keys off the session'sdeviceId, not the forgeableX-Device-Idheader.- Sentinel parser annotates partial-malformed entries instead of collapsing the whole block.
Security
- APNs send-side defence-in-depth (hex format at registration, cross-user-hijack guard at the token layer, redacted send-side payload logging).
- Coolify webhook URL scrubbed from tracked files.
- Coach prose encryption-at-rest restored after the feedback FK migration.
Refactor
revokeDeviceCascade(deviceId)helper collapses three inline call sites.useCoachPrefs()hook collapses fetch + mutate + invalidate triplet.buildCoachSnapshottakes a singlescoperecord instead of seven booleans.- OpenAPI registry uses static imports for deterministic generator output.
Deferred to v1.4.24
- Pearson incomplete-beta p-value, OpenAPI drift gate flip to hard-fail, settings-cog vs per-message-controls UX consolidation,
coach-prefs.test.tsNextRequestURL mock fix, security MED + LOW cluster.
Deferred to v1.5
- iOS native client P1 (login + dashboard + widget), Apple Health sync P2, Coach extended for HRV / Sleep / Resting HR / Steps P3, per-metric APNs alerts P4.
Full changelog: https://github.com/MBombeck/HealthLog/blob/main/CHANGELOG.md