package api import ( "encoding/json" "net/http" "strings" "github.com/go-chi/chi/v5" "github.com/sandrodach/mindsift/internal/auth" "github.com/sandrodach/mindsift/internal/domain" "github.com/sandrodach/mindsift/internal/storage" ) func (s *Server) handleCaptureInbox(w http.ResponseWriter, r *http.Request) { userID := auth.GetUserID(r.Context()) var req struct { Content string `json:"content"` Priority string `json:"priority"` Source string `json:"source"` DueDate string `json:"dueDate"` DueDate2 string `json:"due_date"` Target string `json:"target"` // inbox | task | list_item | document TargetListID string `json:"targetListId"` TargetList2 string `json:"target_list_id"` TaskListID string `json:"taskListId"` TaskListID2 string `json:"task_list_id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "Ungültiges JSON") return } dueDate := req.DueDate if dueDate == "" { dueDate = req.DueDate2 } targetListID := req.TargetListID if targetListID == "" { targetListID = req.TargetList2 } taskListID := req.TaskListID if taskListID == "" { taskListID = req.TaskListID2 } source := strings.ToLower(strings.TrimSpace(req.Source)) if source == "" { source = strings.ToLower(strings.TrimSpace(r.URL.Query().Get("source"))) } if source == "" { source = strings.ToLower(strings.TrimSpace(r.Header.Get("X-Source"))) } if source == "" { // When called via API key or webhook token, automatically default source to "webhook" if r.Header.Get("X-API-Key") != "" || strings.HasPrefix(r.Header.Get("Authorization"), "Bearer mindsift_sec_") || strings.HasPrefix(r.URL.Query().Get("token"), "mindsift_sec_") { source = "webhook" } else { source = "manual" } } thought, err := s.store.CreateThoughtWithDetails(userID, req.Content, req.Priority, source, dueDate, taskListID, "todo", "") if err != nil { writeStoreError(w, http.StatusBadRequest, err) return } target := strings.TrimSpace(req.Target) if target == "task" || target == "document" || (target == "list_item" && targetListID != "") { if converted, convErr := s.store.UpdateThoughtTriage(userID, thought.ID, "keep", target, targetListID, dueDate, taskListID); convErr == nil && converted != nil { thought = converted if target == "list_item" && targetListID != "" { if lst, lErr := s.store.GetListByID(userID, targetListID); lErr == nil && lst != nil { s.wsHub.BroadcastListEvent(lst.ID, "LIST_UPDATED", lst) } } } } s.wsHub.BroadcastToUsers(s.taskAudience(thought), "TASK_CREATED", thought) writeJSON(w, http.StatusCreated, thought) } func (s *Server) handleGetThoughts(w http.ResponseWriter, r *http.Request) { userID := auth.GetUserID(r.Context()) status := r.URL.Query().Get("status") thoughts, err := s.store.GetThoughts(userID, status, 250) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, thoughts) } func (s *Server) handleGetThought(w http.ResponseWriter, r *http.Request) { userID := auth.GetUserID(r.Context()) thoughtID := chi.URLParam(r, "id") thought, err := s.store.GetThoughtByID(userID, thoughtID) if err != nil || thought == nil { writeError(w, http.StatusNotFound, "Aufgabe nicht gefunden") return } writeJSON(w, http.StatusOK, thought) } func (s *Server) handleGetTriageDeck(w http.ResponseWriter, r *http.Request) { userID := auth.GetUserID(r.Context()) thoughts, err := s.store.GetThoughts(userID, "inbox", 100) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, thoughts) } func (s *Server) handleTriageAction(w http.ResponseWriter, r *http.Request) { userID := auth.GetUserID(r.Context()) thoughtID := chi.URLParam(r, "id") var req struct { Action string `json:"action"` // keep | discard | restore ConvertTo string `json:"convertTo"` ConvertTo2 string `json:"convert_to"` TargetListID string `json:"targetListId"` TargetList2 string `json:"target_list_id"` TaskListID string `json:"taskListId"` TaskListID2 string `json:"task_list_id"` DueDate string `json:"dueDate"` DueDate2 string `json:"due_date"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "Ungültiges JSON") return } convertTo := req.ConvertTo if convertTo == "" { convertTo = req.ConvertTo2 } targetListID := req.TargetListID if targetListID == "" { targetListID = req.TargetList2 } taskListID := req.TaskListID if taskListID == "" { taskListID = req.TaskListID2 } dueDate := req.DueDate if dueDate == "" { dueDate = req.DueDate2 } updated, err := s.store.UpdateThoughtTriage(userID, thoughtID, req.Action, convertTo, targetListID, dueDate, taskListID) if err != nil { writeStoreError(w, http.StatusBadRequest, err) return } // If converted to a list item, broadcast via WebSocket to the space if convertTo == "list_item" && targetListID != "" { if lst, err := s.store.GetListByID(userID, targetListID); err == nil && lst != nil { s.wsHub.BroadcastListEvent(lst.ID, "LIST_UPDATED", lst) } } s.wsHub.BroadcastToUsers(s.taskAudience(updated), "TASK_UPDATED", updated) writeJSON(w, http.StatusOK, updated) } func (s *Server) handleGetTasks(w http.ResponseWriter, r *http.Request) { userID := auth.GetUserID(r.Context()) taskListID := r.URL.Query().Get("taskListId") tasks, err := s.store.GetTasks(userID, taskListID) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, tasks) } func (s *Server) handleReorderTasks(w http.ResponseWriter, r *http.Request) { userID := auth.GetUserID(r.Context()) taskListID := chi.URLParam(r, "id") if taskListID == "" { taskListID = r.URL.Query().Get("taskListId") } var req struct { Items []domain.ThoughtReorderItem `json:"items"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "Ungültiges JSON") return } if taskListID != "" && !s.store.CanEditTaskList(userID, taskListID) { writeError(w, http.StatusForbidden, storage.ErrTaskListReadOnly.Error()) return } // ReorderTasks updates by ID only, so every item must be a task the caller may edit audience := []string{userID} for _, it := range req.Items { th, err := s.store.GetThoughtByID(userID, it.ID) if err != nil { writeError(w, http.StatusNotFound, "Aufgabe nicht gefunden") return } if th.TaskListID != "" && !s.store.CanEditTaskList(userID, th.TaskListID) { writeError(w, http.StatusForbidden, storage.ErrTaskListReadOnly.Error()) return } audience = append(audience, s.taskAudience(th)...) } if err := s.store.ReorderTasks(taskListID, req.Items); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } s.wsHub.BroadcastToUsers(audience, "TASK_REORDERED", map[string]interface{}{ "taskListId": taskListID, "items": req.Items, "userId": userID, }) writeJSON(w, http.StatusOK, map[string]bool{"success": true}) } func (s *Server) handleUpdateThought(w http.ResponseWriter, r *http.Request) { userID := auth.GetUserID(r.Context()) thoughtID := chi.URLParam(r, "id") var req struct { Content *string `json:"content"` IsCompleted *bool `json:"isCompleted"` DueDate *string `json:"dueDate"` Priority *string `json:"priority"` TaskListID *string `json:"taskListId"` KanbanStatus *string `json:"kanbanStatus"` Notes *string `json:"notes"` Subtasks *[]domain.SubTask `json:"subtasks"` Tags *[]string `json:"tags"` Assignees *[]string `json:"assignees"` Recurrence *string `json:"recurrence"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "Ungültiges JSON") return } before, _ := s.store.GetThoughtByID(userID, thoughtID) // The rule is set first, so completing a task in the same request already repeats it if req.Recurrence != nil { if err := s.store.SetTaskRecurrence(userID, thoughtID, strings.TrimSpace(*req.Recurrence)); err != nil { writeStoreError(w, http.StatusBadRequest, err) return } } updated, err := s.store.UpdateThoughtDetails(userID, thoughtID, req.Content, req.IsCompleted, req.DueDate, req.Priority, req.TaskListID, req.KanbanStatus, req.Notes, req.Subtasks, req.Tags) if err != nil { writeStoreError(w, http.StatusBadRequest, err) return } if req.Assignees != nil { assignees, err := s.store.SetTaskAssignees(userID, thoughtID, *req.Assignees) if err != nil { writeStoreError(w, http.StatusBadRequest, err) return } updated.Assignees = assignees } if docID := s.store.SyncDocBlockAndTask(userID, thoughtID, updated.IsCompleted); docID != "" { _, head, _ := s.store.GetDocOpsSince(docID, 0, 0) s.notifyDocChanged(docID, head) } s.wsHub.BroadcastToUsers(append(s.taskAudience(before), s.taskAudience(updated)...), "TASK_UPDATED", updated) writeJSON(w, http.StatusOK, updated) } func (s *Server) handleUnlinkEnhancedTask(w http.ResponseWriter, r *http.Request) { userID := auth.GetUserID(r.Context()) thoughtID := chi.URLParam(r, "id") s.store.UnlinkTaskFromDocBlock(userID, thoughtID) writeJSON(w, http.StatusOK, map[string]bool{"unlinked": true}) } func (s *Server) handleDeleteThought(w http.ResponseWriter, r *http.Request) { userID := auth.GetUserID(r.Context()) thoughtID := chi.URLParam(r, "id") th, err := s.store.GetThoughtByID(userID, thoughtID) if err == nil && th.TaskListID != "" && !s.store.CanEditTaskList(userID, th.TaskListID) { writeError(w, http.StatusForbidden, storage.ErrTaskListReadOnly.Error()) return } audience := s.taskAudience(th) s.store.UnlinkTaskFromDocBlock(userID, thoughtID) if err := s.store.DeleteThought(userID, thoughtID); err != nil { writeStoreError(w, http.StatusInternalServerError, err) return } s.wsHub.BroadcastToUsers(audience, "TASK_DELETED", map[string]string{"id": thoughtID}) writeJSON(w, http.StatusOK, map[string]bool{"deleted": true}) } func (s *Server) handlePurgeDiscarded(w http.ResponseWriter, r *http.Request) { userID := auth.GetUserID(r.Context()) count, err := s.store.PurgeDiscardedThoughts(userID) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]int64{"purged": count}) } // --- SEARCH & EXPORT --- func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) { userID := auth.GetUserID(r.Context()) q := r.URL.Query().Get("q") res, err := s.store.SearchGlobal(userID, q) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, res) }