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/chains_timeline_test.go | 43 +++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 internal/handlers/chains_timeline_test.go (limited to 'internal/handlers/chains_timeline_test.go') diff --git a/internal/handlers/chains_timeline_test.go b/internal/handlers/chains_timeline_test.go new file mode 100644 index 0000000..76ceb68 --- /dev/null +++ b/internal/handlers/chains_timeline_test.go @@ -0,0 +1,43 @@ +package handlers + +import ( + "context" + "testing" + "time" +) + +func TestBuildTimeline_PopulatesChainBadgeForUnlockedTask(t *testing.T) { + s, cleanup := setupTestDB(t) + defer cleanup() + + chain, err := s.CreateChain("Ham Radio Track", []string{"Study Technician", "Pass exam", "Study General"}) + if err != nil { + t.Fatal(err) + } + tasks, err := s.GetChainTasks(chain.ID) + if err != nil { + t.Fatal(err) + } + + now := time.Now() + items, err := BuildTimeline(context.Background(), s, now.Add(-time.Hour), now.Add(24*time.Hour)) + if err != nil { + t.Fatalf("BuildTimeline: %v", err) + } + + var found bool + for _, item := range items { + if item.ID == tasks[0].ID { + found = true + if item.ChainPosition != 1 || item.ChainTotal != 3 { + t.Errorf("ChainPosition/ChainTotal = %d/%d, want 1/3", item.ChainPosition, item.ChainTotal) + } + } + if item.ID == tasks[1].ID || item.ID == tasks[2].ID { + t.Errorf("locked chain task %q should not appear in the timeline", item.ID) + } + } + if !found { + t.Fatal("expected the unlocked chain task (position 0) to appear in the timeline") + } +} -- cgit v1.2.3