From a73f313fde25252f860845af27445c90e9dfadc9 Mon Sep 17 00:00:00 2001 From: kutesir Date: Thu, 9 Jul 2026 22:46:58 +0300 Subject: [PATCH] model: give TaskItem a real createdAt so countdown widgets stop resetting (KC-65) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Track Countdown widget already preferred task.createdAt as its progress start date, but TaskItem never had that field — it was always nil, so every task fell back to a fragile "first render" anchor that can reset to now. New tasks now record a real creation timestamp; old tasks keep the existing fallback since there's no way to know when they were actually made. Co-Authored-By: Claude Opus 4.8 --- KisaniCal/ISSUES.md | 42 +++++++++++++++++++++++++++++++++ KisaniCal/Models/TaskItem.swift | 4 ++++ 2 files changed, 46 insertions(+) diff --git a/KisaniCal/ISSUES.md b/KisaniCal/ISSUES.md index c7c2b3b..d9130c6 100644 --- a/KisaniCal/ISSUES.md +++ b/KisaniCal/ISSUES.md @@ -2250,3 +2250,45 @@ non-deprecated overload: `activity.end(nil, dismissalPolicy: .immediate)`. Files: `KisaniCal/Managers/LiveActivityManager.swift`, `KisaniCalWidgets/StopCountdownIntent.swift`. + +## KC-65 — TaskItem never actually stored a creation date + +Status: Implemented (not build-verified — no iOS runtime here) +Reported by: User, from a Track Countdown lock-screen widget stuck at 0% +Area: Model / widgets (Track Countdown progress dots) + +### Description +The countdown-dots widget (`KisaniCalWidgets.swift`'s `eventStartDate`) was +already written to prefer `task.createdAt` as the progress bar's start +date — but `TaskItem` never had a `createdAt` property at all, so it always +decoded to `nil` and every task fell through to a fallback chain: a +per-task "first render" anchor persisted in `UserDefaults.kisani` +(`kisani.widget.progressDots.anchor.`), or failing that, an inferred +legacy start (`remainingDays * 2`, capped 30–365 days). That anchor can +reset to "now" under several conditions (widget re-added, storage cleared, +the `looksLikeUpgradeAnchor` heuristic firing) — which is exactly the +"starting from scratch on every update" behavior the user was seeing. + +### Change +Added `var createdAt: Date? = Date()` to `TaskItem` (`Models/TaskItem.swift`) +— optional so existing saved tasks decode safely (missing key → `nil`, +same pattern as `recurrenceEnd`), but the `Date()` default is evaluated +fresh at each construction site, so every newly created task now genuinely +records its creation moment. No call sites needed updating: `addTask` uses +the compiler-synthesized memberwise init and doesn't pass `createdAt` +explicitly, so it gets the live default; `occurrenceCopy` copies the +struct (`var c = t`), so recurring-task instances correctly inherit the +series' original `createdAt` rather than getting a fresh one. + +The widget-side plumbing (`WidgetData.swift`'s `RawTask`/`WidgetTask` +decode, `eventStartDate`'s preference order) already existed and needed no +change — it was just never being fed a real value. + +Files: `KisaniCal/Models/TaskItem.swift`. + +### Note +Not build-verified (no iOS runtime here). Self-reviewed: balanced braces/ +parens (one pre-existing stray `(` predates this change, confirmed earlier +in KC-62). Existing tasks (created before this fix) still have no real +creation date and will keep using the anchor/inferred fallback — there's +no way to retroactively know when they were actually created. diff --git a/KisaniCal/Models/TaskItem.swift b/KisaniCal/Models/TaskItem.swift index 4d71bdd..826e55f 100644 --- a/KisaniCal/Models/TaskItem.swift +++ b/KisaniCal/Models/TaskItem.swift @@ -5,6 +5,10 @@ import WidgetKit struct TaskItem: Identifiable, Codable, Equatable { var id = UUID() var title: String + // Optional so existing saved tasks decode safely (missing key → nil); the + // `Date()` default only fires for genuinely new tasks constructed after + // this field existed — it's evaluated fresh at each construction site. + var createdAt: Date? = Date() var dueDate: Date? var hasTime: Bool = false var isComplete: Bool = false