mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 15:02:04 +00:00
fix(compliance,ingest): bound the point window and the two unbounded write paths
`classifyIntakeTiming` treated `windowStart === windowEnd` as an overnight wrap, inflating a degenerate point window into a ~30h on-time band — a dose taken many hours late still classified `on_time` and fed a perfect-compliance streak. Widen the point window symmetrically instead, matching the treatment `resolveLegacyWindowBounds` already applies. `/api/measurements/batch` never applied the entry-instant plausibility bound its single-entry twin has carried since v1.17 W1b, so a future-dated row landed and permanently won 'latest reading' while poisoning the rollup buckets and the canonical picker. Skip per entry, like the range guard. The past floor stays at 1900 — a historical backfill is a legitimate shape here. The intake CSV import had neither an upper date bound nor a rate limiter, while running up to 1000 sequential find + create + consume iterations per call. Add both, on the bulk twin's bucket shape and budget.
This commit is contained in:
1 parent
ef10ac5581
commit
f5889affb7
6 files changed
+336
-3
No files matched your search
@@ -1234,6 +1234,43 @@ describe("classifyIntakeTiming", () => {
|
||||
);
|
||||
});
|
||||
|
||||
// A degenerate POINT window (`windowStart === windowEnd`) is a real shape on
|
||||
// legacy rows. It used to take the overnight branch and inflate into a ~30h
|
||||
// on-time band, so a dose taken many hours late still counted `on_time` and
|
||||
// fed a perfect-compliance streak. It now widens symmetrically by the default
|
||||
// on-time half-width, matching `resolveLegacyWindowBounds`.
|
||||
describe("degenerate point window", () => {
|
||||
it('keeps a dose at the point itself "on_time"', () => {
|
||||
const takenAt = new Date("2025-01-15T07:00:00Z");
|
||||
expect(
|
||||
classifyIntakeTiming(takenAt, "07:00", "07:00", scheduledDate),
|
||||
).toBe("on_time");
|
||||
});
|
||||
|
||||
it('does not classify a dose ~16h late as "on_time"', () => {
|
||||
const takenAt = new Date("2025-01-15T23:00:00Z");
|
||||
expect(
|
||||
classifyIntakeTiming(takenAt, "07:00", "07:00", scheduledDate),
|
||||
).toBe("very_late");
|
||||
});
|
||||
|
||||
it("does not swallow the whole day into the on-time band", () => {
|
||||
// 13:00 is 6h past the point — outside the widened band + graces.
|
||||
const takenAt = new Date("2025-01-15T13:00:00Z");
|
||||
expect(
|
||||
classifyIntakeTiming(takenAt, "07:00", "07:00", scheduledDate),
|
||||
).not.toBe("on_time");
|
||||
});
|
||||
|
||||
it("still wraps a genuine overnight window", () => {
|
||||
// 00:30 the next day sits inside a 23:00 -> 01:00 window.
|
||||
const takenAt = new Date("2025-01-16T00:30:00Z");
|
||||
expect(
|
||||
classifyIntakeTiming(takenAt, "23:00", "01:00", scheduledDate),
|
||||
).toBe("on_time");
|
||||
});
|
||||
});
|
||||
|
||||
it('returns "on_time" when taken within the window', () => {
|
||||
const takenAt = new Date("2025-01-15T08:30:00Z");
|
||||
expect(classifyIntakeTiming(takenAt, "08:00", "09:00", scheduledDate)).toBe(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Extracted from the former single-file `compliance.ts`. See `../compliance.ts`
|
||||
// (the barrel) for the module map. Pure move — no logic changes.
|
||||
|
||||
import { DOSE_WINDOW_DEFAULTS } from "@/lib/medications/scheduling/dose-window-defaults";
|
||||
import { localHmAsUtc } from "@/lib/tz/local-day";
|
||||
|
||||
export type IntakeTimingClass =
|
||||
@@ -68,11 +69,21 @@ export function classifyIntakeTiming(
|
||||
): IntakeTimingClass {
|
||||
if (takenAt === null) return "missed";
|
||||
|
||||
const start = toDateOnDay(windowStart, scheduledDate, options?.tz);
|
||||
let start = toDateOnDay(windowStart, scheduledDate, options?.tz);
|
||||
let end = toDateOnDay(windowEnd, scheduledDate, options?.tz);
|
||||
|
||||
// Handle overnight windows (e.g. windowStart="23:00", windowEnd="01:00")
|
||||
if (end <= start) {
|
||||
// A degenerate POINT window (`windowStart === windowEnd` — a single dose time
|
||||
// echoed into both fields, a real shape on legacy rows) used to fall into the
|
||||
// overnight branch below and inflate into a ~30h on-time band, so a dose taken
|
||||
// many hours late still classified `on_time` and fed a perfect-compliance
|
||||
// streak. Widen it symmetrically by the default on-time half-width instead,
|
||||
// matching `resolveLegacyWindowBounds` in `@/lib/medications/window-status`.
|
||||
if (end.getTime() === start.getTime()) {
|
||||
const halfMs = DOSE_WINDOW_DEFAULTS.dailyOnTimeMinutes * 60 * 1000;
|
||||
start = new Date(start.getTime() - halfMs);
|
||||
end = new Date(end.getTime() + halfMs);
|
||||
} else if (end < start) {
|
||||
// Genuine overnight window (e.g. windowStart="23:00", windowEnd="01:00").
|
||||
end = new Date(end.getTime() + 24 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user