From f6687db926e03eb44e5d72d321021f7738600357 Mon Sep 17 00:00:00 2001 From: Martin Kagamino Lehoux Date: Tue, 8 Sep 2026 09:56:55 +0200 Subject: Anchor routine due dates on UTC calendar days Daily routine done yesterday is due today regardless of time of day; same anchoring in health series so chart and badge agree. --- src/lib/shared/routine-metrics.test.ts | 15 +++++++++++++++ src/lib/shared/routine-metrics.ts | 14 ++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) (limited to 'src/lib/shared') diff --git a/src/lib/shared/routine-metrics.test.ts b/src/lib/shared/routine-metrics.test.ts index a63069c..1df39ab 100644 --- a/src/lib/shared/routine-metrics.test.ts +++ b/src/lib/shared/routine-metrics.test.ts @@ -63,6 +63,21 @@ describe('computeRoutineState', () => { const state = computeRoutineState(routine, [], now); expect(state.health).toBe(75); }); + it('is due the calendar day after completion for a daily routine', () => { + const daily = { ...routine, frequencyDays: 1 }; + const doneAt = '2026-09-07T20:00:00.000Z'; // yesterday evening + const now = Date.parse('2026-09-08T08:00:00.000Z'); // this morning, <24h later + const state = computeRoutineState(daily, [{ routineId: 1, doneAt }], now); + expect(state.status).toBe('due'); + }); + + it('is ok later the same day it was done for a daily routine', () => { + const daily = { ...routine, frequencyDays: 1 }; + const doneAt = '2026-09-08T08:00:00.000Z'; + const now = Date.parse('2026-09-08T20:00:00.000Z'); + const state = computeRoutineState(daily, [{ routineId: 1, doneAt }], now); + expect(state.status).toBe('ok'); + }); }); describe('computeHealthSeries', () => { diff --git a/src/lib/shared/routine-metrics.ts b/src/lib/shared/routine-metrics.ts index 99e5c56..59413c2 100644 --- a/src/lib/shared/routine-metrics.ts +++ b/src/lib/shared/routine-metrics.ts @@ -1,8 +1,16 @@ // Pure routine metrics, shared by server services and UI. No db access. // All timestamps are ISO strings; all math is done in ms then converted to days. +// Due dates anchor on UTC calendar days, not exact 24h intervals: a daily +// routine done yesterday is due today, whatever the time of day. const DAY_MS = 24 * 60 * 60 * 1000; +/** Midnight UTC starting the calendar day containing ms. */ +function startOfUtcDay(ms: number): number { + const d = new Date(ms); + return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()); +} + export type RoutineStatus = 'ok' | 'due' | 'late' | 'veryLate'; export interface RoutineInput { @@ -48,7 +56,9 @@ export function computeRoutineState( const referenceMs = lastDoneMs ?? (Number.isFinite(createdMs) ? createdMs : now); const periodMs = routine.frequencyDays * DAY_MS; - const dueMs = referenceMs + periodMs; + // Calendar-day anchor: the period starts at midnight of the reference day, + // so any new calendar day past the frequency flips the routine to due. + const dueMs = startOfUtcDay(referenceMs) + periodMs; const rawRatio = (now - dueMs) / periodMs; const overdueRatio = Math.max(0, rawRatio); // Buckets on overdue time in units of the routine's own frequency: @@ -127,7 +137,7 @@ export function computeHealthSeries( refMs = completionMs[nextCompletion]; nextCompletion++; } - const dueMs = refMs + periodMs; + const dueMs = startOfUtcDay(refMs) + periodMs; const rawRatio = (t - dueMs) / periodMs; return Math.max(0, Math.round(100 - Math.max(0, rawRatio) * 100)); }; -- cgit v1.3