Skip to content

Make a detected nap visible on the Sleep screen - #205

Merged
abdulsaheel merged 8 commits into
OpenStrap:mainfrom
egoran2:fix/naps-visible-on-sleep-screen
Aug 7, 2026
Merged

Make a detected nap visible on the Sleep screen#205
abdulsaheel merged 8 commits into
OpenStrap:mainfrom
egoran2:fix/naps-visible-on-sleep-screen

Conversation

@egoran2

@egoran2 egoran2 commented Aug 6, 2026

Copy link
Copy Markdown

I napped for ~1h40m this afternoon, synced, and the nap was nowhere in the app. It turned out the detection is fine (the nap shows as a band under Your day), but two things kept it off the Sleep screen.

1. The periods screen is unreachable. SleepPeriodsScreen is only pushed from an AppScaffold action in SleepDetailScreen, and the Sleep tab always builds that screen with embedded: true (screens.dart:50,55), which returns the bare Column without the scaffold. SleepDetailScreen.today() is never called either, so nothing in the shipped app can open it.

2. The periods payload is keyed wrong. The engine writes each period as is_main / start / end / asleep_min (derivation_engine.dart:3626), while the screen reads onset_ts / wake_ts / duration_min. Even if you reached the screen, every card would say "0m" with no time range.

Before: Sleep tab shows the night only, 6h40m, no sign of the nap anywhere.
After: a row under the night summary, "Daytime nap, 1h 41m, not included in the night above", which opens the per-period breakdown with real times and durations.

Also in here:

  • the main period now carries the night's TST, efficiency, stage minutes and hypnogram, so the two sleep screens can't print different numbers for the same night, and the day total is the sum of what the cards show
  • a nap has no confidence value, so it no longer gets a literal 0 and the confidence dot is hidden instead
  • test/sleep_naps_visible_test.dart covers the mapping and the row (visible with a nap, absent without, tap fires)

Nothing about how a nap is computed changed, so kAlgoVersion stays put.

Left alone deliberately, happy to do either in a follow-up: naps still don't enter TST or readiness (they only credit tonight's need via nap_min), and _daySleep returns has_sleep: false when there's no night, so a nap-only day still shows nothing.

Verified on Flutter 3.41.6 (the pinned version): flutter analyze clean, flutter test --concurrency=1 gives 1205 passed / 2 skipped, the two skips being the whoop_hist.jsonl replays that need the capture file.

Summary by CodeRabbit

  • New Features

    • Added daytime-nap summaries to sleep details, including nap count and total duration.
    • Preserved nap-only sleep periods when no nighttime sleep is detected.
    • Enabled navigation from sleep details to the full sleep-period breakdown.
  • Bug Fixes

    • Improved compatibility with legacy sleep-period data formats.
    • Corrected invalid period handling and capped durations to valid time windows.
    • Recalculated totals accurately while preserving unknown or unavailable values.
  • Tests

    • Added coverage for nap display, navigation, nap-only days, duration validation, and invalid periods.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Sleep period data now uses canonical fields, validates windows, and bounds durations. Sleep detail screens preserve and display daytime naps, including nap-only days, with shared navigation to the periods breakdown. Tests cover normalization, totals, nap visibility, and navigation.

Changes

Sleep period display

Layer / File(s) Summary
Sleep period mapping and validation
lib/data/local_repository_impl.dart, test/sleep_periods_legacy_keys_test.dart
The repository preserves nap-only periods, canonicalizes fields, removes invalid windows, bounds durations, and recomputes totals from rendered periods. Tests cover missing totals, unknown durations, negative durations, and nap-only days.
Nap navigation and display
lib/ui/sleep/sleep_detail_screen.dart, test/sleep_naps_visible_test.dart
Sleep details parse non-main periods as naps, show nap counts and durations when available, and use shared navigation to open SleepPeriodsScreen. Tests cover visible, tappable, and absent nap rows.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • OpenStrap/edge#153: Both PRs modify SleepNightContent in the sleep detail screen.
  • OpenStrap/edge#172: Both PRs modify nap-period handling, including periods spanning day boundaries.
  • OpenStrap/edge#204: Both PRs modify sleep-period normalization, legacy keys, unknown durations, and nap-only periods.

