Repository navigation
Description
What happened
On one iPhone, NOOP's Apple Health write-back put HRV, respiratory rate, sleep and resting HR into Health, but never wrote the 1-minute heart-rate stream, with no error and no sign on any screen. A reader that scores recovery from overnight heart rate (Bevel) then had no NOOP heart rate, so it showed no resting HR and no Recovery. HRV and sleep from NOOP did reach it.
I found two independent causes in StrandiOS/Health/HealthKitBridge.swift. Both are still on main (checked 2 Oct).
1. writeHeartRate reads one device id; every chart reads a union
writeHeartRate calls whoopStore.hrBuckets(deviceId: noopDeviceId, …). The charts read repo.hrBuckets(from:to:), which unions the active strap, all registered straps and the canonical id (rawPhysiologyReadIds, the #904/#908 family). When the strap's heart rate sits under another of those ids, the charts plot it and the writer finds nothing. guard !buckets.isEmpty else { return } then skips silently.
I added a one-line status to the Apple Health screen to see this. It read: "Heart rate not written: NOOP has no strap heart rate since 18 Sep", while Today plotted that heart rate.
Fix: read through repo.hrBuckets(from:to:bucketSeconds:), the same read the charts use. After that the write ran.
(Android's HealthConnectWriter.writeHeartRate takes the registry's active strap id on purpose (#814), so this may be iOS-only. I haven't tested Android.)
2. A new WRITE type is never re-requested
requestNewReadTypesIfNeeded compares a fingerprint of the read ids only. The heart-rate / active-energy / distance write types were added after many people granted access, and they stay .notDetermined. Every writer guards on .sharingAuthorized, so they're skipped without any error. The fingerprint comment already explains this for read types (#949); it just doesn't cover writes.
Fix: include the write ids (minus writeDenied) in readTypeSignature. HealthKit shows the sheet only for still-undetermined types, so nobody is re-asked about what they've already answered. On my phone the sheet appeared once, and I allowed everything.
Third, smaller: say what the write did
Both of the above are silent: skipped, no error, indistinguishable from success. One line on the Apple Health screen after each heart-rate write (permission off or not asked / nothing to write / failed with the error / N samples, newest time) made all of this diagnosable in minutes. It would make a good always-on diagnostic, since the rule in AGENTS.md is that rare events should not go quiet.
Observation (not a claim): where the nightly vitals are stamped
The one-per-night vitals (HRV, resp rate, SpO2, resting HR) are stamped at the wake instant, which is the end of the sleep interval. After I moved them to the middle of the night's longest sleep (a small HealthWriteback.midSleepTs helper), Bevel started showing HRV and breathing for the night. That was not a clean A/B test, since other things changed too, so please treat it as a lead.
Also seen (iOS)
A first heart-rate write after granting is a large backfill (about 20k samples) done in oldest-first chunks. If the phone locks mid-way, HealthKit throws Protected health data is inaccessible, and the newest, most useful nights are the ones left unwritten. Writing newest nights first, or recording the failure and retrying on unlock, would help.
The fixes are small and I can send them as a PR (iOS, plus an Android check) if you'd like them in this shape.
Activity
fix(ios): export chart HR sources and refresh write authorization
fix(ios): align HR export sources and refresh write authorization
Merge upstream/main through 12.0.0 (9f98f81) into the fork