step 5 (partial): strip dropped packages from production code
Removed all references to dropped packages in non-test code.
Zero dropped imports, store refs, or model types remaining.
Major removals:
- scheduler/ package (entire dir) — tasks moved to extension track
- taskutil/ package (entire dir)
- health/ package (entire dir) — provider/tool health
- sandbox/provider_module.go — provider.complete module
- handlers: workflow_entry, workflow_instances, workflow_forms,
workflow_assignments, workflow_monitor, health_admin (6 files)
- auth/session.go, middleware/session_auth.go
- store/{postgres,sqlite}/sessions.go, tasks.go
Rewrites:
- store/{postgres,sqlite}/health.go — kernel-only Prune
(ws_tickets, rate_limit_counters, stale presence)
- handlers/admin.go: 847→467 lines (stripped provider CRUD)
- handlers/teams.go: 520→411 lines (stripped model listing)
- sandbox/runner.go: removed ProviderResolver
- sandbox/workflow_module.go: 216→83 lines (definition-only)
- main.go: 1579→1325 lines (stripped dropped package init)
- pages/pages.go: stripped channel/session lookups
- events/types.go: stripped chat/channel/workspace event routes
- models: stripped stale types, constants, notification types
Store interface cleanup:
- Removed SessionStore, TaskStore from Stores struct
- Stripped workflow assignment methods from WorkflowStore iface
- Stripped assignment methods from PG+SQLite workflow stores
- Updated testhelper.go table list to kernel-only
Migrations: removed 008_tasks.sql (both dialects)
-9665/+69 lines across 51 files.
This commit is contained in:
@@ -16,7 +16,6 @@ import (
|
||||
"switchboard-core/crypto"
|
||||
"switchboard-core/database"
|
||||
"switchboard-core/models"
|
||||
"switchboard-core/providers"
|
||||
"switchboard-core/storage"
|
||||
"switchboard-core/store"
|
||||
"switchboard-core/tools/search"
|
||||
@@ -438,325 +437,6 @@ func (h *AdminHandler) VaultStatus(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, status)
|
||||
}
|
||||
|
||||
// ── Provider Configs (Global) ───────────────
|
||||
|
||||
func (h *AdminHandler) ListGlobalConfigs(c *gin.Context) {
|
||||
cfgs, err := h.stores.Providers.ListGlobal(c.Request.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list configs"})
|
||||
return
|
||||
}
|
||||
|
||||
// Redact API keys but expose has_key flag for UI
|
||||
type configWithKey struct {
|
||||
models.ProviderConfig
|
||||
HasKey bool `json:"has_key"`
|
||||
}
|
||||
out := make([]configWithKey, len(cfgs))
|
||||
for i, cfg := range cfgs {
|
||||
out[i] = configWithKey{
|
||||
ProviderConfig: cfg,
|
||||
HasKey: cfg.HasKey(),
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": out})
|
||||
}
|
||||
|
||||
func (h *AdminHandler) CreateGlobalConfig(c *gin.Context) {
|
||||
// Wrapper struct: models.ProviderConfig has APIKeyEnc tagged json:"-"
|
||||
// so ShouldBindJSON would silently drop the api_key field.
|
||||
var req struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Provider string `json:"provider" binding:"required"`
|
||||
Endpoint string `json:"endpoint" binding:"required"`
|
||||
APIKey string `json:"api_key"`
|
||||
ModelDefault string `json:"model_default,omitempty"`
|
||||
Config map[string]interface{} `json:"config,omitempty"`
|
||||
Headers map[string]interface{} `json:"headers,omitempty"`
|
||||
Settings map[string]interface{} `json:"settings,omitempty"`
|
||||
IsPrivate bool `json:"is_private,omitempty"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := providers.Get(req.Provider); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "unsupported provider: " + req.Provider,
|
||||
"supported_providers": providers.List(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
cfg := &models.ProviderConfig{
|
||||
Name: req.Name,
|
||||
Provider: req.Provider,
|
||||
Endpoint: req.Endpoint,
|
||||
ModelDefault: req.ModelDefault,
|
||||
Config: models.JSONMap(req.Config),
|
||||
Headers: models.JSONMap(req.Headers),
|
||||
Settings: models.JSONMap(req.Settings),
|
||||
Scope: models.ScopeGlobal,
|
||||
KeyScope: models.ScopeGlobal,
|
||||
IsActive: true,
|
||||
IsPrivate: req.IsPrivate,
|
||||
}
|
||||
|
||||
// Encrypt the API key
|
||||
if req.APIKey != "" {
|
||||
if h.vault != nil {
|
||||
enc, nonce, err := h.vault.EncryptForScope(req.APIKey, "global", "")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to encrypt API key"})
|
||||
return
|
||||
}
|
||||
cfg.APIKeyEnc = enc
|
||||
cfg.KeyNonce = nonce
|
||||
} else {
|
||||
// No vault configured — store raw bytes (unencrypted fallback)
|
||||
cfg.APIKeyEnc = []byte(req.APIKey)
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.stores.Providers.Create(c.Request.Context(), cfg); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create config"})
|
||||
return
|
||||
}
|
||||
|
||||
h.auditLog(c, "provider.create", "provider_config", cfg.ID, gin.H{"name": cfg.Name, "provider": cfg.Provider})
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"id": cfg.ID,
|
||||
"scope": cfg.Scope,
|
||||
"name": cfg.Name,
|
||||
"provider": cfg.Provider,
|
||||
"endpoint": cfg.Endpoint,
|
||||
"model_default": cfg.ModelDefault,
|
||||
"config": cfg.Config,
|
||||
"headers": cfg.Headers,
|
||||
"settings": cfg.Settings,
|
||||
"is_active": cfg.IsActive,
|
||||
"is_private": cfg.IsPrivate,
|
||||
"has_key": cfg.HasKey(),
|
||||
"created_at": cfg.CreatedAt,
|
||||
"updated_at": cfg.UpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AdminHandler) UpdateGlobalConfig(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
// Wrapper struct: ProviderConfigPatch has APIKeyEnc tagged json:"-"
|
||||
var req struct {
|
||||
models.ProviderConfigPatch
|
||||
APIKey *string `json:"api_key,omitempty"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
patch := req.ProviderConfigPatch
|
||||
// Transfer api_key → encrypted fields
|
||||
if req.APIKey != nil && *req.APIKey != "" {
|
||||
if h.vault != nil {
|
||||
enc, nonce, err := h.vault.EncryptForScope(*req.APIKey, "global", "")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to encrypt API key"})
|
||||
return
|
||||
}
|
||||
patch.APIKeyEnc = enc
|
||||
patch.KeyNonce = nonce
|
||||
} else {
|
||||
patch.APIKeyEnc = []byte(*req.APIKey)
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.stores.Providers.Update(c.Request.Context(), id, patch); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update config"})
|
||||
return
|
||||
}
|
||||
|
||||
h.auditLog(c, "provider.update", "provider_config", id, nil)
|
||||
c.JSON(http.StatusOK, gin.H{"message": "config updated"})
|
||||
}
|
||||
|
||||
func (h *AdminHandler) DeleteGlobalConfig(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
// Delete associated catalog entries first
|
||||
h.stores.Catalog.DeleteForProvider(c.Request.Context(), id)
|
||||
if err := h.stores.Providers.Delete(c.Request.Context(), id); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete config"})
|
||||
return
|
||||
}
|
||||
h.auditLog(c, "provider.delete", "provider_config", id, nil)
|
||||
c.JSON(http.StatusOK, gin.H{"message": "config deleted"})
|
||||
}
|
||||
|
||||
// ── Model Catalog ───────────────────────────
|
||||
|
||||
func (h *AdminHandler) ListModelConfigs(c *gin.Context) {
|
||||
entries, err := h.stores.Catalog.ListAllGlobal(c.Request.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list models"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": entries})
|
||||
}
|
||||
|
||||
func (h *AdminHandler) FetchModels(c *gin.Context) {
|
||||
var req struct {
|
||||
ProviderConfigID string `json:"provider_config_id"`
|
||||
}
|
||||
c.ShouldBindJSON(&req)
|
||||
|
||||
// If no specific provider, fetch from ALL global providers
|
||||
if req.ProviderConfigID == "" {
|
||||
configs, err := h.stores.Providers.ListGlobal(c.Request.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list providers"})
|
||||
return
|
||||
}
|
||||
if len(configs) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no providers configured — add one first"})
|
||||
return
|
||||
}
|
||||
|
||||
totalAdded, totalUpdated, totalFetched := 0, 0, 0
|
||||
var errs []string
|
||||
for _, cfg := range configs {
|
||||
if !cfg.IsActive {
|
||||
continue
|
||||
}
|
||||
added, updated, fetched, err := h.fetchModelsForProvider(c, &cfg)
|
||||
if err != nil {
|
||||
errs = append(errs, cfg.Provider+": "+err.Error())
|
||||
continue
|
||||
}
|
||||
totalAdded += added
|
||||
totalUpdated += updated
|
||||
totalFetched += fetched
|
||||
}
|
||||
|
||||
result := gin.H{
|
||||
"message": "models synced",
|
||||
"added": totalAdded,
|
||||
"updated": totalUpdated,
|
||||
"total": totalFetched,
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
result["errors"] = errs
|
||||
}
|
||||
c.JSON(http.StatusOK, result)
|
||||
return
|
||||
}
|
||||
|
||||
// Single provider fetch
|
||||
cfg, err := h.stores.Providers.GetByID(c.Request.Context(), req.ProviderConfigID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "provider config not found"})
|
||||
return
|
||||
}
|
||||
|
||||
added, updated, fetched, err := h.fetchModelsForProvider(c, cfg)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrUpstreamTimeout) {
|
||||
c.JSON(http.StatusGatewayTimeout, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": "failed to fetch models: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "models synced",
|
||||
"added": added,
|
||||
"updated": updated,
|
||||
"total": fetched,
|
||||
})
|
||||
}
|
||||
|
||||
// fetchModelsForProvider fetches and syncs models for a single provider config.
|
||||
func (h *AdminHandler) fetchModelsForProvider(c *gin.Context, cfg *models.ProviderConfig) (added, updated, total int, err error) {
|
||||
// Decrypt the API key for the provider API call
|
||||
apiKey := ""
|
||||
if len(cfg.APIKeyEnc) > 0 {
|
||||
if h.vault != nil {
|
||||
apiKey, err = h.vault.Decrypt(cfg.APIKeyEnc, cfg.KeyNonce, cfg.KeyScope, "")
|
||||
if err != nil {
|
||||
return 0, 0, 0, fmt.Errorf("failed to decrypt API key: %w", err)
|
||||
}
|
||||
} else {
|
||||
// No vault — key stored as raw bytes (unencrypted fallback)
|
||||
apiKey = string(cfg.APIKeyEnc)
|
||||
}
|
||||
}
|
||||
|
||||
result, err := syncProviderModels(c.Request.Context(), h.stores, cfg, apiKey)
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
|
||||
h.auditLog(c, "models.fetch", "provider_config", cfg.ID, gin.H{
|
||||
"added": result.Added, "updated": result.Updated, "total": result.Total,
|
||||
})
|
||||
|
||||
return result.Added, result.Updated, result.Total, nil
|
||||
}
|
||||
|
||||
func (h *AdminHandler) UpdateModelConfig(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req struct {
|
||||
Visibility *string `json:"visibility"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Visibility != nil {
|
||||
if err := h.stores.Catalog.SetVisibility(c.Request.Context(), id, *req.Visibility); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update visibility"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "model updated"})
|
||||
}
|
||||
|
||||
func (h *AdminHandler) BulkUpdateModels(c *gin.Context) {
|
||||
var req struct {
|
||||
ProviderConfigID string `json:"provider_config_id"`
|
||||
Visibility string `json:"visibility" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
if req.ProviderConfigID != "" {
|
||||
err = h.stores.Catalog.BulkSetVisibility(c.Request.Context(), req.ProviderConfigID, req.Visibility)
|
||||
} else {
|
||||
err = h.stores.Catalog.BulkSetVisibilityAll(c.Request.Context(), req.Visibility)
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to bulk update"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "bulk update complete"})
|
||||
}
|
||||
|
||||
func (h *AdminHandler) DeleteModelConfig(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := h.stores.Catalog.Delete(c.Request.Context(), id); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete model"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "model deleted"})
|
||||
}
|
||||
|
||||
// ── Stats ───────────────────────────────────
|
||||
|
||||
func (h *AdminHandler) GetStats(c *gin.Context) {
|
||||
@@ -764,12 +444,10 @@ func (h *AdminHandler) GetStats(c *gin.Context) {
|
||||
stats := gin.H{}
|
||||
|
||||
userCount, _ := h.stores.Users.CountAll(ctx)
|
||||
channelCount, _ := h.stores.Channels.CountAll(ctx)
|
||||
messageCount, _ := h.stores.Messages.CountAll(ctx)
|
||||
teamCount, _ := h.stores.Teams.CountAll(ctx)
|
||||
|
||||
stats["users"] = userCount
|
||||
stats["channels"] = channelCount
|
||||
stats["messages"] = messageCount
|
||||
stats["teams"] = teamCount
|
||||
|
||||
c.JSON(http.StatusOK, stats)
|
||||
}
|
||||
@@ -786,62 +464,3 @@ func (h *AdminHandler) auditLog(c *gin.Context, action, resourceType, resourceID
|
||||
}
|
||||
AuditLog(h.stores.Audit, c, action, resourceType, resourceID, meta)
|
||||
}
|
||||
|
||||
// ── Archived Channels (v0.23.2) ──────────────
|
||||
|
||||
func (h *AdminHandler) ListArchivedChannels(c *gin.Context) {
|
||||
page, perPage, offset := parsePagination(c)
|
||||
|
||||
channels, total, err := h.stores.Channels.ListArchived(c.Request.Context(), store.ListOptions{
|
||||
Limit: perPage,
|
||||
Offset: offset,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"})
|
||||
return
|
||||
}
|
||||
|
||||
// Retention config
|
||||
retentionTTL := 0
|
||||
if ttlCfg, err := h.stores.GlobalConfig.Get(c.Request.Context(), "retention_ttl_days"); err == nil {
|
||||
if v, ok := ttlCfg["value"].(float64); ok {
|
||||
retentionTTL = int(v)
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"data": channels,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"per_page": perPage,
|
||||
"retention_ttl_days": retentionTTL,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AdminHandler) PurgeChannel(c *gin.Context) {
|
||||
channelID := c.Param("id")
|
||||
|
||||
// Clean up file storage blobs before CASCADE deletes the DB references
|
||||
if h.objStore != nil {
|
||||
prefix := fmt.Sprintf("files/%s", channelID)
|
||||
if err := h.objStore.DeletePrefix(c.Request.Context(), prefix); err != nil {
|
||||
log.Printf("⚠️ storage cleanup for purged channel %s failed: %v", channelID, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.stores.Channels.Purge(c.Request.Context(), channelID); err != nil {
|
||||
msg := err.Error()
|
||||
switch {
|
||||
case msg == "channel not found":
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": msg})
|
||||
case msg == "channel must be archived before purging":
|
||||
c.JSON(http.StatusConflict, gin.H{"error": msg})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "purge failed"})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
h.auditLog(c, "channel.purged", "channel", channelID, nil)
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user