aboutsummaryrefslogtreecommitdiff
path: root/src/lib/shared/routine-metrics.ts
diff options
context:
space:
mode:
authorMartin Kagamino Lehoux <martin@lehoux.net>2026-09-16 07:34:24 +0200
committerMartin Kagamino Lehoux <martin@lehoux.net>2026-09-16 07:34:24 +0200
commit2e426068152f97f1615bb9ddc33940a38e7b6861 (patch)
tree8fe39763c91f495af9e33356887c2c77812bfd2b /src/lib/shared/routine-metrics.ts
parent2a7c4f969874b850e30170f3aef5b578677220b0 (diff)
use created_at as first due_at
Diffstat (limited to 'src/lib/shared/routine-metrics.ts')
-rw-r--r--src/lib/shared/routine-metrics.ts21
1 files changed, 16 insertions, 5 deletions
diff --git a/src/lib/shared/routine-metrics.ts b/src/lib/shared/routine-metrics.ts
index 953a025..7758f27 100644
--- a/src/lib/shared/routine-metrics.ts
+++ b/src/lib/shared/routine-metrics.ts
@@ -92,10 +92,12 @@ export function computeRoutineState(
const period = periodMs(routine.frequencyDays);
// 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.
+ // A never-done routine has no completed period: its creation day is its
+ // due date, so it ages exactly like a done one whose anchor is creation.
const due = reference
.toZonedDateTimeISO(UTC)
.startOfDay()
- .add({ days: routine.frequencyDays })
+ .add({ days: lastDone === null ? 0 : routine.frequencyDays })
.toInstant();
const rawRatio = (now.epochMilliseconds - due.epochMilliseconds) / period;
const overdueRatio = Math.max(0, rawRatio);
@@ -141,11 +143,12 @@ function healthAtPoint(
reference: Temporal.Instant,
frequencyDays: number,
t: Temporal.Instant,
+ neverDone = false,
): number {
const due = reference
.toZonedDateTimeISO(UTC)
.startOfDay()
- .add({ days: frequencyDays })
+ .add({ days: neverDone ? 0 : frequencyDays })
.toInstant();
const rawRatio =
(t.epochMilliseconds - due.epochMilliseconds) / periodMs(frequencyDays);
@@ -156,7 +159,8 @@ function healthAtPoint(
* Daily health samples from the routine's creation to now: 100 right after a
* 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).
+ * Each sample anchors on the latest completion at or before it (or creation,
+ * which is then itself the due date for a never-done routine).
* Uses the routine's current frequency across the whole history.
*/
export function computeHealthSeries(
@@ -183,7 +187,12 @@ export function computeHealthSeries(
reference = doneAt[nextCompletion];
nextCompletion++;
}
- return healthAtPoint(reference, routine.frequencyDays, t);
+ return healthAtPoint(
+ reference,
+ routine.frequencyDays,
+ t,
+ nextCompletion === 0,
+ );
};
for (
@@ -250,7 +259,9 @@ export function computeProjectHealthSeries(
);
const anchor =
doneAt.length > 0 ? (maxInstant(doneAt) ?? now) : createdOrNow(r);
- return sum + healthAtPoint(anchor, r.frequencyDays, t);
+ return (
+ sum + healthAtPoint(anchor, r.frequencyDays, t, doneAt.length === 0)
+ );
}, 0);
return Math.round(total / alive.length);
};