diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-07-12 07:40:30 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-07-12 07:40:30 +0000 |
| commit | f01b2924bf0b62fc20af0d09581d101418455b1c (patch) | |
| tree | 703bed9c8286fa4f48c5085c2a95268f9c6b07b7 /internal | |
| parent | af0d59d78524127f6cc76e43a7e397da01788d97 (diff) | |
fix(timeline): include overdue native tasks in timeline and widget
GetNativeTasksByDateRange's SQL bound (due_date >= start) excluded any
task overdue from a previous day before ComputeDaySection ever got a
chance to mark it IsOverdue, so both the web Timeline view and the
widget API (which both call BuildTimeline with start = today) silently
dropped overdue tasks entirely -- only the Tasks tab (unbounded
GetNativeTasks) showed them. Added GetOverdueNativeTasks and folded its
results into BuildTimeline alongside the ranged fetch.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/handlers/timeline_logic.go | 28 | ||||
| -rw-r--r-- | internal/handlers/timeline_logic_test.go | 50 | ||||
| -rw-r--r-- | internal/store/native_tasks.go | 20 |
3 files changed, 97 insertions, 1 deletions
diff --git a/internal/handlers/timeline_logic.go b/internal/handlers/timeline_logic.go index 67ba09d..e7c1279 100644 --- a/internal/handlers/timeline_logic.go +++ b/internal/handlers/timeline_logic.go @@ -125,7 +125,10 @@ func BuildTimeline(ctx context.Context, s *store.Store, start, end time.Time) ([ } } - // 6. Fetch native (doot-owned) tasks + // 6. Fetch native (doot-owned) tasks due within the range, plus any + // overdue tasks (due before the range's start) so they still surface -- + // GetNativeTasksByDateRange's lower bound would otherwise drop them + // before ComputeDaySection ever gets a chance to mark them IsOverdue. nativeDated, err := s.GetNativeTasksByDateRange(start, end) if err != nil { log.Printf("Warning: failed to read native tasks: %v", err) @@ -149,6 +152,29 @@ func BuildTimeline(ctx context.Context, s *store.Store, start, end time.Time) ([ } } + nativeOverdue, err := s.GetOverdueNativeTasks(start) + if err != nil { + log.Printf("Warning: failed to read overdue native tasks: %v", err) + } else { + for _, task := range nativeOverdue { + if task.DueDate == nil { + continue + } + item := models.TimelineItem{ + ID: task.ID, + Type: models.TimelineItemTypeTask, + Title: task.Content, + Time: *task.DueDate, + Description: task.Description, + OriginalItem: task, + IsCompleted: task.Completed, + Source: "doot", + } + item.ComputeDaySection(now) + items = append(items, item) + } + } + nativeUndated, err := s.GetUndatedNativeTasks() if err != nil { log.Printf("Warning: failed to read undated native tasks: %v", err) diff --git a/internal/handlers/timeline_logic_test.go b/internal/handlers/timeline_logic_test.go index 0b7bc37..7d3f6b5 100644 --- a/internal/handlers/timeline_logic_test.go +++ b/internal/handlers/timeline_logic_test.go @@ -293,6 +293,56 @@ func TestBuildTimeline_ReadsCalendarEventsFromStore(t *testing.T) { } } +// TestBuildTimeline_IncludesOverdueNativeTasks proves the 2026-07-12 fix: a +// native task whose due_date is BEFORE the requested range's start must +// still appear in the timeline, marked IsOverdue, instead of being silently +// dropped. Root cause: GetNativeTasksByDateRange's SQL bound (due_date >= +// start) excluded overdue tasks from ever being fetched, so ComputeDaySection +// never got a chance to set IsOverdue -- both the web Timeline view and the +// widget API (which both call BuildTimeline with a start of "today") showed +// zero overdue tasks even though the Tasks tab (which calls the unbounded +// GetNativeTasks) showed them fine. +func TestBuildTimeline_IncludesOverdueNativeTasks(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + + now := time.Now() + today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) + overdueDate := today.AddDate(0, 0, -12) // 12 days in the past + + if err := db.CreateNativeTask(models.Task{ + ID: "overdue-1", + Content: "Pay the water bill", + DueDate: &overdueDate, + }); err != nil { + t.Fatalf("Failed to create native task: %v", err) + } + + start := today + end := today.AddDate(0, 0, 2) + + items, err := BuildTimeline(context.Background(), db, start, end) + if err != nil { + t.Fatalf("BuildTimeline failed: %v", err) + } + + var found *models.TimelineItem + for i := range items { + if items[i].ID == "overdue-1" { + found = &items[i] + } + } + if found == nil { + t.Fatal("expected overdue native task to appear in timeline, but it was missing") + } + if !found.IsOverdue { + t.Error("expected overdue native task to have IsOverdue = true") + } + if found.Title != "Pay the water bill" { + t.Errorf("Title: got %q, want %q", found.Title, "Pay the water bill") + } +} + func TestFetchCalendarEvents_CacheFallbackOnAPIError(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() diff --git a/internal/store/native_tasks.go b/internal/store/native_tasks.go index 6f8f999..f43a160 100644 --- a/internal/store/native_tasks.go +++ b/internal/store/native_tasks.go @@ -46,6 +46,26 @@ func (s *Store) GetNativeTasksByDateRange(start, end time.Time) ([]models.Task, return scanNativeTasks(rows) } +// GetOverdueNativeTasks returns non-completed native tasks whose due date is +// before the given time. BuildTimeline calls this alongside +// GetNativeTasksByDateRange, whose lower bound excludes anything due before +// the requested range's start -- without this, a task overdue from a +// previous day never gets fetched at all, so it never reaches +// ComputeDaySection to be marked IsOverdue. +func (s *Store) GetOverdueNativeTasks(before time.Time) ([]models.Task, error) { + rows, err := s.db.Query(` + SELECT id, content, description, project_name, due_date, priority, completed, labels, created_at + FROM native_tasks + WHERE completed = 0 AND due_date IS NOT NULL AND due_date < ? + ORDER BY due_date ASC, priority DESC + `, before) + if err != nil { + return nil, err + } + defer func() { _ = rows.Close() }() + return scanNativeTasks(rows) +} + // GetUndatedNativeTasks returns non-completed native tasks with no due date. func (s *Store) GetUndatedNativeTasks() ([]models.Task, error) { rows, err := s.db.Query(` |
