summaryrefslogtreecommitdiff
path: root/internal/api/executions.go
blob: 114425e8a3957d1e9ba094dbf1959695613b462a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
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
		}
	}

	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=<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
}