summaryrefslogtreecommitdiff
path: root/internal/api/executions.go
blob: 4d8ba9c9b950feb2acfc8df22bc5fd965d7c62ed (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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
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)
}

// handleGetDashboardStats returns pre-aggregated error, throughput, and billing stats.
// GET /api/stats?window=7d|24h
func (s *Server) handleGetDashboardStats(w http.ResponseWriter, r *http.Request) {
	window := 7 * 24 * time.Hour
	if r.URL.Query().Get("window") == "24h" {
		window = 24 * time.Hour
	}
	since := time.Now().Add(-window)

	stats, err := s.store.QueryDashboardStats(since)
	if err != nil {
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
		return
	}
	writeJSON(w, http.StatusOK, stats)
}

// handleGetAgentStatus returns the current status of all agents and recent rate-limit events.
// GET /api/agents/status?since=<RFC3339>
func (s *Server) handleGetAgentStatus(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
		}
	}

	events, err := s.store.ListAgentEvents(since)
	if err != nil {
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
		return
	}
	if events == nil {
		events = []storage.AgentEvent{}
	}

	writeJSON(w, http.StatusOK, map[string]interface{}{
		"agents": s.pool.AgentStatuses(),
		"events": events,
	})
}

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