package api import ( "fmt" "net/http" "os" "strconv" "strings" "time" "github.com/thepeterstone/claudomator/internal/storage" ) // handleListRecentExecutions returns executions across all tasks. // GET /api/executions?since=&limit=&task_id= func (s *Server) handleListRecentExecutions(w http.ResponseWriter, r *http.Request) { since := time.Now().Add(-24 * time.Hour) if v := r.URL.Query().Get("since"); v != "" { if t, err := time.Parse(time.RFC3339, v); err == nil { since = t } } const maxLimit = 1000 limit := 50 if v := r.URL.Query().Get("limit"); v != "" { if n, err := strconv.Atoi(v); err == nil && n > 0 { limit = n } } if limit > maxLimit { limit = maxLimit } taskID := r.URL.Query().Get("task_id") execs, err := s.store.ListRecentExecutions(since, limit, taskID) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } if execs == nil { execs = []*storage.RecentExecution{} } writeJSON(w, http.StatusOK, execs) } // handleGetExecutionLog returns the tail of an execution log. // GET /api/executions/{id}/log?tail=&follow= // If follow=true, streams as SSE (delegates to handleStreamLogs). // If follow=false (default), returns last N raw lines as plain text. func (s *Server) handleGetExecutionLog(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") exec, err := s.store.GetExecution(id) if err != nil { http.Error(w, "execution not found", http.StatusNotFound) return } if r.URL.Query().Get("follow") == "true" { s.handleStreamLogs(w, r) return } tailN := 500 if v := r.URL.Query().Get("tail"); v != "" { if n, err := strconv.Atoi(v); err == nil && n > 0 { tailN = n } } if exec.StdoutPath == "" { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusOK) return } content, err := tailLogFile(exec.StdoutPath, tailN) if err != nil { http.Error(w, "could not read log", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusOK) fmt.Fprint(w, content) } // handleGetAgentStatus returns the current status of all agents and recent rate-limit events. // GET /api/agents/status?since= func (s *Server) handleGetAgentStatus(w http.ResponseWriter, r *http.Request) { since := time.Now().Add(-24 * time.Hour) if v := r.URL.Query().Get("since"); v != "" { if t, err := time.Parse(time.RFC3339, v); err == nil { since = t } } events, err := s.store.ListAgentEvents(since) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } if events == nil { events = []storage.AgentEvent{} } writeJSON(w, http.StatusOK, map[string]interface{}{ "agents": s.pool.AgentStatuses(), "events": events, }) } // tailLogFile reads the last n lines from the file at path. func tailLogFile(path string, n int) (string, error) { data, err := os.ReadFile(path) if err != nil { return "", err } content := strings.TrimRight(string(data), "\n") if content == "" { return "", nil } lines := strings.Split(content, "\n") if len(lines) > n { lines = lines[len(lines)-n:] } return strings.Join(lines, "\n"), nil }