From f3c84018f971b9bd7cafc781e81332f13a49ee55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 8 Aug 2026 09:38:30 +0200 Subject: [PATCH] Record a backfilled dose at its slot's own time Marking an older missed dose as taken from the Verlauf pinned the right slot but stamped the intake with the current moment. The body builder is a pure helper now, anchored on the slot instant, matching both the optimistic paint and the skipped-to-taken flip that already behaved this way. --- CHANGELOG.md | 9 ++++++++ public/sw.js | 2 +- .../dose-history-ledger-compute.test.ts | 21 +++++++++++++++++++ .../dose-history-ledger-compute.ts | 15 +++++++++++++ .../medications/dose-history-ledger.tsx | 16 ++++++-------- 5 files changed, 52 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f81deb5d..d7ef0ce74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## [Unreleased] +### Fixed + +- Marking an older missed dose as taken from the intake history now records + the dose at the slot's own date and time. Marking Tuesday 09:00 from a + Thursday evening used to land on the right slot but stamp the intake with + the current moment; the recorded time now matches the claim being made. The + optimistic paint had promised this all along, and the skipped-to-taken flip + in the same view already behaved this way. + ## [1.37.1] — 2026-08-08 ### Added diff --git a/public/sw.js b/public/sw.js index d2632934b..d62c81e76 100644 --- a/public/sw.js +++ b/public/sw.js @@ -36,7 +36,7 @@ try { // v1.4.38.4 → v1.4.42. Do not hand-edit; bump `package.json` and rebuild. const CACHE_VERSION = (typeof self !== "undefined" && self.__APP_VERSION__) || - /* @sw-version-fallback */ "v1.37.0"; + /* @sw-version-fallback */ "v1.37.1"; const STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`; const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`; // v1.18.6 — read-only data cache for a curated allowlist of safe GET `/api/*` diff --git a/src/components/medications/__tests__/dose-history-ledger-compute.test.ts b/src/components/medications/__tests__/dose-history-ledger-compute.test.ts index 420d73a1b..496be10f6 100644 --- a/src/components/medications/__tests__/dose-history-ledger-compute.test.ts +++ b/src/components/medications/__tests__/dose-history-ledger-compute.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { applyOptimisticSlotMark, + slotMarkRequestBody, complianceFromLedger, formatSlotDelta, groupLedgerByDay, @@ -195,3 +196,23 @@ describe("applyOptimisticSlotMark", () => { expect(next.rows[0].status).toBe("taken_late"); }); }); + +describe("slotMarkRequestBody", () => { + const row = { at: "2026-08-04T07:00:00.000Z" }; + + it("anchors takenAt on the slot instant, not on now", () => { + const body = slotMarkRequestBody(row, "taken"); + expect(body).toEqual({ + skipped: false, + scheduledFor: "2026-08-04T07:00:00.000Z", + takenAt: "2026-08-04T07:00:00.000Z", + }); + }); + + it("keeps the skipped arm free of takenAt", () => { + expect(slotMarkRequestBody(row, "skipped")).toEqual({ + skipped: true, + scheduledFor: "2026-08-04T07:00:00.000Z", + }); + }); +}); diff --git a/src/components/medications/dose-history-ledger-compute.ts b/src/components/medications/dose-history-ledger-compute.ts index 06f51e084..9bb009e35 100644 --- a/src/components/medications/dose-history-ledger-compute.ts +++ b/src/components/medications/dose-history-ledger-compute.ts @@ -204,3 +204,18 @@ export function applyOptimisticSlotMark( }); return { ...payload, rows }; } + +/** + * Request body for marking a ledger slot from the Verlauf. The taken arm + * anchors `takenAt` on the slot instant: marking Tuesday 09:00 from a + * Thursday evening records a dose taken Tuesday 09:00, which is the claim + * the person is making. The optimistic paint above already promised this. + */ +export function slotMarkRequestBody( + row: Pick, + action: "taken" | "skipped", +): Record { + return action === "skipped" + ? { skipped: true, scheduledFor: row.at } + : { skipped: false, scheduledFor: row.at, takenAt: row.at }; +} diff --git a/src/components/medications/dose-history-ledger.tsx b/src/components/medications/dose-history-ledger.tsx index 9204ed83d..82b6ae50e 100644 --- a/src/components/medications/dose-history-ledger.tsx +++ b/src/components/medications/dose-history-ledger.tsx @@ -106,6 +106,7 @@ import { formatSlotDelta, groupLedgerByDay, isSlotActionable, + slotMarkRequestBody, type LedgerPayload, type LedgerRow, } from "@/components/medications/dose-history-ledger-compute"; @@ -273,18 +274,13 @@ export function DoseHistoryLedger({ try { // Pin the displayed slot so the server marks THAT dose (the canonical - // slot upsert), not a now-snap to the nearest one. The POST returns - // the created event; its id drives the Undo affordance (the same - // soft-delete route the cards use). + // slot upsert), not a now-snap to the nearest one — and anchor + // `takenAt` on the same instant, matching the optimistic paint. The + // POST returns the created event; its id drives the Undo affordance + // (the same soft-delete route the cards use). const created = await apiPost<{ id?: string } | undefined>( `/api/medications/${medicationId}/intake`, - action === "skipped" - ? { skipped: true, scheduledFor: row.at } - : { - skipped: false, - scheduledFor: row.at, - takenAt: new Date().toISOString(), - }, + slotMarkRequestBody(row, action), ); const eventId: string | undefined = created?.id; toast.success(