package api import ( "encoding/json" "net/http" "github.com/google/uuid" "github.com/thepeterstone/claudomator/internal/storage" "github.com/thepeterstone/claudomator/internal/story" ) func (s *Server) handleListStories(w http.ResponseWriter, r *http.Request) { filter := storage.StoryFilter{ Status: r.URL.Query().Get("status"), EpicID: r.URL.Query().Get("epic_id"), } stories, err := s.store.ListStories(filter) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } if stories == nil { stories = []*story.Story{} } writeJSON(w, http.StatusOK, stories) } func (s *Server) handleCreateStory(w http.ResponseWriter, r *http.Request) { var st story.Story if err := json.NewDecoder(r.Body).Decode(&st); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON: " + err.Error()}) return } if st.Name == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"}) return } if st.ID == "" { st.ID = uuid.New().String() } if err := s.store.CreateStory(&st); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusCreated, st) } func (s *Server) handleGetStory(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") st, err := s.store.GetStory(id) if err != nil { writeJSON(w, http.StatusNotFound, map[string]string{"error": "story not found"}) return } writeJSON(w, http.StatusOK, st) } // handleUpdateStory replaces all editable fields on a story (everything // except id/created_at). No status-machine validation is performed in this // phase — the status string is written as given, same trust level as // handleUpdateProject. func (s *Server) handleUpdateStory(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") existing, err := s.store.GetStory(id) if err != nil { writeJSON(w, http.StatusNotFound, map[string]string{"error": "story not found"}) return } createdAt := existing.CreatedAt if err := json.NewDecoder(r.Body).Decode(existing); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON: " + err.Error()}) return } existing.ID = id // ensure ID cannot be changed via body existing.CreatedAt = createdAt if err := s.store.UpdateStory(existing); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusOK, existing) } // taskTreeNode is one task in a story's task-tree/graph response. type taskTreeNode struct { ID string `json:"id"` Name string `json:"name"` State string `json:"state"` AgentType string `json:"agent_type"` Role string `json:"role,omitempty"` ParentTaskID string `json:"parent_task_id,omitempty"` DependsOn []string `json:"depends_on"` } // taskTreeResponse is the payload for GET /api/stories/{id}/task-tree. type taskTreeResponse struct { StoryID string `json:"story_id"` RootTaskID string `json:"root_task_id"` Nodes []taskTreeNode `json:"nodes"` } // handleStoryTaskTree walks the task graph realizing a story, starting at // its root_task_id and following both parent_task_id children (via // ListSubtasks) and depends_on edges in either direction (via // ListDependents, so a sibling task that merely depends on something already // in the tree is still discovered even if it isn't a literal // parent_task_id child). Returns one flat node list; clients reconstruct // the graph from each node's parent_task_id/depends_on. If the story has no // root_task_id set, returns an empty node list, not an error. func (s *Server) handleStoryTaskTree(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") st, err := s.store.GetStory(id) if err != nil { writeJSON(w, http.StatusNotFound, map[string]string{"error": "story not found"}) return } resp := taskTreeResponse{StoryID: id, RootTaskID: st.RootTaskID, Nodes: []taskTreeNode{}} if st.RootTaskID == "" { writeJSON(w, http.StatusOK, resp) return } visited := map[string]bool{} queue := []string{st.RootTaskID} for len(queue) > 0 { tid := queue[0] queue = queue[1:] if visited[tid] { continue } visited[tid] = true t, err := s.store.GetTask(tid) if err != nil { // A referenced task ID no longer resolves (e.g. deleted); skip it // rather than failing the whole tree. continue } dependsOn := t.DependsOn if dependsOn == nil { dependsOn = []string{} } resp.Nodes = append(resp.Nodes, taskTreeNode{ ID: t.ID, Name: t.Name, State: string(t.State), AgentType: t.Agent.Type, Role: t.Agent.Role, ParentTaskID: t.ParentTaskID, DependsOn: dependsOn, }) if children, err := s.store.ListSubtasks(tid); err == nil { for _, c := range children { if !visited[c.ID] { queue = append(queue, c.ID) } } } if dependents, err := s.store.ListDependents(tid); err == nil { for _, d := range dependents { if !visited[d.ID] { queue = append(queue, d.ID) } } } } writeJSON(w, http.StatusOK, resp) }