summaryrefslogtreecommitdiff
path: root/internal/api/logs.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/logs.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/logs.go')
-rw-r--r--internal/api/logs.go52
1 files changed, 52 insertions, 0 deletions
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) {