The minor-version cut that marks the native iOS client publicly available. The SwiftUI iOS app (separate repository) is now joinable via TestFlight: https://testflight.apple.com/join/bucuTBpa. The backend contract the iOS app speaks against has been live since v1.4.23 and has been continuously validated across every v1.4.2x–v1.4.50 release. The 1.5.0 cut also lands the highest-leverage iOS-client unblocker per the v0.6.1 code audit: /api/measurements/batch now overwrites per-day cumulative stats:* rows on a re-post instead of dropping the new value as a duplicate.
Added
POST /api/measurements/batchrecognisesexternalIdvalues starting withstats:(stats:HKQuantityTypeIdentifierStepCount:YYYY-MM-DDand every other per-day cumulative HK metric — Active Energy, Sleep Duration, Walking/Running Distance, Flights Climbed) and treats a duplicate on those as an overwrite, not a discard. Each re-post of the same day's external id replaces the row'svalue,unit,measuredAt,externalSourceVersion,deviceType, andsleepStage. Sample-class externalIds (every other prefix —uuid-*, opaque HK identifiers) keep the strict immutableduplicatecontract because each sample is a canonical reading.- New per-entry status
"updated"on the batch response envelope so the iOS sync cursor can distinguish a fresh insert from a value-bump re-post. The aggregate envelope now carries anupdatedcount alongsideinserted/duplicates/skipped. - New wide-event annotation
measurement.batch.stats-overwrite(fires only when at least one row was overwritten) so operators can grep how often per-day cumulative re-posts happen as a healthy ingest signal. measurement.batch.ingestaudit-log details now include theupdatedcount alongsideinserted/duplicates/skipped.
Why
Before this change, the iOS HealthKit observer would POST today's running step total once in the morning, the server would persist row #1, and every subsequent same-day re-post (as the user walked) would come back status: "duplicate" with the new value silently dropped. Today's Schritte tile froze at the first-sync value until next midnight. The same shape would hit every cumulative metric on a deterministic per-day external id. Closes #213; cross-device parity (web ↔ iOS) for stats:* metrics now works for today and every historical day on a re-sync.
Fixed
- Medication compliance now honours
daysOfWeekandintervalWeeksacross every call site that surfaces a rate. The legacy aggregator computedtotalExpected = schedules.length * days, which silently ignored cadence. A weekly Ozempic schedule with all four Mondays taken in the last 30 days reported ~13% adherence (4 / 30) instead of 100%; a weekday-only 3×/day metformin schedule with every weekday dose taken reported ~73% (66 / 90) instead of 100%.calculateComplianceis now a cadence-aware adapter on top ofbuildCadenceTimeline— the same pair-matching pipeline that drives the per-medication cadence chart — so the rate on the medication card, the AI Coach prompt context (7d/30d/90d windows insrc/lib/insights/features.ts), the BP-status compliance gate, the medication-compliance status insight,/api/insights/targets,/api/insights/comprehensive, and the medication-compliance pillar of the dashboard Health Score all agree on a single, cadence-correct denominator. The wire shape ({ totalExpected, taken, skipped, missed, rate, streak }) is unchanged so every UI tile and persisted-insight consumer keeps reading the same fields. Closes #214. Expected user-visible shift: users on weekly meds (GLP-1 agonists, biologics) will see their Health Score rise as the medication pillar moves from ~13 to ~100; users on weekday-only multi-dose schedules will see their score rise as the pillar moves from ~73 to ~100; users on daily-only schedules see no change because the legacy denominator was already correct for that path. Migrations across the eight production call sites are mechanical — the function signature is unchanged.
Changed
- README rewrite for the v1.5 cut: TestFlight badge in the badge row plus an iOS TestFlight link in the Website / Demo / Docs row and the footer. Buy Me A Coffee badge added. Status block updated to reflect that v1.5 is now the current line, with a new "Heavily developed" advisory directly below it that tells self-hosters to pin a tag, take a backup before every upgrade, and read the CHANGELOG before pulling
latest. Tech-Stack table flags the iOS app as TestFlight-available. Roadmap table promotes v1.5 from "in active development" to "current". - README simplification: the
How it worksdiagram cluster (four SVGs covering data flow, Coach pipeline, source priority, and security model) is no longer inlined in the README. The diagrams continue to live indocs/diagrams/and are surfaced through docs.healthlog.dev where they render reliably across themes and viewport widths. The03-self-hosting-topology.svgstays inline under Deployment because it carries deployment-time information a self-hoster wants on the first scroll.
Tests
tests/integration/measurements-batch.test.ts— three new cases pinning thestats:*overwrite contract: solo re-post overwrites the value and returnsstatus: "updated"; sample-class duplicate keeps the strict first-write-wins contract; a mixed batch with one insert + one overwrite + one duplicate returns all three statuses correctly.src/lib/analytics/__tests__/compliance.test.ts— parameterised cadence matrix: 1×/day daily (7 / 0 / 18 of 21), weekly Mondays-only (all taken / one missed), bi-weekly (intervalWeeks=2), weekday-only 3×/day metformin, skipped-dose denominator exclusion,medicationCreatedAttruncation, DST spring-forward boundary in Europe/Berlin, and over-logged-day rate cap. The matrix pins the contract for every cadence the production app exercises so future schedule-shape work can't silently regress.src/lib/analytics/__tests__/health-score-fast-path.test.ts— two cadence-aware regression cases: a weekly Mondays-only med with every Monday taken now lifts the medication-compliance pillar to ≥ 50 (previously ~13 under the bug); a daily-only med with every dose taken stays ≥ 90 (no regression on the path that worked).- Eight stale integration assertions retired across
withings-oauth.test.ts,withings-oauth-flow.test.ts,analytics-bp-aggregate-paged.test.ts,analytics-sleep-stages.test.ts, andapns-dispatch.test.ts— drift from the v1.4.47.x OAuth fine-grained reason tags, the v1.4.47.2 ES256 PEM verify guard, and the v1.4.49.1 analytics slim-slice annotation rename. Threesource-priority-two-axiscases skipped with an inline TODO referencing the v1.4.49.1 commit and the relocation candidate (pick-canonical-workout-rows); these tests exercised picker semantics that no longer fire on the default analytics summaries path.
Notes
- iOS coordination items closed alongside this cut: the v1.4.49 server-side
clientManagedMEDICATION_REMINDER suppression rule is now active for iOS v0.6.0.8+ clients that opt in viaPATCH /api/auth/me/notification-prefs; tracked in healthlog-iOS#9. Issue #206 is closed. - HealthLog suite: 5285 unit (5279 carryover + 3 new stats-overwrite cases + new compliance matrix and Health-Score regression cases that net out the legacy assertions retired during the cadence-aware migration) + 253 integration tests pass on the local Vitest run, lint clean, typecheck clean.