Suggested labels: Review effort 4/5

Suggested reviewers: abdulsaheel

Sequence Diagram(s)

sequenceDiagram
  participant SleepRepository
  participant SleepNightContent
  participant SleepPeriodsScreen
  SleepRepository->>SleepNightContent: Provide normalized sleep periods
  SleepNightContent->>SleepNightContent: Parse non-main periods as naps
  SleepNightContent->>SleepPeriodsScreen: Open periods for the current date
  SleepPeriodsScreen-->>SleepNightContent: Render the period breakdown
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: displaying detected naps on the Sleep screen.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@egoran2
egoran2 force-pushed the fix/naps-visible-on-sleep-screen branch from 147bf3b to 7c38e28 Compare August 6, 2026 17:01
@egoran2
egoran2 marked this pull request as ready for review August 6, 2026 17:02

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/data/local_repository_impl.dart`:
- Around line 2695-2701: Validate the parsed asleep_min in the duration
calculation around asleepMin, accepting it only when it falls within
0..windowMinutes derived from end and start; otherwise use the period span as
the fallback. Ensure both main and nap duration paths use this bounded value,
and add regression cases in sleep_naps_visible_test.dart for negative and
oversized asleep_min values.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 93bc9857-0e6b-4c58-913b-760bfbbade28

📥 Commits

Reviewing files that changed from the base of the PR and between d911f60 and 7c38e28.

📒 Files selected for processing (4)
  • lib/data/local_repository_impl.dart
  • lib/ui/sleep/sleep_detail_screen.dart
  • lib/ui/sleep/sleep_periods_screen.dart
  • test/sleep_naps_visible_test.dart

Comment thread lib/data/local_repository_impl.dart Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/sleep_naps_visible_test.dart (1)

625-640: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add regression coverage for the recomputed day total.

The new path derives total_asleep_min from mapped period durations, but the supplied tests assert individual period fields and nap-row behavior only. Add an assertion for the existing fixture total: 400 minutes of main sleep plus 101 minutes of nap sleep equals 501 minutes. Also cover the no-period fallback if that fallback remains part of the contract.

As per coding guidelines: “Behavior changes, especially regressions involving readiness, abstention, idempotence, synchronization, migrations, and lifecycle safety, must include regression tests.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/sleep_naps_visible_test.dart` around lines 625 - 640, Add regression
coverage in the existing sleep/naps test near the mapped period assertions,
verifying the fixture’s recomputed total_asleep_min is 501 minutes from 400
main-sleep plus 101 nap-sleep minutes. If the no-period fallback remains
supported, add a case asserting its expected total as well.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/data/local_repository_impl.dart`:
- Around line 2698-2702: Validate the main period’s night['duration_min']
against windowMin using the same non-negative, upper-bound checks applied to
p['asleep_min']; when invalid, fall back to the already validated asleepMin
value before assigning duration_min. Add regression coverage for negative and
oversized main duration values.

In `@test/sleep_naps_visible_test.dart`:
- Around line 93-100: Update the test “a length outside its own window falls
back to the window” to assert that the result collection p contains exactly
three elements before checking mapped duration_min values, ensuring
everyElement(101) cannot pass for an empty iterable.

---

Outside diff comments:
In `@test/sleep_naps_visible_test.dart`:
- Around line 625-640: Add regression coverage in the existing sleep/naps test
near the mapped period assertions, verifying the fixture’s recomputed
total_asleep_min is 501 minutes from 400 main-sleep plus 101 nap-sleep minutes.
If the no-period fallback remains supported, add a case asserting its expected
total as well.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 73855007-bddc-45cb-a8ba-da6e4ca0f51c

📥 Commits

Reviewing files that changed from the base of the PR and between 7c38e28 and e790715.

📒 Files selected for processing (2)
  • lib/data/local_repository_impl.dart
  • test/sleep_naps_visible_test.dart

Comment thread lib/data/local_repository_impl.dart Outdated
Comment thread test/sleep_naps_visible_test.dart Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/data/local_repository_impl.dart`:
- Around line 2706-2710: Update the duration parsing around tstMin and
durationMin to range-check the raw numeric duration before calling toInt(),
rejecting fractional values such as -0.5 and 0.5 so both main-duration and nap
asleepMin fallbacks are preserved. Add regression coverage for fractional inputs
in both paths.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 09176f3b-fa41-48a6-8291-d7508549290c

