aboutsummaryrefslogtreecommitdiff
path: root/src/lib/shared
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/shared')
-rw-r--r--src/lib/shared/routine-metrics.test.ts15
-rw-r--r--src/lib/shared/routine-metrics.ts14
2 files changed, 27 insertions, 2 deletions
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));
};