Changeset 0.9.0 (#50)

This commit is contained in:
2026-02-23 01:57:28 +00:00
parent 15be26c516
commit 8264aa6016
94 changed files with 9812 additions and 8574 deletions

View File

@@ -11,6 +11,8 @@ import (
"git.gobha.me/xcaliber/chat-switchboard/handlers"
"git.gobha.me/xcaliber/chat-switchboard/middleware"
"git.gobha.me/xcaliber/chat-switchboard/providers"
"git.gobha.me/xcaliber/chat-switchboard/store"
postgres "git.gobha.me/xcaliber/chat-switchboard/store/postgres"
_ "git.gobha.me/xcaliber/chat-switchboard/tools" // registers built-in tools via init()
)
@@ -20,6 +22,8 @@ func main() {
// Register LLM providers
providers.Init()
var stores store.Stores
if err := database.Connect(cfg); err != nil {
log.Printf("⚠ Database unavailable: %v", err)
log.Println(" Running in unmanaged mode (no persistence)")
@@ -28,8 +32,12 @@ func main() {
if err := database.Migrate(); err != nil {
log.Fatalf("❌ Schema migration failed: %v", err)
}
// Initialize store layer
stores = postgres.NewStores(database.DB)
// Bootstrap admin from env (K8s secret) — upserts on every restart
handlers.BootstrapAdmin(cfg)
handlers.BootstrapAdmin(cfg, stores)
}
defer database.Close()
@@ -37,7 +45,6 @@ func main() {
r.Use(middleware.CORS())
// ── Base path group ──────────────────────
// All routes live under cfg.BasePath (e.g. "/dev", "/test", or "")
base := r.Group(cfg.BasePath)
// ── EventBus + WebSocket Hub ─────────────
@@ -54,11 +61,11 @@ func main() {
})
})
// WebSocket endpoint — auth via ?token= query param
// WebSocket endpoint
base.GET("/ws", middleware.Auth(cfg), hub.HandleWebSocket)
// ── Auth routes (rate limited) ──────────────
auth := handlers.NewAuthHandler(cfg)
auth := handlers.NewAuthHandler(cfg, stores)
authLimiter := middleware.NewRateLimiter(1, 5)
api := base.Group("/api/v1")
@@ -72,9 +79,8 @@ func main() {
"database": database.IsConnected(),
"providers": providers.List(),
}
// Include registration status for frontend
if database.IsConnected() {
info["registration_enabled"] = isRegistrationOpen()
info["registration_enabled"] = handlers.IsRegistrationEnabled(stores)
}
c.JSON(200, info)
})
@@ -92,7 +98,7 @@ func main() {
protected := api.Group("")
protected.Use(middleware.Auth(cfg))
{
// Channels (unified: replaces /chats)
// Channels
channels := handlers.NewChannelHandler()
protected.GET("/channels", channels.ListChannels)
protected.POST("/channels", channels.CreateChannel)
@@ -116,21 +122,26 @@ func main() {
comp := handlers.NewCompletionHandler()
protected.POST("/chat/completions", comp.Complete)
// API Configs
apiCfg := handlers.NewAPIConfigHandler()
protected.GET("/api-configs", apiCfg.ListConfigs)
protected.POST("/api-configs", apiCfg.CreateConfig)
protected.GET("/api-configs/:id", apiCfg.GetConfig)
protected.PUT("/api-configs/:id", apiCfg.UpdateConfig)
protected.DELETE("/api-configs/:id", apiCfg.DeleteConfig)
// Provider Configs (user-facing — replaces /api-configs)
provCfg := handlers.NewProviderConfigHandler(stores)
protected.GET("/api-configs", provCfg.ListConfigs) // backward compat
protected.POST("/api-configs", provCfg.CreateConfig)
protected.GET("/api-configs/:id", provCfg.GetConfig)
protected.PUT("/api-configs/:id", provCfg.UpdateConfig)
protected.DELETE("/api-configs/:id", provCfg.DeleteConfig)
protected.GET("/api-configs/:id/models", provCfg.ListModels)
protected.POST("/api-configs/:id/models/fetch", provCfg.FetchModels)
// Models (per-config and aggregate)
protected.GET("/api-configs/:id/models", apiCfg.ListModels)
protected.GET("/models", apiCfg.ListAllModels)
protected.GET("/models/enabled", apiCfg.ListEnabledModels)
protected.GET("/models/preferences", handlers.GetModelPreferences)
protected.PUT("/models/preferences", handlers.SetModelPreference)
protected.POST("/models/preferences/bulk", handlers.BulkSetModelPreferences)
// Models (unified resolver — replaces scattered endpoints)
modelH := handlers.NewModelHandler(stores)
protected.GET("/models/enabled", modelH.ListEnabledModels)
protected.GET("/models", modelH.ListEnabledModels) // alias
// Model Preferences
modelPrefs := handlers.NewModelPrefsHandler(stores)
protected.GET("/models/preferences", modelPrefs.GetPreferences)
protected.PUT("/models/preferences", modelPrefs.SetPreference)
protected.POST("/models/preferences/bulk", modelPrefs.BulkSetPreferences)
// User Settings & Profile
settings := handlers.NewSettingsHandler()
@@ -142,24 +153,31 @@ func main() {
protected.GET("/settings", settings.GetSettings)
protected.PUT("/settings", settings.UpdateSettings)
// Model Presets (user)
presets := handlers.NewPresetHandler()
protected.GET("/presets", presets.ListUserPresets)
protected.POST("/presets", presets.CreateUserPreset)
protected.PUT("/presets/:id", presets.UpdateUserPreset)
protected.DELETE("/presets/:id", presets.DeleteUserPreset)
// Personas (replaces /presets)
personas := handlers.NewPersonaHandler(stores)
protected.GET("/presets", personas.ListUserPersonas) // backward compat
protected.POST("/presets", personas.CreateUserPersona)
protected.PUT("/presets/:id", personas.UpdateUserPersona)
protected.DELETE("/presets/:id", personas.DeleteUserPersona)
protected.POST("/presets/:id/avatar", handlers.UploadPresetAvatar)
protected.DELETE("/presets/:id/avatar", handlers.DeletePresetAvatar)
// Notes
notes := handlers.NewNoteHandler()
protected.GET("/notes", notes.List)
protected.POST("/notes", notes.Create)
protected.GET("/notes/search", notes.Search)
protected.GET("/notes/folders", notes.ListFolders)
protected.POST("/notes/bulk-delete", notes.BulkDelete)
protected.GET("/notes/:id", notes.Get)
protected.PUT("/notes/:id", notes.Update)
protected.DELETE("/notes/:id", notes.Delete)
// Teams (user: my teams)
teams := handlers.NewTeamHandler()
protected.GET("/teams/mine", teams.MyTeams)
// Team admin self-service (requires team admin role, not sys-admin)
// Team admin self-service
teamScoped := protected.Group("/teams/:teamId")
teamScoped.Use(middleware.RequireTeamAdmin())
{
@@ -176,22 +194,15 @@ func main() {
teamScoped.DELETE("/providers/:id", teams.DeleteTeamProvider)
teamScoped.GET("/providers/:id/models", teams.ListTeamProviderModels)
// Team presets
teamPresets := handlers.NewPresetHandler()
teamScoped.GET("/presets", teamPresets.ListTeamPresets)
teamScoped.POST("/presets", teamPresets.CreateTeamPreset)
teamScoped.DELETE("/presets/:id", teamPresets.DeleteTeamPreset)
// Team personas
teamPersonas := handlers.NewPersonaHandler(stores)
teamScoped.GET("/presets", teamPersonas.ListTeamPersonas)
teamScoped.POST("/presets", teamPersonas.CreateTeamPersona)
teamScoped.DELETE("/presets/:id", teamPersonas.DeleteTeamPersona)
}
protected.POST("/notes", notes.Create)
protected.GET("/notes/search", notes.Search)
protected.GET("/notes/folders", notes.ListFolders)
protected.POST("/notes/bulk-delete", notes.BulkDelete)
protected.GET("/notes/:id", notes.Get)
protected.PUT("/notes/:id", notes.Update)
protected.DELETE("/notes/:id", notes.Delete)
// Public global settings (non-admin users can read safe subset)
adm := handlers.NewAdminHandler()
adm := handlers.NewAdminHandler(stores)
protected.GET("/settings/public", adm.PublicSettings)
}
@@ -200,7 +211,7 @@ func main() {
admin.Use(middleware.Auth(cfg))
admin.Use(middleware.RequireAdmin())
{
adm := handlers.NewAdminHandler()
adm := handlers.NewAdminHandler(stores)
// User management
admin.GET("/users", adm.ListUsers)
@@ -218,35 +229,31 @@ func main() {
// Stats
admin.GET("/stats", adm.GetStats)
// Global API Configs
// Global Provider Configs
admin.GET("/configs", adm.ListGlobalConfigs)
admin.POST("/configs", adm.CreateGlobalConfig)
admin.PUT("/configs/:id", adm.UpdateGlobalConfig)
admin.DELETE("/configs/:id", adm.DeleteGlobalConfig)
// Model Configs
// Model Catalog
admin.GET("/models", adm.ListModelConfigs)
admin.POST("/models/fetch", adm.FetchModels)
admin.PUT("/models/bulk", adm.BulkUpdateModels)
admin.PUT("/models/:id", adm.UpdateModelConfig)
admin.DELETE("/models/:id", adm.DeleteModelConfig)
// Model Presets (admin)
presetAdm := handlers.NewPresetHandler()
admin.GET("/presets", presetAdm.ListAdminPresets)
admin.POST("/presets", presetAdm.CreateAdminPreset)
admin.PUT("/presets/:id", presetAdm.UpdateAdminPreset)
admin.DELETE("/presets/:id", presetAdm.DeleteAdminPreset)
// Personas (admin global)
personaAdm := handlers.NewPersonaHandler(stores)
admin.GET("/presets", personaAdm.ListAdminPersonas)
admin.POST("/presets", personaAdm.CreateAdminPersona)
admin.PUT("/presets/:id", personaAdm.UpdateAdminPersona)
admin.DELETE("/presets/:id", personaAdm.DeleteAdminPersona)
admin.POST("/presets/:id/avatar", handlers.UploadPresetAvatar)
admin.DELETE("/presets/:id/avatar", handlers.DeletePresetAvatar)
// Teams (admin)
teamAdm := handlers.NewTeamHandler()
admin.GET("/teams", teamAdm.ListTeams)
// Audit log
admin.GET("/audit", adm.ListAuditLog)
admin.GET("/audit/actions", adm.ListAuditActions)
admin.POST("/teams", teamAdm.CreateTeam)
admin.GET("/teams/:id", teamAdm.GetTeam)
admin.PUT("/teams/:id", teamAdm.UpdateTeam)
@@ -255,6 +262,10 @@ func main() {
admin.POST("/teams/:id/members", teamAdm.AddMember)
admin.PUT("/teams/:id/members/:memberId", teamAdm.UpdateMember)
admin.DELETE("/teams/:id/members/:memberId", teamAdm.RemoveMember)
// Audit log
admin.GET("/audit", adm.ListAuditLog)
admin.GET("/audit/actions", adm.ListAuditActions)
}
}
@@ -271,7 +282,3 @@ func main() {
log.Fatalf("Failed to start server: %v", err)
}
}
func isRegistrationOpen() bool {
return handlers.IsRegistrationEnabled()
}