From 143166ce759ce2cb0133b7438db36b844a9db1a7 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Sun, 18 Jan 2026 14:06:26 -1000 Subject: Implement Trello task heuristics for Tasks tab (Phase 3 Step 6) Add filtering logic to show Trello cards as actionable tasks when they have due dates OR are in lists named like "todo", "doing", "in progress", "tasks", "next", or "today". This makes the Tasks tab more useful by surfacing cards that represent work items even without explicit due dates. Co-Authored-By: Claude Opus 4.5 --- internal/api/trello.go | 4 ++ internal/handlers/handlers.go | 43 ++++++++++++++++ internal/handlers/heuristic_test.go | 97 +++++++++++++++++++++++++++++++++++++ internal/handlers/tabs.go | 17 ++++++- internal/models/types.go | 14 +++--- 5 files changed, 167 insertions(+), 8 deletions(-) create mode 100644 internal/handlers/heuristic_test.go (limited to 'internal') diff --git a/internal/api/trello.go b/internal/api/trello.go index 9c18ade..7140f79 100644 --- a/internal/api/trello.go +++ b/internal/api/trello.go @@ -225,6 +225,10 @@ func (c *TrelloClient) GetBoardsWithCards(ctx context.Context) ([]models.Board, // Fetch cards cards, err := c.GetCards(ctx, boards[i].ID) if err == nil { + // Set BoardName for each card + for j := range cards { + cards[j].BoardName = boards[i].Name + } // It is safe to write to specific indices of the slice concurrently boards[i].Cards = cards } diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index c3e49ed..9ba6351 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -6,6 +6,8 @@ import ( "html/template" "log" "net/http" + "sort" + "strings" "sync" "time" @@ -302,6 +304,47 @@ func (h *Handler) aggregateData(ctx context.Context, forceRefresh bool) (*models wg.Wait() + // Filter Trello cards into tasks based on heuristic + var trelloTasks []models.Card + for _, board := range data.Boards { + for _, card := range board.Cards { + listNameLower := strings.ToLower(card.ListName) + isTask := card.DueDate != nil || + strings.Contains(listNameLower, "todo") || + strings.Contains(listNameLower, "doing") || + strings.Contains(listNameLower, "progress") || + strings.Contains(listNameLower, "task") + + if isTask { + trelloTasks = append(trelloTasks, card) + } + } + } + + // Sort trelloTasks: earliest due date first, nil last, then by board name + sort.Slice(trelloTasks, func(i, j int) bool { + // Both have due dates: compare dates + if trelloTasks[i].DueDate != nil && trelloTasks[j].DueDate != nil { + if !trelloTasks[i].DueDate.Equal(*trelloTasks[j].DueDate) { + return trelloTasks[i].DueDate.Before(*trelloTasks[j].DueDate) + } + // Same due date, fall through to board name comparison + } + + // Only one has due date: that one comes first + if trelloTasks[i].DueDate != nil && trelloTasks[j].DueDate == nil { + return true + } + if trelloTasks[i].DueDate == nil && trelloTasks[j].DueDate != nil { + return false + } + + // Both nil or same due date: sort by board name + return trelloTasks[i].BoardName < trelloTasks[j].BoardName + }) + + data.TrelloTasks = trelloTasks + return data, nil } diff --git a/internal/handlers/heuristic_test.go b/internal/handlers/heuristic_test.go new file mode 100644 index 0000000..f76fdc0 --- /dev/null +++ b/internal/handlers/heuristic_test.go @@ -0,0 +1,97 @@ +package handlers + +import ( + "net/http/httptest" + "os" + "strings" + "testing" + "time" + + "task-dashboard/internal/models" + "task-dashboard/internal/store" +) + +func TestHandleTasks_Heuristic(t *testing.T) { + // Create temp database file + tmpFile, err := os.CreateTemp("", "test_heuristic_*.db") + if err != nil { + t.Fatalf("Failed to create temp db: %v", err) + } + tmpFile.Close() + defer os.Remove(tmpFile.Name()) + + // Save current directory and change to project root + originalDir, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get working directory: %v", err) + } + + // Change to project root (2 levels up from internal/handlers) + if err := os.Chdir("../../"); err != nil { + t.Fatalf("Failed to change to project root: %v", err) + } + defer os.Chdir(originalDir) + + // Initialize store (this runs migrations) + db, err := store.New(tmpFile.Name()) + if err != nil { + t.Fatalf("Failed to initialize store: %v", err) + } + defer db.Close() + + // Seed Data + // Board 1: Has actionable lists + board1 := models.Board{ID: "b1", Name: "Work Board"} + + // Card 1: Has Due Date (Should appear) + due := time.Now() + card1 := models.Card{ID: "c1", Name: "Due Task", ListID: "l1", ListName: "Backlog", DueDate: &due, BoardName: "Work Board"} + + // Card 2: No Due Date, Actionable List (Should appear) + card2 := models.Card{ID: "c2", Name: "Doing Task", ListID: "l2", ListName: "Doing", BoardName: "Work Board"} + + // Card 3: No Due Date, Non-Actionable List (Should NOT appear) + card3 := models.Card{ID: "c3", Name: "Backlog Task", ListID: "l1", ListName: "Backlog", BoardName: "Work Board"} + + // Card 4: No Due Date, "To Do" List (Should appear) + card4 := models.Card{ID: "c4", Name: "Todo Task", ListID: "l3", ListName: "To Do", BoardName: "Work Board"} + + board1.Cards = []models.Card{card1, card2, card3, card4} + + if err := db.SaveBoards([]models.Board{board1}); err != nil { + t.Fatalf("Failed to save boards: %v", err) + } + + // Create Handler + h := NewTabsHandler(db) + + // Skip if templates are not loaded + if h.templates == nil { + t.Skip("Templates not available in test environment") + } + + req := httptest.NewRequest("GET", "/tabs/tasks", nil) + w := httptest.NewRecorder() + + // Execute + h.HandleTasks(w, req) + + // Verify + resp := w.Body.String() + + // Check for presence of expected tasks + if !strings.Contains(resp, "Due Task") { + t.Errorf("Expected 'Due Task' to be present") + } + if !strings.Contains(resp, "Doing Task") { + t.Errorf("Expected 'Doing Task' to be present") + } + if !strings.Contains(resp, "Todo Task") { + t.Errorf("Expected 'Todo Task' to be present") + } + + // Check for absence of non-expected tasks + if strings.Contains(resp, "Backlog Task") { + t.Errorf("Expected 'Backlog Task' to be ABSENT") + } +} diff --git a/internal/handlers/tabs.go b/internal/handlers/tabs.go index c23910d..ce9c34f 100644 --- a/internal/handlers/tabs.go +++ b/internal/handlers/tabs.go @@ -5,12 +5,25 @@ import ( "log" "net/http" "sort" + "strings" "time" "task-dashboard/internal/models" "task-dashboard/internal/store" ) +// isActionableList returns true if the list name indicates an actionable list +func isActionableList(name string) bool { + lower := strings.ToLower(name) + return strings.Contains(lower, "doing") || + strings.Contains(lower, "in progress") || + strings.Contains(lower, "to do") || + strings.Contains(lower, "todo") || + strings.Contains(lower, "tasks") || + strings.Contains(lower, "next") || + strings.Contains(lower, "today") +} + // TabsHandler handles tab-specific rendering with Atom model type TabsHandler struct { store *store.Store @@ -65,10 +78,10 @@ func (h *TabsHandler) HandleTasks(w http.ResponseWriter, r *http.Request) { } } - // Convert Trello cards with due dates + // Convert Trello cards with due dates or in actionable lists for _, board := range boards { for _, card := range board.Cards { - if card.DueDate != nil { + if card.DueDate != nil || isActionableList(card.ListName) { atoms = append(atoms, models.CardToAtom(card)) } } diff --git a/internal/models/types.go b/internal/models/types.go index 245f25f..fab732f 100644 --- a/internal/models/types.go +++ b/internal/models/types.go @@ -52,12 +52,13 @@ type Board struct { // Card represents a Trello card type Card struct { - ID string `json:"id"` - Name string `json:"name"` - ListID string `json:"list_id"` - ListName string `json:"list_name"` - DueDate *time.Time `json:"due_date,omitempty"` - URL string `json:"url"` + ID string `json:"id"` + Name string `json:"name"` + ListID string `json:"list_id"` + ListName string `json:"list_name"` + BoardName string `json:"board_name"` + DueDate *time.Time `json:"due_date,omitempty"` + URL string `json:"url"` } // Project represents a Todoist project @@ -85,6 +86,7 @@ type DashboardData struct { Notes []Note `json:"notes"` Meals []Meal `json:"meals"` Boards []Board `json:"boards,omitempty"` + TrelloTasks []Card `json:"trello_tasks,omitempty"` Projects []Project `json:"projects,omitempty"` LastUpdated time.Time `json:"last_updated"` Errors []string `json:"errors,omitempty"` -- cgit v1.2.3