From dc5c403a3628ca6fb1d3d41085cef9aebc1c0a80 Mon Sep 17 00:00:00 2001 From: kutesir Date: Wed, 8 Jul 2026 23:05:18 +0300 Subject: [PATCH] tasks: fix timed recurring tasks vanishing from Today after their time passes (KC-54) Real bug: todayTasks excludes timed tasks whose time has passed ('belongs in overdue'), but overdueTasks deliberately excludes ALL recurring tasks (it feeds postpone/postponeAllOverdue, which mutate dueDate by id -- for a recurring task that's the recurrence anchor, not just today's occurrence; including them there would let Postpone All corrupt the whole series). Recurring tasks fell into the gap between the two rules and vanished from the Today tab entirely once their time passed, while Calendar's day view (no time-of-day gating) kept showing them fine. Fix: todayTasks only excludes past-time NON-recurring tasks. Recurring occurrences always stay in Today, matching Calendar, without ever touching the overdue/postpone path. Co-Authored-By: Claude Opus 4.8 --- KisaniCal/ISSUES.md | 37 +++++++++++++++++++++++++++++++++ KisaniCal/Models/TaskItem.swift | 9 ++++++-- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/KisaniCal/ISSUES.md b/KisaniCal/ISSUES.md index ac4f555..c9af11d 100644 --- a/KisaniCal/ISSUES.md +++ b/KisaniCal/ISSUES.md @@ -1790,3 +1790,40 @@ Files: `KisaniCalWidgets/KisaniCalWidgets.swift`. Not build-verified (no iOS runtime here). To use: on the Lock Screen, tap the widget area → Add Widgets → Wenza → Progress Dots → long-press → Edit Widget → set Mode to This Day / This Week / This Year (This Month already configured). + +--- + +## KC-54 — Timed recurring tasks vanish from Today once their time passes + +**Status:** Fixed (not build-verified) +**Reported by:** User (screenshots: Calendar day view showed "Look into DPO +certificate" 11 AM and "Read Azure 104" 5 PM; neither appeared anywhere in the +Today tab — not in Today, not in Overdue, not in Completed) + +### Root cause +Two rules interacted badly for **recurring** tasks: +- `todayTasks` deliberately excludes timed tasks whose time has passed today + ("belongs in overdue, not today"). +- `overdueTasks` deliberately excludes ALL recurring tasks (`!recurs($0)`), + because it feeds `postpone`/`postponeAllOverdue`, which mutate a task's + `dueDate` directly by id — for a recurring task that field is the + **recurrence rule's anchor date**, not just "today's occurrence." Including + recurring occurrences there would let "Postpone All" shift/corrupt the whole + series. + +Net effect: once a recurring task's today-occurrence time passed, it was +excluded from Today (rule 1) but never picked up by Overdue (rule 2) — it fell +into a gap and disappeared from the Today tab entirely, while Calendar's day +view (`occurrences(on:)`, which has no time-of-day gating) kept showing it +normally. Not a display bug — real data was correctly stored, just not +surfaced. + +### Fix +`todayTasks`' "past time → excluded" rule now only applies to non-recurring +tasks (`!$0.isRecurring`). A recurring task's today-occurrence always stays in +Today regardless of time — matching what Calendar already shows, and avoiding +the postpone/recurrence-anchor risk entirely (recurring rows are never routed +through `overdueTasks`). One-line, additive filter change; `activeRows()` / +`occurrenceCopy` already preserve `isRecurring` on the occurrence copy. + +Files: `TaskItem.swift`. diff --git a/KisaniCal/Models/TaskItem.swift b/KisaniCal/Models/TaskItem.swift index 5c18b80..e117c6d 100644 --- a/KisaniCal/Models/TaskItem.swift +++ b/KisaniCal/Models/TaskItem.swift @@ -253,10 +253,15 @@ class TaskViewModel: ObservableObject { let cal = Calendar.current let start = cal.startOfDay(for: now) let end = cal.date(byAdding: .day, value: 1, to: start)! - // Timed tasks whose time has already passed belong in overdue, not today. + // Timed ONE-OFF tasks whose time has already passed belong in overdue, + // not today. Recurring occurrences always stay in Today regardless of + // time: overdueTasks intentionally excludes recurring tasks (postponing + // one would shift the recurrence rule's anchor date, not just today's + // occurrence), so without this a past-time recurring task would vanish + // from the app entirely instead of showing here like Calendar shows it. return activeRows().filter { guard let d = $0.dueDate else { return false } - if $0.hasTime && d < now { return false } + if $0.hasTime && d < now && !$0.isRecurring { return false } return d >= start && d < end } .sorted { ($0.dueDate ?? .distantFuture) < ($1.dueDate ?? .distantFuture) }