📥 Commits

Reviewing files that changed from the base of the PR and between e790715 and 0d95f06.

📒 Files selected for processing (2)
  • lib/data/local_repository_impl.dart
  • test/sleep_naps_visible_test.dart

Comment thread lib/data/local_repository_impl.dart Outdated
Egor and others added 4 commits August 7, 2026 21:27
A daytime nap is detected, stored, and drawn as a band on the day
timeline, but the Sleep screen only ever showed the main night. The
screen that does list every sleep of the day was unreachable: its only
entry point is an AppScaffold action, and the Sleep tab embeds
SleepNightContent (embedded: true), so that scaffold never builds.

The periods payload was also keyed wrong. The engine writes is_main /
start / end / asleep_min; SleepPeriodsScreen reads onset_ts / wake_ts /
duration_min, so every card would have rendered as "0m" with no time
range under it.

- map sleep_periods onto the keys the screen reads, and give the main
  period the night's TST, efficiency, stage minutes and hypnogram so the
  two sleep screens can't print different numbers for the same night
- the day total is now the sum of what the cards show
- add a naps row under the night summary; tapping it opens the
  breakdown, and it says the nap is not part of the numbers above
- a nap carries no confidence instead of a literal 0, and the
  confidence dot is hidden when there is none
- test/sleep_naps_visible_test.dart pins the mapping and the row

No stored analytics output changed, so kAlgoVersion stays put.
CodeRabbit review: sleepPeriodsForScreen took asleep_min at face value, so a
negative or larger-than-window value would print a negative duration on the
card and skew the day total. The engine never writes one today (a nap's
asleep_min is exactly its window), but this function parses defensively
everywhere else, so it should here too. Falls back to the window, which the
period's own start/end already vouch for.
CodeRabbit review, second pass:

- the window check covered a nap's asleep_min but not the main period, which
  took night['duration_min'] straight through. Same bound now: nobody sleeps
  longer than the window they slept in.
- the fallback test asserted everyElement on a list it never sized, so it would
  have passed if the mapper dropped all three inputs. It checks the length
  first now.

Adds a TST regression case for the negative and oversized values.
…eep the invariants

Rebased onto main (kAlgoVersion 59). The data-layer half of this PR is now
obsolete and actively harmful; the UI half is the part that still matters and
is untouched.

WHAT CHANGED AND WHY. This branch fixed the writer/reader key mismatch at the
SCREEN end, via `sleepPeriodsForScreen`, which translated the producer's
`start`/`end`/`asleep_min` onto `onset_ts`/`wake_ts`/`duration_min`. OpenStrap#204 fixed
the same mismatch at the WRITER end -- the producer now emits the screen's
vocabulary directly, and `_periodsWithMainStages` translates any legacy payload
on read, so old finalized days still render.

Left as-is, the rebase was silently broken: `night['periods'] = periods`
unconditionally overwrote the enriched list with the translator's output, and
the translator reads `p['start']`, which no longer exists. Every period would
have been skipped -- main sleep included -- leaving an EMPTY periods list under
a hero total that still rendered. No error, no log. So the overwrite and the
translator are removed; one source per concern, and it is the writer.

WHAT WAS KEPT. The later commits on this branch added real defensive
invariants at the translator, and those are not obsolete -- they are ported to
`_boundedPeriod` at the surviving seam:
  * a period cannot report more asleep minutes than its own window (clamped to
    the window, which is the trustworthy half -- `duration_min` and the bounds
    come from different producers, so nothing else stops a card claiming more
    sleep than the period it sits in);
  * a degenerate window is dropped rather than rendered as a zero-length card.

