Initial commit — Director app (API + UI)

This commit is contained in:
2026-04-07 15:18:16 -05:00
commit 5f29db67f3
44 changed files with 6727 additions and 0 deletions

119
api/main.go Normal file
View File

@@ -0,0 +1,119 @@
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)
// 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)
}
// 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)
}
}