fix(doctor-report): resolve recovery by night in the user timezone

Pass the report's timezone into the canonical recovery resolution so the PDF
buckets a WHOOP-native row and its computed proxy onto the same physiological
night — the proxy is stamped the day that ended while WHOOP stamps the wake
morning — instead of a raw calendar day. Aligns the printed recovery summary
with the tile, the coach, and the companion feed.
This commit is contained in:
Marc-André Bombeck committed 2026-06-14 17:57:21 +02:00
1 parent 5831143e26
commit 5b88401935
2 files changed
+18 -9

No files matched your search

@@ -14,11 +14,14 @@ describe("summariseCanonicalRecovery", () => {
).toBeNull();
});
it("prefers the WHOOP-native row over the COMPUTED proxy per day", () => {
// The same day has a low proxy AND a high native row. The PDF must read the
// native value, not blend both into one average.
it("prefers the WHOOP-native row over the COMPUTED proxy per night", () => {
// ONE physiological night (wake morning Jun 1) carries both rows on their
// real, off-by-one stamps: WHOOP stamps the wake morning (Jun 1), the
// COMPUTED proxy stamps the day that ended (May 31 noon). The resolver
// shifts the proxy onto the wake day so they collapse to one night, and the
// PDF reads the native value, not a blend.
const summary = summariseCanonicalRecovery([
m("RECOVERY_SCORE", 50, "2026-06-01T12:00:00Z", "COMPUTED"),
m("RECOVERY_SCORE", 50, "2026-05-31T12:00:00Z", "COMPUTED"),
m("RECOVERY_SCORE", 80, "2026-06-01T06:00:00Z", "WHOOP"),
]);
expect(summary).not.toBeNull();
@@ -30,14 +33,18 @@ describe("summariseCanonicalRecovery", () => {
expect(summary!.count).toBe(1);
});
it("summarises across days using one canonical row each", () => {
it("summarises across nights using one canonical row each", () => {
// Night A (wake Jun 1): WHOOP 80 + its COMPUTED proxy 40 (stamped May 31
// noon) collapse to the native 80. Night B (wake Jun 2): a COMPUTED-only
// proxy 60, stamped Jun 1 noon, stands alone.
const summary = summariseCanonicalRecovery([
m("RECOVERY_SCORE", 60, "2026-06-02T12:00:00Z", "COMPUTED"),
m("RECOVERY_SCORE", 60, "2026-06-01T12:00:00Z", "COMPUTED"),
m("RECOVERY_SCORE", 80, "2026-06-01T06:00:00Z", "WHOOP"),
m("RECOVERY_SCORE", 40, "2026-06-01T12:00:00Z", "COMPUTED"),
m("RECOVERY_SCORE", 40, "2026-05-31T12:00:00Z", "COMPUTED"),
]);
expect(summary!.count).toBe(2);
// Canonical set: 06-02 COMPUTED 60, 06-01 WHOOP 80 (proxy 40 dropped).
// Canonical set: night B COMPUTED 60 (latest stamp), night A WHOOP 80
// (proxy 40 dropped).
expect(summary!.latest).toBe(60);
expect(summary!.min).toBe(60);
expect(summary!.max).toBe(80);