diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-03-23 06:50:43 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-03-23 06:50:43 +0000 |
| commit | b0688c8819da1b7fcb4a97b6ec1fa58050e4841e (patch) | |
| tree | c62b4ea3e5cdb56225c39ad930dd3e5584053827 /internal/handlers/agent.go | |
| parent | ef7a45361996b7a49226a0b088e2599f2801d017 (diff) | |
feat: complete Agent Context API Phase 2 & 3 (Write/Create/Management)
- Implement write operations (complete, uncomplete, update due date, update task)
- Implement create operations (create task, add shopping item)
- Add Trusted Agents management UI in Settings with revocation support
- Fix SQLite timestamp scanning bug for completed tasks
- Add comprehensive unit tests for all new agent endpoints
- Update worklog and feature documentation
Diffstat (limited to 'internal/handlers/agent.go')
| -rw-r--r-- | internal/handlers/agent.go | 271 |
1 files changed, 271 insertions, 0 deletions
diff --git a/internal/handlers/agent.go b/internal/handlers/agent.go index aa3f000..6d6079f 100644 --- a/internal/handlers/agent.go +++ b/internal/handlers/agent.go @@ -9,8 +9,11 @@ import ( "net/http" "time" + "github.com/go-chi/chi/v5" + "task-dashboard/internal/config" "task-dashboard/internal/models" + "task-dashboard/internal/store" ) // ----------------------------------------------------------------------------- @@ -385,6 +388,274 @@ func (h *Handler) buildCompletedLog(limit int) []agentCompletedItem { } // ----------------------------------------------------------------------------- +// Write Handlers +// ----------------------------------------------------------------------------- + +// HandleAgentTaskComplete handles POST /agent/tasks/{id}/complete +func (h *Handler) HandleAgentTaskComplete(w http.ResponseWriter, r *http.Request) { + h.handleAgentTaskToggle(w, r, true) +} + +// HandleAgentTaskUncomplete handles POST /agent/tasks/{id}/uncomplete +func (h *Handler) HandleAgentTaskUncomplete(w http.ResponseWriter, r *http.Request) { + h.handleAgentTaskToggle(w, r, false) +} + +// handleAgentTaskToggle handles both complete and uncomplete operations for agents +func (h *Handler) handleAgentTaskToggle(w http.ResponseWriter, r *http.Request, complete bool) { + id := chi.URLParam(r, "id") + source := r.URL.Query().Get("source") + + if id == "" || source == "" { + http.Error(w, "id and source are required", http.StatusBadRequest) + return + } + + var err error + ctx := r.Context() + switch source { + case "todoist": + if complete { + err = h.todoistClient.CompleteTask(ctx, id) + } else { + err = h.todoistClient.ReopenTask(ctx, id) + } + case "trello": + err = h.trelloClient.UpdateCard(ctx, id, map[string]interface{}{"closed": complete}) + case "gtasks": + listID := r.URL.Query().Get("listId") + if listID == "" { + listID = "@default" + } + if h.googleTasksClient != nil { + if complete { + err = h.googleTasksClient.CompleteTask(ctx, listID, id) + } else { + err = h.googleTasksClient.UncompleteTask(ctx, listID, id) + } + } else { + http.Error(w, "Google Tasks not configured", http.StatusServiceUnavailable) + return + } + default: + http.Error(w, "Unknown source: "+source, http.StatusBadRequest) + return + } + + if err != nil { + http.Error(w, "Failed to toggle task: "+err.Error(), http.StatusInternalServerError) + return + } + + if complete { + title, dueDate := h.getAtomDetails(id, source) + _ = h.store.SaveCompletedTask(source, id, title, dueDate) + switch source { + case "todoist": + _ = h.store.DeleteTask(id) + case "trello": + _ = h.store.DeleteCard(id) + } + } else { + switch source { + case "todoist": + _ = h.store.InvalidateCache(store.CacheKeyTodoistTasks) + case "trello": + _ = h.store.InvalidateCache(store.CacheKeyTrelloBoards) + } + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) +} + +// HandleAgentTaskUpdateDue handles PATCH /agent/tasks/{id}/due +func (h *Handler) HandleAgentTaskUpdateDue(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "id") + source := r.URL.Query().Get("source") + + var req struct { + Due *time.Time `json:"due"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + var err error + ctx := r.Context() + switch source { + case "todoist": + err = h.todoistClient.UpdateTask(ctx, id, map[string]interface{}{"due_datetime": req.Due}) + case "trello": + err = h.trelloClient.UpdateCard(ctx, id, map[string]interface{}{"due": req.Due}) + default: + http.Error(w, "Source does not support due date updates via this endpoint", http.StatusBadRequest) + return + } + + if err != nil { + http.Error(w, "Failed to update due date: "+err.Error(), http.StatusInternalServerError) + return + } + + // Invalidate cache + switch source { + case "todoist": + _ = h.store.InvalidateCache(store.CacheKeyTodoistTasks) + case "trello": + _ = h.store.InvalidateCache(store.CacheKeyTrelloBoards) + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) +} + +// HandleAgentTaskUpdate handles PATCH /agent/tasks/{id} +func (h *Handler) HandleAgentTaskUpdate(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "id") + source := r.URL.Query().Get("source") + + var updates map[string]interface{} + if err := json.NewDecoder(r.Body).Decode(&updates); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + var err error + ctx := r.Context() + switch source { + case "todoist": + // Map generic updates to Todoist specific ones if needed + if title, ok := updates["title"].(string); ok { + updates["content"] = title + delete(updates, "title") + } + err = h.todoistClient.UpdateTask(ctx, id, updates) + case "trello": + if title, ok := updates["title"].(string); ok { + updates["name"] = title + delete(updates, "title") + } + if desc, ok := updates["description"].(string); ok { + updates["desc"] = desc + delete(updates, "description") + } + err = h.trelloClient.UpdateCard(ctx, id, updates) + default: + http.Error(w, "Source does not support updates via this endpoint", http.StatusBadRequest) + return + } + + if err != nil { + http.Error(w, "Failed to update task: "+err.Error(), http.StatusInternalServerError) + return + } + + // Invalidate cache + switch source { + case "todoist": + _ = h.store.InvalidateCache(store.CacheKeyTodoistTasks) + case "trello": + _ = h.store.InvalidateCache(store.CacheKeyTrelloBoards) + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) +} + +// HandleAgentTaskCreate handles POST /agent/tasks +func (h *Handler) HandleAgentTaskCreate(w http.ResponseWriter, r *http.Request) { + var req struct { + Title string `json:"title"` + Source string `json:"source"` + DueDate *time.Time `json:"due_date"` + ProjectID string `json:"project_id"` + ListID string `json:"list_id"` + Priority int `json:"priority"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + if req.Title == "" || req.Source == "" { + http.Error(w, "title and source are required", http.StatusBadRequest) + return + } + + var err error + ctx := r.Context() + switch req.Source { + case "todoist": + _, err = h.todoistClient.CreateTask(ctx, req.Title, req.ProjectID, req.DueDate, req.Priority) + _ = h.store.InvalidateCache(store.CacheKeyTodoistTasks) + case "trello": + if req.ListID == "" { + http.Error(w, "list_id is required for Trello", http.StatusBadRequest) + return + } + _, err = h.trelloClient.CreateCard(ctx, req.ListID, req.Title, "", req.DueDate) + _ = h.store.InvalidateCache(store.CacheKeyTrelloBoards) + default: + http.Error(w, "Unsupported source for task creation", http.StatusBadRequest) + return + } + + if err != nil { + http.Error(w, "Failed to create task: "+err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) +} + +// HandleAgentShoppingAdd handles POST /agent/shopping +func (h *Handler) HandleAgentShoppingAdd(w http.ResponseWriter, r *http.Request) { + var req struct { + Name string `json:"name"` + Store string `json:"store"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + if req.Name == "" || req.Store == "" { + http.Error(w, "name and store are required", http.StatusBadRequest) + return + } + + if err := h.store.SaveUserShoppingItem(req.Name, req.Store); err != nil { + http.Error(w, "Failed to save shopping item: "+err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) +} + +// HandleDeleteAgent handles DELETE /settings/agents/{id} +func (h *Handler) HandleDeleteAgent(w http.ResponseWriter, r *http.Request) { + agentID := chi.URLParam(r, "id") + if agentID == "" { + http.Error(w, "agent ID is required", http.StatusBadRequest) + return + } + + if err := h.store.RevokeAgent(agentID); err != nil { + http.Error(w, "Failed to revoke agent: "+err.Error(), http.StatusInternalServerError) + return + } + + // Also invalidate their sessions + _ = h.store.InvalidatePreviousAgentSessions(agentID) + + w.WriteHeader(http.StatusOK) +} + +// ----------------------------------------------------------------------------- // Middleware // ----------------------------------------------------------------------------- |
