diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fd37ba70..26203b096 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,15 @@ unusable, that session alone is reported back and the rest of the batch still lands. +### 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/messages/de.json b/messages/de.json index 329c13522..f7ea8d42a 100644 --- a/messages/de.json +++ b/messages/de.json @@ -891,7 +891,7 @@ "quickTagsHelp": "Häufig genutzte Tags – ein Tipp genügt", "quickTagsExpand": "Weitere", "loadError": "Stimmungseinträge konnten nicht geladen werden.", - "sectionLevelA": "Fünf Skalen", + "sectionLevelA": "Gefühlslage", "sectionLevelAHelp": "Optional. Was du nicht angibst, bleibt leer.", "dimensionNotAnswered": "nicht angegeben", "dimensionClear": "leeren", @@ -3120,8 +3120,6 @@ "navRecovery": "Erholung", "workouts": { "title": "Workouts", - "askWhy": "Warum?", - "askWhyPrompt": "Warum ist diese Einheit so ausgefallen? Erkläre mir meine eigenen Werte dazu.", "description": "Letzte Läufe, Touren, Spaziergänge und Krafteinheiten — quellenübergreifend entdoppelt zwischen Apple Watch und Withings.", "emptyState": { "title": "Noch keine Workouts", @@ -3646,8 +3644,8 @@ }, "inTargetTitle": "Im Zielbereich", "dimensions": { - "title": "Fünf Skalen", - "none": "Für diese fünf Skalen ist noch nichts erfasst.", + "title": "Gefühlslage", + "none": "Zur Gefühlslage ist noch nichts erfasst.", "window": "{days} Tage", "noneInWindow": "nichts im Zeitraum", "higherIsMore": "höher = {anchor}", diff --git a/messages/en.json b/messages/en.json index 76cc7a265..cccbac213 100644 --- a/messages/en.json +++ b/messages/en.json @@ -891,7 +891,7 @@ "quickTagsHelp": "Your frequent tags — one tap", "quickTagsExpand": "More", "loadError": "Could not load your mood entries.", - "sectionLevelA": "Five scales", + "sectionLevelA": "State of mind", "sectionLevelAHelp": "Optional. Anything you leave alone stays empty.", "dimensionNotAnswered": "not recorded", "dimensionClear": "clear", @@ -3120,8 +3120,6 @@ "navRecovery": "Recovery", "workouts": { "title": "Workouts", - "askWhy": "Ask why", - "askWhyPrompt": "Why did this session read the way it did? Walk me through my own numbers from it.", "description": "Recent runs, rides, walks and strength sessions, deduped across Apple Watch and Withings sources.", "emptyState": { "title": "No workouts yet", @@ -3646,8 +3644,8 @@ }, "inTargetTitle": "In your range", "dimensions": { - "title": "Five scales", - "none": "Nothing recorded for these five scales yet.", + "title": "State of mind", + "none": "Nothing recorded on your state of mind yet.", "window": "{days} days", "noneInWindow": "nothing in this range", "higherIsMore": "higher = {anchor}", diff --git a/messages/es.json b/messages/es.json index 9dadf056b..74e76baef 100644 --- a/messages/es.json +++ b/messages/es.json @@ -891,7 +891,7 @@ "quickTagsHelp": "Tus etiquetas frecuentes: un toque", "quickTagsExpand": "Más", "loadError": "No se pudieron cargar tus registros de estado de ánimo.", - "sectionLevelA": "Cinco escalas", + "sectionLevelA": "Estado emocional", "sectionLevelAHelp": "Opcional. Lo que no toques queda vacío.", "dimensionNotAnswered": "sin indicar", "dimensionClear": "borrar", @@ -3120,8 +3120,6 @@ "navRecovery": "Recuperación", "workouts": { "title": "Entrenamientos", - "askWhy": "Por qué", - "askWhyPrompt": "¿Por qué salió así esta sesión? Explícame mis propios datos de ella.", "description": "Carreras, salidas en bici, paseos y sesiones de fuerza recientes, deduplicados entre Apple Watch y Withings.", "emptyState": { "title": "Aún no hay entrenamientos", @@ -3646,8 +3644,8 @@ }, "inTargetTitle": "En tu rango", "dimensions": { - "title": "Cinco escalas", - "none": "Todavía no hay nada registrado en estas cinco escalas.", + "title": "Estado emocional", + "none": "Todavía no hay nada registrado sobre tu estado emocional.", "window": "{days} días", "noneInWindow": "nada en este periodo", "higherIsMore": "más alto = {anchor}", diff --git a/messages/fr.json b/messages/fr.json index 553da5fe7..1b775ad39 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -891,7 +891,7 @@ "quickTagsHelp": "Vos tags fréquents — en un geste", "quickTagsExpand": "Plus", "loadError": "Impossible de charger vos entrées d'humeur.", - "sectionLevelA": "Cinq échelles", + "sectionLevelA": "État d'esprit", "sectionLevelAHelp": "Facultatif. Ce que vous ne touchez pas reste vide.", "dimensionNotAnswered": "non renseigné", "dimensionClear": "effacer", @@ -3120,8 +3120,6 @@ "navRecovery": "Récupération", "workouts": { "title": "Entraînements", - "askWhy": "Pourquoi", - "askWhyPrompt": "Pourquoi cette séance s'est-elle passée ainsi ? Explique-moi mes propres chiffres.", "description": "Courses, sorties à vélo, marches et séances de musculation récentes, dédupliquées entre Apple Watch et Withings.", "emptyState": { "title": "Aucun entraînement", @@ -3646,8 +3644,8 @@ }, "inTargetTitle": "Dans votre plage", "dimensions": { - "title": "Cinq échelles", - "none": "Rien n'est encore enregistré pour ces cinq échelles.", + "title": "État d'esprit", + "none": "Rien n'est encore enregistré pour l'état d'esprit.", "window": "{days} jours", "noneInWindow": "rien sur cette période", "higherIsMore": "plus haut = {anchor}", diff --git a/messages/it.json b/messages/it.json index 792bb59bf..bf043b9f0 100644 --- a/messages/it.json +++ b/messages/it.json @@ -891,7 +891,7 @@ "quickTagsHelp": "I tuoi tag frequenti: un tocco", "quickTagsExpand": "Altro", "loadError": "Impossibile caricare le voci dell'umore.", - "sectionLevelA": "Cinque scale", + "sectionLevelA": "Stato d'animo", "sectionLevelAHelp": "Facoltativo. Quello che non tocchi resta vuoto.", "dimensionNotAnswered": "non indicato", "dimensionClear": "cancella", @@ -3120,8 +3120,6 @@ "navRecovery": "Recupero", "workouts": { "title": "Allenamenti", - "askWhy": "Perché", - "askWhyPrompt": "Perché questa sessione è andata così? Spiegami i miei dati di questa sessione.", "description": "Corse, uscite in bici, camminate e sessioni di forza recenti, deduplicati tra Apple Watch e Withings.", "emptyState": { "title": "Nessun allenamento", @@ -3646,8 +3644,8 @@ }, "inTargetTitle": "Nel tuo intervallo", "dimensions": { - "title": "Cinque scale", - "none": "Per queste cinque scale non c'è ancora nulla.", + "title": "Stato d'animo", + "none": "Non c'è ancora nulla sullo stato d'animo.", "window": "{days} giorni", "noneInWindow": "nulla in questo periodo", "higherIsMore": "più alto = {anchor}", diff --git a/messages/pl.json b/messages/pl.json index 0c8475bde..88f262069 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -891,7 +891,7 @@ "quickTagsHelp": "Twoje częste tagi — jedno dotknięcie", "quickTagsExpand": "Więcej", "loadError": "Nie udało się załadować wpisów nastroju.", - "sectionLevelA": "Pięć skal", + "sectionLevelA": "Samopoczucie", "sectionLevelAHelp": "Opcjonalne. To, czego nie ruszysz, zostaje puste.", "dimensionNotAnswered": "nie podano", "dimensionClear": "wyczyść", @@ -3120,8 +3120,6 @@ "navRecovery": "Regeneracja", "workouts": { "title": "Treningi", - "askWhy": "Dlaczego", - "askWhyPrompt": "Dlaczego ta sesja wypadła w ten sposób? Wyjaśnij mi moje własne wyniki.", "description": "Ostatnie biegi, jazdy, marsze i treningi siłowe — zdeduplikowane między Apple Watch a Withings.", "emptyState": { "title": "Brak treningów", @@ -3646,8 +3644,8 @@ }, "inTargetTitle": "W Twoim zakresie", "dimensions": { - "title": "Pięć skal", - "none": "Dla tych pięciu skal nie ma jeszcze żadnych danych.", + "title": "Samopoczucie", + "none": "Nie zapisano jeszcze żadnych danych o samopoczuciu.", "window": "{days} dni", "noneInWindow": "brak w tym okresie", "higherIsMore": "wyżej = {anchor}", 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/app/insights/workouts/[id]/page.tsx b/src/app/insights/workouts/[id]/page.tsx index 2780caf84..c242b1dce 100644 --- a/src/app/insights/workouts/[id]/page.tsx +++ b/src/app/insights/workouts/[id]/page.tsx @@ -9,7 +9,6 @@ import { useTranslations } from "@/lib/i18n/context"; import { useWorkoutDetail } from "@/hooks/use-workouts"; import { useModulePageGuard } from "@/hooks/use-module-page-guard"; import { BackLink } from "@/components/ui/back-link"; -import { CoachLaunchButton } from "@/components/insights/coach-launch-button"; import { SubPageShell } from "@/components/insights/sub-page-shell"; import { Skeleton } from "@/components/ui/skeleton"; import { QueryErrorCard } from "@/components/ui/query-error-card"; @@ -31,7 +30,7 @@ import { * Layout, top to bottom (mobile-first single column): * hero header → reserved Activity-Insight seam (renders nothing today) * → stats grid + sport-average line → HR curve → effort zones → GPS - * route → per-km splits → "that day" → coach launch. + * route → per-km splits → "that day". * * "That day" answers the last question the page is for: what the rest of * the day looked like around the session. It renders the day's own pulse @@ -40,8 +39,8 @@ import { * * Every data-less section returns `null` (hide, don't render empty), so * an aggregates-only workout (a Strava ride with no wearable, a manual - * entry) reads as hero + stats + "that day" + coach — compact but - * honest, no empty shells. + * entry) reads as hero + stats + "that day" — compact but honest, no + * empty shells. * * Data flows from `GET /api/workouts/{id}?compact=1` (v1.4.32 + the #67 * enrichment fields). The `canonicalId` pointer still resolves a @@ -120,18 +119,6 @@ export default function InsightsWorkoutDetailPage({ - {/* 2026-07-17 UX-flows audit F6-1 — `workouts` narrows the - snapshot the first coach turn reads. v1.31.0 — `workoutId` pins - THIS session's own numbers as one additional snapshot section, so - the answer is about this workout rather than about training in - general, and the opener auto-sends so it lands directly. */} - )} 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(