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/logs.go | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) (limited to 'internal/api/logs.go') diff --git a/internal/api/logs.go b/internal/api/logs.go index 0354943..1ba4b00 100644 --- a/internal/api/logs.go +++ b/internal/api/logs.go @@ -19,6 +19,12 @@ type logStore interface { GetExecution(id string) (*storage.Execution, error) } +// taskLogStore is the minimal storage interface needed by handleStreamTaskLogs. +type taskLogStore interface { + GetExecution(id string) (*storage.Execution, error) + GetLatestExecution(taskID string) (*storage.Execution, error) +} + const maxTailDuration = 30 * time.Minute var terminalStates = map[string]bool{ @@ -46,6 +52,52 @@ type logContentBlock struct { Input json.RawMessage `json:"input,omitempty"` } +// handleStreamTaskLogs streams the latest execution log for a task via SSE. +// GET /api/tasks/{id}/logs/stream +func (s *Server) handleStreamTaskLogs(w http.ResponseWriter, r *http.Request) { + taskID := r.PathValue("id") + exec, err := s.taskLogStore.GetLatestExecution(taskID) + if err != nil { + http.Error(w, "task not found", http.StatusNotFound) + return + } + + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("X-Accel-Buffering", "no") + + flusher, ok := w.(http.Flusher) + if !ok { + http.Error(w, "streaming not supported", http.StatusInternalServerError) + return + } + + ctx := r.Context() + + if terminalStates[exec.Status] { + if exec.StdoutPath != "" { + if f, err := os.Open(exec.StdoutPath); err == nil { + defer f.Close() + var offset int64 + for _, line := range readNewLines(f, &offset) { + select { + case <-ctx.Done(): + return + default: + } + emitLogLine(w, flusher, line) + } + } + } + } else if exec.Status == string(task.StateRunning) { + tailRunningExecution(ctx, w, flusher, s.taskLogStore, exec) + return + } + + fmt.Fprintf(w, "event: done\ndata: {}\n\n") + flusher.Flush() +} + // handleStreamLogs streams parsed execution log content via SSE. // GET /api/executions/{id}/logs/stream func (s *Server) handleStreamLogs(w http.ResponseWriter, r *http.Request) { -- cgit v1.2.3