2026-05-06 17:29:44 -04:00
|
|
|
// Command server starts the Control Center Go backend API server.
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
"log/slog"
|
|
|
|
|
"net/http"
|
|
|
|
|
"os"
|
|
|
|
|
"os/signal"
|
|
|
|
|
"syscall"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"code.cubecraftcreations.com/CubeCraft-Creations/Control-Center/go-backend/internal/config"
|
2026-05-07 14:16:05 -04:00
|
|
|
"code.cubecraftcreations.com/CubeCraft-Creations/Control-Center/go-backend/internal/db"
|
2026-05-08 19:58:06 -04:00
|
|
|
"code.cubecraftcreations.com/CubeCraft-Creations/Control-Center/go-backend/internal/gateway"
|
2026-05-06 17:29:44 -04:00
|
|
|
"code.cubecraftcreations.com/CubeCraft-Creations/Control-Center/go-backend/internal/handler"
|
2026-05-08 19:58:06 -04:00
|
|
|
"code.cubecraftcreations.com/CubeCraft-Creations/Control-Center/go-backend/internal/repository"
|
2026-05-06 17:29:44 -04:00
|
|
|
"code.cubecraftcreations.com/CubeCraft-Creations/Control-Center/go-backend/internal/router"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
// ── Configuration ──────────────────────────────────────────────────────
|
|
|
|
|
cfg := config.Load()
|
|
|
|
|
|
|
|
|
|
// ── Logging ────────────────────────────────────────────────────────────
|
|
|
|
|
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
|
|
|
|
Level: parseLogLevel(cfg.LogLevel),
|
|
|
|
|
}))
|
|
|
|
|
slog.SetDefault(logger)
|
|
|
|
|
|
2026-05-08 19:58:06 -04:00
|
|
|
// ── Database ───────────────────────────────────────────────────────────
|
|
|
|
|
pool, err := db.New(cfg.DatabaseURL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
slog.Error("database connection failed", "error", err)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
defer pool.Close()
|
|
|
|
|
|
|
|
|
|
// ── Repositories (PostgreSQL-backed) ───────────────────────────────────
|
|
|
|
|
agentRepo := repository.NewAgentRepository(pool.Pool)
|
|
|
|
|
sessionRepo := repository.NewSessionRepository(pool.Pool)
|
|
|
|
|
taskRepo := repository.NewTaskRepository(pool.Pool)
|
|
|
|
|
projectRepo := repository.NewProjectRepository(pool.Pool)
|
|
|
|
|
|
|
|
|
|
// ── Seed demo agents on first boot ─────────────────────────────────────
|
|
|
|
|
if err := gateway.SeedDemoAgents(context.Background(), agentRepo); err != nil {
|
|
|
|
|
slog.Error("seed demo agents failed", "error", err)
|
|
|
|
|
os.Exit(1)
|
2026-05-07 14:16:05 -04:00
|
|
|
}
|
|
|
|
|
|
2026-05-08 19:58:06 -04:00
|
|
|
// ── SSE Broker ─────────────────────────────────────────────────────────
|
|
|
|
|
broker := handler.NewBroker()
|
2026-05-06 17:29:44 -04:00
|
|
|
|
|
|
|
|
// ── HTTP handler ───────────────────────────────────────────────────────
|
2026-05-08 19:58:06 -04:00
|
|
|
h := handler.NewHandler(agentRepo, sessionRepo, taskRepo, projectRepo)
|
2026-05-06 17:29:44 -04:00
|
|
|
|
|
|
|
|
// ── Router ─────────────────────────────────────────────────────────────
|
2026-05-07 14:16:05 -04:00
|
|
|
r := router.New(&router.Dependencies{
|
|
|
|
|
Handler: h,
|
2026-05-08 19:58:06 -04:00
|
|
|
Pool: pool,
|
2026-05-07 14:16:05 -04:00
|
|
|
CORSOrigin: cfg.CORSOrigin,
|
2026-05-08 19:58:06 -04:00
|
|
|
Broker: broker,
|
2026-05-07 14:16:05 -04:00
|
|
|
})
|
2026-05-06 17:29:44 -04:00
|
|
|
|
2026-05-20 11:16:05 +00:00
|
|
|
// ── Gateway clients (WS primary, REST fallback) ───────────────────
|
|
|
|
|
// WS gateway client (primary path)
|
CUB-200: implement WebSocket gateway client with v3 protocol
Replace REST poller with WebSocket client as primary gateway connection:
- wsclient.go: WebSocket client with v3 handshake (connect.challenge →
connect → hello-ok), frame routing (req/res/event), JSON-RPC Send(),
auto-reconnect with exponential backoff (1s → 30s max)
- sync.go: Initial sync via agents.list + sessions.list RPCs, merge
session runtime state into AgentCardData, broadcast fleet.update
- events.go: Real-time event handlers for sessions.changed, presence,
and agent.config — DB update first, then SSE broadcast
- client.go: REST poller retained as fallback (WS is primary)
- config.go: Add GATEWAY_WS_URL and OPENCLAW_GATEWAY_TOKEN env vars
- main.go: Wire WS client as primary, REST as fallback
- .env.example: Document new WS config vars
Fallback: If WS connection fails, seeded demo data + REST polling
remain available.
2026-05-20 11:33:17 +00:00
|
|
|
wsClient := gateway.NewWSClient(gateway.WSConfig{
|
|
|
|
|
URL: cfg.WSGatewayURL,
|
|
|
|
|
AuthToken: cfg.WSGatewayToken,
|
|
|
|
|
}, agentRepo, broker, logger)
|
|
|
|
|
|
2026-05-20 11:16:05 +00:00
|
|
|
// REST gateway client (fallback — only polls if WS fails to connect)
|
|
|
|
|
gwClient := gateway.NewClient(gateway.Config{
|
|
|
|
|
URL: cfg.GatewayRestURL,
|
|
|
|
|
PollInterval: cfg.GatewayRestPollInterval,
|
2026-05-08 19:58:06 -04:00
|
|
|
}, agentRepo, broker)
|
|
|
|
|
|
2026-05-20 11:16:05 +00:00
|
|
|
// Wire them together: REST defers to WS when WS is connected
|
|
|
|
|
wsClient.SetRESTClient(gwClient)
|
|
|
|
|
gwClient.SetWSClient(wsClient)
|
CUB-200: implement WebSocket gateway client with v3 protocol
Replace REST poller with WebSocket client as primary gateway connection:
- wsclient.go: WebSocket client with v3 handshake (connect.challenge →
connect → hello-ok), frame routing (req/res/event), JSON-RPC Send(),
auto-reconnect with exponential backoff (1s → 30s max)
- sync.go: Initial sync via agents.list + sessions.list RPCs, merge
session runtime state into AgentCardData, broadcast fleet.update
- events.go: Real-time event handlers for sessions.changed, presence,
and agent.config — DB update first, then SSE broadcast
- client.go: REST poller retained as fallback (WS is primary)
- config.go: Add GATEWAY_WS_URL and OPENCLAW_GATEWAY_TOKEN env vars
- main.go: Wire WS client as primary, REST as fallback
- .env.example: Document new WS config vars
Fallback: If WS connection fails, seeded demo data + REST polling
remain available.
2026-05-20 11:33:17 +00:00
|
|
|
|
2026-05-08 19:58:06 -04:00
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
CUB-200: implement WebSocket gateway client with v3 protocol
Replace REST poller with WebSocket client as primary gateway connection:
- wsclient.go: WebSocket client with v3 handshake (connect.challenge →
connect → hello-ok), frame routing (req/res/event), JSON-RPC Send(),
auto-reconnect with exponential backoff (1s → 30s max)
- sync.go: Initial sync via agents.list + sessions.list RPCs, merge
session runtime state into AgentCardData, broadcast fleet.update
- events.go: Real-time event handlers for sessions.changed, presence,
and agent.config — DB update first, then SSE broadcast
- client.go: REST poller retained as fallback (WS is primary)
- config.go: Add GATEWAY_WS_URL and OPENCLAW_GATEWAY_TOKEN env vars
- main.go: Wire WS client as primary, REST as fallback
- .env.example: Document new WS config vars
Fallback: If WS connection fails, seeded demo data + REST polling
remain available.
2026-05-20 11:33:17 +00:00
|
|
|
// Start WS client first (primary)
|
|
|
|
|
go wsClient.Start(ctx)
|
2026-05-20 11:16:05 +00:00
|
|
|
// Start REST client (will wait for WS, then stand down or fall back)
|
|
|
|
|
go gwClient.Start(ctx)
|
2026-05-08 19:58:06 -04:00
|
|
|
|
2026-05-06 17:29:44 -04:00
|
|
|
// ── Server ─────────────────────────────────────────────────────────────
|
|
|
|
|
srv := &http.Server{
|
|
|
|
|
Addr: fmt.Sprintf(":%d", cfg.Port),
|
|
|
|
|
Handler: r,
|
|
|
|
|
ReadTimeout: 10 * time.Second,
|
|
|
|
|
WriteTimeout: 15 * time.Second,
|
|
|
|
|
IdleTimeout: 60 * time.Second,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Graceful shutdown
|
|
|
|
|
quit := make(chan os.Signal, 1)
|
|
|
|
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
|
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
|
slog.Info("server starting", "port", cfg.Port, "env", cfg.Environment)
|
|
|
|
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
|
|
|
slog.Error("server failed", "error", err)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
<-quit
|
|
|
|
|
slog.Info("shutting down server...")
|
|
|
|
|
|
CUB-200: implement WebSocket gateway client with v3 protocol
Replace REST poller with WebSocket client as primary gateway connection:
- wsclient.go: WebSocket client with v3 handshake (connect.challenge →
connect → hello-ok), frame routing (req/res/event), JSON-RPC Send(),
auto-reconnect with exponential backoff (1s → 30s max)
- sync.go: Initial sync via agents.list + sessions.list RPCs, merge
session runtime state into AgentCardData, broadcast fleet.update
- events.go: Real-time event handlers for sessions.changed, presence,
and agent.config — DB update first, then SSE broadcast
- client.go: REST poller retained as fallback (WS is primary)
- config.go: Add GATEWAY_WS_URL and OPENCLAW_GATEWAY_TOKEN env vars
- main.go: Wire WS client as primary, REST as fallback
- .env.example: Document new WS config vars
Fallback: If WS connection fails, seeded demo data + REST polling
remain available.
2026-05-20 11:33:17 +00:00
|
|
|
cancel() // stop gateway clients
|
2026-05-06 17:29:44 -04:00
|
|
|
|
2026-05-08 19:58:06 -04:00
|
|
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
|
|
|
defer shutdownCancel()
|
|
|
|
|
|
|
|
|
|
if err := srv.Shutdown(shutdownCtx); err != nil {
|
2026-05-06 17:29:44 -04:00
|
|
|
slog.Error("server forced to shutdown", "error", err)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
slog.Info("server exited cleanly")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func parseLogLevel(level string) slog.Level {
|
|
|
|
|
switch level {
|
|
|
|
|
case "debug":
|
|
|
|
|
return slog.LevelDebug
|
|
|
|
|
case "warn":
|
|
|
|
|
return slog.LevelWarn
|
|
|
|
|
case "error":
|
|
|
|
|
return slog.LevelError
|
|
|
|
|
default:
|
|
|
|
|
return slog.LevelInfo
|
|
|
|
|
}
|
CUB-200: implement WebSocket gateway client with v3 protocol
Replace REST poller with WebSocket client as primary gateway connection:
- wsclient.go: WebSocket client with v3 handshake (connect.challenge →
connect → hello-ok), frame routing (req/res/event), JSON-RPC Send(),
auto-reconnect with exponential backoff (1s → 30s max)
- sync.go: Initial sync via agents.list + sessions.list RPCs, merge
session runtime state into AgentCardData, broadcast fleet.update
- events.go: Real-time event handlers for sessions.changed, presence,
and agent.config — DB update first, then SSE broadcast
- client.go: REST poller retained as fallback (WS is primary)
- config.go: Add GATEWAY_WS_URL and OPENCLAW_GATEWAY_TOKEN env vars
- main.go: Wire WS client as primary, REST as fallback
- .env.example: Document new WS config vars
Fallback: If WS connection fails, seeded demo data + REST polling
remain available.
2026-05-20 11:33:17 +00:00
|
|
|
}
|