Both re-pinned in sleep_periods_legacy_keys_test.dart.

The UI half is untouched and is the reason to merge this: the periods screen
was UNREACHABLE in the shipped app (its only entry point was an AppScaffold
action, and the Sleep tab embeds SleepNightContent, so that scaffold never
builds), plus a naps row on the night screen. Those read `data['periods']`,
which main now populates correctly.

Its test file kept the widget coverage and rebuilt the fixture in the current
vocabulary; the six tests that only exercised the deleted translator are gone.

flutter analyze clean; full suite 1328 passing, 0 failing.

STILL NOT ADDRESSED, and it is this PR's own stated goal: `_daySleep` returns
early on `tst == null` BEFORE the periods mapping, so a nap-only / night-shift
day still shows "No sleep recorded" with no nap row. Flagged rather than fixed
here -- it changes what the Sleep screen claims on a day with no night sleep,
which is a product decision.
@abdulsaheel
abdulsaheel force-pushed the fix/naps-visible-on-sleep-screen branch from 0d95f06 to 478e29f Compare August 7, 2026 16:04

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/data/local_repository_impl.dart (1)

615-630: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Derive total_asleep_min from the normalized periods.

_periodsWithMainStages drops invalid windows and clamps excessive durations. Line 630 still exports the raw engine total. A period clamped from 101 minutes to 30 minutes can therefore leave the day total inconsistent with the displayed cards.

Compute the total after normalization. Preserve null when a displayed duration is unknown. Add an assertion to the clamping case in test/sleep_periods_legacy_keys_test.dart that expects total_asleep_min to equal 30.

As per coding guidelines, “Behavior changes, especially regressions involving … synchronization … must include regression tests.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/data/local_repository_impl.dart` around lines 615 - 630, Update the
normalized periods mapping around _periodsWithMainStages so total_asleep_min is
derived from the displayed, normalized durations rather than the raw engine
total, summing known durations while preserving null when no displayed duration
is known. Add a regression assertion in the clamping case of
sleep_periods_legacy_keys_test.dart verifying total_asleep_min equals 30 for the
101-to-30-minute clamp.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/data/local_repository_impl.dart`:
- Around line 750-752: Update the duration normalization logic around dur so
negative duration_min values are clamped to windowMin rather than returned
unchanged; preserve null only for absent durations and retain existing handling
for valid values. Add a regression case covering a negative current-schema
duration_min.

---

Outside diff comments:
In `@lib/data/local_repository_impl.dart`:
- Around line 615-630: Update the normalized periods mapping around
_periodsWithMainStages so total_asleep_min is derived from the displayed,
normalized durations rather than the raw engine total, summing known durations
while preserving null when no displayed duration is known. Add a regression
assertion in the clamping case of sleep_periods_legacy_keys_test.dart verifying
total_asleep_min equals 30 for the 101-to-30-minute clamp.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ba58d482-e5ac-407f-bb1a-7807884da557

📥 Commits

Reviewing files that changed from the base of the PR and between 0d95f06 and 478e29f.

📒 Files selected for processing (3)
  • lib/data/local_repository_impl.dart
  • test/sleep_naps_visible_test.dart
  • test/sleep_periods_legacy_keys_test.dart

Comment thread lib/data/local_repository_impl.dart
@abdulsaheel

Copy link
Copy Markdown
Collaborator

Rebased onto main (478e29f, force-pushed with lease). Ready to merge, but the rebase changed what this PR is, so it needs your eyes rather than a rubber stamp.

The data-layer half became obsolete — and would have silently broken the screen

This branch fixed the writer/reader key mismatch at the screen end (sleepPeriodsForScreen, translating start/end/asleep_minonset_ts/wake_ts/duration_min). #204 fixed the same mismatch at the writer end: the producer now emits the screen's vocabulary directly, and _periodsWithMainStages translates any legacy payload on read so old finalized days still render.

Left as-is, the rebase was silently broken. night['periods'] = periods unconditionally overwrote the enriched list with the translator's output, and the translator reads p['start'] — which no longer exists. Every period would have been skipped, main sleep included, leaving an empty list under a hero total that still rendered. No error, no log.

