Merge branch 'fix/v1437-review' into release/v1.43.7

# Conflicts:
#	src/app/api/analytics/route.ts
This commit is contained in:
Marc-André Bombeck committed 2026-10-20 06:20:42 +02:00
commit 45c027aee2
217 files changed
+3636 -2127

No files matched your search

+3 -1
View File
@@ -18,6 +18,7 @@
* to the derived contract cannot leave this document behind.
*/
import { z } from "zod/v4";
import { recoveryEmptySchema } from "./recovery-empty";
import type { ZodOpenApiObject } from "zod-openapi";
import { SCORE_PILLAR_IDS } from "@/lib/analytics/score/types";
@@ -140,11 +141,12 @@ const analyticsSummariesSlice = z
),
lastSeenByType,
sleepSourceDiscrepancy,
recoveryEmpty: recoveryEmptySchema,
})
.meta({
id: "AnalyticsSummariesSlice",
description:
"What `?slice=summaries` answers: the per-type summaries and their freshness, and nothing thick. Four keys, and the caller gets them from two SQL passes rather than the default slice's chain.",
"What `?slice=summaries` answers: the per-type summaries and their freshness, the recovery page's empty reason, and nothing thick. Five keys, and the caller gets them from two SQL passes rather than the default slice's chain.",
});
// ── Correlations ─────────────────────────────────────────────────────
+9 -1
View File
@@ -5,6 +5,7 @@
* Schemas come from `src/lib/validations/*` where shared with the
* runtime request parsing, so the wire contract stays single-source.
*/
import { recoveryEmptySchema } from "../recovery-empty";
import { trendSentiment, trendVerdict } from "../trend-verdict";
import { z } from "zod/v4";
import { measurementSourceEnum, measurementTypeEnum } from "../shared";
@@ -324,7 +325,7 @@ export const sleepScoreValue = z
.enum(["few_nights", "no_main_sleep"])
.optional()
.describe(
"Only on a null `consistency` / `timing`: why it is withheld. `few_nights` — fewer than three nights in the window have a main sleep on the clock (count in `rhythmNights`); `no_main_sleep` — the latest day had only a daytime nap. Absent otherwise.",
"Only on a null `consistency` / `timing`: why it is withheld. `few_nights` — fewer than `rhythmMinNights` nights in the window have a main sleep on the clock (count in `rhythmNights`); `no_main_sleep` — the latest day had only a daytime nap. Absent otherwise.",
),
}),
)
@@ -336,6 +337,12 @@ export const sleepScoreValue = z
.describe(
"Nights in the window with a main sleep placed on the clock — what consistency and timing are computed from. A daytime nap is not one; a short night is.",
),
rhythmMinNights: z
.number()
.int()
.describe(
'How many `rhythmNights` consistency and timing need before they are scored; below it they carry `gap: "few_nights"`.',
),
series: z
.array(z.number())
.describe(
@@ -2032,6 +2039,7 @@ export const analyticsSummariesSliceResponse = z
),
lastSeenByType: z.record(z.string(), lastSeenSlot),
sleepSourceDiscrepancy,
recoveryEmpty: recoveryEmptySchema,
})
.meta({
id: "AnalyticsSummariesSlice",
+3 -3
View File
@@ -465,7 +465,7 @@ const sleepNightResponse = z
.meta({
id: "SleepNightResource",
description:
"One reconstructed sleep night: the main session's hypnogram segments + breakdown, plus same-wake-day naps surfaced separately.",
"One reconstructed sleep night: the main session's hypnogram segments + breakdown, plus same-wake-day naps surfaced separately. A session is a nap when it holds under three hours asleep centred between 10:00 and 20:00 local time; the main session is the longest session that is not a nap. A day whose only sleep is naps has `main: null` and its naps.",
});
// ── Sleep rhythm (sleep-debt + chronotype, v1.17.0) ──────────────────
@@ -804,7 +804,7 @@ export const measurementPaths: NonNullable<ZodOpenApiObject["paths"]> = {
summary: "List measurements",
description:
"Filter by type + date range. Response is paged via offset/limit; default limit 100.\n\n" +
"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. `type=SLEEP_DURATION` returns one row per night, paged the same way over nights. 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.",
"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.",
requestParams: {
query: listMeasurementsSchema,
},
@@ -1118,7 +1118,7 @@ export const measurementPaths: NonNullable<ZodOpenApiObject["paths"]> = {
tags: ["Measurements"],
summary: "Reconstructed sleep night (hypnogram source, v1.11.5)",
description:
"Returns one night's reconstructed sleep session for the phase-progression (hypnogram) view: the canonical 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. A read-only view over existing per-stage SLEEP_DURATION rows — no schema, no new measurement type.",
"Returns one night's reconstructed sleep session for the phase-progression (hypnogram) view: the canonical 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.",
requestParams: {
query: z.object({
date: z
+10
View File
@@ -0,0 +1,10 @@
import { z } from "zod/v4";
import { RECOVERY_EMPTY_REASONS } from "@/lib/insights/recovery-empty";
/** Why the recovery page is empty; shared by both slim-slice schemas. */
export const recoveryEmptySchema = z
.enum(RECOVERY_EMPTY_REASONS)
.nullable()
.describe(
"Why the recovery page has nothing to chart, resolved on the server; null when any recovery type (ANS_CHARGE, DAY_STRAIN, WORKOUT_STRAIN, CARDIO_LOAD, AVERAGE_HEART_RATE, MAX_HEART_RATE, ENERGY_EXPENDITURE_KJ, RESILIENCE) has a reading. `oura_permission`: Oura is connected and its grant leaves out the scope the resilience level needs, one reconnect fixes it. `oura_no_resilience`: Oura is connected and no level has arrived yet. `general`: anyone else. Clients render the empty note from this value and never work it out from the connection status.",
);