fix(compliance): route early intakes through the compliant bucket

The medication compliance route and the achievement perfect-day
heuristic both bucket the classifier output into `on_time` / `late` /
`very_late`. After the classifier started emitting `early`, both
paths would have silently dropped those doses into `very_late`
(via the trailing `else`). Treat `early` as compliant alongside
`on_time` so the heatmap stays green and the perfect-day streak
survives a proactive logger. The compliance route also surfaces a
dedicated `early` count on `DailyComplianceEntry` for any future
consumer that wants to break it out from `on_time`.

Updates the cadence-pairing docstring to reflect the widened grace.
This commit is contained in:
Marc-André Bombeck committed 2026-05-16 20:24:57 +02:00
1 parent 36d5398c92
commit 4984bce577
3 files changed
+14 -4

No files matched your search

@@ -278,7 +278,9 @@ function getOnTimePerfectDaySeries(
scheduledDate,
);
if (timing !== "on_time") {
// v1.4.34 IW-C — `early` is also a compliant bucket (dose taken
// within the 3h pre-window grace), so it preserves a perfect day.
if (timing !== "on_time" && timing !== "early") {
isPerfectDay = false;
break;
}
@@ -80,6 +80,7 @@ export const GET = apiHandler(
let onTime = 0;
let late = 0;
let veryLate = 0;
let early = 0;
for (const evt of takenEvents) {
if (medication.schedules.length === 0) {
@@ -111,7 +112,13 @@ export const GET = apiHandler(
dayStart, // the scheduled date
);
// v1.4.34 IW-C — `early` is the new compliant bucket; it counts
// alongside `onTime` for the heatmap so a proactive logger reads
// green. The classifier still emits a distinct `"early"` value
// for downstream consumers that want to differentiate; the
// separate counter is surfaced on the daily entry below.
if (timing === "on_time") onTime++;
else if (timing === "early") early++;
else if (timing === "late") late++;
else veryLate++;
}
@@ -120,9 +127,10 @@ export const GET = apiHandler(
expected: schedulesPerDay,
taken: takenEvents.length,
skipped: dayEvents.filter((e) => e.skipped).length,
onTime,
onTime: onTime + early,
late,
veryLate,
early,
};
}
+2 -2
View File
@@ -70,8 +70,8 @@ const WEEK_MS = 7 * DAY_MS;
* Match window for pairing an actual intake event to an expected
* dose: +/- 12 hours around the slot's center.
*
* Rationale: the existing classifyIntakeTiming uses a 1-hour grace
* before the window + ~2-hour late tolerance after. For cadence
* Rationale: the existing classifyIntakeTiming uses a 3-hour grace
* around the window + a configurable late tolerance after. For cadence
* visualisation we want a wider matching radius so users who logged a
* weekly shot a day late still see it paired with that week's slot
* (and the slot reads `taken`, not `missed` followed by an "extra").