From 4427f6c2e54167575d3898f5bb71b0406202eee3 Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Fri, 16 Oct 2026 05:46:50 +0200 Subject: [PATCH] fix(tasks): a cycle chip counts its days left whether ticked or not It printed the unticked days left, so ticking a cycle off dropped its figure by one, and on the last day read 0d. Tasks now carry daysLeft, the period's days from today, and the chip prints that. Found in the whole-branch review. Co-Authored-By: Claude Opus 5.5 --- src/shared/goal.ts | 4 +++- src/shared/task.ts | 3 ++- test/task.test.ts | 12 ++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/shared/goal.ts b/src/shared/goal.ts index 804eb73..1ea27b7 100644 --- a/src/shared/goal.ts +++ b/src/shared/goal.ts @@ -86,6 +86,8 @@ export interface GoalStatus { open: number | null; /** Fewer days left than check-ins needed. The target is never lowered. */ shortfall: boolean; + /** A task's days left in its period from today, ticked or not — what a cycle's chip shows. */ + daysLeft?: number | undefined; } /** Bounds a walk over keys when the end is unknown; matches daily.ts. */ @@ -207,7 +209,7 @@ export function goalSummary(goal: Goal | undefined): string { /** The Dailies chip's short count. */ export function goalCount(s: GoalStatus): string { // "cy" means nothing; the days left in a cycle say where it is. - if (s.scope === "cycle") return `${num(s.count)}/${num(s.required)} · ${s.open ?? 0}d`; + if (s.scope === "cycle") return `${num(s.count)}/${num(s.required)} · ${s.daysLeft ?? s.open ?? 0}d`; const unit = s.scope === "week" ? " wk" : s.scope === "month" ? " mo" : ""; return `${num(s.count)}/${num(s.required)}${unit}`; } diff --git a/src/shared/task.ts b/src/shared/task.ts index aa183fb..8c964bd 100644 --- a/src/shared/task.ts +++ b/src/shared/task.ts @@ -87,7 +87,7 @@ export function taskStatus(input: { return { scope, measure: "amount", periodStart: start, required, count, met: count >= required, needed: Math.max(0, required - count), - open: left.length, shortfall: false, + open: left.length, shortfall: false, daysLeft: left.length, }; } @@ -100,6 +100,7 @@ export function taskStatus(input: { return { scope, measure: "days", periodStart: start, required, count, met: count >= required, needed, open, shortfall: needed > open, + daysLeft: left.length, }; } diff --git a/test/task.test.ts b/test/task.test.ts index 5c1f5a5..e7aca8b 100644 --- a/test/task.test.ts +++ b/test/task.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test"; import { RESERVED_ID_SEGMENTS } from "../src/shared/custom.ts"; import { nextResetMs } from "../src/shared/daily.ts"; +import { goalCount } from "../src/shared/goal.ts"; import { mintTaskId, TaskSchema, taskCaption, taskStatus, type Task } from "../src/shared/task.ts"; /** @@ -171,3 +172,14 @@ describe("a task on another server's clock", () => { expect(after.periodStart).toBe("2026-10-08"); }); }); + +describe("a cycle chip's days left", () => { + test("is the days left in the cycle, ticked or not", () => { + const t = task({ period: { unit: "cycle", days: 14, from: "2026-10-16" }, measure: { kind: "days", required: 1 } }); + const at = (logged: string[], day: string) => + goalCount(taskStatus({ task: t, region: "europe", now: noon(day), logged })); + expect(at([], "2026-10-20")).toBe("0/1 · 10d"); + expect(at(["2026-10-20"], "2026-10-20")).toBe("1/1 · 10d"); + expect(at(["2026-10-29"], "2026-10-29")).toBe("1/1 · 1d"); + }); +});