Repository navigation
Add opt-in "optimal strain reached" notification — iOS/macOS twin (#593) - #597
Merged
Merged
Conversation
Swift twin of the Android strain-target nudge (PR #596), keeping the pure policy byte-identical (feature-level parity contract). - StrainTargetNotifier.StrainTargetPolicy: pure once-per-day crossing gate + NOOP's-own copy (NOT WHOOP's decompiled strings), semantics identical to the Android StrainTargetPolicy: fires only when enabled, both day strain and target known, strain >= target, and not already fired today; a nil target (recovery calibrating) never fires. - StrainTargetNotifier.onDayUpdate: BatteryNotifier idiom (requestAuthorization up front on enable, status-only check at fire time); the persisted day marker advances only after an authorized post, matching the Android notifier. - BehaviorStore.strainTargetNudge: default OFF, UserDefaults-backed. - AppModel: the repo.$days sink also evaluates the strain target; converts the stored 0-100 Effort to the 0-21 coupled axis via UnitFormatter.effortValue and gates against CoupledView.optimalStrainRange(recovery:).lowerBound — the same shipped formatter + #43 band the Android call site uses. - AutomationsView: "Strain target" card (shared, so reachable on macOS AND iOS), with the reevaluateIllness enable-time idiom so flipping it on when today's target is already reached posts without waiting for the next refresh. - Localizable.xcstrings: 6 new keys with de/es/fr (audit --ci passes). Tests: StrandTests/StrainTargetPolicyTests mirrors the Android StrainTargetPolicyTest fixtures byte-for-byte (7 cases). Verified on Linux: swiftc -parse clean on all touched files; i18n audit exit 0. App-target compile + StrandTests need an xcodebuild/app-build.yml run (macOS).
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
…nbr#593) (ryanbr#597) Swift twin of the Android strain-target nudge (PR ryanbr#596), keeping the pure policy byte-identical (feature-level parity contract). - StrainTargetNotifier.StrainTargetPolicy: pure once-per-day crossing gate + NOOP's-own copy (NOT WHOOP's decompiled strings), semantics identical to the Android StrainTargetPolicy: fires only when enabled, both day strain and target known, strain >= target, and not already fired today; a nil target (recovery calibrating) never fires. - StrainTargetNotifier.onDayUpdate: BatteryNotifier idiom (requestAuthorization up front on enable, status-only check at fire time); the persisted day marker advances only after an authorized post, matching the Android notifier. - BehaviorStore.strainTargetNudge: default OFF, UserDefaults-backed. - AppModel: the repo.$days sink also evaluates the strain target; converts the stored 0-100 Effort to the 0-21 coupled axis via UnitFormatter.effortValue and gates against CoupledView.optimalStrainRange(recovery:).lowerBound — the same shipped formatter + #43 band the Android call site uses. - AutomationsView: "Strain target" card (shared, so reachable on macOS AND iOS), with the reevaluateIllness enable-time idiom so flipping it on when today's target is already reached posts without waiting for the next refresh. - Localizable.xcstrings: 6 new keys with de/es/fr (audit --ci passes). Tests: StrandTests/StrainTargetPolicyTests mirrors the Android StrainTargetPolicyTest fixtures byte-for-byte (7 cases). Verified on Linux: swiftc -parse clean on all touched files; i18n audit exit 0. App-target compile + StrandTests need an xcodebuild/app-build.yml run (macOS).
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…nbr#593) (ryanbr#597) Swift twin of the Android strain-target nudge (PR ryanbr#596), keeping the pure policy byte-identical (feature-level parity contract). - StrainTargetNotifier.StrainTargetPolicy: pure once-per-day crossing gate + NOOP's-own copy (NOT WHOOP's decompiled strings), semantics identical to the Android StrainTargetPolicy: fires only when enabled, both day strain and target known, strain >= target, and not already fired today; a nil target (recovery calibrating) never fires. - StrainTargetNotifier.onDayUpdate: BatteryNotifier idiom (requestAuthorization up front on enable, status-only check at fire time); the persisted day marker advances only after an authorized post, matching the Android notifier. - BehaviorStore.strainTargetNudge: default OFF, UserDefaults-backed. - AppModel: the repo.$days sink also evaluates the strain target; converts the stored 0-100 Effort to the 0-21 coupled axis via UnitFormatter.effortValue and gates against CoupledView.optimalStrainRange(recovery:).lowerBound — the same shipped formatter + ryanbr#43 band the Android call site uses. - AutomationsView: "Strain target" card (shared, so reachable on macOS AND iOS), with the reevaluateIllness enable-time idiom so flipping it on when today's target is already reached posts without waiting for the next refresh. - Localizable.xcstrings: 6 new keys with de/es/fr (audit --ci passes). Tests: StrandTests/StrainTargetPolicyTests mirrors the Android StrainTargetPolicyTest fixtures byte-for-byte (7 cases). Verified on Linux: swiftc -parse clean on all touched files; i18n audit exit 0. App-target compile + StrandTests need an xcodebuild/app-build.yml run (macOS).
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.
The Swift half of #593, twinning the Android implementation in #596 — the pure policy is byte-identical on both platforms (feature-level parity). Independent of #596 (no file overlap); the two can merge in either order.
What it does
Same opt-in, default-OFF nudge as the Android PR: once per day, when the day's Effort reaches the low end of today's recovery-derived optimal strain band (#43), post an "optimal strain reached" notification.
StrainTargetNotifier.StrainTargetPolicy— pure gate + copy, semantics identical to Android'sStrainTargetPolicy: fires only when enabled, both day strain and target are known, strain ≥ target, not already fired today; a nil target (recovery calibrating) never fires. Copy is NOOP's own wording — not WHOOP's decompiled strings (same clean-room stance as Add opt-in "optimal strain reached" notification (#593) #596).onDayUpdate— theBatteryNotifieridiom:requestAuthorization()up front when the toggle is enabled, status-only check at fire time, notification id"strain-target". The persisted day marker advances only after an authorized post, matching the Android notifier (a notifications-denied day still notifies once re-enabled while the same day shows the reached target).BehaviorStore.strainTargetNudge— default OFF.AppModel— the existingrepo.$dayssink also evaluates the strain target off the resolved today-row (repo.today, the same logical-day resolution every dashboard surface uses); converts stored 0–100 Effort → 0–21 via the shippedUnitFormatter.effortValue(_, .whoop)and gates onCoupledView.optimalStrainRange(recovery:).lowerBound— the exact same formatter + band the Android call site reads.AutomationsView— a "Strain target" card next to Battery alerts.AutomationsViewis shared, so the toggle is reachable on macOS and iOS (unlike the macOS-only NotificationSettings screen). Enabling it also evaluates immediately (thereevaluateIllnessidiom), so flipping it on when today's target is already reached posts without waiting for the next refresh.Localizable.xcstrings— 6 new keys with de/es/fr translations (informal de/es, formal fr, matching the existing catalog tone; Int interpolation as%lldper catalog convention).Platform-appropriate divergences (deliberate)
behavior.strainTargetLastDay), the BatteryNotifier convention, vs SharedPreferences on Android. Same semantics.Tests
StrandTests/StrainTargetPolicyTestsmirrors the AndroidStrainTargetPolicyTestbyte-for-byte — same 7 fixtures, same expectations, including the copy assertions that NOOP's wording is present and WHOOP's strings are absent.Verification (honest)
swiftc -parseclean on all six touched files;Tools/i18n_audit.py --ci main→ exit 0 (no new un-extracted literals, focus locales complete); xcstrings JSON validated.Strand/,StrandTests/) — not covered byswift-packagesCI. Anxcodebuild -scheme Strand … test/app-build.ymlrun is required to confirm compile + the 7 policy tests, and an on-device smoke test for actual delivery. New files are picked up by XcodeGen's directory globs (noproject.ymlchange needed).