Files
HealthLog/src/hooks/use-sleep-night.ts
T
Marc-André Bombeck 3cf67d05f9 fix(ui): let the read-failure primitives decide by the error
About forty compact rows and seventy cards still offered a retry on a
spent read budget and drew an error card on a refusal, while a third
primitive (ReadFailureRow) did the right thing at eight places.

QueryErrorRow and QueryErrorCard now take the query's `error` and read
it through readFailureKind, which moves to src/lib/api next to
ApiError: a refusal (403) draws the surface's refused state or nothing,
a spent budget (429) says so without a retry, an ended session (401)
says so and offers sign-in instead of going blank, and only a real
failure offers the retry. ReadFailureRow is gone and every site passes
its error; a guard test fails on a site that does not.

The coach read strip keeps its card in the error state and holds one
body height across loading, failure and a one-line read.
2026-10-20 03:38:57 +02:00

83 lines
2.4 KiB
TypeScript

"use client";
import { useQuery } from "@tanstack/react-query";
import { apiGet } from "@/lib/api/api-fetch";
import { queryKeys } from "@/lib/query-keys";
/**
* One reconstructed night from `GET /api/sleep/night?date=YYYY-MM-DD`.
*
* The date is the WAKE day: the night a person woke from into that day.
* The server owns the reconstruction (stage folding, cross-source pick,
* WHOOP timeline synthesis) and the day keying, so this hook only carries
* the resolved figures across.
*
* The route is gated on the `sleep` module and answers 403 when it is off.
* Callers gate the mount on `useModuleEnabled("sleep")` so a switched-off
* module never paints as a failed read.
*/
export interface SleepNightSegment {
stage: string;
start: string;
end: string;
minutes: number;
}
export interface SleepNightSession {
/** `YYYY-MM-DD` wake-day key. */
night: string;
source: string | null;
start: string;
end: string;
asleepMinutes: number;
inBedMinutes: number | null;
awakeMinutes: number | null;
/** Mid-sleep awakenings. `0` is a real answer, not an absence. */
awakenings: number;
reconstructed: boolean;
stages: Record<string, number>;
segments: SleepNightSegment[];
}
export interface SleepNightPayload {
night: string | null;
/** `null` when nothing scorable was recorded for that night. */
main: SleepNightSession | null;
naps: SleepNightSession[];
}
export interface UseSleepNightResult {
/** `undefined` until the route has answered — never confuse it with a
* night that carried nothing. */
data: SleepNightPayload | undefined;
isError: boolean;
/** The failed read's error, for the error row to read. */
error: unknown;
refetch: () => void;
}
/**
* Reads the night a person woke from into `date`.
*
* `enabled` is the caller's gate — pass `false` when the user is signed
* out or the sleep module is disabled, and nothing is fetched.
*/
export function useSleepNight(
date: string | undefined,
enabled: boolean,
): UseSleepNightResult {
const { data, isError, error, refetch } = useQuery({
queryKey: queryKeys.sleepNight(date),
queryFn: () =>
apiGet<SleepNightPayload>(
`/api/sleep/night?date=${encodeURIComponent(date as string)}`,
),
enabled: enabled && Boolean(date),
staleTime: 5 * 60_000,
refetchOnWindowFocus: false,
});
return { data, isError, error, refetch: () => void refetch() };
}