package main import ( "context" "log" "os" "director/api/db" "director/api/handlers" "director/api/pubsub" "director/api/ws" "github.com/gin-gonic/gin" ) func main() { dsn := os.Getenv("DATABASE_URL") if dsn == "" { dsn = "postgres://director:director_local_2026@127.0.0.1:5432/director?sslmode=disable" } redisAddr := os.Getenv("REDIS_URL") if redisAddr == "" { redisAddr = "127.0.0.1:6379" } database, err := db.Connect(dsn) if err != nil { log.Fatalf("database connection failed: %v", err) } defer database.Close() hub := ws.NewHub() go hub.Run() // Redis pub/sub — bridges external events (OpenClaw agents) to WebSocket clients ps := pubsub.New(redisAddr, hub) if err := ps.Ping(context.Background()); err != nil { log.Fatalf("redis connection failed: %v", err) } defer ps.Close() go ps.Subscribe(context.Background()) r := gin.Default() // CORS for local dev r.Use(func(c *gin.Context) { c.Header("Access-Control-Allow-Origin", "*") c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization") if c.Request.Method == "OPTIONS" { c.AbortWithStatus(204) return } c.Next() }) api := r.Group("/api") { h := handlers.New(database, ps) // Plans api.GET("/plans", h.ListPlans) api.POST("/plans", h.CreatePlan) api.GET("/plans/:id", h.GetPlan) api.PATCH("/plans/:id", h.UpdatePlan) // Tasks api.GET("/plans/:id/tasks", h.ListTasks) api.POST("/plans/:id/tasks", h.CreateTask) api.PATCH("/tasks/:id", h.UpdateTask) // Agents api.GET("/agents", h.ListAgents) api.PATCH("/agents/:id", h.UpdateAgent) api.GET("/agents/:id/next-task", h.NextTask) // Director Queue api.GET("/queue", h.ListQueue) api.PATCH("/queue/:id", h.UpdateQueueItem) // Pending Questions api.GET("/questions", h.ListQuestions) api.POST("/questions", h.CreateQuestion) api.PATCH("/questions/:id", h.AnswerQuestion) // Messages api.GET("/plans/:id/messages", h.ListMessages) api.POST("/plans/:id/messages", h.CreateMessage) api.PATCH("/messages/:id", h.UpdateMessage) // System / Settings api.GET("/system/claude-mode", h.GetClaudeMode) api.POST("/system/claude-mode", h.SetClaudeMode) api.GET("/system/claude-usage", h.GetClaudeUsage) api.POST("/system/claude-usage", h.SetClaudeUsage) api.POST("/system/claude-kill-switch", h.ClaudeKillSwitch) // Stats api.GET("/stats", h.GetStats) } // WebSocket r.GET("/ws", func(c *gin.Context) { ws.HandleWebSocket(hub, c.Writer, c.Request) }) // Health r.GET("/health", func(c *gin.Context) { if err := ps.Ping(c.Request.Context()); err != nil { c.JSON(500, gin.H{"status": "error", "redis": err.Error()}) return } c.JSON(200, gin.H{"status": "ok"}) }) port := os.Getenv("PORT") if port == "" { port = "8090" } log.Printf("Director API starting on :%s", port) if err := r.Run(":" + port); err != nil { log.Fatalf("server failed: %v", err) } }