diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-08-10 03:16:39 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-08-10 03:16:39 +0000 |
| commit | 9a3ece4fef431c2c35af239f3df6eeaa10ddeaf4 (patch) | |
| tree | d36c63e17810183fd8ee250dcb0296f42be9f6b3 /internal/handlers/widget.go | |
| parent | b4bb981d98b0c2d4758f5acc0e1deb1f2a651c79 (diff) | |
Fix widget showing tomorrow's dated tasks under today
Root cause: doot-native and Google Tasks due dates are always
midnight-anchored (even when a task genuinely has a due date), which trips
TimelineItem.ComputeDaySection's "midnight means no specific time" heuristic
and sets IsAllDay=true on them. TimelineItemToWidgetItem then left wi.Start
nil for any Task/GTask with IsAllDay=true, and the Android client's
undated/floating pool (DootWidget.kt's `floating` list) has zero per-day
awareness -- it just packs items forward from "now" -- so a task due
tomorrow rendered as if due today. This was previously diagnosed and
deliberately set aside (see project_doot_isallday_midnight_bug memory) with
a simpler proposed fix (gate ComputeDaySection's heuristic to Event/Meal
types); re-verifying that plan against the actual code before implementing
it surfaced a real problem with it: it would also flip IsAllDay to false for
same-day dated tasks, moving them from the web's untimed-item strip into the
hourly grid with a fabricated "12:00 AM" time label -- fixing the widget by
breaking the web's currently-correct rendering.
Actual fix: added TimelineItem.Undated, a signal genuinely independent of
IsAllDay -- true only for the caller's two genuinely-dateless constructions
(nativeUndated tasks, gtasks with no due date), both of which already use
Time=now as a layout placeholder rather than a real date. ComputeDaySection
and IsAllDay are untouched, so web rendering is unaffected. This gate.go
change routes on that new signal instead: dated Task/GTask items now get
wi.Start populated (so the Android client's existing, already-correct
Start-based day bucketing runs for them) while genuinely undated ones keep
today's nil-Start floating treatment exactly as before.
Also fixed the same latent bug in wi.DueDate's guard while in the same
code path: it only checked !Time.IsZero(), but nativeUndated's Time=now
placeholder is non-zero, so an undated task's Android detail popup would
have shown a fabricated due date of whatever moment the request happened
to run. Now guards on !Undated too.
Verified: added TestTimelineItemToWidgetItem_DatedGTaskGetsStart and
TestTimelineItemToWidgetItem_UndatedTaskWithPlaceholderTime_NilDueDate,
and updated the three existing tests that had encoded the bug's old
"Start stays nil for any IsAllDay task" assumption as expected behavior
(TestTimelineItemToWidgetItem_Task, _DootTaskGetsDueDate,
_AllDayTask_KeepsFloatingBehavior -- the last one needed Undated: true
added since it no longer implies undated on its own). Proved the fix by
reverting internal/handlers/widget.go to the pre-fix condition against a
real backup and confirming exactly the three tests exercising the new
behavior fail, then restored from that backup and confirmed byte-identical.
go build/vet/test all clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
Diffstat (limited to 'internal/handlers/widget.go')
| -rw-r--r-- | internal/handlers/widget.go | 36 |
1 files changed, 27 insertions, 9 deletions
diff --git a/internal/handlers/widget.go b/internal/handlers/widget.go index 915909e..1f6911b 100644 --- a/internal/handlers/widget.go +++ b/internal/handlers/widget.go @@ -70,10 +70,24 @@ func TimelineItemToWidgetItem(item models.TimelineItem) models.WidgetItem { // client can pin them to the top of the correct day's section instead of // losing them in the floating-task hourly-slot packer, which has no // concept of "all day" and can push a slot past the visible grid range - // entirely. Tasks keep the existing nil-Start "floating" treatment - // regardless of IsAllDay -- only Start is set for them (never End), and - // only when they have a real time. - if !item.Time.IsZero() && (!item.IsAllDay || item.Type == models.TimelineItemTypeEvent) { + // entirely. + // + // Dated Task/GTask items (item.Undated == false) get the same Start + // treatment as Events, for the same reason -- their client-side day + // bucketing (Android's WidgetRoot todayScheduledEvents/tomorrowItems + // split) is keyed entirely off Start, so a nil Start drops them into the + // undated/floating pool regardless of their actual due date. That pool + // has zero day-awareness (see DootWidget.kt's `floating` list), so a + // task due tomorrow rendered as if due today (2026-08-10 report). This + // is deliberately gated on Undated, not IsAllDay -- IsAllDay stays true + // for these tasks (doot-native/Google Tasks due dates are ALWAYS + // midnight-anchored even with a real due date, see + // reference_google_tasks_due_date_utc_quirk), which is why the simpler + // "just check IsAllDay" fix doesn't work here: Card and truly-undated + // Task/GTask items must keep the exact opposite behavior (nil Start), + // and only Undated tells them apart. + isDatedTask := (item.Type == models.TimelineItemTypeTask || item.Type == models.TimelineItemTypeGTask) && !item.Undated + if !item.Time.IsZero() && (!item.IsAllDay || item.Type == models.TimelineItemTypeEvent || isDatedTask) { t := item.Time wi.Start = &t if item.Type == models.TimelineItemTypeEvent { @@ -94,11 +108,15 @@ func TimelineItemToWidgetItem(item models.TimelineItem) models.WidgetItem { } } - // DueDate is independent of Start/IsAllDay -- doot tasks deliberately - // keep Start nil (see the "floating task" doc comment above) so the - // client's SlotPacker positions them, but the Android detail popup - // still needs to know the real due date to display and reschedule it. - if item.Type == models.TimelineItemTypeTask && item.Source == "doot" && !item.Time.IsZero() { + // DueDate is a separate signal from Start/IsAllDay -- populated whenever + // a doot task genuinely has a due date (Undated == false), regardless of + // whether Start also got set above, so the Android detail popup can + // display/reschedule it. Guarding on !item.Undated (not just + // !item.Time.IsZero()) matters because nativeUndated tasks use Time=now + // as a layout placeholder, not a zero value -- without this guard, + // an undated task's detail popup would show a fabricated "due" date of + // whatever moment the request happened to run. + if item.Type == models.TimelineItemTypeTask && item.Source == "doot" && !item.Time.IsZero() && !item.Undated { due := item.Time wi.DueDate = &due } |
