Repository navigation
Workouts (iOS/macOS): read HR under the recording strap, not the active id (#512 twin) - #513
Merged
Merged
Conversation
…ve id Swift twin of #512. reconcileWorkoutHrWithTrace read every workout's HR window under the Repository's single active deviceId, so a bout auto-detected on a SECOND WHOOP (its hrSample rows banked under that strap's id) read the active strap's empty window and its Avg HR / Effort went un-reconciled. Single-WHOOP installs matched the active id, so only multi-WHOOP users were affected. Resolve the read key per row via the new nonisolated static Repository.workoutHrDeviceId(source:activeStrapId:): a DETECTED row's own `source` IS its computed strap id ("<base>-noop"), so strip the suffix to read HR under the raw "<base>". MANUAL and IMPORTED rows carry no strap id in `source`, so they reconcile against the active strap as before (single-device: "<base>-noop" strips to the active id, so the read is byte-identical). Display-only projection (never persisted). New WorkoutHrDeviceKeyTests pins the resolution. Parity note: the Kotlin twin also keys MANUAL rows on their stored deviceId; the Swift WorkoutRow read-model carries no deviceId, so a manual session created on a non-active strap reconciles here against the active strap — a minor, display-only divergence; the detected-dominant multi-WHOOP bug is fixed on both platforms.
ryanbr
added a commit
that referenced
this pull request
Jul 16, 2026
…#514) The 9.0.1 release generated its AppChangelog title as a raw Kotlin literal (title = "German, French & Spanish, ..."), unlike every other entry which uses uiString(R.string.l10n_app_changelog_...). The full-tree i18n audit flags the raw literal, and since i18n-coverage runs only on pull_request (never on the release's push to main), it landed unaudited and now red-checks every open PR (#507, #512, #513) on a line none of them touched. Localize it like the prior entries: title -> uiString(R.string.l10n_app_changelog_german_french_spanish_pull_to_sync_1109bda2), with the string added to values/ (en) + the de/es/fr focus locales. Key follows the established scheme (first-6-word slug + sha1(title)[:8]; verified it reproduces the 9.0.0/8.7.0/8.6.2 keys). i18n_audit.py --ci: OK no hardcoded literals. compileFullDebugKotlin: OK. Apple side is unaffected (SwiftUI auto-extracts literals).
ryanbr
added a commit
that referenced
this pull request
Jul 17, 2026
Numbers are main's current 292/203 +1, matching the previous staging commit's pattern, so the .staging app upgrades cleanly over the last testing build (290) and stays above main's released high-water. Carries everything merged since the 9.0.1 release: - Workouts read HR under the recording strap, not a hardcoded id (#512 Kotlin, #513 Swift twin) - Oura 0x60/0x80 IBI decoders corrected to the ring's real byte layout (#511) - Whoop5Config enable_sig12 corrected to ASCII '1' (#522) - Sleep time edits save as one validated window (#525) - "Recompute this night" for deleted sleep windows (#526) - Stress marker values stay readable (#507) - Faster history sync + faster Bluetooth link, both EXPERIMENTAL and default-off (#536/#537/#538) - the whole point of this build is field data on those - i18n changelog-title fix + main-push audit (#514/#535) versionName stays 9.0.2 here for the staging identity only; the real 9.0.2 release still bumps main separately (and must be raised past 293 first).
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
…ryanbr#514) The 9.0.1 release generated its AppChangelog title as a raw Kotlin literal (title = "German, French & Spanish, ..."), unlike every other entry which uses uiString(R.string.l10n_app_changelog_...). The full-tree i18n audit flags the raw literal, and since i18n-coverage runs only on pull_request (never on the release's push to main), it landed unaudited and now red-checks every open PR (ryanbr#507, ryanbr#512, ryanbr#513) on a line none of them touched. Localize it like the prior entries: title -> uiString(R.string.l10n_app_changelog_german_french_spanish_pull_to_sync_1109bda2), with the string added to values/ (en) + the de/es/fr focus locales. Key follows the established scheme (first-6-word slug + sha1(title)[:8]; verified it reproduces the 9.0.0/8.7.0/8.6.2 keys). i18n_audit.py --ci: OK no hardcoded literals. compileFullDebugKotlin: OK. Apple side is unaffected (SwiftUI auto-extracts literals).
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
…ve id (ryanbr#513) Swift twin of ryanbr#512. reconcileWorkoutHrWithTrace read every workout's HR window under the Repository's single active deviceId, so a bout auto-detected on a SECOND WHOOP (its hrSample rows banked under that strap's id) read the active strap's empty window and its Avg HR / Effort went un-reconciled. Single-WHOOP installs matched the active id, so only multi-WHOOP users were affected. Resolve the read key per row via the new nonisolated static Repository.workoutHrDeviceId(source:activeStrapId:): a DETECTED row's own `source` IS its computed strap id ("<base>-noop"), so strip the suffix to read HR under the raw "<base>". MANUAL and IMPORTED rows carry no strap id in `source`, so they reconcile against the active strap as before (single-device: "<base>-noop" strips to the active id, so the read is byte-identical). Display-only projection (never persisted). New WorkoutHrDeviceKeyTests pins the resolution. Parity note: the Kotlin twin also keys MANUAL rows on their stored deviceId; the Swift WorkoutRow read-model carries no deviceId, so a manual session created on a non-active strap reconciles here against the active strap — a minor, display-only divergence; the detected-dominant multi-WHOOP bug is fixed on both platforms.
ryanbr
added a commit
that referenced
this pull request
Jul 24, 2026
…ed my-whoop (#767, #717) (#777) Workout auto-detection silently stopped for live-BLE users while sleep, charge and everything else kept syncing. Root cause: AutoWorkoutNudge read HR via repo.hrSamples("my-whoop", ...) — a hardcoded id — but a live strap banks its raw under its OWN fresh id ("whoop-<uuid>", #908), so the read returned nothing and detect() bailed at 'hr.size < 2'. The mid-July active-strap-id migration (#175/#512/#513) moved sleep/charge/health/iOS reads to the active-strap union but missed this one. Fix: read the hrSamplesUnion(activeDeviceId) — importedSourceIds(active) ∪ my-whoop, deduped — exactly what every other Today read and iOS Repository.autoDetectCandidate() already use. Android-only; iOS was never affected (it already reads the union). No behaviour change for single-WHOOP installs, whose active id resolves to my-whoop (one id, identical read). App-target Android; not compiled here (aapt2 x86-only on this host) - run ./gradlew compileFullDebugKotlin before merge.
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…ryanbr#514) The 9.0.1 release generated its AppChangelog title as a raw Kotlin literal (title = "German, French & Spanish, ..."), unlike every other entry which uses uiString(R.string.l10n_app_changelog_...). The full-tree i18n audit flags the raw literal, and since i18n-coverage runs only on pull_request (never on the release's push to main), it landed unaudited and now red-checks every open PR (ryanbr#507, ryanbr#512, ryanbr#513) on a line none of them touched. Localize it like the prior entries: title -> uiString(R.string.l10n_app_changelog_german_french_spanish_pull_to_sync_1109bda2), with the string added to values/ (en) + the de/es/fr focus locales. Key follows the established scheme (first-6-word slug + sha1(title)[:8]; verified it reproduces the 9.0.0/8.7.0/8.6.2 keys). i18n_audit.py --ci: OK no hardcoded literals. compileFullDebugKotlin: OK. Apple side is unaffected (SwiftUI auto-extracts literals).
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…ve id (ryanbr#513) Swift twin of ryanbr#512. reconcileWorkoutHrWithTrace read every workout's HR window under the Repository's single active deviceId, so a bout auto-detected on a SECOND WHOOP (its hrSample rows banked under that strap's id) read the active strap's empty window and its Avg HR / Effort went un-reconciled. Single-WHOOP installs matched the active id, so only multi-WHOOP users were affected. Resolve the read key per row via the new nonisolated static Repository.workoutHrDeviceId(source:activeStrapId:): a DETECTED row's own `source` IS its computed strap id ("<base>-noop"), so strip the suffix to read HR under the raw "<base>". MANUAL and IMPORTED rows carry no strap id in `source`, so they reconcile against the active strap as before (single-device: "<base>-noop" strips to the active id, so the read is byte-identical). Display-only projection (never persisted). New WorkoutHrDeviceKeyTests pins the resolution. Parity note: the Kotlin twin also keys MANUAL rows on their stored deviceId; the Swift WorkoutRow read-model carries no deviceId, so a manual session created on a non-active strap reconciles here against the active strap — a minor, display-only divergence; the detected-dominant multi-WHOOP bug is fixed on both platforms.
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…ed my-whoop (ryanbr#767, ryanbr#717) (ryanbr#777) Workout auto-detection silently stopped for live-BLE users while sleep, charge and everything else kept syncing. Root cause: AutoWorkoutNudge read HR via repo.hrSamples("my-whoop", ...) — a hardcoded id — but a live strap banks its raw under its OWN fresh id ("whoop-<uuid>", ryanbr#908), so the read returned nothing and detect() bailed at 'hr.size < 2'. The mid-July active-strap-id migration (ryanbr#175/ryanbr#512/ryanbr#513) moved sleep/charge/health/iOS reads to the active-strap union but missed this one. Fix: read the hrSamplesUnion(activeDeviceId) — importedSourceIds(active) ∪ my-whoop, deduped — exactly what every other Today read and iOS Repository.autoDetectCandidate() already use. Android-only; iOS was never affected (it already reads the union). No behaviour change for single-WHOOP installs, whose active id resolves to my-whoop (one id, identical read). App-target Android; not compiled here (aapt2 x86-only on this host) - run ./gradlew compileFullDebugKotlin before merge.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Swift/iOS+macOS twin of #512.
reconcileWorkoutHrWithTrace(the display-time Avg-HR/Effort reconcile) read every workout's HR window under the Repository's single activedeviceId. A bout auto-detected on a second WHOOP — itshrSamplerows banked under that strap's id — therefore read the active strap's empty window (samples.count < minSamples), so its Avg HR went un-reconciled from the trace and a null Effort wasn't recomputed. Single-WHOOP installs matched the active id, so only multi-WHOOP users were affected.Fix
New
nonisolated static Repository.workoutHrDeviceId(source:activeStrapId:)resolves the read key per row:sourceis the computed strap id ("<base>-noop"), so strip the suffix and read HR under the raw"<base>". A bout detected on a 2nd WHOOP now reads its own strap instead of the active one's empty window.source, so they reconcile against the active strap as before.Phase 2 of the reconcile computes the key on the main actor (via
WorkoutSource.classify) and captures only the resultingSendableStringinto eachwithTaskGroupchild — no new actor-crossing.Why it's safe
"my-whoop-noop"strips to"my-whoop"== the old activedeviceId; manual/imported unchanged.Parity note (vs the Kotlin #512 twin)
Kotlin keys manual rows on their stored
deviceIdtoo; the Swift read-modelWorkoutRow(WhoopStore,Codable) carries nodeviceId, so a manual session created on a non-active strap reconciles here against the active strap — a minor, display-only divergence. The detected-dominant multi-WHOOP bug is fixed on both platforms; closing the manual-row gap would mean threading the source id through the workout fetch/dedup, deferred as not worth the risk to the single-device path.Test / verification
WorkoutHrDeviceKeyTests(StrandTests) mirrors the KotlinWorkoutHrDeviceKeyTest. App-target compile (macOS + iOS) verified via an on-demandapp-build.ymlrun (app-target Swift has no default CI).