mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 22:26:13 +00:00
fix(insights): unify sleep reconstruction and make WHOOP recovery canonical
Route the derived Sleep Score through the canonical night reconstruction instead of its own UTC-day grouping, so a night logged by more than one source is deduplicated to a single writer rather than summed. The score and the readiness that folds it now read true per-night totals; multi-source nights drop to their real figure. Emitted minutes and the night stage map round to integers. Resolve recovery to one canonical series — WHOOP-native when present, the computed proxy as fallback — in the wellness reader, the doctor report, and the coach snapshot, and stop presenting the proxy and the native score as two numbers. A WHOOP-connected account's recovery now reads the native value on the tile, the chart, and the printed report alike.
This commit is contained in:
commit
e27bd8b9bf
16 files changed
+529
-106
No files matched your search
@@ -0,0 +1,46 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { summariseCanonicalRecovery } from "@/lib/doctor-report-data";
|
||||
|
||||
function m(type: string, value: number, iso: string, source: string) {
|
||||
return { type, value, measuredAt: new Date(iso), source: source as never };
|
||||
}
|
||||
|
||||
describe("summariseCanonicalRecovery", () => {
|
||||
it("returns null when no recovery rows exist", () => {
|
||||
expect(
|
||||
summariseCanonicalRecovery([
|
||||
m("STRESS_SCORE", 40, "2026-06-01T12:00:00Z", "COMPUTED"),
|
||||
]),
|
||||
).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.
|
||||
const summary = summariseCanonicalRecovery([
|
||||
m("RECOVERY_SCORE", 50, "2026-06-01T12:00:00Z", "COMPUTED"),
|
||||
m("RECOVERY_SCORE", 80, "2026-06-01T06:00:00Z", "WHOOP"),
|
||||
]);
|
||||
expect(summary).not.toBeNull();
|
||||
// Only the canonical (WHOOP) row counts — avg/min/max/latest all 80, not 65.
|
||||
expect(summary!.latest).toBe(80);
|
||||
expect(summary!.avg).toBe(80);
|
||||
expect(summary!.min).toBe(80);
|
||||
expect(summary!.max).toBe(80);
|
||||
expect(summary!.count).toBe(1);
|
||||
});
|
||||
|
||||
it("summarises across days using one canonical row each", () => {
|
||||
const summary = summariseCanonicalRecovery([
|
||||
m("RECOVERY_SCORE", 60, "2026-06-02T12:00:00Z", "COMPUTED"),
|
||||
m("RECOVERY_SCORE", 80, "2026-06-01T06:00:00Z", "WHOOP"),
|
||||
m("RECOVERY_SCORE", 40, "2026-06-01T12:00:00Z", "COMPUTED"),
|
||||
]);
|
||||
expect(summary!.count).toBe(2);
|
||||
// Canonical set: 06-02 COMPUTED 60, 06-01 WHOOP 80 (proxy 40 dropped).
|
||||
expect(summary!.latest).toBe(60);
|
||||
expect(summary!.min).toBe(60);
|
||||
expect(summary!.max).toBe(80);
|
||||
expect(summary!.avg).toBe(70);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user