summaryrefslogtreecommitdiff
path: root/internal/store/native_tasks.go
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-07-17 22:22:37 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-07-17 22:22:37 +0000
commitb007fee8fb5b39a5f9b369c59af71ac9e795ceaf (patch)
treea5b999b8f4c23a52ae9249675753a92a77993008 /internal/store/native_tasks.go
parent70e6dd75130e70f2db83096c23eaa75326b183a2 (diff)
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
Diffstat (limited to 'internal/store/native_tasks.go')
-rw-r--r--internal/store/native_tasks.go50
1 files changed, 44 insertions, 6 deletions
diff --git a/internal/store/native_tasks.go b/internal/store/native_tasks.go
index 5d03bde..6f8ba7a 100644
--- a/internal/store/native_tasks.go
+++ b/internal/store/native_tasks.go
@@ -10,6 +10,7 @@ import (
"strings"
"time"
+ "task-dashboard/internal/config"
"task-dashboard/internal/models"
)
@@ -24,7 +25,9 @@ var ErrNativeTaskNotFound = errors.New("native task not found")
func (s *Store) GetNativeTasks() ([]models.Task, error) {
rows, err := s.db.Query(`
SELECT id, content, description, project_name, project_id, due_date, priority, completed, labels, created_at,
- recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id, next_occurrence_override, estimated_minutes
+ recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id, next_occurrence_override, estimated_minutes,
+ chain_id, chain_position, chain_unlocked,
+ bucket_id, bucket_state, bucket_last_active_at
FROM native_tasks
WHERE completed = 0
ORDER BY CASE WHEN due_date IS NULL THEN 1 ELSE 0 END, due_date ASC, priority DESC
@@ -43,9 +46,12 @@ func (s *Store) GetNativeTasks() ([]models.Task, error) {
func (s *Store) GetNativeTasksByDateRange(start, end time.Time) ([]models.Task, error) {
rows, err := s.db.Query(`
SELECT id, content, description, project_name, project_id, due_date, priority, completed, labels, created_at,
- recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id, next_occurrence_override, estimated_minutes
+ recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id, next_occurrence_override, estimated_minutes,
+ chain_id, chain_position, chain_unlocked,
+ bucket_id, bucket_state, bucket_last_active_at
FROM native_tasks
WHERE completed = 0 AND due_date IS NOT NULL AND due_date >= ? AND due_date < ?
+ AND (chain_id = '' OR chain_unlocked = 1)
ORDER BY due_date ASC, priority DESC
`, start, end)
if err != nil {
@@ -64,9 +70,12 @@ func (s *Store) GetNativeTasksByDateRange(start, end time.Time) ([]models.Task,
func (s *Store) GetOverdueNativeTasks(before time.Time) ([]models.Task, error) {
rows, err := s.db.Query(`
SELECT id, content, description, project_name, project_id, due_date, priority, completed, labels, created_at,
- recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id, next_occurrence_override, estimated_minutes
+ recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id, next_occurrence_override, estimated_minutes,
+ chain_id, chain_position, chain_unlocked,
+ bucket_id, bucket_state, bucket_last_active_at
FROM native_tasks
WHERE completed = 0 AND due_date IS NOT NULL AND due_date < ?
+ AND (chain_id = '' OR chain_unlocked = 1)
ORDER BY due_date ASC, priority DESC
`, before)
if err != nil {
@@ -80,9 +89,13 @@ func (s *Store) GetOverdueNativeTasks(before time.Time) ([]models.Task, error) {
func (s *Store) GetUndatedNativeTasks() ([]models.Task, error) {
rows, err := s.db.Query(`
SELECT id, content, description, project_name, project_id, due_date, priority, completed, labels, created_at,
- recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id, next_occurrence_override, estimated_minutes
+ recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id, next_occurrence_override, estimated_minutes,
+ chain_id, chain_position, chain_unlocked,
+ bucket_id, bucket_state, bucket_last_active_at
FROM native_tasks
WHERE completed = 0 AND due_date IS NULL
+ AND (chain_id = '' OR chain_unlocked = 1)
+ AND bucket_state != 'dormant'
ORDER BY priority DESC, created_at ASC
`)
if err != nil {
@@ -96,7 +109,9 @@ func (s *Store) GetUndatedNativeTasks() ([]models.Task, error) {
func (s *Store) GetNativeTaskByID(id string) (*models.Task, error) {
rows, err := s.db.Query(`
SELECT id, content, description, project_name, project_id, due_date, priority, completed, labels, created_at,
- recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id, next_occurrence_override, estimated_minutes
+ recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id, next_occurrence_override, estimated_minutes,
+ chain_id, chain_position, chain_unlocked,
+ bucket_id, bucket_state, bucket_last_active_at
FROM native_tasks
WHERE id = ?
`, id)
@@ -166,6 +181,21 @@ func (s *Store) CompleteNativeTask(id string) error {
return err
}
+ if task.ChainID != "" {
+ if err := s.advanceChain(task.ChainID, task.ChainPosition); err != nil {
+ return err
+ }
+ }
+
+ if task.BucketID != "" {
+ now := config.Now()
+ if _, err := s.db.Exec(`
+ UPDATE native_tasks SET bucket_state = 'dormant', due_date = NULL, bucket_last_active_at = ?, updated_at = ? WHERE id = ?
+ `, now, now, id); err != nil {
+ return err
+ }
+ }
+
if task.RecurrenceSeriesID == "" {
return nil
}
@@ -334,7 +364,9 @@ func (s *Store) SetNextOccurrenceOverride(id string, date time.Time) error {
func (s *Store) GetSeriesNeedingNextIteration(now time.Time) ([]models.Task, error) {
rows, err := s.db.Query(`
SELECT id, content, description, project_name, project_id, due_date, priority, completed, labels, created_at,
- recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id, next_occurrence_override, estimated_minutes
+ recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id, next_occurrence_override, estimated_minutes,
+ chain_id, chain_position, chain_unlocked,
+ bucket_id, bucket_state, bucket_last_active_at
FROM native_tasks t1
WHERE recurrence_series_id != ''
AND due_date IS NOT NULL AND due_date <= ?
@@ -395,12 +427,18 @@ func scanNativeTasks(rows interface {
var dueDateStr *string
var weekdaysStr string
var nextOverrideStr string
+ var bucketLastActiveAt sql.NullTime
if err := rows.Scan(
&t.ID, &t.Content, &t.Description, &t.ProjectName, &t.ProjectID, &dueDateStr, &t.Priority, &t.Completed, &labelsJSON, &t.CreatedAt,
&t.RecurrenceFreq, &t.RecurrenceInterval, &weekdaysStr, &t.RecurrenceSeriesID, &nextOverrideStr, &t.EstimatedMinutes,
+ &t.ChainID, &t.ChainPosition, &t.ChainUnlocked,
+ &t.BucketID, &t.BucketState, &bucketLastActiveAt,
); err != nil {
return nil, err
}
+ if bucketLastActiveAt.Valid {
+ t.BucketLastActiveAt = &bucketLastActiveAt.Time
+ }
if dueDateStr != nil {
if parsed, err := time.Parse(time.RFC3339, *dueDateStr); err == nil {
t.DueDate = &parsed