aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lib/server/services/routines.test.ts5
-rw-r--r--src/lib/shared/routine-metrics.test.ts49
-rw-r--r--src/lib/shared/routine-metrics.ts19
3 files changed, 49 insertions, 24 deletions
diff --git a/src/lib/server/services/routines.test.ts b/src/lib/server/services/routines.test.ts
index 04151c2..22b49ff 100644
--- a/src/lib/server/services/routines.test.ts
+++ b/src/lib/server/services/routines.test.ts
@@ -134,9 +134,8 @@ describe('routines service', () => {
const r = await add(projectId, 'clean kitchen', 7, db);
const createdAt = Date.parse(r.createdAt);
await complete(r.id, null, new Date(createdAt).toISOString(), db);
-
- // 15 days after completion with a 7-day routine => very late
- const now = createdAt + 15 * DAY_MS;
+ // 22 days after completion with a 7-day routine => two periods overdue
+ const now = createdAt + 22 * DAY_MS;
const dashboard = await loadProjectDashboard(projectId, now, db);
expect(dashboard.routines).toHaveLength(1);
expect(dashboard.routines[0].status).toBe('veryLate');
diff --git a/src/lib/shared/routine-metrics.test.ts b/src/lib/shared/routine-metrics.test.ts
index 57b8c38..d18fd18 100644
--- a/src/lib/shared/routine-metrics.test.ts
+++ b/src/lib/shared/routine-metrics.test.ts
@@ -45,24 +45,31 @@ describe('computeRoutineState', () => {
expect(state.health).toBe(100);
});
- it('is very urgent (veryLate) when two weeks old for a weekly routine', () => {
- const now = Date.parse(CREATED) + 15 * DAY_MS;
+ it('is very urgent (veryLate) when two full periods overdue', () => {
+ const now = Date.parse(CREATED) + 22 * DAY_MS;
const state = computeRoutineState(routine, [], now);
expect(state.status).toBe('veryLate');
- expect(state.overdueDays).toBe(8);
+ expect(state.overdueDays).toBe(15);
expect(state.health).toBe(0);
});
- it('is late when overdue by half a frequency', () => {
+ it('is due (not late) when overdue by half a frequency', () => {
const now = Date.parse(CREATED) + 10.5 * DAY_MS;
const state = computeRoutineState(routine, [], now);
+ expect(state.status).toBe('due');
+ });
+
+ it('is late when a full period overdue', () => {
+ const now = Date.parse(CREATED) + 14 * DAY_MS;
+ const state = computeRoutineState(routine, [], now);
expect(state.status).toBe('late');
+ expect(state.health).toBe(50);
});
- it('health of 75 means a quarter of the period overdue', () => {
- const now = Date.parse(CREATED) + 7 * DAY_MS + 1.75 * DAY_MS;
+ it('health of 50 means a full period overdue', () => {
+ const now = Date.parse(CREATED) + 7 * DAY_MS + 7 * DAY_MS;
const state = computeRoutineState(routine, [], now);
- expect(state.health).toBe(75);
+ expect(state.health).toBe(50);
});
it('is due the calendar day after completion for a daily routine', () => {
const daily = { ...routine, frequencyDays: 1 };
@@ -72,6 +79,24 @@ describe('computeRoutineState', () => {
expect(state.status).toBe('due');
});
+ it('a daily done yesterday is late tomorrow, not today', () => {
+ const daily = { ...routine, frequencyDays: 1 };
+ const doneAt = '2026-09-07T20:00:00.000Z'; // yesterday evening
+ const completions = [{ routineId: 1, doneAt }];
+ const today = computeRoutineState(
+ daily,
+ completions,
+ Date.parse('2026-09-08T20:00:00.000Z'),
+ );
+ expect(today.status).toBe('due');
+ const tomorrow = computeRoutineState(
+ daily,
+ completions,
+ Date.parse('2026-09-09T20:00:00.000Z'),
+ );
+ expect(tomorrow.status).toBe('late');
+ });
+
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';
@@ -82,15 +107,15 @@ describe('computeRoutineState', () => {
});
describe('computeHealthSeries', () => {
- it('decays from 100 at creation to 0 two periods late when never done', () => {
- const now = Date.parse(CREATED) + 15 * DAY_MS;
+ it('decays from 100 at creation to 0 two periods overdue when never done', () => {
+ const now = Date.parse(CREATED) + 22 * DAY_MS;
const series = computeHealthSeries(routine, [], now);
expect(series[0].health).toBe(100);
const at8d = series.find((p) => p.at === Date.parse(CREATED) + 8 * DAY_MS);
- expect(at8d?.health).toBe(86); // 1/7 of a period overdue
+ expect(at8d?.health).toBe(93); // 1/7 of a period overdue
const last = series[series.length - 1];
expect(last.at).toBe(now);
- expect(last.health).toBe(0); // more than a full period overdue
+ expect(last.health).toBe(0); // more than two full periods overdue
});
it('resets to 100 right after a completion, then decays again (sawtooth)', () => {
@@ -105,7 +130,7 @@ describe('computeHealthSeries', () => {
expect(rightAfter?.health).toBe(100);
// 8 days after the completion: 1 day into the 7-day period overdue.
const at8d = series.find((p) => p.at === doneAt + 8 * DAY_MS);
- expect(at8d?.health).toBe(86);
+ expect(at8d?.health).toBe(93);
});
it('a second completion resets the decay again', () => {
diff --git a/src/lib/shared/routine-metrics.ts b/src/lib/shared/routine-metrics.ts
index 891a0c9..cffeb85 100644
--- a/src/lib/shared/routine-metrics.ts
+++ b/src/lib/shared/routine-metrics.ts
@@ -39,7 +39,7 @@ export interface RoutineState {
/** Time past due, in units of the routine's own frequency (0 if not due). */
overdueRatio: number;
status: RoutineStatus;
- /** 100 on schedule, 0 when overdue by a full frequency, floor at 0. */
+ /** 100 on schedule, 50 when a full period overdue, 0 at two periods, floor at 0. */
health: number;
}
@@ -61,17 +61,17 @@ export function computeRoutineState(
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:
- // not due / due now / late (half a period overdue) / very late (a full
- // period overdue — a weekly routine two weeks old lands here).
+ // Buckets on overdue time in units of the routine's own frequency: due
+ // until a full period is missed (a daily done yesterday is due today, not
+ // late), late once an occurrence was skipped entirely, very late after two.
// A routine that was never done is always at least due: there is
// something to do, even right after creation.
const status: RoutineStatus =
rawRatio < 0 && lastDoneMs !== null
? 'ok'
- : overdueRatio < 0.5
+ : overdueRatio < 1
? 'due'
- : overdueRatio < 1
+ : overdueRatio < 2
? 'late'
: 'veryLate';
@@ -85,7 +85,7 @@ export function computeRoutineState(
overdueDays: Math.floor(overdueRatio * routine.frequencyDays),
overdueRatio,
status,
- health: Math.max(0, Math.round(100 - overdueRatio * 100)),
+ health: Math.max(0, Math.round(100 - overdueRatio * 50)),
};
}
@@ -107,12 +107,13 @@ export interface HealthPoint {
function healthAtPoint(refMs: number, periodMs: number, t: number): number {
const dueMs = startOfUtcDay(refMs) + periodMs;
const rawRatio = (t - dueMs) / periodMs;
- return Math.max(0, Math.round(100 - Math.max(0, rawRatio) * 100));
+ return Math.max(0, Math.round(100 - Math.max(0, rawRatio) * 50));
}
/**
* Daily health samples from the routine's creation to now: 100 right after a
- * completion, then a linear decay to 0 once a full period is overdue (sawtooth).
+ * completion, then a linear decay to 0 once two full periods are overdue
+ * (sawtooth).
* Each sample anchors on the latest completion at or before it (or creation).
* Uses the routine's current frequency across the whole history.
*/