Files
HealthLog/src/lib/targets/sleep-builder.ts
T
Marc-André Bombeck 62eb431d66 release: v1.32.0 — hardening and scalability (#580)
* release: prepare v1.32.0

* test: stabilize water capture re-entry
2026-07-22 04:14:15 +02:00

91 lines
2.6 KiB
TypeScript

import {
classifySleepDuration,
getSleepDurationRange,
} from "@/lib/analytics/classifications";
import { reconstructSleepNights } from "@/lib/analytics/sleep-night";
import { makeRangeClassifier, rollupConsistency } from "./consistency";
import type { TargetItem, TargetSleepStageRow, TargetTrend } from "./types";
interface SleepTargetInput {
sleepStageRows: TargetSleepStageRow[];
timezone: string;
sourcePriorityJson: unknown;
now: Date;
}
export function buildSleepTarget({
sleepStageRows,
timezone,
sourcePriorityJson,
now,
}: SleepTargetInput): TargetItem {
const sleepRange = getSleepDurationRange();
const nights = reconstructSleepNights(
sleepStageRows,
timezone,
sourcePriorityJson,
)
.filter((night) => night.asleepMinutes > 0)
.sort((a, b) => a.measuredAt.getTime() - b.measuredAt.getTime());
const roundHours = (value: number) => Math.round(value * 100) / 100;
const sleepEvents = nights.map((night) => ({
measuredAt: night.measuredAt,
value: roundHours(night.asleepMinutes / 60),
}));
const latestNight = nights.length > 0 ? nights[nights.length - 1] : null;
const current = latestNight
? roundHours(latestNight.asleepMinutes / 60)
: null;
const average30 =
sleepEvents.length > 0
? roundHours(
sleepEvents.reduce((sum, event) => sum + event.value, 0) /
sleepEvents.length,
)
: null;
let classification: TargetItem["classification"] = null;
if (current != null) {
const classified = classifySleepDuration(current);
classification = {
category: classified.category,
color: classified.color,
};
}
let trend: TargetTrend = null;
if (sleepEvents.length >= 4) {
const mid = Math.floor(sleepEvents.length / 2);
const firstHalf = sleepEvents.slice(0, mid);
const secondHalf = sleepEvents.slice(mid);
const averageFirst =
firstHalf.reduce((sum, event) => sum + event.value, 0) / firstHalf.length;
const averageSecond =
secondHalf.reduce((sum, event) => sum + event.value, 0) /
secondHalf.length;
const difference = averageSecond - averageFirst;
const threshold = averageFirst * 0.02;
if (difference > threshold) trend = "up";
else if (difference < -threshold) trend = "down";
else trend = "stable";
}
return {
type: "SLEEP_DURATION",
label: "Sleep duration",
current,
average30,
trend,
unit: "h",
range: sleepRange,
classification,
source: "AASM/SRS",
...rollupConsistency({
events: sleepEvents,
classify: makeRangeClassifier(sleepRange),
timezone,
now,
}),
};
}