Unify sleep-night reconstruction onto the canonical engine

Replace the divergent UTC-day night reconstructor in the Sleep Score with a
thin adapter over reconstructSleepNights, the shared engine the dashboard,
hypnogram, and doctor surfaces already read. The old path bucketed rows by UTC
calendar day and never deduplicated writers, so a multi-source night (WHOOP +
Apple Health for the same sleep) summed both writers and roughly doubled the
total. The adapter inherits session-clustering, local-wake-day keying, and
multi-source writer dedup, then derives the score-only fields the canonical
night does not carry: REM and Deep minutes from the stage map, the stage-
breakdown flag from any granular stage, and the midpoint from the asleep span.

Select source and deviceType in the Sleep Score read so the writer-dedup
ladder fires, and thread the user source priority through so the chosen writer
matches every other sleep surface. Round emitted asleep and in-bed minutes to
whole numbers, and round the per-stage map in the sleep-night DTO, matching the
integer contract the native client decodes.

A multi-source user's sleep score and the readiness score that folds it drop
once the double-count is removed, reflecting the true single-writer night.
This commit is contained in:
Marc-André Bombeck committed 2026-06-14 17:14:14 +02:00
1 parent a2260d5bb8
commit b4b9d8d9d7
7 files changed
+169 -95

No files matched your search

+8 -1
View File
@@ -64,7 +64,14 @@ function serializeSession(s: SleepSession) {
inBedMinutes: s.inBedMinutes === null ? null : Math.round(s.inBedMinutes),
awakeMinutes: s.awakeMinutes === null ? null : Math.round(s.awakeMinutes),
awakenings: s.awakenings,
stages: s.stages,
// iOS #18 — round the per-stage map too; the underlying totals sum
// second-precision segments and otherwise serialise as e.g. 88.4999.
stages: Object.fromEntries(
Object.entries(s.stages).map(([stage, minutes]) => [
stage,
Math.round(minutes),
]),
),
segments: s.segments.map((seg) => ({
stage: seg.stage,
start: seg.start.toISOString(),