mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
A twice-daily medication ended up with two rows for one dose slot: the projector / reminder worker mints a pending row (source REMINDER) at the slot's `localHmAsUtc` instant, while a "taken" write from the per-med or bulk intake route inserted a fresh row (source WEB / API). Because `source` is part of the `(userId, medicationId, scheduledFor, source)` unique key — and the written instant could drift by a minute from the projected one — the two rows coexisted: one taken, one still pending and overdue. The card then read the slot as taken, counted 100%, and suppressed the "due now" prompt for a dose the user had not taken. Resolve every scheduled intake write to the slot's canonical instant before writing and upsert in place, source-agnostic: - `resolve-slot-instant.ts` snaps an incoming instant to the nearest scheduled slot for that day (enumerated through the canonical recurrence engine, so PRN and cyclic off-weeks resolve to no slot) and anchors it on the same `localHmAsUtc` the projector and worker use. Off-slot and as-needed writes resolve to null and keep insert semantics. - The per-med and bulk routes look up the existing row at the canonical `(userId, medicationId, scheduledFor)` ignoring source and update it in place — mirroring the status-toggle route — instead of inserting a sibling. The bulk per-entry response gains an additive `updated` status. - The reminder worker's missed-dose mint now skips a slot that already carries an actioned row (taken or skipped) from any source, not only an existing pending REMINDER row. PRN / as-needed writes are unaffected. Regression tests pin the collapse (exact and one-minute drift), the PRN exemption, and the worker guard.