mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
fix(sleep): dedup the bare ASLEEP aggregate across the stage breakdown and hypnogram
Apple Health writes an unspecified ASLEEP aggregate alongside the granular CORE/DEEP/REM partition for the same period. The asleep total already preferred the granular partition, but the per-night/per-session `stages` map and the hypnogram `segments` list recorded every stage row unconditionally, so a night ended up carrying the granular set PLUS its ~equal bare twin — a double-height green ASLEEP block on the stacked bar and the hypnogram, and a doubled total in the analytics stage breakdown. Route the `stages` map and the `segments` list through the same granular-over-bare gate as the asleep total: when a session carries a granular stage, drop the redundant bare ASLEEP (and stage-less) rows everywhere. WHOOP never emits a bare ASLEEP, so WHOOP nights are unaffected. Apply the gate per session, not over the merged night pool, so a granular overnight no longer strips a same-day bare-only nap's minutes from the night total. Render the hypnogram per-segment in the stage palette (one ReferenceArea per segment) instead of a single flat cyan line, and show the breakdown plus totals footer whenever any per-stage minutes exist — reserving the unavailable state for a truly empty session — so a legacy stage-summary night still surfaces its breakdown. Thread the user's source priority into the CSV export's sleep dedup, and honour offset with a true pre-slice night count on the sleep list.
This commit is contained in:
1 parent
c0065437a7
commit
766b395900
7 files changed
+343
-156
No files matched your search
@@ -24,6 +24,7 @@ import { apiError, getClientIp } from "@/lib/api-response";
|
||||
import { checkRateLimit } from "@/lib/rate-limit";
|
||||
import { toCSV, formatMeasurementsForExport } from "@/lib/export";
|
||||
import { resolveUserTimezone } from "@/lib/tz/resolver";
|
||||
import { loadUserSourcePriority } from "@/lib/rollups/measurement-read";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export const GET = apiHandler(async (request: NextRequest) => {
|
||||
@@ -44,18 +45,20 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
? "raw"
|
||||
: "night";
|
||||
|
||||
const [measurements, userTz] = await Promise.all([
|
||||
const [measurements, userTz, sourcePriorityJson] = await Promise.all([
|
||||
prisma.measurement.findMany({
|
||||
where,
|
||||
orderBy: { measuredAt: "desc" },
|
||||
}),
|
||||
resolveUserTimezone(user.id),
|
||||
loadUserSourcePriority(user.id),
|
||||
]);
|
||||
|
||||
const csv = toCSV(
|
||||
formatMeasurementsForExport(measurements, userTz, {
|
||||
granularity,
|
||||
sleepTz: userTz,
|
||||
sourcePriorityJson,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
formatMoodEntriesForExport,
|
||||
} from "@/lib/export";
|
||||
import { resolveUserTimezone } from "@/lib/tz/resolver";
|
||||
import { loadUserSourcePriority } from "@/lib/rollups/measurement-read";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
/**
|
||||
@@ -49,7 +50,10 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
}
|
||||
|
||||
const userId = user.id;
|
||||
const userTz = await resolveUserTimezone(userId);
|
||||
const [userTz, sourcePriorityJson] = await Promise.all([
|
||||
resolveUserTimezone(userId),
|
||||
loadUserSourcePriority(userId),
|
||||
]);
|
||||
|
||||
const data: Record<string, unknown> = {};
|
||||
|
||||
@@ -63,10 +67,12 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
});
|
||||
// v1.11.5 — sleep collapses to one row per night by default (the
|
||||
// formatter's `night` granularity), matching the per-type CSV route.
|
||||
// Thread the user's source-priority so a multi-source night dedups to
|
||||
// the same canonical source the UI shows.
|
||||
const measurementRecords = formatMeasurementsForExport(
|
||||
measurements,
|
||||
userTz,
|
||||
{ sleepTz: userTz },
|
||||
{ sleepTz: userTz, sourcePriorityJson },
|
||||
);
|
||||
data.measurements =
|
||||
format === "csv" ? toCSV(measurementRecords) : measurementRecords;
|
||||
|
||||
Reference in new issue
Block a user