All checks were successful
Dev Build / build-test (pull_request) Successful in 2m23s
- 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
199 lines
5.5 KiB
Go
199 lines
5.5 KiB
Go
// Package gateway provides an OpenClaw gateway integration client that
|
|
// polls agent states, persists them via the repository layer, and broadcasts
|
|
// changes through the SSE broker for real-time frontend updates.
|
|
package gateway
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"time"
|
|
|
|
"code.cubecraftcreations.com/CubeCraft-Creations/Control-Center/go-backend/internal/handler"
|
|
"code.cubecraftcreations.com/CubeCraft-Creations/Control-Center/go-backend/internal/models"
|
|
"code.cubecraftcreations.com/CubeCraft-Creations/Control-Center/go-backend/internal/repository"
|
|
)
|
|
|
|
// Client polls the OpenClaw gateway for agent status and keeps the database
|
|
// and SSE broker in sync.
|
|
type Client struct {
|
|
url string
|
|
pollInterval time.Duration
|
|
httpClient *http.Client
|
|
agents repository.AgentRepo
|
|
broker *handler.Broker
|
|
}
|
|
|
|
// Config holds gateway client configuration, typically loaded from environment.
|
|
type Config struct {
|
|
URL string
|
|
PollInterval time.Duration
|
|
}
|
|
|
|
// DefaultConfig returns sensible defaults for local development.
|
|
func DefaultConfig() Config {
|
|
return Config{
|
|
URL: "http://localhost:18789/api/agents",
|
|
PollInterval: 5 * time.Second,
|
|
}
|
|
}
|
|
|
|
// NewClient returns a gateway client wired to the given repository and broker.
|
|
func NewClient(cfg Config, agents repository.AgentRepo, broker *handler.Broker) *Client {
|
|
return &Client{
|
|
url: cfg.URL,
|
|
pollInterval: cfg.PollInterval,
|
|
httpClient: &http.Client{Timeout: 10 * time.Second},
|
|
agents: agents,
|
|
broker: broker,
|
|
}
|
|
}
|
|
|
|
// Start begins the polling loop. It runs until ctx is cancelled.
|
|
func (c *Client) Start(ctx context.Context) {
|
|
slog.Info("gateway client starting",
|
|
"url", c.url,
|
|
"pollInterval", c.pollInterval.String())
|
|
|
|
ticker := time.NewTicker(c.pollInterval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
slog.Info("gateway client stopped")
|
|
return
|
|
case <-ticker.C:
|
|
c.poll(ctx)
|
|
}
|
|
}
|
|
}
|
|
|
|
// poll fetches agent states from the gateway and syncs to the database.
|
|
func (c *Client) poll(ctx context.Context) {
|
|
resp, err := c.httpClient.Get(c.url)
|
|
if err != nil {
|
|
slog.Warn("gateway poll failed", "error", err)
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
slog.Warn("gateway returned non-200", "status", resp.StatusCode)
|
|
return
|
|
}
|
|
|
|
var agents []models.AgentCardData
|
|
if err := json.NewDecoder(resp.Body).Decode(&agents); err != nil {
|
|
slog.Warn("gateway response parse failed", "error", err)
|
|
return
|
|
}
|
|
|
|
for _, ga := range agents {
|
|
// Check if agent already exists; if so, update; otherwise create.
|
|
existing, err := c.agents.Get(ctx, ga.ID)
|
|
if err != nil {
|
|
// Not found — create it
|
|
if err := c.agents.Create(ctx, ga); err != nil {
|
|
slog.Warn("gateway agent create failed", "id", ga.ID, "error", err)
|
|
continue
|
|
}
|
|
slog.Info("gateway agent created", "id", ga.ID, "status", ga.Status)
|
|
c.broker.Broadcast("agent.status", ga)
|
|
continue
|
|
}
|
|
|
|
// If status changed, update and broadcast
|
|
if existing.Status != ga.Status {
|
|
updated, err := c.agents.Update(ctx, ga.ID, models.UpdateAgentRequest{
|
|
Status: &ga.Status,
|
|
})
|
|
if err != nil {
|
|
slog.Warn("gateway agent update failed", "id", ga.ID, "error", err)
|
|
continue
|
|
}
|
|
c.broker.Broadcast("agent.status", updated)
|
|
slog.Debug("agent status changed",
|
|
"id", ga.ID,
|
|
"from", existing.Status,
|
|
"to", ga.Status)
|
|
}
|
|
}
|
|
}
|
|
|
|
// SeedDemoAgents inserts the five known demo agents if the agents table is
|
|
// empty. Call this once on application startup after migrations have run.
|
|
func SeedDemoAgents(ctx context.Context, agents repository.AgentRepo) error {
|
|
count, err := agents.Count(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("count agents for seeding: %w", err)
|
|
}
|
|
if count > 0 {
|
|
return nil // already seeded
|
|
}
|
|
|
|
slog.Info("seeding demo agents")
|
|
demoAgents := []models.AgentCardData{
|
|
{
|
|
ID: "otto",
|
|
DisplayName: "Otto",
|
|
Role: "Orchestrator",
|
|
Status: models.AgentStatusActive,
|
|
CurrentTask: strPtr("Orchestrating tasks"),
|
|
SessionKey: "otto-session",
|
|
Channel: "discord",
|
|
LastActivity: time.Now().UTC().Format(time.RFC3339),
|
|
},
|
|
{
|
|
ID: "rex",
|
|
DisplayName: "Rex",
|
|
Role: "Frontend Dev",
|
|
Status: models.AgentStatusIdle,
|
|
SessionKey: "rex-session",
|
|
Channel: "discord",
|
|
LastActivity: time.Now().UTC().Add(-10 * time.Minute).Format(time.RFC3339),
|
|
},
|
|
{
|
|
ID: "dex",
|
|
DisplayName: "Dex",
|
|
Role: "Backend Dev",
|
|
Status: models.AgentStatusThinking,
|
|
CurrentTask: strPtr("Designing API contracts"),
|
|
SessionKey: "dex-session",
|
|
Channel: "discord",
|
|
LastActivity: time.Now().UTC().Format(time.RFC3339),
|
|
},
|
|
{
|
|
ID: "hex",
|
|
DisplayName: "Hex",
|
|
Role: "Database Specialist",
|
|
Status: models.AgentStatusActive,
|
|
CurrentTask: strPtr("Reviewing schema migrations"),
|
|
SessionKey: "hex-session",
|
|
Channel: "discord",
|
|
LastActivity: time.Now().UTC().Format(time.RFC3339),
|
|
},
|
|
{
|
|
ID: "pip",
|
|
DisplayName: "Pip",
|
|
Role: "Edge Device Dev",
|
|
Status: models.AgentStatusIdle,
|
|
SessionKey: "pip-session",
|
|
Channel: "discord",
|
|
LastActivity: time.Now().UTC().Add(-1 * time.Hour).Format(time.RFC3339),
|
|
},
|
|
}
|
|
|
|
for _, a := range demoAgents {
|
|
if err := agents.Create(ctx, a); err != nil {
|
|
return fmt.Errorf("seed agent %s: %w", a.ID, err)
|
|
}
|
|
}
|
|
slog.Info("demo agents seeded", "count", len(demoAgents))
|
|
return nil
|
|
}
|
|
|
|
func strPtr(s string) *string { return &s }
|