summaryrefslogtreecommitdiff
path: root/internal/handlers
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-01-18 15:24:58 -1000
committerPeter Stone <thepeterstone@gmail.com>2026-01-18 15:24:58 -1000
commit8dbb6f43577b8a86e94ef7aaee196f9743356643 (patch)
tree8713bf776f0c01c3d0fc94d906667e2e839e79f3 /internal/handlers
parent143166ce759ce2cb0133b7438db36b844a9db1a7 (diff)
Implement unified task completion with cache sync (Phase 3 Step 7)
Add checkbox UI to Tasks tab for completing Todoist tasks and archiving Trello cards. Fix cache synchronization so completed items stay gone after page reload by deleting them from SQLite cache after API success. - Add HandleCompleteAtom handler routing to Todoist/Trello APIs - Add DeleteTask/DeleteCard store methods for cache removal - Add htmx.process() calls after innerHTML updates in app.js - Add comprehensive tests for completion and cache behavior Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'internal/handlers')
-rw-r--r--internal/handlers/handlers.go55
-rw-r--r--internal/handlers/handlers_test.go269
2 files changed, 324 insertions, 0 deletions
diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go
index 9ba6351..b3bc8e4 100644
--- a/internal/handlers/handlers.go
+++ b/internal/handlers/handlers.go
@@ -672,3 +672,58 @@ func (h *Handler) HandleCompleteTask(w http.ResponseWriter, r *http.Request) {
// Return empty response (task will be removed from DOM)
w.WriteHeader(http.StatusOK)
}
+
+// HandleCompleteAtom handles completion of a unified task (Atom)
+func (h *Handler) HandleCompleteAtom(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ if err := r.ParseForm(); err != nil {
+ http.Error(w, "Failed to parse form", http.StatusBadRequest)
+ log.Printf("Error parsing form: %v", err)
+ return
+ }
+
+ id := r.FormValue("id")
+ source := r.FormValue("source")
+
+ if id == "" || source == "" {
+ http.Error(w, "Missing id or source", http.StatusBadRequest)
+ return
+ }
+
+ var err error
+ switch source {
+ case "todoist":
+ err = h.todoistClient.CompleteTask(ctx, id)
+ case "trello":
+ // Archive the card (closed = true)
+ updates := map[string]interface{}{
+ "closed": true,
+ }
+ err = h.trelloClient.UpdateCard(ctx, id, updates)
+ default:
+ http.Error(w, "Unknown source: "+source, http.StatusBadRequest)
+ return
+ }
+
+ if err != nil {
+ http.Error(w, "Failed to complete task", http.StatusInternalServerError)
+ log.Printf("Error completing atom (source=%s, id=%s): %v", source, id, err)
+ return
+ }
+
+ // Remove from local cache
+ switch source {
+ case "todoist":
+ if err := h.store.DeleteTask(id); err != nil {
+ log.Printf("Warning: failed to delete task from cache: %v", err)
+ }
+ case "trello":
+ if err := h.store.DeleteCard(id); err != nil {
+ log.Printf("Warning: failed to delete card from cache: %v", err)
+ }
+ }
+
+ // Return 200 OK with empty body to remove the element from DOM
+ w.WriteHeader(http.StatusOK)
+}
diff --git a/internal/handlers/handlers_test.go b/internal/handlers/handlers_test.go
index ac940bb..3ea2a3e 100644
--- a/internal/handlers/handlers_test.go
+++ b/internal/handlers/handlers_test.go
@@ -395,3 +395,272 @@ func TestHandleGetMeals(t *testing.T) {
t.Errorf("Expected 0 meals when client is nil, got %d", len(meals))
}
}
+
+// mockTodoistClientWithComplete tracks CompleteTask calls
+type mockTodoistClientWithComplete struct {
+ mockTodoistClient
+ completedTaskIDs []string
+ completeErr error
+}
+
+func (m *mockTodoistClientWithComplete) CompleteTask(ctx context.Context, taskID string) error {
+ if m.completeErr != nil {
+ return m.completeErr
+ }
+ m.completedTaskIDs = append(m.completedTaskIDs, taskID)
+ return nil
+}
+
+// mockTrelloClientWithUpdate tracks UpdateCard calls
+type mockTrelloClientWithUpdate struct {
+ mockTrelloClient
+ updatedCards []string
+ updateErr error
+}
+
+func (m *mockTrelloClientWithUpdate) UpdateCard(ctx context.Context, cardID string, updates map[string]interface{}) error {
+ if m.updateErr != nil {
+ return m.updateErr
+ }
+ m.updatedCards = append(m.updatedCards, cardID)
+ return nil
+}
+
+// TestHandleCompleteAtom_Todoist tests completing a Todoist task
+func TestHandleCompleteAtom_Todoist(t *testing.T) {
+ db, cleanup := setupTestDB(t)
+ defer cleanup()
+
+ // Save a task to the cache
+ tasks := []models.Task{
+ {
+ ID: "task123",
+ Content: "Test task",
+ Completed: false,
+ Labels: []string{},
+ CreatedAt: time.Now(),
+ },
+ }
+ if err := db.SaveTasks(tasks); err != nil {
+ t.Fatalf("Failed to save test task: %v", err)
+ }
+
+ // Verify task exists in cache
+ cachedTasks, _ := db.GetTasks()
+ if len(cachedTasks) != 1 {
+ t.Fatalf("Expected 1 task in cache, got %d", len(cachedTasks))
+ }
+
+ // Create handler with mock client
+ mockTodoist := &mockTodoistClientWithComplete{}
+ h := &Handler{
+ store: db,
+ todoistClient: mockTodoist,
+ config: &config.Config{},
+ }
+
+ // Create request
+ req := httptest.NewRequest("POST", "/complete-atom", nil)
+ req.Form = map[string][]string{
+ "id": {"task123"},
+ "source": {"todoist"},
+ }
+ w := httptest.NewRecorder()
+
+ // Execute handler
+ h.HandleCompleteAtom(w, req)
+
+ // Check response
+ if w.Code != http.StatusOK {
+ t.Errorf("Expected status 200, got %d", w.Code)
+ }
+
+ // Verify CompleteTask was called on the API
+ if len(mockTodoist.completedTaskIDs) != 1 || mockTodoist.completedTaskIDs[0] != "task123" {
+ t.Errorf("Expected CompleteTask to be called with 'task123', got %v", mockTodoist.completedTaskIDs)
+ }
+
+ // Verify task was deleted from cache
+ cachedTasks, _ = db.GetTasks()
+ if len(cachedTasks) != 0 {
+ t.Errorf("Expected task to be deleted from cache, but found %d tasks", len(cachedTasks))
+ }
+}
+
+// TestHandleCompleteAtom_Trello tests completing a Trello card
+func TestHandleCompleteAtom_Trello(t *testing.T) {
+ db, cleanup := setupTestDB(t)
+ defer cleanup()
+
+ // Save a board with a card to the cache
+ boards := []models.Board{
+ {
+ ID: "board1",
+ Name: "Test Board",
+ Cards: []models.Card{
+ {
+ ID: "card456",
+ Name: "Test Card",
+ ListID: "list1",
+ ListName: "To Do",
+ },
+ },
+ },
+ }
+ if err := db.SaveBoards(boards); err != nil {
+ t.Fatalf("Failed to save test board: %v", err)
+ }
+
+ // Verify card exists in cache
+ cachedBoards, _ := db.GetBoards()
+ if len(cachedBoards) != 1 || len(cachedBoards[0].Cards) != 1 {
+ t.Fatalf("Expected 1 board with 1 card in cache")
+ }
+
+ // Create handler with mock client
+ mockTrello := &mockTrelloClientWithUpdate{}
+ h := &Handler{
+ store: db,
+ trelloClient: mockTrello,
+ config: &config.Config{},
+ }
+
+ // Create request
+ req := httptest.NewRequest("POST", "/complete-atom", nil)
+ req.Form = map[string][]string{
+ "id": {"card456"},
+ "source": {"trello"},
+ }
+ w := httptest.NewRecorder()
+
+ // Execute handler
+ h.HandleCompleteAtom(w, req)
+
+ // Check response
+ if w.Code != http.StatusOK {
+ t.Errorf("Expected status 200, got %d", w.Code)
+ }
+
+ // Verify UpdateCard was called on the API
+ if len(mockTrello.updatedCards) != 1 || mockTrello.updatedCards[0] != "card456" {
+ t.Errorf("Expected UpdateCard to be called with 'card456', got %v", mockTrello.updatedCards)
+ }
+
+ // Verify card was deleted from cache
+ cachedBoards, _ = db.GetBoards()
+ if len(cachedBoards[0].Cards) != 0 {
+ t.Errorf("Expected card to be deleted from cache, but found %d cards", len(cachedBoards[0].Cards))
+ }
+}
+
+// TestHandleCompleteAtom_MissingParams tests error handling for missing parameters
+func TestHandleCompleteAtom_MissingParams(t *testing.T) {
+ db, cleanup := setupTestDB(t)
+ defer cleanup()
+
+ h := &Handler{
+ store: db,
+ config: &config.Config{},
+ }
+
+ tests := []struct {
+ name string
+ id string
+ source string
+ }{
+ {"missing id", "", "todoist"},
+ {"missing source", "task123", ""},
+ {"both missing", "", ""},
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ req := httptest.NewRequest("POST", "/complete-atom", nil)
+ req.Form = map[string][]string{
+ "id": {tc.id},
+ "source": {tc.source},
+ }
+ w := httptest.NewRecorder()
+
+ h.HandleCompleteAtom(w, req)
+
+ if w.Code != http.StatusBadRequest {
+ t.Errorf("Expected status 400 for %s, got %d", tc.name, w.Code)
+ }
+ })
+ }
+}
+
+// TestHandleCompleteAtom_UnknownSource tests error handling for unknown source
+func TestHandleCompleteAtom_UnknownSource(t *testing.T) {
+ db, cleanup := setupTestDB(t)
+ defer cleanup()
+
+ h := &Handler{
+ store: db,
+ config: &config.Config{},
+ }
+
+ req := httptest.NewRequest("POST", "/complete-atom", nil)
+ req.Form = map[string][]string{
+ "id": {"task123"},
+ "source": {"unknown"},
+ }
+ w := httptest.NewRecorder()
+
+ h.HandleCompleteAtom(w, req)
+
+ if w.Code != http.StatusBadRequest {
+ t.Errorf("Expected status 400 for unknown source, got %d", w.Code)
+ }
+}
+
+// TestHandleCompleteAtom_APIError tests that API errors are handled correctly
+func TestHandleCompleteAtom_APIError(t *testing.T) {
+ db, cleanup := setupTestDB(t)
+ defer cleanup()
+
+ // Save a task to the cache
+ tasks := []models.Task{
+ {
+ ID: "task123",
+ Content: "Test task",
+ Completed: false,
+ Labels: []string{},
+ CreatedAt: time.Now(),
+ },
+ }
+ if err := db.SaveTasks(tasks); err != nil {
+ t.Fatalf("Failed to save test task: %v", err)
+ }
+
+ // Create handler with mock client that returns an error
+ mockTodoist := &mockTodoistClientWithComplete{
+ completeErr: context.DeadlineExceeded,
+ }
+ h := &Handler{
+ store: db,
+ todoistClient: mockTodoist,
+ config: &config.Config{},
+ }
+
+ req := httptest.NewRequest("POST", "/complete-atom", nil)
+ req.Form = map[string][]string{
+ "id": {"task123"},
+ "source": {"todoist"},
+ }
+ w := httptest.NewRecorder()
+
+ h.HandleCompleteAtom(w, req)
+
+ // Should return 500 on API error
+ if w.Code != http.StatusInternalServerError {
+ t.Errorf("Expected status 500 on API error, got %d", w.Code)
+ }
+
+ // Verify task was NOT deleted from cache (rollback behavior)
+ cachedTasks, _ := db.GetTasks()
+ if len(cachedTasks) != 1 {
+ t.Errorf("Task should NOT be deleted from cache on API error, found %d tasks", len(cachedTasks))
+ }
+}