mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
Surface sleep-debt and chronotype on the sleep page
Compute cumulative sleep debt and the MCTQ chronotype server-side off the canonical night reconstruction the Sleep Score reads, and render them on the sleep page: a sleep-debt headline with a calm partial state under the night threshold, and a chronotype card whose social-jetlag detail and corrected mid-sleep sit behind an advanced disclosure. Both hold a learning state on thin data and never assert a total or a band. Serve the same values to the dashboard summary and a new GET /api/sleep/rhythm read so the native client renders identical figures. Free vs work nights default to weekend in the user's timezone until day-type tracking exists.
This commit is contained in:
1 parent
4884f019b7
commit
d1ba01548f
22 files changed
+1489
-2
No files matched your search
@@ -216,6 +216,94 @@ const sleepNightResponse = z
|
||||
"One reconstructed sleep night: the main session's hypnogram segments + breakdown, plus same-wake-day naps surfaced separately.",
|
||||
});
|
||||
|
||||
// ── Sleep rhythm (sleep-debt + chronotype, v1.17.0) ──────────────────
|
||||
//
|
||||
// Server-authoritative timing signals computed off the SAME canonical night
|
||||
// reconstruction the Sleep Score reads. Both carry a calm not-yet-ready state
|
||||
// (`partial` / `learning`) below their night thresholds — they never assert a
|
||||
// total / band off thin data.
|
||||
const sleepDebtResource = z.object({
|
||||
state: z
|
||||
.enum(["partial", "ready"])
|
||||
.describe(
|
||||
"`partial` until enough tracked nights exist (calm 'still learning'); `ready` once the rolling window can assert a cumulative debt.",
|
||||
),
|
||||
debtMinutes: z
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative()
|
||||
.describe("Cumulative sleep debt over the window, in minutes, after caps."),
|
||||
needMinutes: z
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative()
|
||||
.describe("Age-based sleep need (minutes) used for the per-night deficit."),
|
||||
nightsCounted: z.number().int().nonnegative(),
|
||||
windowNights: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.describe("Rolling window length in nights."),
|
||||
nightsUntilReady: z
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative()
|
||||
.describe("Nights still needed before the debt is asserted (0 when ready)."),
|
||||
});
|
||||
|
||||
const chronotypeBandEnum = z.enum([
|
||||
"extreme_early",
|
||||
"early",
|
||||
"intermediate",
|
||||
"late",
|
||||
"extreme_late",
|
||||
]);
|
||||
|
||||
const chronotypeResource = z.object({
|
||||
state: z
|
||||
.enum(["learning", "ready"])
|
||||
.describe(
|
||||
"`learning` until enough free-day nights exist (no band asserted); `ready` once the free-day sample is large enough.",
|
||||
),
|
||||
msfMinutes: z
|
||||
.number()
|
||||
.nullable()
|
||||
.describe("Mid-sleep on free days (minutes-of-day), null while learning."),
|
||||
msfScMinutes: z
|
||||
.number()
|
||||
.nullable()
|
||||
.describe(
|
||||
"Sleep-debt-corrected mid-sleep on free days (MSFsc, minutes-of-day), null while learning.",
|
||||
),
|
||||
band: chronotypeBandEnum
|
||||
.nullable()
|
||||
.describe("MCTQ chronotype band off MSFsc, null while learning."),
|
||||
socialJetlagMinutes: z
|
||||
.number()
|
||||
.nullable()
|
||||
.describe(
|
||||
"Social jetlag = circular |MSF_work − MSF_free| in minutes, null when one side is missing.",
|
||||
),
|
||||
freeNightsCounted: z.number().int().nonnegative(),
|
||||
workNightsCounted: z.number().int().nonnegative(),
|
||||
freeNightsUntilReady: z
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative()
|
||||
.describe("Free-day nights still needed before a band is asserted."),
|
||||
});
|
||||
|
||||
const sleepRhythmResponse = z
|
||||
.object({
|
||||
sleepDebt: sleepDebtResource,
|
||||
chronotype: chronotypeResource,
|
||||
})
|
||||
.meta({
|
||||
id: "SleepRhythmResource",
|
||||
description:
|
||||
"Server-authoritative sleep-rhythm read: cumulative sleep debt over the rolling window + MCTQ chronotype (MSF/MSFsc band, social jetlag). Free vs work nights default to weekend = free in the user's timezone (no work calendar). A view over existing per-stage SLEEP_DURATION rows — no schema, no new type.",
|
||||
});
|
||||
|
||||
// ── Time-series adapter (iOS chart source) ───────────────────────────
|
||||
//
|
||||
// v1.16.16 — documents the long-shipped `GET /api/measurements/series`
|
||||
@@ -511,4 +599,23 @@ export const measurementPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/sleep/rhythm": {
|
||||
get: {
|
||||
tags: ["Measurements"],
|
||||
summary: "Sleep rhythm — sleep-debt + chronotype (v1.17.0)",
|
||||
description:
|
||||
"Returns the two server-authoritative sleep-timing signals the Sleep page + iOS render off the same canonical night reconstruction the Sleep Score uses: cumulative `sleepDebt` over the rolling window (calm `partial` state under the night threshold) and MCTQ `chronotype` (MSF/MSFsc band + social jetlag, `learning` state until enough free-day nights exist). Free vs work nights default to weekend = free in the user's timezone (no work calendar). A read-only view over existing per-stage SLEEP_DURATION rows — no schema, no new measurement type.",
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Sleep-debt + chronotype DTO.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(sleepRhythmResponse, "SleepRhythmResponse"),
|
||||
},
|
||||
},
|
||||
},
|
||||
...stdResponses,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user