diff --git a/src/app/api/gamification/achievements/route.ts b/src/app/api/gamification/achievements/route.ts index fc4efe2e0..b04f4a72e 100644 --- a/src/app/api/gamification/achievements/route.ts +++ b/src/app/api/gamification/achievements/route.ts @@ -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; } diff --git a/src/app/api/medications/[id]/compliance/route.ts b/src/app/api/medications/[id]/compliance/route.ts index 5eb4f1c93..ebc2c11d5 100644 --- a/src/app/api/medications/[id]/compliance/route.ts +++ b/src/app/api/medications/[id]/compliance/route.ts @@ -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, }; } diff --git a/src/lib/medications/scheduling/cadence.ts b/src/lib/medications/scheduling/cadence.ts index 332b59ed6..34683770b 100644 --- a/src/lib/medications/scheduling/cadence.ts +++ b/src/lib/medications/scheduling/cadence.ts @@ -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").