From fd42a54d96fcd3342941caaeb61a4b0d5d3f1b4f Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Fri, 6 Mar 2026 23:55:07 +0000 Subject: recover: restore untracked work from recovery branch (no Gemini changes) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recovered files with no Claude→Agent contamination: - docs/adr/002-task-state-machine.md - internal/api/logs.go/logs_test.go: task-level log streaming endpoint - internal/api/validate.go/validate_test.go: POST /api/tasks/validate - internal/api/server_test.go, storage/db_test.go: expanded test coverage - scripts/reset-failed-tasks, reset-running-tasks - web/app.js, index.html, style.css: frontend improvements - web/test/: active-tasks-tab, delete-button, filter-tabs, sort-tasks tests Manually applied from server.go diff (skipping Claude→Agent rename): - taskLogStore field + validateCmdPath field - DELETE /api/tasks/{id} route + handleDeleteTask - GET /api/tasks/{id}/logs/stream route - POST /api/tasks/{id}/resume route + handleResumeTimedOutTask - handleCancelTask: allow cancelling PENDING/QUEUED tasks directly Co-Authored-By: Claude Sonnet 4.6 --- internal/api/executions.go | 100 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 internal/api/executions.go (limited to 'internal/api/executions.go') diff --git a/internal/api/executions.go b/internal/api/executions.go new file mode 100644 index 0000000..d9214c0 --- /dev/null +++ b/internal/api/executions.go @@ -0,0 +1,100 @@ +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 + } + } + + limit := 50 + if v := r.URL.Query().Get("limit"); v != "" { + if n, err := strconv.Atoi(v); err == nil && n > 0 { + limit = n + } + } + + 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) +} + +// 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 +} -- cgit v1.2.3