mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
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.
83 lines
2.4 KiB
TypeScript
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() };
|
|
}
|