mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
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:
1 parent
3004f55875
commit
63bbf0931c
20 files changed
+339
-94
No files matched your search
@@ -28,6 +28,17 @@ vi.mock("@/lib/modules/gate", () => ({
|
||||
requireModuleEnabled: vi.fn(async () => ({ enabled: true })),
|
||||
}));
|
||||
|
||||
// The latest-night walk pages nights in SQL; its own behaviour is pinned by
|
||||
// the SQL parity integration suite. Here it is stubbed to a fixed day so the
|
||||
// route's "no date" arm can be read.
|
||||
vi.mock("@/lib/measurements/list/sleep-nights", async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<
|
||||
typeof import("@/lib/measurements/list/sleep-nights")
|
||||
>();
|
||||
return { ...actual, latestMainSleepNight: vi.fn(async () => null) };
|
||||
});
|
||||
|
||||
vi.mock("@/lib/db-compat", () => ({
|
||||
ensureDbCompatibility: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
@@ -49,6 +60,7 @@ vi.mock("next/headers", () => ({
|
||||
import { GET } from "../route";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { getSession } from "@/lib/auth/session";
|
||||
import { latestMainSleepNight } from "@/lib/measurements/list/sleep-nights";
|
||||
|
||||
const SESSION_OK = {
|
||||
session: { id: "sess-1", expiresAt: new Date(Date.now() + 3_600_000) },
|
||||
@@ -133,15 +145,13 @@ describe("GET /api/sleep/night", () => {
|
||||
expect(Number.isInteger(body.data.main.awakeMinutes)).toBe(true);
|
||||
});
|
||||
|
||||
it("defaults to the most recent scorable night when date omitted", async () => {
|
||||
it("defaults to the latest main night when date omitted", async () => {
|
||||
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
|
||||
vi.mocked(prisma.measurement.findMany).mockResolvedValue([
|
||||
stage("2026-06-02T03:00:00.000Z", "CORE", 200),
|
||||
stage("2026-06-04T03:00:00.000Z", "CORE", 300),
|
||||
] as never);
|
||||
vi.mocked(prisma.measurement.findFirst).mockResolvedValue({
|
||||
measuredAt: new Date("2026-06-04T03:00:00.000Z"),
|
||||
} as never);
|
||||
vi.mocked(latestMainSleepNight).mockResolvedValueOnce("2026-06-04");
|
||||
const res = await GET(req(""));
|
||||
const body = await res.json();
|
||||
expect(body.data.night).toBe("2026-06-04");
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
* (WHOOP) — the client renders the hypnogram but labels it an approximate
|
||||
* layout. A real-series source (Apple Health / Withings / Fitbit) is false.
|
||||
*
|
||||
* `date` omitted → the most recent night that holds sleep, however old.
|
||||
* `date` omitted → the most recent night with a main sleep, however old; a
|
||||
* run of nap-only days since is stepped over (each stays reachable by its
|
||||
* `date`). An account that has only ever napped gets its latest nap day.
|
||||
*
|
||||
* Only the stage rows within a day and a half of the night are read, as the
|
||||
* list's drill-down reads them (`readNightSessions`), so any past night is
|
||||
@@ -39,7 +41,7 @@ import {
|
||||
type SleepSession,
|
||||
} from "@/lib/analytics/sleep-night";
|
||||
import {
|
||||
latestSleepNight,
|
||||
latestMainSleepNight,
|
||||
readNightSessions,
|
||||
} from "@/lib/measurements/list/sleep-nights";
|
||||
import { loadUserSourcePriority } from "@/lib/rollups/measurement-read";
|
||||
@@ -131,7 +133,9 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
]);
|
||||
|
||||
const targetDay =
|
||||
date ?? (await latestSleepNight(user.id, tz, priorityJson)) ?? undefined;
|
||||
date ??
|
||||
(await latestMainSleepNight(user.id, tz, priorityJson)) ??
|
||||
undefined;
|
||||
const { main, naps } = targetDay
|
||||
? pickMainNightAndNaps(
|
||||
(await readNightSessions(user.id, targetDay, tz, priorityJson))
|
||||
|
||||
Reference in new issue
Block a user