Files
Control-Center/go-backend/internal/handler/agent.go
Joshua e8ced74429
All checks were successful
Dev Build / build-test (pull_request) Successful in 2m23s
CUB-123: integrate gateway, wire PostgreSQL repositories, add SSE streaming
- Create repository/ package with pgx-backed CRUD for agents, sessions, tasks, projects
- Define AgentRepo/SessionRepo/TaskRepo/ProjectRepo interfaces
- Update handler to use repository interfaces instead of in-memory stores
- Add SSE broker with GET /api/events endpoint (text/event-stream)
- Add gateway client that polls OpenClaw for agent states
- Add GATEWAY_URL and GATEWAY_POLL_INTERVAL config fields
- Seed 5 demo agents (Otto, Rex, Dex, Hex, Pip) on empty DB
- Update router to wire SSE broker
- All 21 handler tests pass with mock repos
2026-05-08 19:58:06 -04:00

163 lines
5.0 KiB
Go

// Package handler contains HTTP handlers for the Control Center API.
// Each handler is a method on a Handler struct that receives its
// dependencies through dependency injection — now wired to PostgreSQL-backed
// repository implementations instead of in-memory stores.
package handler
import (
"encoding/json"
"log/slog"
"net/http"
"time"
"code.cubecraftcreations.com/CubeCraft-Creations/Control-Center/go-backend/internal/models"
"code.cubecraftcreations.com/CubeCraft-Creations/Control-Center/go-backend/internal/repository"
"github.com/go-chi/chi/v5"
"github.com/go-playground/validator/v10"
)
// Handler groups all route handlers and their dependencies.
type Handler struct {
Agents repository.AgentRepo
Sessions repository.SessionRepo
Tasks repository.TaskRepo
Projects repository.ProjectRepo
validate *validator.Validate
}
// NewHandler returns a fully wired Handler with repository backends.
func NewHandler(
ar repository.AgentRepo,
sr repository.SessionRepo,
tr repository.TaskRepo,
pr repository.ProjectRepo,
) *Handler {
v := validator.New()
v.RegisterValidation("agentStatus", validateAgentStatus)
return &Handler{
Agents: ar,
Sessions: sr,
Tasks: tr,
Projects: pr,
validate: v,
}
}
// ─── Agent Handlers ────────────────────────────────────────────────────────────
// ListAgents handles GET /api/agents.
func (h *Handler) ListAgents(w http.ResponseWriter, r *http.Request) {
statusFilter := models.AgentStatus(r.URL.Query().Get("status"))
allAgents, err := h.Agents.List(r.Context(), statusFilter)
if err != nil {
slog.Error("list agents failed", "error", err)
writeJSON(w, http.StatusInternalServerError, models.ErrorResponse{Error: "failed to list agents"})
return
}
page, pageSize := parsePagination(r)
start, end := paginateSlice(len(allAgents), page, pageSize)
totalCount, _ := h.Agents.Count(r.Context())
writeJSON(w, http.StatusOK, models.PaginatedResponse{
Data: allAgents[start:end],
TotalCount: totalCount,
Page: page,
PageSize: pageSize,
HasMore: end < len(allAgents),
})
}
// GetAgent handles GET /api/agents/{id}.
func (h *Handler) GetAgent(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
agent, err := h.Agents.Get(r.Context(), id)
if err != nil {
writeJSON(w, http.StatusNotFound, models.ErrorResponse{Error: "agent not found"})
return
}
writeJSON(w, http.StatusOK, agent)
}
// CreateAgent handles POST /api/agents.
func (h *Handler) CreateAgent(w http.ResponseWriter, r *http.Request) {
var req models.CreateAgentRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, models.ErrorResponse{Error: "invalid request body"})
return
}
if err := h.validate.Struct(req); err != nil {
writeJSON(w, http.StatusUnprocessableEntity, models.ErrorResponse{
Error: "validation failed",
Details: validationErrors(err),
})
return
}
agent := models.AgentCardData{
ID: req.ID,
DisplayName: req.DisplayName,
Role: req.Role,
Status: req.Status,
CurrentTask: req.CurrentTask,
SessionKey: req.SessionKey,
Channel: req.Channel,
LastActivity: time.Now().UTC().Format(time.RFC3339),
}
if err := h.Agents.Create(r.Context(), agent); err != nil {
writeJSON(w, http.StatusConflict, models.ErrorResponse{Error: "agent with this ID already exists"})
return
}
writeJSON(w, http.StatusCreated, agent)
}
// UpdateAgent handles PUT /api/agents/{id}.
func (h *Handler) UpdateAgent(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
var req models.UpdateAgentRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, models.ErrorResponse{Error: "invalid request body"})
return
}
if err := h.validate.Struct(req); err != nil {
writeJSON(w, http.StatusUnprocessableEntity, models.ErrorResponse{
Error: "validation failed",
Details: validationErrors(err),
})
return
}
agent, err := h.Agents.Update(r.Context(), id, req)
if err != nil {
writeJSON(w, http.StatusNotFound, models.ErrorResponse{Error: "agent not found"})
return
}
writeJSON(w, http.StatusOK, agent)
}
// DeleteAgent handles DELETE /api/agents/{id}.
func (h *Handler) DeleteAgent(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
if err := h.Agents.Delete(r.Context(), id); err != nil {
writeJSON(w, http.StatusNotFound, models.ErrorResponse{Error: "agent not found"})
return
}
w.WriteHeader(http.StatusNoContent)
}
// AgentHistory handles GET /api/agents/{id}/history.
func (h *Handler) AgentHistory(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
if _, err := h.Agents.Get(r.Context(), id); err != nil {
writeJSON(w, http.StatusNotFound, models.ErrorResponse{Error: "agent not found"})
return
}
// History is not currently persisted in PostgreSQL — return stub.
writeJSON(w, http.StatusOK, []models.AgentStatusHistoryEntry{})
}