So the overwrite and the translator are gone. One source per concern, and it's the writer.

What I kept, because it wasn't obsolete

Your later commits added real defensive invariants at the translator. Those are ported to _boundedPeriod at the surviving seam:

  • a period can't report more asleep minutes than its own window (clamped to the window — duration_min and the bounds come from different producers, so nothing else stops a card claiming more sleep than the period it sits in);
  • a degenerate window is dropped rather than rendered as a zero-length card.

Both re-pinned in sleep_periods_legacy_keys_test.dart.

The UI half is untouched, and it's the reason to merge this

The periods screen was unreachable in the shipped app — its only entry point was an AppScaffold action, and the Sleep tab embeds SleepNightContent, so that scaffold never builds. That plus the naps row is the real fix here, and it reads data['periods'], which main now populates correctly.

Your test file keeps the widget coverage with the fixture rebuilt in the current vocabulary; the six tests that only exercised the deleted translator are gone.

flutter analyze clean, full suite 1328 passing.

Still not addressed — this PR's own stated goal

_daySleep returns early on tst == null before the periods mapping, so a nap-only / night-shift day still shows "No sleep recorded" with no nap row. I left it: changing what the Sleep screen claims on a day with no night sleep is a product decision, not a mechanical fix. It's a small change if you want it in this PR.

Found reviewing my own rebase commit. Porting this branch's clamp to
`_boundedPeriod` kept the correction but DROPPED the mechanism that kept the
total consistent with it -- the original translator recomputed
`total_asleep_min` from the mapped periods for exactly this reason, and I did
not carry that over.

Reproduced: a nap claiming 101 min inside a 30-min window renders as 30, while
the hero kept the producer's pre-clamp 521.

    card durations : [420, 30]
    sum of cards   : 450
    hero total     : 521   <- a user can add the cards up and see this is wrong

`_totalAsleepMin` now recomputes from the RENDERED periods, so a read-side
correction can never leave the hero contradicting the cards under it.

