Skip to content

feat(push): export sleep performance and consistency in protocol 1.1 - #2715

Open
fearthedear wants to merge 2 commits into
ryanbr:mainfrom
fearthedear:feat/self-hosted-push-sleep-performance-v1-1
Open

fearthedear wants to merge 2 commits into
ryanbr:mainfrom
fearthedear:feat/self-hosted-push-sleep-performance-v1-1

Conversation

@fearthedear

@fearthedear fearthedear commented Oct 6, 2026 •

Copy link
Copy Markdown

Summary

Extends the existing Android self-hosted push protocol 1.1 proposal with two explicit nullable fields on dailyMetric: sleepPerformance and sleepConsistency.

  • sleepPerformance remains the exact matching source/day metricSeries.sleep_performance projection.
  • sleepConsistency uses the matching source/day imported metricSeries.sleep_consistency series when the latest exported day has an imported point. Otherwise it reproduces NOOP's existing Sleep-screen bedtime-onset score from same-source sessions: trailing up-to-14 onsets, edited onset respected, local wake-day attribution, population SD mapped with 100 * (1 - SD / 120), and null until three sessions exist.
  • Protocol 1.0's exact field set is unchanged. No raw sleep data, recovery-as-sleep-score mapping, or general metricSeries stream is added.

The consistency computation is Android-specific because the current self-hosted push sender is Android-first; the wire definition is documented for future platform senders.

Verification

  • Added protocol/wire tests for v1.1 fields, 0–100 bounds, v1.0 rejection/compatibility, and coordinator serialization.
  • Added Room/Robolectric coverage for matching source/day, imported-series preference, edited bedtime, wake-day mapping, the exact UI formula, and null/insufficient-history behavior.
  • git diff --check passes.
  • Android unit tests were not run locally: this host has no Java/JDK executable. The Android CI workflow should run for this PR.

fearthedear and others added 2 commits October 6, 2026 12:18
Co-authored-by: fearthedear <12115297+fearthedear@users.noreply.github.com>
Co-authored-by: fearthedear <12115297+fearthedear@users.noreply.github.com>
@fearthedear fearthedear changed the title feat(push): export daily sleep performance in protocol 1.1 feat(push): export sleep performance and consistency in protocol 1.1 Oct 9, 2026
@ryanbr

ryanbr commented Oct 13, 2026

Copy link
Copy Markdown
Owner

Approved the workflow runs, so Android CI has now run this and is green.

I diffed SleepConsistencyPushProjection.byWakeDay against SleepModelLogic.consistencySeries point by point, because a second copy of a scoring formula is the thing most worth checking here. The reproduction is faithful:

  • sessions ordered by startTs
  • onset value is startTsAdjusted ?: startTs
  • bed minutes as hour * 60 + minute, with < 12 * 60 wrapped + 24 * 60
  • fewer than 3 sessions yields nothing
  • trailing window [max(0, i - 13), i], skipped below 3 points
  • POPULATION variance (divided by window.size, not size - 1)
  • 100 * (1 - sd / 120) clamped to 0 through 100

The zone differs in form only, and resolves the same: the UI path uses Calendar.getInstance() and yours takes an injected ZoneId that PushDao.kt:178 fills with ZoneId.systemDefault(). Injecting it is the better shape for a test, and it agrees at runtime.

The day attribution is the one part with no counterpart to check against

consistencySeries never keys by day. It returns an ordered list and the screen takes scores.lastOrNull(), so the day mapping here is new code rather than a reproduction, and it has to join dailyMetric.day.

It keys on Instant.ofEpochSecond(ordered[index].endTs).atZone(zoneId).toLocalDate(), a plain midnight boundary. NOOP has a dedicated local-day helper (LocalDayWindows.dayKey) and a 04:00 logical-day convention used elsewhere (#144, with the pre-04:00 carve-out in #304). If dailyMetric.day is not midnight-derived, then a session ending between 00:00 and 03:59 keys to one day while its metric row carries another, and computedConsistency[day] comes back null for that night. Could you confirm which derivation the day column uses, and reuse that helper rather than a raw toLocalDate()? Most nights end after 04:00 so the two agree, which is exactly what makes this the kind of gap that ships.

One thing worth adding while the twin is fresh

The formula matches today and nothing keeps it matching. A test that runs both implementations over the same session list and asserts equality costs very little and makes the copy self-policing, so the next edit to the UI formula fails here instead of silently exporting a different number than the screen shows. SleepModelLogic.consistencySeries is internal, so a test in the same module can reach both.

The rest reads well: holding protocol 1.0's field set fixed, preferring the imported series when the latest exported day has a point, and the 0 through 100 bound tests are each the right call. The Android-specific note in the body is fair given where the sender lives.

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