mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 14:40:52 +00:00
Apple Health (and some WHOOP shapes) write BOTH an unspecified ASLEEP aggregate row AND the granular CORE/DEEP/REM breakdown for the same sleep period. The per-night reconstruction summed both, so a real ~8 h night surfaced as ~16 h across every sleep surface — the chart showed ~16-18 h points and the target-range 30-day average read ~16.4 h/day. Prefer the granular partition: when any CORE/DEEP/REM stage is present for a night/source, count only those; fall back to the bare ASLEEP (or a stage-less legacy/manual) row only when no granular stage exists. IN_BED and AWAKE stay excluded; dual-source nights still collapse to one canonical source via the user's sleep priority ladder. The same rule applies inside the per-source asleep tiebreak so a source writing both shapes is not over-weighted. Align the surfaces that bypassed the shared helper: - insights/targets: SLEEP_DURATION was averaged as raw per-stage rows (minutes) and fed into a target band expressed in hours, producing the bogus 16.4 h/day. Reconstruct per-night time-asleep, convert to hours, and classify + roll up consistency on the nightly hour totals. - coach snapshot: route the sleep block through reconstructSleepNights so the prompt narrates the same nightly numbers (was summing every stage row, including IN_BED + AWAKE). - sleep card chart: read /api/measurements/series?kind=sleep (night reconstruction, hours) instead of the generic measurement daily-average path, and label the axis in hours instead of minutes. Add regression fixtures: a night with both a bare ASLEEP row and the granular breakdown asserts the granular total (no double-count); a dual-source night with both shapes per source; and a multi-night 30-day average that lands at ~8 h, not ~16 h.
53 lines
1.9 KiB
TypeScript
53 lines
1.9 KiB
TypeScript
"use client";
|
|
|
|
import { HealthChartDynamic } from "@/components/charts/health-chart-dynamic";
|
|
import { useTranslations } from "@/lib/i18n/context";
|
|
import type { ComparisonBaseline } from "@/lib/dashboard-layout";
|
|
|
|
/**
|
|
* v1.4.25 W4c — nightly TIME-ASLEEP over time.
|
|
*
|
|
* Thin wrapper around `<HealthChartDynamic>` so the sleep sub-page gets
|
|
* the same chart-cog parity (`chartKey="sleep"`) as the dashboard chart
|
|
* surfaces.
|
|
*
|
|
* Data path: SLEEP_DURATION rows are stored per-stage (one row per stage
|
|
* per night, in MINUTES). `<HealthChart>` special-cases SLEEP_DURATION to
|
|
* read `/api/measurements/series?kind=sleep`, which reconstructs ONE point
|
|
* per night carrying the night's TIME-ASLEEP in HOURS (CORE + DEEP + REM,
|
|
* bare-ASLEEP only when no granular stage exists, IN_BED + AWAKE excluded,
|
|
* dual-source nights collapsed to one source). The chart therefore renders
|
|
* a nightly trend in hours that matches every other sleep surface.
|
|
*
|
|
* Unit semantics: the chart plots HOURS — the same unit the series adapter
|
|
* returns and the `<SleepOverview>` headline shows — so the axis reads e.g.
|
|
* "8 h", not the inflated per-stage minutes the legacy path surfaced.
|
|
*
|
|
* v1.4.28 R3d (BK-F-M2) — the inline `dynamic()` call site was retired
|
|
* in favour of the shared `<HealthChartDynamic>` re-export so the
|
|
* `<ChartSkeleton>` + `ssr: false` configuration stays in one place.
|
|
*/
|
|
export interface SleepDurationChartProps {
|
|
compareBaseline?: ComparisonBaseline;
|
|
userTimezone?: string;
|
|
}
|
|
|
|
export function SleepDurationChart({
|
|
compareBaseline,
|
|
userTimezone,
|
|
}: SleepDurationChartProps) {
|
|
const { t } = useTranslations();
|
|
return (
|
|
<HealthChartDynamic
|
|
chartKey="sleep"
|
|
types={["SLEEP_DURATION"]}
|
|
title={t("charts.sleep")}
|
|
colors={["#8be9fd"]}
|
|
unit="h"
|
|
yAxisUnit="h"
|
|
compareBaseline={compareBaseline}
|
|
userTimezone={userTimezone}
|
|
/>
|
|
);
|
|
}
|