From 5e1035e47a51a043aba168c15ed880a4654486ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 10 Jun 2026 21:41:12 +0200 Subject: [PATCH] fix(medications): drop the due-context line when no slot is plausibly near History rows recorded before a schedule's first minted slot paired with an anchor days or weeks away and read 'due 09:00 (-1904 h)'. The nearest-anchor fallback now caps at the band's capture reach plus one inter-slot gap; beyond that the row simply reads off-schedule. --- .../scheduling/__tests__/dose-history.test.ts | 23 +++++++++++++++++++ .../medications/scheduling/dose-history.ts | 21 +++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/lib/medications/scheduling/__tests__/dose-history.test.ts b/src/lib/medications/scheduling/__tests__/dose-history.test.ts index c0a7056ef..c98a6ea37 100644 --- a/src/lib/medications/scheduling/__tests__/dose-history.test.ts +++ b/src/lib/medications/scheduling/__tests__/dose-history.test.ts @@ -459,3 +459,26 @@ describe("reconstructDoseHistory", () => { expect(times).toEqual([...times].sort((a, b) => a - b)); }); }); + +describe("suggestNearestSlot fallback cap", () => { + it("omits due-context when the nearest anchor sits days away", async () => { + const { suggestNearestSlot } = await import("../dose-history"); + const mk = (iso: string) => { + const at = new Date(iso); + return { + at, + timeOfDay: "09:00", + onTimeStart: new Date(at.getTime() - 60 * 60_000), + onTimeEnd: new Date(at.getTime() + 60 * 60_000), + overdueEnd: new Date(at.getTime() + 4 * 60 * 60_000), + } as never; + }; + // First minted slot is 2026-05-31 (startsOn); the take predates it by 6 days. + const bands = [mk("2026-05-31T07:00:00Z"), mk("2026-06-01T07:00:00Z")]; + const far = suggestNearestSlot(new Date("2026-05-25T09:26:00Z"), bands, () => false); + expect(far).toBeNull(); + // A take inside the suggestion zone still gets its context. + const near = suggestNearestSlot(new Date("2026-05-31T08:30:00Z"), bands, () => false); + expect(near?.timeOfDay).toBe("09:00"); + }); +}); diff --git a/src/lib/medications/scheduling/dose-history.ts b/src/lib/medications/scheduling/dose-history.ts index 2f0d500e0..f713be1dc 100644 --- a/src/lib/medications/scheduling/dose-history.ts +++ b/src/lib/medications/scheduling/dose-history.ts @@ -314,6 +314,27 @@ export function suggestNearestSlot( } } + // The nearest-anchor fallback only makes sense as due-context when the + // anchor sits within one cadence step of the take. A take recorded before + // the schedule's first minted slot (e.g. history predating startsOn) would + // otherwise pair with a slot days or weeks away and the history row would + // read "due 09:00 (-1904 h)" — meaningless. Cap the fallback at the band's + // own capture reach plus one inter-slot gap; beyond that, no due-context. + if (bestAny && bestSuggest === null) { + const b = bestAny.band; + const reach = b.overdueEnd.getTime() - b.onTimeStart.getTime(); + const idx = sorted.indexOf(b); + const gapMs = + sorted.length > 1 + ? Math.abs( + (sorted[Math.min(idx + 1, sorted.length - 1)].at.getTime() || + b.at.getTime()) - + (sorted[Math.max(idx - 1, 0)].at.getTime() || b.at.getTime()), + ) / Math.max(1, Math.min(idx + 1, sorted.length - 1) - Math.max(idx - 1, 0)) + : reach; + if (bestAny.dist > reach + gapMs) return null; + } + const pick = bestSuggest ?? bestAny; if (!pick) return null; return {