Skip to content

Feature the night underway instead of a 4am clock key - #42

Draft
gsbernstein wants to merge 6 commits into
masterfrom
cursor/feature-night-underway-d0f3
Draft

Feature the night underway instead of a 4am clock key#42
gsbernstein wants to merge 6 commits into
masterfrom
cursor/feature-night-underway-d0f3

Conversation

@gsbernstein

Copy link
Copy Markdown
Owner

Problem

Two different rules decided which day a night belonged to. Sessions were bucketed by midpoint + 6h (SleepSession.dateForGrouping), but the summary card looked them up under startOfDay(now - 4h). Between midnight and 4am those disagree.

Concretely: sleep 10:00 PM Monday to 2:00 AM Tuesday, then open the app at 2:30 AM. The session's midpoint is midnight, so +6h files it under Tuesday, while the lookup key is 2:30 AM − 4hMonday. The card showed "No sleep data available" while Recent Sleep listed that same session one card below. The empty key also drove recentSourceAppLinks, so the app offered to "Open Oura to sync last night's sleep" for data it already had.

A second case hit outside that window: a 9–11 PM block has a 10 PM midpoint, so it files under the next day. At 11:30 PM it was invisible to a card keyed to today.

Changes

One definition of a sleep day. SleepDay owns the 6pm boundary, and dateForGrouping now delegates to it. Applied to a session's midpoint it buckets the session; applied to now it yields the sleep day underway — same arithmetic, two uses.

Feature the night underway, don't hardcode 4am. The card now shows the sleep day underway, so waking briefly at 2am shows tonight's sleep so far rather than yesterday's total. That's deliberate: a partial night is a real answer to "where do I stand if I don't fall back asleep," and nothing is locked in.

Fallback is deliberately narrow. Only in the small hours — before Constants.smallHoursEndHour — does an empty night fall back to the night before, covering sleep still in progress or a tracker that hasn't synced. Later in the day an empty night stays empty so the no-data state and the sync prompt can do their job instead of resurrecting older sleep. This is why the daytime needs no fallback at all: the sleep day underway already is last night's completed sleep.

Time Night underway has data? Featured
2:30 AM partial tonight's partial
2:30 AM nothing yet previous night (small-hours fallback)
noon last night's sleep that night
noon nothing the empty night, so the sync prompt shows
11 PM nothing the empty night (no fallback)
11 PM evening nap the nap, which buckets to the night underway

Made the rule testable. The selection lived in a ContentView computed property, reachable only by running the app at the relevant hours. It's now a pure function over the nights that have data, with now and the calendar injected.

Added a test target. BedtimeTests (Swift Testing), 12 cases: the 6pm boundary either side, both DST transitions, and every branch of the rule including the 4am cutoff. All inject a fixed America/New_York calendar so results don't depend on the machine's time zone.

Verification

Not verifiable in this environment: per AGENTS.md, Cursor Cloud VMs are Linux and this project can't be built, run, or tested without Xcode and the iOS SDK. The tests are written but have never been executed.

project.pbxproj was edited outside Xcode, so I re-parsed it afterward and confirmed both targets resolve, all object ids are well-formed and unique, and no references dangle. Still worth a sanity check on open:

  • Project opens with BedtimeTests visible in the navigator, and ⌘U runs the 12 tests.
  • The 2:30 AM case: seed a night ending after midnight, set the clock past midnight, and confirm the card shows it instead of "No sleep data" with an Oura prompt.
  • A daytime launch with no sleep recorded still shows the no-data state and sync links.

One deliberate omission: the card is still titled "Last Night" even when showing sleep in progress. A dynamic "Tonight" title needs a rule for when a night stops being tonight, and the obvious candidates all misfire on the evening-nap case, so I left it rather than invent one.

Open in Web Open in Cursor 

cursoragent and others added 6 commits August 7, 2026 22:31
lastNightKey bucketed by startOfDay(now - 4h) while sessions bucket by
midpoint + 6h, so between midnight and 4am the card looked under yesterday
for sleep filed under today: a 2am wake-up showed "No sleep data" while
Recent Sleep listed that same session, and the empty key also offered to
sync data that was already present.

Both now derive the day from one place, SleepDay. The card features the
sleep day underway, so partial sleep shows as it accumulates, falling back
to the previous night only in the small hours when nothing is recorded yet.
After that an empty night stays empty so the sync prompt can appear.

Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
The rule lived in a ContentView computed property, so it could only be
exercised by running the app at the hours in question. Moving it beside the
day math it depends on makes it a pure function over the nights that have
data, with now and the calendar injectable.

Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Twelve Swift Testing cases pin the 6pm sleep-day boundary, both DST
transitions, and each branch of the featured-night rule: small-hours
partial sleep, the small-hours fallback, the 4am cutoff, daytime staying
empty so the sync prompt can appear, and an evening nap counting as the
night underway. All inject a fixed calendar so they don't depend on the
machine's time zone.

The project file was edited outside Xcode, so it was re-parsed afterward to
confirm both targets resolve with no dangling references.

Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
smallHoursEndHour read awkwardly: "hours" meant the wee hours while "Hour"
meant an hour of the day. A range says it once, matches sleepBankDaysRange,
and reads as a membership test at the call site.

Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
The guard's success path meant "don't fall back," so the branch that
returned the previous night was the one that fell through it. An if states
the case directly: nothing recorded yet and still the small hours means use
the night before.

Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
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