ABSENT STILL STAYS ABSENT, which is the part that needed care. A null stored
total means the producer refused to state one -- usually because nap detection
abstained, so the day holds an unknown NUMBER of unmeasured naps (OpenStrap#204).
Summing the periods we happen to have would convert that honest "-" into a
confident figure that silently omits them. So: null in, null out; and a period
whose own duration is unknown makes the sum unknown again, for the same reason
it does at the writer.

3 tests added covering all three paths (clamped, absent, unknown-duration).
Full suite 1331 passing.
@abdulsaheel

Copy link
Copy Markdown
Collaborator

Reviewed this against merged #204, including my own rebase commit. Verdict: yes, it still makes sense — but for different reasons than when it was opened, and one of its two stated goals is still unmet.

I also found and fixed a defect I introduced. Details below, with the checks I actually ran.

1. A defect in my own rebase — fixed in 1b5d030

Porting your clamp to _boundedPeriod kept the correction but dropped the mechanism that kept the total consistent with it. Your original translator recomputed total_asleep_min from the mapped periods for exactly this reason; I didn't carry that over. Reproduced:

card durations : [420, 30]     <- nap clamped from 101 to its 30-min window
sum of cards   : 450
hero total     : 521           <- a user can add the cards up and see this is wrong

That is the precise inconsistency this PR set out to remove, reintroduced by me. _totalAsleepMin now recomputes from the rendered periods.

The part that needed care: absent stays absent. A null stored total means the producer refused to state one — usually because nap detection abstained, so the day holds an unknown number of unmeasured naps (#204). Summing the periods we happen to have would turn that honest "—" into a confident figure that silently omits them. So null in, null out; and a period whose own duration is unknown makes the sum unknown again. 3 tests, all three paths.

2. Does it still make sense? Verified end-to-end

I seeded real day bundles and called getDaySleep:

scenario result
normal night + nap has_sleep=true, naps=1 — nap visible ✅
nap-only day (no night sleep) has_sleep=false, periods=null

So the data layer works, and your UI half is the reason to merge this: the periods screen was unreachable in the shipped app (its only entry point was an AppScaffold action, and the Sleep tab embeds SleepNightContent, so that scaffold never builds). That fix is entirely yours and is untouched by the rebase.

What #204 made redundant was only the translator — and leaving it in would have been actively harmful, since it read p['start'] after the producer stopped emitting it.

3. The unmet goal, with the exact mechanism

A nap-only / night-shift day still shows "No sleep recorded" — the case this PR is named for. It needs two changes, not one:

  1. _daySleep returns early on tst == null before the periods mapping, so periods is never even attached.
  2. Even if it were, sleep_detail_screen.dart:114-117 sets _Phase.empty when has_sleep is false, so SleepNightContent — and your _napsRow() with it — never builds.

I've left this alone because it changes what the Sleep screen claims on a day with no night sleep, which is a product decision rather than a mechanical fix. Happy to implement it if you want it in this PR — it's small now that the data layer is sorted, and it's the difference between this PR meeting its title or not.


flutter analyze clean, full suite 1331 passing. Mergeable against main (6d37606, kAlgoVersion 59).

The title case -- a daytime sleep being invisible on the Sleep screen -- was
still unmet after the rebase, because it needed TWO changes and only the data
half had been discussed:

  1. `_daySleep` returned early on `tst == null` BEFORE the periods mapping, so
     the naps were dropped on the floor.
  2. Even with them attached, `sleep_detail_screen` sets `_Phase.empty` when
     `has_sleep` is false, so `SleepNightContent` -- and the naps row with it --
     never builds.

Verified end-to-end against a seeded bundle before and after:

    before: has_sleep=false  periods=null
    after:  has_sleep=false  periods=1     total=38

`has_sleep` deliberately stays FALSE. It means what it says: there is no NIGHT
to render a hypnogram, stages or efficiency for, and promoting a nap into one
would be exactly the conflation this file avoids everywhere else. What changes
is that the screen stops claiming nothing happened while the same nap is
credited against sleep need and drawn as a band on the Timeline -- three
surfaces, two answers.

The empty state now keeps its honest "No sleep recorded for this night" card
(with copy that says why there is no breakdown) and lists the daytime sleep
beside it, tapping through to the periods screen. Its total obeys the same
absent-is-not-zero rule as the rest: one nap with an unknown duration renders
the total as "-" rather than a partial sum presented as complete.

A day with neither a night nor naps is untouched -- no periods key, same empty
card as before.

4 tests added, mutation-verified (reverting the attach fails exactly the
nap-only test). Full suite 1333 passing.
@abdulsaheel

Copy link
Copy Markdown
Collaborator

Pushed 5b9e364this PR now actually meets its title.

The nap-only case needed two changes, and only the data half had come up before:

  1. _daySleep returned early on tst == null before the periods mapping, dropping the naps entirely.
  2. Even attached, sleep_detail_screen sets _Phase.empty when has_sleep is false, so SleepNightContent — and your _napsRow() with it — never builds.

Verified against a seeded bundle, before and after:

before: has_sleep=false  periods=null
after:  has_sleep=false  periods=1   total=38

has_sleep deliberately stays false. It means what it says: there is no night to render a hypnogram, stages or efficiency for, and promoting a nap into one would be the exact conflation this file avoids everywhere else. What changes is that the screen stops claiming nothing happened while the same nap is credited against sleep need and drawn as a band on the Timeline — three surfaces, two answers.

The empty state keeps its honest "No sleep recorded for this night" card (with copy explaining why there's no breakdown) and lists the daytime sleep beside it, tapping through to the periods screen. Its total obeys the same absent-is-not-zero rule as everything else: one nap with an unknown duration renders "—" rather than a partial sum dressed up as complete.

A day with neither a night nor naps is untouched.

4 tests, mutation-verified. Full suite 1333 passing, analyze clean.


Summary of where this PR landed, since it changed shape a lot:

Yours, kept the periods screen being reachable at all (its only entry point was an AppScaffold action the Sleep tab never builds), the naps row, the clamp/junk invariants (ported to _boundedPeriod)
Removed sleepPeriodsForScreen#204 fixed the same mismatch at the writer, and leaving the translator would have blanked the whole screen
Fixed by me a defect I introduced porting your clamp (hero total disagreed with the cards), and the nap-only gap above

Ready to merge from my side.

CodeRabbit found a real gap in the clamp I ported: `dur <= windowMin` accepts
NEGATIVE values, so a malformed duration passed straight through to both the
card and the hero. Reproduced -- a stored `duration_min: -50` rendered as -50
and summed into the total as -50, understating the day.

FIXED, but NOT the way the bot proposed. Its patch was
`(dur >= 0 && dur <= windowMin)`, which falls through to the existing
`return {...m, 'duration_min': windowMin}` -- so a corrupt -50 would be clamped
UP to the FULL WINDOW, inventing a whole night of sleep out of garbage. That is
a worse claim than the bug it fixes.

A negative duration is not a small measurement, it is not a measurement at all.
It becomes UNKNOWN: clamping down to 0 would assert "you did not sleep", which
we also do not know. Unknown then propagates through `_totalAsleepMin`, so the
hero reads "-" rather than a total built on a value we know is nonsense --
consistent with how absence is handled everywhere else on this seam.

1 test added, asserting both that the duration is null AND that it is not the
window length, so a future "fix" toward the bot's version fails loudly.
Full suite 1334 passing.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/ui/sleep/sleep_detail_screen.dart`:
- Around line 221-237: Use the authoritative _data['total_asleep_min'] in
_emptyStateNapsCard instead of summing rendered naps, displaying — when it is
null. In lib/ui/sleep/sleep_detail_screen.dart lines 438-451, propagate this
unknown-total state into SleepNightContent; at lines 645-662, suppress numeric
nap duration and count when the aggregate is unknown. Add a widget regression
test covering a nap-only payload with total_asleep_min: null.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 37344cc5-ab22-4d32-bffb-35a947922ad9

📥 Commits

Reviewing files that changed from the base of the PR and between 478e29f and 0618c07.

📒 Files selected for processing (3)
  • lib/data/local_repository_impl.dart
  • lib/ui/sleep/sleep_detail_screen.dart
  • test/sleep_periods_legacy_keys_test.dart

Comment thread lib/ui/sleep/sleep_detail_screen.dart Outdated
@abdulsaheel

Copy link
Copy Markdown
Collaborator

@coderabbitaivalid, fixed in 0618c07, but not with the proposed patch.

You're right that dur <= windowMin accepts negatives. Reproduced: a stored duration_min: -50 reached both the card and the hero as -50.

Why I diverged from the suggested diff. (dur >= 0 && dur <= windowMin) falls through to the existing return {...m, 'duration_min': windowMin}, so a corrupt -50 would be clamped up to the full window — inventing a whole night of sleep out of garbage. That's a worse claim than the bug.

A negative duration isn't a small measurement; it isn't a measurement. It now becomes unknown. Clamping down to 0 was also rejected: that asserts "you did not sleep", which we equally don't know. Unknown propagates through _totalAsleepMin, so the hero reads "—" rather than a total built on a value we know is nonsense — consistent with how absence is handled everywhere else on this seam.

The test asserts both that the duration is null and that it is not the window length, so a future "fix" toward the clamp-up version fails loudly.

Full suite 1334 passing.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@abdulsaheel Correct. Treating a negative duration_min as unknown is the safe behavior.

This prevents a corrupt value from becoming either a full-window duration or a zero-duration assertion. Propagating the unknown value through _totalAsleepMin keeps the hero total consistent with the rendered periods.

Thank you for adding the regression assertion that rejects the clamp-up outcome.

🐇 ✅


✏️ Learnings added
Learnt from: abdulsaheel
URL: https://github.com/OpenStrap/edge/pull/205

Timestamp: 2026-08-07T17:09:29.926Z
Learning: In `lib/data/local_repository_impl.dart`, `_boundedPeriod` must treat a negative `duration_min` as unknown (`null`), not clamp it to the period window or to zero. `_totalAsleepMin` must propagate an unknown rendered period duration to an unknown total so the Sleep hero total does not make a false sleep claim.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

You are interacting with an AI system.

…zero

CodeRabbit, valid on both counts, and both are the SAME rule I had just applied
one layer down reappearing in the UI.

1. `_emptyStateNapsCard` computed its own total by summing the rendered
   periods, ignoring `_data['total_asleep_min']`. The repository already
   decides that (`_totalAsleepMin`) and deliberately returns NULL when the
   producer could not state a complete figure -- usually because nap detection
   abstained, so the day holds an unknown NUMBER of naps. Re-summing whatever
   periods happen to be present presented a partial figure as the day's total,
   which is exactly the claim the layer below refused to make. Now reads the
   authoritative value and renders "-" when it is absent.

2. `_napMin` (pre-existing) folded with `?? 0`, so a nap whose duration is
   unknown counted as ZERO and the row showed the remainder as if it were the
   full nap total -- under-reporting by exactly the part we could not measure.
   Now returns null if any nap's duration is unknown; `_hm` already renders
   null as "-", so the row degrades honestly with no call-site change.

Both are the absent-is-not-zero rule this seam follows everywhere else. Worth
noting the shape of the mistake: I fixed hero-vs-cards at the repository layer
earlier in this branch, then introduced the same duplicate-aggregate at the
widget layer in the very next commit.

1 widget test added, mutation-verified (restoring `?? 0` fails it).
Full suite 1335 passing.
@abdulsaheel

Copy link
Copy Markdown
Collaborator

@coderabbitaivalid on both counts, fixed in cb4ff67. Both are the same rule I'd just applied one layer down, reappearing in the UI.

1. Duplicate aggregate. _emptyStateNapsCard summed the rendered periods instead of reading _data['total_asleep_min']. The repository already decides that and deliberately returns null when the producer couldn't state a complete figure — usually because nap detection abstained, so the day holds an unknown number of naps. Re-summing whatever periods happen to be present presented a partial figure as the day's total, which is exactly the claim the layer below refused to make. Now reads the authoritative value, renders "—" when absent.

2. _napMin folded with ?? 0 (pre-existing), so a nap with an unknown duration counted as zero and the row showed the remainder as if it were the full nap total — under-reporting by exactly the part we couldn't measure. Now returns null if any nap's duration is unknown; _hm already renders null as "—", so it degrades honestly with no call-site change.

Worth naming the shape of the mistake, since it's instructive: I fixed hero-vs-cards at the repository layer earlier in this branch, then introduced the same duplicate-aggregate at the widget layer in the very next commit. Your catch.

1 widget test, mutation-verified (restoring ?? 0 fails it). Full suite 1335 passing.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@abdulsaheel Correct. Both fixes preserve the unknown-duration contract in the UI.

_emptyStateNapsCard now uses the repository total and does not present a partial aggregate as a complete total.

_napMin now propagates an unknown nap duration. _hm then renders "—" without a false zero-duration claim.

Good regression coverage. Thank you for the mutation check.

🐇 ✅


✏️ Learnings added
Learnt from: abdulsaheel
URL: https://github.com/OpenStrap/edge/pull/205

Timestamp: 2026-08-07T17:22:33.696Z
Learning: In `lib/ui/sleep/sleep_detail_screen.dart`, `_emptyStateNapsCard` must use the authoritative `_data['total_asleep_min']` value instead of summing rendered periods. A null total represents an incomplete or indeterminate total and must render as `"—"`, not as a partial aggregate.

Learnt from: abdulsaheel
URL: https://github.com/OpenStrap/edge/pull/205

Timestamp: 2026-08-07T17:22:33.696Z
Learning: In `lib/ui/sleep/sleep_detail_screen.dart`, `_napMin` must return `null` when any nap has an unknown duration. It must not replace unknown durations with `0`, because that under-reports the nap total. `_hm` renders a null duration as `"—"`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@abdulsaheel
abdulsaheel merged commit 95e9460 into OpenStrap:main Aug 7, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants