Changeset 0.32.0 (#206)

This commit is contained in:
2026-03-19 18:50:27 +00:00
parent 6668e546fe
commit b1266b0d7c
283 changed files with 2187 additions and 1055 deletions

View File

@@ -13,34 +13,34 @@ import (
"github.com/gin-gonic/gin"
"git.gobha.me/xcaliber/chat-switchboard/auth"
"git.gobha.me/xcaliber/chat-switchboard/compaction"
"git.gobha.me/xcaliber/chat-switchboard/config"
"git.gobha.me/xcaliber/chat-switchboard/crypto"
"git.gobha.me/xcaliber/chat-switchboard/database"
"git.gobha.me/xcaliber/chat-switchboard/events"
"git.gobha.me/xcaliber/chat-switchboard/extraction"
"git.gobha.me/xcaliber/chat-switchboard/filters"
"git.gobha.me/xcaliber/chat-switchboard/sandbox"
"git.gobha.me/xcaliber/chat-switchboard/handlers"
"git.gobha.me/xcaliber/chat-switchboard/health"
"git.gobha.me/xcaliber/chat-switchboard/knowledge"
"git.gobha.me/xcaliber/chat-switchboard/memory"
"git.gobha.me/xcaliber/chat-switchboard/middleware"
"git.gobha.me/xcaliber/chat-switchboard/notifications"
"git.gobha.me/xcaliber/chat-switchboard/pages"
"git.gobha.me/xcaliber/chat-switchboard/providers"
"git.gobha.me/xcaliber/chat-switchboard/roles"
"git.gobha.me/xcaliber/chat-switchboard/routing"
"git.gobha.me/xcaliber/chat-switchboard/scheduler"
"git.gobha.me/xcaliber/chat-switchboard/storage"
"git.gobha.me/xcaliber/chat-switchboard/store"
postgres "git.gobha.me/xcaliber/chat-switchboard/store/postgres"
sqliteStore "git.gobha.me/xcaliber/chat-switchboard/store/sqlite"
"git.gobha.me/xcaliber/chat-switchboard/tools"
"git.gobha.me/xcaliber/chat-switchboard/tools/search"
"git.gobha.me/xcaliber/chat-switchboard/treepath"
"git.gobha.me/xcaliber/chat-switchboard/workspace"
"chat-switchboard/auth"
"chat-switchboard/compaction"
"chat-switchboard/config"
"chat-switchboard/crypto"
"chat-switchboard/database"
"chat-switchboard/events"
"chat-switchboard/extraction"
"chat-switchboard/filters"
"chat-switchboard/sandbox"
"chat-switchboard/handlers"
"chat-switchboard/health"
"chat-switchboard/knowledge"
"chat-switchboard/memory"
"chat-switchboard/middleware"
"chat-switchboard/notifications"
"chat-switchboard/pages"
"chat-switchboard/providers"
"chat-switchboard/roles"
"chat-switchboard/routing"
"chat-switchboard/scheduler"
"chat-switchboard/storage"
"chat-switchboard/store"
postgres "chat-switchboard/store/postgres"
sqliteStore "chat-switchboard/store/sqlite"
"chat-switchboard/tools"
"chat-switchboard/tools/search"
"chat-switchboard/treepath"
"chat-switchboard/workspace"
)
func main() {
@@ -400,9 +400,8 @@ func main() {
// ── WebSocket Hub ─────────────────────────
hub := events.NewHub(bus, middleware.GetAllowedOrigins(cfg))
// ── WebSocket Ticket Store (v0.28.8) ─────
ticketStore := events.NewTicketStore()
defer ticketStore.Stop()
// ── WebSocket Ticket Store (v0.32.0: PG-backed for cross-pod) ─────
ticketAdapter := &events.TicketValidatorAdapter{Store: stores.Tickets}
// ── Notification Service (v0.20.0) ───────
var notifSvc *notifications.Service
@@ -457,8 +456,30 @@ func main() {
})
})
// v0.32.0: Kubernetes probe endpoints
// Liveness: process is alive and serving (no dependency checks).
base.GET("/healthz/live", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
// Readiness: process can serve traffic (PG reachable).
// Failing readiness pulls the pod from the Service — new requests
// route to healthy replicas.
base.GET("/healthz/ready", func(c *gin.Context) {
if database.DB == nil {
c.JSON(503, gin.H{"error": "database not initialized"})
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 2*time.Second)
defer cancel()
if err := database.DB.PingContext(ctx); err != nil {
c.JSON(503, gin.H{"error": "database unavailable"})
return
}
c.JSON(200, gin.H{"status": "ok"})
})
// WebSocket endpoint
base.GET("/ws", middleware.WsAuth(cfg, stores.Users, userCache, ticketStore), hub.HandleWebSocket)
base.GET("/ws", middleware.WsAuth(cfg, stores.Users, userCache, ticketAdapter), hub.HandleWebSocket)
// ── Auth routes (rate limited) ──────────────
authMode, err := auth.ParseMode(cfg.AuthMode)
@@ -500,7 +521,7 @@ func main() {
log.Printf(" 🔑 Auth mode: %s", authMode)
authH := handlers.NewAuthHandler(cfg, stores, uekCache, authProvider)
authLimiter := middleware.NewRateLimiter(5, 5)
authLimiter := middleware.NewRateLimiter(stores.RateLimits, 5, 8)
api := base.Group("/api/v1")
{
@@ -549,7 +570,7 @@ func main() {
// Client fetches this, then connects with ?ticket=<opaque>.
protected.POST("/ws/ticket", func(c *gin.Context) {
userID := c.GetString("user_id")
ticket, err := ticketStore.Issue(userID)
ticket, err := stores.Tickets.Issue(c.Request.Context(), userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to issue ticket"})
return
@@ -596,7 +617,7 @@ func main() {
Ts: time.Now().UnixMilli(),
}
for _, pid := range pids {
hub.SendToUser(pid, evt)
hub.PublishToUser(pid, evt)
}
}
c.JSON(200, gin.H{"ok": true})
@@ -983,6 +1004,20 @@ func main() {
teamAssignH := handlers.NewWorkflowAssignmentHandler(stores, hub)
teamScoped.GET("/assignments", teamAssignH.ListForTeam)
// Team workflows — self-service (v0.31.2)
teamWfH := handlers.NewWorkflowHandler(stores)
teamScoped.GET("/workflows", teamWfH.ListTeamWorkflows)
teamScoped.POST("/workflows", teamWfH.CreateTeamWorkflow)
teamScoped.GET("/workflows/:id", teamWfH.GetTeamWorkflow)
teamScoped.PATCH("/workflows/:id", teamWfH.UpdateTeamWorkflow)
teamScoped.DELETE("/workflows/:id", teamWfH.DeleteTeamWorkflow)
teamScoped.GET("/workflows/:id/stages", teamWfH.ListTeamWorkflowStages)
teamScoped.POST("/workflows/:id/stages", teamWfH.CreateTeamWorkflowStage)
teamScoped.PUT("/workflows/:id/stages/:sid", teamWfH.UpdateTeamWorkflowStage)
teamScoped.DELETE("/workflows/:id/stages/:sid", teamWfH.DeleteTeamWorkflowStage)
teamScoped.PATCH("/workflows/:id/stages/reorder", teamWfH.ReorderTeamWorkflowStages)
teamScoped.POST("/workflows/:id/publish", teamWfH.PublishTeamWorkflow)
// Team tasks — admin CRUD (v0.27.5)
teamTaskH := handlers.NewTaskHandler(stores)
teamScoped.POST("/tasks", middleware.RequirePermission(auth.PermTaskCreate, stores), teamTaskH.CreateTeamTask)
@@ -990,21 +1025,6 @@ func main() {
teamScoped.DELETE("/tasks/:id", middleware.RequirePermission(auth.PermTaskCreate, stores), teamTaskH.Delete)
teamScoped.POST("/tasks/:id/run", middleware.RequirePermission(auth.PermTaskCreate, stores), teamTaskH.RunNow)
teamScoped.POST("/tasks/:id/kill", middleware.RequirePermission(auth.PermTaskCreate, stores), teamTaskH.KillRun)
// Team workflows — self-service CRUD (v0.31.2)
teamWfH := handlers.NewWorkflowHandler(stores)
teamScoped.GET("/workflows", teamWfH.ListTeamWorkflows)
teamScoped.POST("/workflows", middleware.RequirePermission(auth.PermWorkflowCreate, stores), teamWfH.CreateTeamWorkflow)
teamScoped.GET("/workflows/:id", teamWfH.GetTeamWorkflow)
teamScoped.PATCH("/workflows/:id", middleware.RequirePermission(auth.PermWorkflowCreate, stores), teamWfH.UpdateTeamWorkflow)
teamScoped.DELETE("/workflows/:id", middleware.RequirePermission(auth.PermWorkflowCreate, stores), teamWfH.DeleteTeamWorkflow)
teamScoped.GET("/workflows/:id/stages", teamWfH.ListTeamWorkflowStages)
teamScoped.POST("/workflows/:id/stages", middleware.RequirePermission(auth.PermWorkflowCreate, stores), teamWfH.CreateTeamWorkflowStage)
teamScoped.PUT("/workflows/:id/stages/:sid", middleware.RequirePermission(auth.PermWorkflowCreate, stores), teamWfH.UpdateTeamWorkflowStage)
teamScoped.DELETE("/workflows/:id/stages/:sid", middleware.RequirePermission(auth.PermWorkflowCreate, stores), teamWfH.DeleteTeamWorkflowStage)
teamScoped.PATCH("/workflows/:id/stages/reorder", middleware.RequirePermission(auth.PermWorkflowCreate, stores), teamWfH.ReorderTeamWorkflowStages)
teamScoped.POST("/workflows/:id/publish", middleware.RequirePermission(auth.PermWorkflowCreate, stores), teamWfH.PublishTeamWorkflow)
teamScoped.GET("/workflows/:id/versions/:version", teamWfH.GetTeamWorkflowVersion)
}
// Team task viewing for all members (v0.27.5)