From b007fee8fb5b39a5f9b369c59af71ac9e795ceaf Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Fri, 17 Jul 2026 22:22:37 +0000 Subject: Implement linear task chains and recurring maintenance buckets Backend, web timeline, and Android widget wiring for the last two unimplemented items from doot-future-task-scheduling-ideas. Chains: task_chains table + chain_id/chain_position/chain_unlocked on native_tasks (migration 026), WIP-limit-1 advancement hooked into CompleteNativeTask, locked tasks excluded from all date-based queries, 5 new /api/widget/chains* endpoints, an N/M position badge on web and Android widget rows. Buckets: maintenance_buckets table + bucket_id/bucket_state/ bucket_last_active_at on native_tasks (migration 027), staleness-then-priority selection scoring, a new RunBucketCycleCheck scheduler loop, 5 new endpoints including the distinct Defer action, a Defer button on web and Android widget rows. Also corrected stale "not yet approved" status headers on the two already-shipped specs this work depended on (labels/projects, budgets/ availability) -- their headers were never updated after implementation. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL --- internal/handlers/timeline_logic.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) (limited to 'internal/handlers/timeline_logic.go') diff --git a/internal/handlers/timeline_logic.go b/internal/handlers/timeline_logic.go index b90b61e..e3da760 100644 --- a/internal/handlers/timeline_logic.go +++ b/internal/handlers/timeline_logic.go @@ -12,6 +12,22 @@ import ( "task-dashboard/internal/store" ) +// setChainBadge populates item.ChainPosition/ChainTotal (1-indexed) when +// task belongs to a chain. Only the currently-unlocked task in a chain ever +// reaches BuildTimeline (locked tasks are excluded at the store layer), so +// this runs at most once per chain per call -- no memoization needed. +func setChainBadge(s *store.Store, item *models.TimelineItem, task models.Task) { + if task.ChainID == "" { + return + } + tasks, err := s.GetChainTasks(task.ChainID) + if err != nil { + return + } + item.ChainPosition = task.ChainPosition + 1 + item.ChainTotal = len(tasks) +} + // BuildTimeline aggregates and normalizes data into a timeline structure func BuildTimeline(ctx context.Context, s *store.Store, start, end time.Time) ([]models.TimelineItem, error) { var items []models.TimelineItem @@ -159,6 +175,8 @@ func BuildTimeline(ctx context.Context, s *store.Store, start, end time.Time) ([ Source: "doot", ProjectColor: projectColors[task.ProjectID], } + setChainBadge(s, &item, task) + item.BucketState = task.BucketState item.ComputeDaySection(now) items = append(items, item) } @@ -183,6 +201,8 @@ func BuildTimeline(ctx context.Context, s *store.Store, start, end time.Time) ([ Source: "doot", ProjectColor: projectColors[task.ProjectID], } + setChainBadge(s, &item, task) + item.BucketState = task.BucketState item.ComputeDaySection(now) items = append(items, item) } @@ -205,6 +225,8 @@ func BuildTimeline(ctx context.Context, s *store.Store, start, end time.Time) ([ IsAllDay: true, ProjectColor: projectColors[task.ProjectID], } + setChainBadge(s, &item, task) + item.BucketState = task.BucketState item.ComputeDaySection(now) items = append(items, item) } -- cgit v1.2.3