fix(sleep): one day total for every sleep reader, and the latest night is a night

A day's sleep is the main night plus its naps (daySleepMinutes). The
series, the sleep score and the composition already read it that way; the
sleep list, the CSV export and the correlation sleep channel read the main
night alone, so a day of naps listed as 0 h and an average over the list
counted it as a night without sleep. The list now carries the day total in
value and the main night alone in the new mainAsleepMinutes field; the nap
caption reads as included.

GET /api/sleep/night without a date answered the latest day holding any
sleep, so a nap after the last night left the hypnogram with no night to
draw. It now walks back to the latest night with a main sleep; nap days
stay reachable by date.

The SQL night page bounded a night by its asleep sessions only, so an
in-bed-only session of the same night fell outside a chunked read that the
one-night read counted. The span now covers every session of the night.
This commit is contained in:
Marc-André Bombeck committed 2026-10-20 09:28:40 +02:00
1 parent 3004f55875
commit 63bbf0931c
20 files changed
+339 -94

No files matched your search

+64 -9
View File
@@ -4207,11 +4207,13 @@ paths:
With `groupBy=day` on a cumulative type the rows are one sum per local day, and `limit` / `offset` page over
those days; every returned day is complete, which is why `valueMin` / `valueMax` (a filter on single readings)
are refused with 422 beside `groupBy=day`. `type=SLEEP_DURATION` returns one row per night, paged the same way
over nights; every night of the page has a row, and a day whose only sleep is daytime naps carries `value` 0
with its naps in `napCount` / `napAsleepMinutes`. Its `dayKey` is the same day `/api/sleep/night?date=` answers
for. On both, `meta.total` counts the days or nights in the filtered window. A day-sum page takes `limit` as
asked (up to the list's 5000); a sleep page holds at most 1000 nights, and `meta.limit` echoes the value
applied.
over nights; every night of the page has a row, and `value` is the day's time asleep, the main night plus its
naps (the same total `/api/measurements/series?kind=sleep` and the sleep score read), with the main night alone
in `mainAsleepMinutes` and the naps in `napCount` / `napAsleepMinutes`; a day whose only sleep is daytime naps
carries the naps' minutes and `mainAsleepMinutes: null`. Its `dayKey` is the same day `/api/sleep/night?date=`
answers for. On both, `meta.total` counts the days or nights in the filtered window. A day-sum page takes
`limit` as asked (up to the list's 5000); a sleep page holds at most 1000 nights, and `meta.limit` echoes the
value applied.
parameters:
- in: query
name: type
@@ -4732,16 +4734,18 @@ paths:
source's stage segments (each with an absolute start/end), the per-stage breakdown, asleep/in-bed/awake totals,
the mid-sleep awakenings count, and same-wake-day naps surfaced separately. Session-clustered, keyed by the
local wake day, collapsed to one source via the sleep priority ladder so two sources never overlay. `date`
omitted returns the most recent night holding sleep, however old. Any past date is answered, with no window cut.
A read-only view over existing per-stage SLEEP_DURATION rows — no schema, no new measurement type."
omitted returns the most recent night with a main sleep, however old; later days of naps alone are stepped over
and stay reachable by `date`. An account that has only ever napped gets its latest day of naps. Any past date is
answered, with no window cut. A read-only view over existing per-stage SLEEP_DURATION rows — no schema, no new
measurement type."
parameters:
- in: query
name: date
schema:
description: Wake-day key (YYYY-MM-DD). Omit for the most recent night.
description: Wake-day key (YYYY-MM-DD). Omit for the most recent night with a main sleep.
type: string
pattern: ^\d{4}-\d{2}-\d{2}$
description: Wake-day key (YYYY-MM-DD). Omit for the most recent night.
description: Wake-day key (YYYY-MM-DD). Omit for the most recent night with a main sleep.
responses:
"200":
description: Reconstructed night.
@@ -46284,6 +46288,23 @@ components:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
mainAsleepMinutes:
description: The main night's own time asleep in minutes; null on a day of naps alone. On a sleep row `value` is the
day's total, the main night plus its naps.
anyOf:
- type: integer
minimum: 0
maximum: 9007199254740991
- type: "null"
napCount:
description: Naps on the same wake day.
type: integer
minimum: 0
maximum: 9007199254740991
napAsleepMinutes:
description: Time asleep across those naps, in minutes.
type: number
minimum: 0
required:
- id
- type
@@ -47108,6 +47129,23 @@ components:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
mainAsleepMinutes:
description: The main night's own time asleep in minutes; null on a day of naps alone. On a sleep row `value` is the
day's total, the main night plus its naps.
anyOf:
- type: integer
minimum: 0
maximum: 9007199254740991
- type: "null"
napCount:
description: Naps on the same wake day.
type: integer
minimum: 0
maximum: 9007199254740991
napAsleepMinutes:
description: Time asleep across those naps, in minutes.
type: number
minimum: 0
required:
- id
- type
@@ -47289,6 +47327,23 @@ components:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
mainAsleepMinutes:
description: The main night's own time asleep in minutes; null on a day of naps alone. On a sleep row `value` is the
day's total, the main night plus its naps.
anyOf:
- type: integer
minimum: 0
maximum: 9007199254740991
- type: "null"
napCount:
description: Naps on the same wake day.
type: integer
minimum: 0
maximum: 9007199254740991
napAsleepMinutes:
description: Time asleep across those naps, in minutes.
type: number
minimum: 0
status:
type: string
enum: