summaryrefslogtreecommitdiff
path: root/internal/api/executions.go
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-03-06 23:55:07 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-03-06 23:55:07 +0000
commitfd42a54d96fcd3342941caaeb61a4b0d5d3f1b4f (patch)
tree0b9ef3b7f0ac3981aa310435d014c9f5e21089d4 /internal/api/executions.go
parent7d4890cde802974b94db24071f63e7733c3670fd (diff)
recover: restore untracked work from recovery branch (no Gemini changes)
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 <noreply@anthropic.com>
Diffstat (limited to 'internal/api/executions.go')
-rw-r--r--internal/api/executions.go100
1 files changed, 100 insertions, 0 deletions
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=<RFC3339>&limit=<int>&task_id=<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=<int>&follow=<bool>
+// 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
+}