diff options
| -rw-r--r-- | SESSION_STATE.md | 50 | ||||
| -rw-r--r-- | internal/api/todoist.go | 29 | ||||
| -rw-r--r-- | internal/api/todoist_test.go | 12 | ||||
| -rw-r--r-- | internal/api/trello.go | 8 | ||||
| -rw-r--r-- | internal/handlers/handlers.go | 4 | ||||
| -rw-r--r-- | internal/handlers/timeline_logic_test.go | 160 | ||||
| -rw-r--r-- | internal/models/atom.go | 40 | ||||
| -rw-r--r-- | internal/models/types.go | 8 | ||||
| -rwxr-xr-x | scripts/bugs | 4 | ||||
| -rwxr-xr-x | scripts/resolve-bug | 23 |
10 files changed, 280 insertions, 58 deletions
diff --git a/SESSION_STATE.md b/SESSION_STATE.md index 091929d..5f68a3d 100644 --- a/SESSION_STATE.md +++ b/SESSION_STATE.md @@ -1,34 +1,28 @@ # Session State -**Active Task:** None +## Current Focus +Bug verification and fixes completed. -**Completed Tasks:** -- **Obsidian Removal:** ✅ -- **Authentication:** ✅ -- **VPS Deployment Preparation:** ✅ -- **Issue Batch (001-016):** ✅ - - 001: Hide future tasks behind fold - - 002: Modal menu for quick add/bug report - - 003: Fix tap to expand - - 005: Visual task timing differentiation - - 006: Reorder tasks by urgency - - 007: Fix outdated Todoist link - - 009: Keep completed tasks visible until refresh - - 010: Fix quick add timestamp (evening date bug) - - 015: Random landscape background - - 016: Click task to edit details +## Status +* [x] **Build Fix:** Added missing `fmt` import in `internal/models/atom.go` +* [x] **Bug Fix:** Trello cards missing - changed `filter=visible` to `filter=open` +* [x] **Bug Verification:** Resolved already-fixed bugs from DB (#24-27, #32, #39) +* [x] **Bug #36 Fixed:** Recurring tasks now hidden until due day + - Added `IsRecurring` field to Task model and Atom + - Updated Todoist API to parse `is_recurring` from due object + - Filter recurring future tasks from display -**Current Status:** [REVIEW_READY] +## Scripts Created +- `scripts/bugs` - List all bugs from production DB +- `scripts/resolve-bug <id>` - Resolve a bug by ID -**Files Modified:** -- `internal/api/todoist.go` - Updated URL format, added UpdateTask method -- `internal/api/interfaces.go` - Added UpdateTask to TodoistAPI interface -- `internal/handlers/handlers.go` - Added task detail/update handlers, completed task HTML response -- `internal/handlers/tabs.go` - Added urgency sorting, future task partitioning -- `internal/handlers/handlers_test.go` - Added UpdateTask mock -- `internal/models/atom.go` - Added IsFuture field -- `cmd/dashboard/main.go` - Added task detail/update routes -- `web/templates/index.html` - Added unified modal, task edit modal, random background -- `web/templates/partials/tasks-tab.html` - Checkbox complete, expand details, urgency styling, future fold +## Remaining Items (Feature Requests, not bugs) +- #12: Research task durations +- #28: Bugs as first-class atoms +- #30: Consistent background opacity +- #31: PlanToEat ingredients +- #33-38: Shopping/timeline features -**All Issues Complete - Ready for Review** +## Next Steps +* Deploy and verify recurring task fix +* Prioritize feature requests diff --git a/internal/api/todoist.go b/internal/api/todoist.go index 6c998cf..2c94e08 100644 --- a/internal/api/todoist.go +++ b/internal/api/todoist.go @@ -41,11 +41,8 @@ type todoistTaskResponse struct { ProjectID string `json:"project_id"` Priority int `json:"priority"` Labels []string `json:"labels"` - Due *struct { - Date string `json:"date"` - Datetime string `json:"datetime"` - } `json:"due"` - URL string `json:"url"` + Due *dueInfo `json:"due"` + URL string `json:"url"` CreatedAt string `json:"created_at"` } @@ -73,11 +70,8 @@ type SyncItemResponse struct { ProjectID string `json:"project_id"` Priority int `json:"priority"` Labels []string `json:"labels"` - Due *struct { - Date string `json:"date"` - Datetime string `json:"datetime"` - } `json:"due"` - IsCompleted bool `json:"is_completed"` + Due *dueInfo `json:"due"` + IsCompleted bool `json:"is_completed"` IsDeleted bool `json:"is_deleted"` AddedAt string `json:"added_at"` } @@ -125,6 +119,9 @@ func (c *TodoistClient) GetTasks(ctx context.Context) ([]models.Task, error) { } task.DueDate = parseDueDate(apiTask.Due) + if apiTask.Due != nil { + task.IsRecurring = apiTask.Due.IsRecurring + } tasks = append(tasks, task) } @@ -276,10 +273,14 @@ func (c *TodoistClient) ReopenTask(ctx context.Context, taskID string) error { } // parseDueDate parses due date from API response -func parseDueDate(due *struct { - Date string `json:"date"` - Datetime string `json:"datetime"` -}) *time.Time { +// dueInfo represents the due date structure from Todoist API +type dueInfo struct { + Date string `json:"date"` + Datetime string `json:"datetime"` + IsRecurring bool `json:"is_recurring"` +} + +func parseDueDate(due *dueInfo) *time.Time { if due == nil { return nil } diff --git a/internal/api/todoist_test.go b/internal/api/todoist_test.go index 88f94f8..2fa6e28 100644 --- a/internal/api/todoist_test.go +++ b/internal/api/todoist_test.go @@ -104,20 +104,16 @@ func TestTodoistClient_CreateTask_WithDueDate(t *testing.T) { } // Return mock response with due date - dueStruct := struct { - Date string `json:"date"` - Datetime string `json:"datetime"` - }{ - Date: "2026-01-15", - Datetime: "", - } response := todoistTaskResponse{ ID: "task-789", Content: "Task with Due Date", ProjectID: "project-456", URL: "https://todoist.com/task/789", CreatedAt: time.Now().Format(time.RFC3339), - Due: &dueStruct, + Due: &dueInfo{ + Date: "2026-01-15", + Datetime: "", + }, } w.Header().Set("Content-Type", "application/json") diff --git a/internal/api/trello.go b/internal/api/trello.go index 4cf7e9e..a19bbea 100644 --- a/internal/api/trello.go +++ b/internal/api/trello.go @@ -86,7 +86,7 @@ func (c *TrelloClient) GetBoards(ctx context.Context) ([]models.Board, error) { // GetCards fetches all cards for a specific board func (c *TrelloClient) GetCards(ctx context.Context, boardID string) ([]models.Card, error) { params := c.authParams() - params.Set("filter", "visible") + params.Set("filter", "open") params.Set("fields", "id,name,idList,due,url,idBoard") var apiCards []trelloCardResponse @@ -95,7 +95,11 @@ func (c *TrelloClient) GetCards(ctx context.Context, boardID string) ([]models.C return nil, fmt.Errorf("failed to fetch cards: %w", err) } - log.Printf("Trello GetCards: board %s returned %d cards from API", boardID, len(apiCards)) + if len(apiCards) == 0 { + log.Printf("Trello GetCards: board %s returned 0 cards (may have only archived cards)", boardID) + } else { + log.Printf("Trello GetCards: board %s returned %d cards", boardID, len(apiCards)) + } // Fetch lists to get list names lists, err := c.getLists(ctx, boardID) diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 5014f39..5c86ce2 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -996,6 +996,10 @@ func (h *Handler) HandleTabTasks(w http.ResponseWriter, r *http.Request) { var currentAtoms, futureAtoms []models.Atom for _, a := range atoms { + // Don't show recurring tasks until the day they're due + if a.IsRecurring && a.IsFuture { + continue + } if a.IsFuture { futureAtoms = append(futureAtoms, a) } else { diff --git a/internal/handlers/timeline_logic_test.go b/internal/handlers/timeline_logic_test.go new file mode 100644 index 0000000..a0576d6 --- /dev/null +++ b/internal/handlers/timeline_logic_test.go @@ -0,0 +1,160 @@ +package handlers + +import ( + "context" + "os" + "path/filepath" + "testing" + "time" + + "task-dashboard/internal/models" + "task-dashboard/internal/store" + + _ "github.com/mattn/go-sqlite3" +) + +// MockCalendarClient implements GoogleCalendarAPI interface for testing +type MockCalendarClient struct { + Events []models.CalendarEvent + Err error +} + +func (m *MockCalendarClient) GetUpcomingEvents(ctx context.Context, maxResults int) ([]models.CalendarEvent, error) { + return m.Events, m.Err +} + +func (m *MockCalendarClient) GetEventsByDateRange(ctx context.Context, start, end time.Time) ([]models.CalendarEvent, error) { + return m.Events, m.Err +} + +func setupTestStore(t *testing.T) *store.Store { + t.Helper() + tempDir := t.TempDir() + dbPath := filepath.Join(tempDir, "test.db") + migrationDir := filepath.Join(tempDir, "migrations") + + if err := os.MkdirAll(migrationDir, 0755); err != nil { + t.Fatalf("Failed to create migration dir: %v", err) + } + + schema := ` + CREATE TABLE IF NOT EXISTS tasks ( + id TEXT PRIMARY KEY, + content TEXT NOT NULL, + description TEXT, + project_id TEXT, + project_name TEXT, + due_date DATETIME, + priority INTEGER DEFAULT 1, + completed BOOLEAN DEFAULT FALSE, + labels TEXT, + url TEXT, + created_at DATETIME, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + CREATE TABLE IF NOT EXISTS meals ( + id TEXT PRIMARY KEY, + recipe_name TEXT NOT NULL, + date DATETIME, + meal_type TEXT, + recipe_url TEXT, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + CREATE TABLE IF NOT EXISTS boards ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + CREATE TABLE IF NOT EXISTS cards ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + board_id TEXT NOT NULL, + list_id TEXT, + list_name TEXT, + due_date DATETIME, + url TEXT, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + ` + if err := os.WriteFile(filepath.Join(migrationDir, "001_init.sql"), []byte(schema), 0644); err != nil { + t.Fatalf("Failed to write migration file: %v", err) + } + + // Initialize store (this creates tables) + s, err := store.New(dbPath, migrationDir) + if err != nil { + t.Fatalf("Failed to create store: %v", err) + } + return s +} + +func TestBuildTimeline(t *testing.T) { + s := setupTestStore(t) + + // Fix a base time: 2023-01-01 08:00:00 + baseTime := time.Date(2023, 1, 1, 8, 0, 0, 0, time.UTC) + + // Task: 10:00 + taskDate := baseTime.Add(2 * time.Hour) + s.SaveTasks([]models.Task{ + {ID: "t1", Content: "Task 1", DueDate: &taskDate}, + }) + + // Meal: Lunch (defaults to 12:00) + mealDate := baseTime // Date part matters + s.SaveMeals([]models.Meal{ + {ID: "m1", RecipeName: "Lunch", Date: mealDate, MealType: "lunch"}, + }) + + // Card: 14:00 + cardDate := baseTime.Add(6 * time.Hour) + s.SaveBoards([]models.Board{ + { + ID: "b1", + Name: "Board 1", + Cards: []models.Card{ + {ID: "c1", Name: "Card 1", DueDate: &cardDate, ListID: "l1"}, + }, + }, + }) + + // Calendar Event: 09:00 + eventDate := baseTime.Add(1 * time.Hour) + mockCal := &MockCalendarClient{ + Events: []models.CalendarEvent{ + {ID: "e1", Summary: "Event 1", Start: eventDate, End: eventDate.Add(1 * time.Hour)}, + }, + } + + // Test Range: Full Day + start := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) + end := time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC) + + items, err := BuildTimeline(context.Background(), s, mockCal, start, end) + if err != nil { + t.Fatalf("BuildTimeline failed: %v", err) + } + + if len(items) != 4 { + t.Errorf("Expected 4 items, got %d", len(items)) + } + + // Expected Order: + // 1. Event (09:00) + // 2. Task (10:00) + // 3. Meal (12:00) + // 4. Card (14:00) + + if items[0].Type != models.TimelineItemTypeEvent { + t.Errorf("Expected item 0 to be Event, got %s", items[0].Type) + } + if items[1].Type != models.TimelineItemTypeTask { + t.Errorf("Expected item 1 to be Task, got %s", items[1].Type) + } + if items[2].Type != models.TimelineItemTypeMeal { + t.Errorf("Expected item 2 to be Meal, got %s", items[2].Type) + } + if items[3].Type != models.TimelineItemTypeCard { + t.Errorf("Expected item 3 to be Card, got %s", items[3].Type) + } +} diff --git a/internal/models/atom.go b/internal/models/atom.go index 10d14d1..3e08896 100644 --- a/internal/models/atom.go +++ b/internal/models/atom.go @@ -1,6 +1,9 @@ package models -import "time" +import ( + "fmt" + "time" +) type AtomSource string @@ -8,6 +11,7 @@ const ( SourceTrello AtomSource = "trello" SourceTodoist AtomSource = "todoist" SourceMeal AtomSource = "plantoeat" + SourceBug AtomSource = "bug" ) type AtomType string @@ -16,6 +20,7 @@ const ( TypeTask AtomType = "task" TypeNote AtomType = "note" TypeMeal AtomType = "meal" + TypeBug AtomType = "bug" ) // Atom represents a unified unit of work or information @@ -34,11 +39,12 @@ type Atom struct { Priority int // Normalized: 1 (Low) to 4 (Urgent) // UI Helpers (to be populated by mappers) - SourceIcon string // e.g., "trello-icon.svg" or emoji - ColorClass string // e.g., "border-blue-500" - IsOverdue bool // True if due date is before today - IsFuture bool // True if due date is after today - HasSetTime bool // True if due time is not midnight (has specific time) + SourceIcon string // e.g., "trello-icon.svg" or emoji + ColorClass string // e.g., "border-blue-500" + IsOverdue bool // True if due date is before today + IsFuture bool // True if due date is after today + HasSetTime bool // True if due time is not midnight (has specific time) + IsRecurring bool // True if this is a recurring task // Original Data (for write operations) Raw interface{} @@ -89,6 +95,7 @@ func TaskToAtom(t Task) Atom { Priority: priority, SourceIcon: "🔴", // Red circle for Todoist ColorClass: "border-red-500", + IsRecurring: t.IsRecurring, Raw: t, } } @@ -135,3 +142,24 @@ func MealToAtom(m Meal) Atom { Raw: m, } } + +// BugToAtom converts a Bug to an Atom +func BugToAtom(b Bug) Atom { + // Bugs get high priority (3) to encourage fixing + priority := 3 + + return Atom{ + ID: fmt.Sprintf("bug-%d", b.ID), + Title: b.Description, + Description: "Bug Report", + Source: SourceBug, + Type: TypeBug, + URL: "", + DueDate: nil, // Bugs don't have due dates + CreatedAt: b.CreatedAt, + Priority: priority, + SourceIcon: "🐛", + ColorClass: "border-red-700", + Raw: b, + } +} diff --git a/internal/models/types.go b/internal/models/types.go index f45e346..0284a3a 100644 --- a/internal/models/types.go +++ b/internal/models/types.go @@ -15,6 +15,7 @@ type Task struct { Labels []string `json:"labels"` URL string `json:"url"` CreatedAt time.Time `json:"created_at"` + IsRecurring bool `json:"is_recurring"` } // Meal represents a meal from PlanToEat @@ -100,6 +101,13 @@ type CalendarEvent struct { HTMLLink string `json:"html_link"` } +// Bug represents a bug report +type Bug struct { + ID int64 `json:"id"` + Description string `json:"description"` + CreatedAt time.Time `json:"created_at"` +} + // CacheMetadata tracks when data was last fetched type CacheMetadata struct { Key string `json:"key"` diff --git a/scripts/bugs b/scripts/bugs new file mode 100755 index 0000000..4f3c064 --- /dev/null +++ b/scripts/bugs @@ -0,0 +1,4 @@ +#!/bin/bash +# List all bugs from the production database + +ssh titanium "sqlite3 -column -header /site/doot.terst.org/data/dashboard.db 'SELECT id, description, created_at FROM bugs ORDER BY id'" diff --git a/scripts/resolve-bug b/scripts/resolve-bug new file mode 100755 index 0000000..a3f0979 --- /dev/null +++ b/scripts/resolve-bug @@ -0,0 +1,23 @@ +#!/bin/bash +# Resolve (delete) a bug by ID + +if [ -z "$1" ]; then + echo "Usage: resolve-bug <bug_id>" + exit 1 +fi + +BUG_ID="$1" + +# Show the bug being resolved +echo "Resolving bug #$BUG_ID:" +ssh titanium "sqlite3 -column /site/doot.terst.org/data/dashboard.db \"SELECT description FROM bugs WHERE id = $BUG_ID\"" + +# Delete the bug +ssh titanium "sqlite3 /site/doot.terst.org/data/dashboard.db \"DELETE FROM bugs WHERE id = $BUG_ID\"" + +if [ $? -eq 0 ]; then + echo "Bug #$BUG_ID resolved." +else + echo "Failed to resolve bug #$BUG_ID" + exit 1 +fi |
