Repository navigation
Save Android sleep time edits as one validated window - #525
Merged
Merged
Conversation
kavemang
marked this pull request as ready for review
July 16, 2026 19:04
5 tasks done
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).
mohdarsal9-sudo
pushed a commit
to mohdarsal9-sudo/noop
that referenced
this pull request
Jul 19, 2026
…'t exceed time-in-bed (ryanbr#259) (ryanbr#270) * sleep: clamp edited-night stages to the effective onset so asleep can't exceed time-in-bed (ryanbr#259) A WHOOP 4.0 morning sleep reported "asleep 6h41m" over "4h33m in bed" — an impossible card. Root: after a bed edit moved the effective onset (01:58 → 06:15), the raw motion was too sparse to re-stage (4.0), so the stored stagesJSON kept its pre-onset segments. dailyAggregateHonoringEdits summed the FULL window while time-in-bed used the edited onset, so asleep exceeded in-bed (and inflated Rest/score). Trim each SELECTED block's stages to its effective onset before summing: clampStagesToOnset drops/trims segments that start before onsetByStart[startTs]. Main-night SELECTION still runs on the original blocks (no ryanbr#525/ryanbr#547 pick change) — only the summed total is clamped. A block already staged from its onset (the common case, and every non-edited night, where onset == start) is unchanged: a no-op. startTsAdjusted is only ever set by a user edit, so effectiveStartTs != startTs implies an edited night — exactly where this applies. Byte-parity Swift + Kotlin. Not addressed (deferred root cause): why 4.0 over-stages a long low-motion pre-onset stretch as sleep in the first place — the sparse-motion staging problem. Tests: Issue259PreOnsetClampTest (Kotlin) + SleepStageTotalsTests twins (Swift) — pre-onset trim + non-edited no-op. Full com.noop.analytics suite green. * sleep: also clamp the DISPLAYED stage minutes to the effective onset (ryanbr#259) The analytics clamp fixed the score, but the card's asleep number is Stages.asleep (light+deep+rem) sourced from the session's stagesJSON directly (sessionStageMins / sumGroupStages on Android, decodeSegments on iOS), with the clamped analytics daily fields only a fallback — so the impossible "6h41m asleep / 4h33m in bed" text was still shown. Trim each session/fragment's stages to its effectiveStartTs at the display parse, reusing SleepStageTotals.clampStagesToOnset (Android) and clamping the segment start in decodeSegments (iOS, which also stops the hypnogram drawing bars before the onset). No-op for non-edited nights (segments already start at the onset). Now the card asleep, the stage bars, the hypnogram, and the score all honor the same effective window. * sleep: clamp the Android hypnogram timeline to the effective onset too (ryanbr#259) The stage-minute clamp fixed the asleep NUMBER on Android, but the hypnogram strip (segments / realSegments) still parsed the raw stagesJSON, so it drew bars for the pre-onset stretch — contradicting the clamped total and diverging from iOS (whose decodeSegments clamps the timeline in the same pass). Clamp the fragment/session segment parse to effectiveStartTs so the chart starts where the edited bedtime does. No-op for non-edited nights. Card number, stage bars, hypnogram + score now all agree on both platforms.
Closed
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
…'t exceed time-in-bed (ryanbr#259) (ryanbr#270) * sleep: clamp edited-night stages to the effective onset so asleep can't exceed time-in-bed (ryanbr#259) A WHOOP 4.0 morning sleep reported "asleep 6h41m" over "4h33m in bed" — an impossible card. Root: after a bed edit moved the effective onset (01:58 → 06:15), the raw motion was too sparse to re-stage (4.0), so the stored stagesJSON kept its pre-onset segments. dailyAggregateHonoringEdits summed the FULL window while time-in-bed used the edited onset, so asleep exceeded in-bed (and inflated Rest/score). Trim each SELECTED block's stages to its effective onset before summing: clampStagesToOnset drops/trims segments that start before onsetByStart[startTs]. Main-night SELECTION still runs on the original blocks (no ryanbr#525/ryanbr#547 pick change) — only the summed total is clamped. A block already staged from its onset (the common case, and every non-edited night, where onset == start) is unchanged: a no-op. startTsAdjusted is only ever set by a user edit, so effectiveStartTs != startTs implies an edited night — exactly where this applies. Byte-parity Swift + Kotlin. Not addressed (deferred root cause): why 4.0 over-stages a long low-motion pre-onset stretch as sleep in the first place — the sparse-motion staging problem. Tests: Issue259PreOnsetClampTest (Kotlin) + SleepStageTotalsTests twins (Swift) — pre-onset trim + non-edited no-op. Full com.noop.analytics suite green. * sleep: also clamp the DISPLAYED stage minutes to the effective onset (ryanbr#259) The analytics clamp fixed the score, but the card's asleep number is Stages.asleep (light+deep+rem) sourced from the session's stagesJSON directly (sessionStageMins / sumGroupStages on Android, decodeSegments on iOS), with the clamped analytics daily fields only a fallback — so the impossible "6h41m asleep / 4h33m in bed" text was still shown. Trim each session/fragment's stages to its effectiveStartTs at the display parse, reusing SleepStageTotals.clampStagesToOnset (Android) and clamping the segment start in decodeSegments (iOS, which also stops the hypnogram drawing bars before the onset). No-op for non-edited nights (segments already start at the onset). Now the card asleep, the stage bars, the hypnogram, and the score all honor the same effective window. * sleep: clamp the Android hypnogram timeline to the effective onset too (ryanbr#259) The stage-minute clamp fixed the asleep NUMBER on Android, but the hypnogram strip (segments / realSegments) still parsed the raw stagesJSON, so it drew bars for the pre-onset stretch — contradicting the clamped total and diverging from iOS (whose decodeSegments clamps the timeline in the same pass). Clamp the fragment/session segment parse to effectiveStartTs so the chart starts where the edited bedtime does. No-op for non-edited nights. Card number, stage bars, hypnogram + score now all agree on both platforms.
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
Co-authored-by: kavemang <kavemang@users.noreply.github.com>
5 of 12 tasks
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…'t exceed time-in-bed (ryanbr#259) (ryanbr#270) * sleep: clamp edited-night stages to the effective onset so asleep can't exceed time-in-bed (ryanbr#259) A WHOOP 4.0 morning sleep reported "asleep 6h41m" over "4h33m in bed" — an impossible card. Root: after a bed edit moved the effective onset (01:58 → 06:15), the raw motion was too sparse to re-stage (4.0), so the stored stagesJSON kept its pre-onset segments. dailyAggregateHonoringEdits summed the FULL window while time-in-bed used the edited onset, so asleep exceeded in-bed (and inflated Rest/score). Trim each SELECTED block's stages to its effective onset before summing: clampStagesToOnset drops/trims segments that start before onsetByStart[startTs]. Main-night SELECTION still runs on the original blocks (no ryanbr#525/ryanbr#547 pick change) — only the summed total is clamped. A block already staged from its onset (the common case, and every non-edited night, where onset == start) is unchanged: a no-op. startTsAdjusted is only ever set by a user edit, so effectiveStartTs != startTs implies an edited night — exactly where this applies. Byte-parity Swift + Kotlin. Not addressed (deferred root cause): why 4.0 over-stages a long low-motion pre-onset stretch as sleep in the first place — the sparse-motion staging problem. Tests: Issue259PreOnsetClampTest (Kotlin) + SleepStageTotalsTests twins (Swift) — pre-onset trim + non-edited no-op. Full com.noop.analytics suite green. * sleep: also clamp the DISPLAYED stage minutes to the effective onset (ryanbr#259) The analytics clamp fixed the score, but the card's asleep number is Stages.asleep (light+deep+rem) sourced from the session's stagesJSON directly (sessionStageMins / sumGroupStages on Android, decodeSegments on iOS), with the clamped analytics daily fields only a fallback — so the impossible "6h41m asleep / 4h33m in bed" text was still shown. Trim each session/fragment's stages to its effectiveStartTs at the display parse, reusing SleepStageTotals.clampStagesToOnset (Android) and clamping the segment start in decodeSegments (iOS, which also stops the hypnogram drawing bars before the onset). No-op for non-edited nights (segments already start at the onset). Now the card asleep, the stage bars, the hypnogram, and the score all honor the same effective window. * sleep: clamp the Android hypnogram timeline to the effective onset too (ryanbr#259) The stage-minute clamp fixed the asleep NUMBER on Android, but the hypnogram strip (segments / realSegments) still parsed the raw stagesJSON, so it drew bars for the pre-onset stretch — contradicting the clamped total and diverging from iOS (whose decodeSegments clamps the timeline in the same pass). Clamp the fragment/session segment parse to effectiveStartTs so the chart starts where the edited bedtime does. No-op for non-edited nights. Card number, stage bars, hypnogram + score now all agree on both platforms.
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
Co-authored-by: kavemang <kavemang@users.noreply.github.com>
mohamedchnafy-ds
pushed a commit
to mohamedchnafy-ds/noop
that referenced
this pull request
Oct 31, 2026
The day lines' rest figure is the main night only (ryanbr#525), so a nap logged in the Sleep tab never reached the Coach. A NAPS block now lists each recent day's naps (clock span, minutes asleep) and the credited total night + naps, the figure the sleep-debt ledger counts, split from the night by the same selector the Naps card uses. Rest and Charge still score the night, as on Android; the block says so, so the Coach does not read a nap into them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HbxaRoQdLqN4Wg4yfikbo6
DX23876
added a commit
to DX23876/noop
that referenced
this pull request
Nov 1, 2026
The day lines' rest figure is the main night only (ryanbr#525), so a nap logged in the Sleep tab never reached the Coach. A NAPS block now lists each of the last 14 days' naps (clock span, minutes asleep) and the credited total night plus naps, split from the night by SleepView.mainNightGroup, the selector the Naps card uses. It says that Rest and Charge score the night only, so the Coach does not read a nap into them. The block rides along with the detailed biometric context and the get_sleep_detail tool result, and is empty for a wearer with no naps. CoachNapContextTests (4 tests). Adopted from mohamedchnafy-ds/noop (042b03c, de84669, written there with Claude) by @mohamedchnafy-ds, wired into the fork's context planner and sleep tool. Analysis migration required: no Co-authored-by: mohamedchnafy-ds <175178324+mohamedchnafy-ds@users.noreply.github.com>
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 changed
Why
The Android editor previously committed bedtime and wake-up independently. Changing bedtime first wrote it against the session's old wake time; changing wake first did the inverse. That intermediate write could produce an inflated or inverted duration and trip the save guard before the user had entered the complete intended window.
The editor now matches the existing Apple behavior: both endpoints remain draft values, and one validated
(start, end)pair reaches persistence. Existing disjoint-window confirmation and future/inverted-window guards remain in place.Addresses the manual sleep-editor bug described in #515 (item 3). The issue's separate re-detection and split-night work remains open.
Validation
./gradlew :app:testFullDebugUnitTest --tests 'com.noop.ui.SleepTimeEditDraftTest' --tests 'com.noop.analytics.SleepEditGuardTest'python3 Tools/i18n_audit.py --ci upstream/maingit diff --check