From 84252756c687044d73a0ea5cebf8088c7c9ed3e8 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Sun, 12 Jul 2026 06:21:40 +0000 Subject: fix(widget): pin all-day calendar events to the top instead of losing them WidgetItem.isAllDay was carried by the client's data model but nothing ever read it. All-day events had no Start at all, so they fell into the same floating-task queue as ordinary untimed tasks; if enough tasks were ahead of one in the queue, SlotPacker could assign it an hour slot past the visible grid range entirely -- not merely unpinned, actually invisible. Server: TimelineItemToWidgetItem now populates Start for all-day CALENDAR EVENTS specifically (their real event date), while leaving undated doot/gtask tasks -- also flagged IsAllDay as a "no specific time" fallback, a different concept -- on the existing nil-Start floating behavior. Client: all-day events are filtered out of the hourly grid/floating-task pipeline entirely, bucketed by day using the new Start date, and rendered in a new pinned AllDayRow section right after the TODAY/TOMORROW headers. --- internal/handlers/widget_test.go | 61 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) (limited to 'internal/handlers/widget_test.go') diff --git a/internal/handlers/widget_test.go b/internal/handlers/widget_test.go index 850dc07..6d741d2 100644 --- a/internal/handlers/widget_test.go +++ b/internal/handlers/widget_test.go @@ -109,6 +109,67 @@ func TestTimelineItemToWidgetItem_Event(t *testing.T) { } } +// TestTimelineItemToWidgetItem_AllDayEvent proves the 2026-07-12 fix: an +// all-day CALENDAR EVENT (Type == event, IsAllDay == true) must get Start +// populated with its real date -- previously Start was nil for every +// IsAllDay item regardless of type, which meant the widget client's +// hourly-slot packer had no date information to pin all-day events to the +// top of the correct day and they could silently fall outside the visible +// grid range instead. End stays nil since there's no meaningful end time to +// show for an all-day item. +func TestTimelineItemToWidgetItem_AllDayEvent(t *testing.T) { + day := time.Date(2026, 7, 12, 0, 0, 0, 0, time.Local) + item := models.TimelineItem{ + ID: "cal-holiday", + Title: "Company Holiday", + Source: "calendar", + Type: models.TimelineItemTypeEvent, + Time: day, + IsAllDay: true, + } + + wi := TimelineItemToWidgetItem(item) + + if wi.Type != "event" { + t.Errorf("Type: got %q, want %q", wi.Type, "event") + } + if !wi.IsAllDay { + t.Error("expected IsAllDay to be true") + } + if wi.Start == nil { + t.Fatal("all-day event should have non-nil Start (needed to pin it to the correct day)") + } + if !wi.Start.Equal(day) { + t.Errorf("Start = %v, want %v", *wi.Start, day) + } + if wi.End != nil { + t.Error("all-day event should have nil End") + } +} + +// TestTimelineItemToWidgetItem_AllDayTask_KeepsFloatingBehavior proves the +// same fix does NOT change behavior for undated doot/gtask tasks, which are +// also flagged IsAllDay as a "no specific time" fallback (see +// TimelineItem.ComputeDaySection) but are a different concept from a real +// all-day calendar event -- they must keep the existing nil-Start +// "floating" treatment so the hourly-slot packer still places them. +func TestTimelineItemToWidgetItem_AllDayTask_KeepsFloatingBehavior(t *testing.T) { + item := models.TimelineItem{ + ID: "undated-task", + Title: "Someday task", + Source: "doot", + Type: models.TimelineItemTypeTask, + Time: time.Now(), + IsAllDay: true, + } + + wi := TimelineItemToWidgetItem(item) + + if wi.Start != nil { + t.Error("an undated task (IsAllDay as a fallback, not a real all-day event) should still have nil Start") + } +} + func TestHandleWidgetComplete_NonCompletable(t *testing.T) { h := &Handler{} body := `{"id":"x","source":"calendar"}` -- cgit v1.2.3