fix(apple-health): take a percent from a fraction or an already-scaled client

Shipped iOS builds multiply oxygen saturation and body fat by 100 before
upload. The importer scaled them again, so 0.97 was stored as 9700, the
plausibility guard rejected the row as out of range inside an HTTP 200,
and the client treats that skip as final and moves its HealthKit anchor
past the sample. Every reading of those two types was lost, silently, on
every install that syncs them.

The contract says values travel raw and the server scales, and that is
what the OpenAPI description publishes. One comment in the schema said
the opposite, that the client must multiply by 100 first, and that is
the line the app was built against. It is corrected here.

The five percent-shaped HealthKit identifiers now take either spelling:
a value above the fraction ceiling is already a percent and passes
through, anything at or below it is scaled. The domains do not overlap,
and the per-type range guard still rejects what is impossible either
way. Current app builds stop losing readings without an app release.

The tests pin both spellings, the range after conversion, and that the
tolerant converter covers the percent class and nothing else: waist
circumference uses the same factor to turn metres into centimetres, and
a blanket change made a 1.02 m waist into 1.02 cm.

Refs MBombeck/healthlog-iOS#7
This commit is contained in:
Marc-André Bombeck committed 2026-09-18 16:40:11 +02:00
1 parent 244d0e4b69
commit 9d9e8570f7
3 files changed
+97 -9

No files matched your search

+1 -1
View File
@@ -1374,7 +1374,7 @@ enum MeasurementType {
BLOOD_GLUCOSE // Canonical storage: mg/dL. Display unit (mg/dL or mmol/L) is user preference.
TOTAL_BODY_WATER // Canonical storage: kilograms (kg of water). Matches Withings' hydration measure (type 77) and Health Connect's TotalBodyWaterRecord. Display can compute % from same-day weight if desired.
BONE_MASS // Canonical storage: kilograms (kg). Matches Withings' bone mass measure (type 88).
OXYGEN_SATURATION // Canonical storage: percent (0..100). Withings ScanWatch reports type 54. Health Connect's OxygenSaturationRecord uses Percentage (already 0..100). HealthKit uses HKQuantityTypeIdentifierOxygenSaturation as a fraction (0..1) — iOS DTO must multiply by 100 before submitting.
OXYGEN_SATURATION // Canonical storage: percent (0..100). Withings ScanWatch reports type 54. Health Connect's OxygenSaturationRecord uses Percentage (already 0..100). HealthKit uses HKQuantityTypeIdentifierOxygenSaturation as a fraction (0..1); clients send the raw HealthKit reading and the server scales it at ingest (see `apple-health-mapping.ts`). This line used to say the opposite, and the iOS client built against it, which cost every user their oxygen-saturation and body-fat readings until the importer learned to take both.
// ── v1.4.23 Apple Health additions ──
HEART_RATE_VARIABILITY // ms — HealthKit `heartRateVariabilitySDNN` (typically one sample per night).
RESTING_HEART_RATE // bpm — HealthKit `restingHeartRate` (one sample per day; distinct from spot PULSE).