Changeset 0.8.3 (#46)

This commit is contained in:
2026-02-22 10:52:47 +00:00
parent 5111d595f7
commit e78a27c06e
11 changed files with 157 additions and 34 deletions

View File

@@ -0,0 +1,36 @@
-- ==========================================
-- Migration 018: Model Visibility
-- ==========================================
-- Replace binary is_enabled with three-state visibility:
-- 'enabled' — visible to all users in model selector
-- 'disabled' — hidden from everyone
-- 'team' — only available to team admins for building presets
-- ==========================================
-- Add new column (idempotent)
ALTER TABLE model_configs ADD COLUMN IF NOT EXISTS visibility VARCHAR(10) DEFAULT 'disabled';
-- Backfill from is_enabled if it still exists
DO $$ BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'model_configs' AND column_name = 'is_enabled'
) THEN
UPDATE model_configs SET visibility = CASE
WHEN is_enabled = true THEN 'enabled'
ELSE 'disabled'
END;
ALTER TABLE model_configs DROP COLUMN is_enabled;
END IF;
END $$;
-- Ensure NOT NULL (safe: DEFAULT already covers new rows)
UPDATE model_configs SET visibility = 'disabled' WHERE visibility IS NULL;
ALTER TABLE model_configs ALTER COLUMN visibility SET NOT NULL;
-- Constraint (drop first for idempotency)
ALTER TABLE model_configs DROP CONSTRAINT IF EXISTS chk_model_visibility;
ALTER TABLE model_configs ADD CONSTRAINT chk_model_visibility
CHECK (visibility IN ('enabled', 'disabled', 'team'));
COMMENT ON COLUMN model_configs.visibility IS 'enabled=all users, team=team admin presets only, disabled=hidden';

View File

@@ -648,14 +648,14 @@ type modelConfigResponse struct {
ProviderName string `json:"provider_name"`
ModelID string `json:"model_id"`
DisplayName *string `json:"display_name"`
IsEnabled bool `json:"is_enabled"`
Visibility string `json:"visibility"`
Capabilities map[string]interface{} `json:"capabilities"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type updateModelConfigRequest struct {
IsEnabled *bool `json:"is_enabled"`
Visibility *string `json:"visibility"`
DisplayName *string `json:"display_name"`
Capabilities map[string]interface{} `json:"capabilities"`
}
@@ -663,7 +663,7 @@ type updateModelConfigRequest struct {
func (h *AdminHandler) ListModelConfigs(c *gin.Context) {
rows, err := database.DB.Query(`
SELECT mc.id, mc.api_config_id, ac.name, mc.model_id, mc.display_name,
mc.is_enabled, mc.capabilities, mc.created_at, mc.updated_at
mc.visibility, mc.capabilities, mc.created_at, mc.updated_at
FROM model_configs mc
JOIN api_configs ac ON mc.api_config_id = ac.id
WHERE ac.user_id IS NULL
@@ -681,7 +681,7 @@ func (h *AdminHandler) ListModelConfigs(c *gin.Context) {
var capsJSON []byte
if err := rows.Scan(
&m.ID, &m.APIConfigID, &m.ProviderName, &m.ModelID, &m.DisplayName,
&m.IsEnabled, &capsJSON, &m.CreatedAt, &m.UpdatedAt,
&m.Visibility, &capsJSON, &m.CreatedAt, &m.UpdatedAt,
); err != nil {
continue
}
@@ -795,13 +795,18 @@ func (h *AdminHandler) UpdateModelConfig(c *gin.Context) {
}
// Build dynamic update
if req.IsEnabled != nil {
if req.Visibility != nil {
v := *req.Visibility
if v != "enabled" && v != "disabled" && v != "team" {
c.JSON(http.StatusBadRequest, gin.H{"error": "visibility must be enabled, disabled, or team"})
return
}
_, err := database.DB.Exec(
`UPDATE model_configs SET is_enabled = $1, updated_at = NOW() WHERE id = $2`,
*req.IsEnabled, modelID,
`UPDATE model_configs SET visibility = $1, updated_at = NOW() WHERE id = $2`,
v, modelID,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update enabled"})
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update visibility"})
return
}
}
@@ -836,19 +841,24 @@ func (h *AdminHandler) UpdateModelConfig(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "model updated"})
}
// BulkUpdateModels enables or disables all models at once
// BulkUpdateModels sets visibility for all models at once
func (h *AdminHandler) BulkUpdateModels(c *gin.Context) {
var req struct {
IsEnabled bool `json:"is_enabled"`
Visibility string `json:"visibility"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.Visibility != "enabled" && req.Visibility != "disabled" && req.Visibility != "team" {
c.JSON(http.StatusBadRequest, gin.H{"error": "visibility must be enabled, disabled, or team"})
return
}
result, err := database.DB.Exec(
`UPDATE model_configs SET is_enabled = $1, updated_at = NOW()`,
req.IsEnabled,
`UPDATE model_configs SET visibility = $1, updated_at = NOW()
WHERE api_config_id IN (SELECT id FROM api_configs WHERE user_id IS NULL)`,
req.Visibility,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to bulk update models"})

View File

@@ -448,7 +448,7 @@ func (h *APIConfigHandler) ListEnabledModels(c *gin.Context) {
SELECT mc.id, mc.model_id, mc.display_name, ac.provider, ac.name, mc.api_config_id, mc.capabilities
FROM model_configs mc
JOIN api_configs ac ON mc.api_config_id = ac.id
WHERE mc.is_enabled = true AND ac.is_active = true AND ac.user_id IS NULL
WHERE mc.visibility = 'enabled' AND ac.is_active = true AND ac.user_id IS NULL
ORDER BY ac.name, mc.model_id
`)
if err == nil {

View File

@@ -455,6 +455,49 @@ func (h *TeamHandler) MyTeams(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"data": teams})
}
// ── Team Models: Available for Presets ──────
// ListAvailableModels returns models with visibility 'enabled' or 'team'
// for team admins building presets. Requires RequireTeamAdmin middleware.
// GET /api/v1/teams/:teamId/models
func (h *TeamHandler) ListAvailableModels(c *gin.Context) {
rows, err := database.DB.Query(`
SELECT mc.id, mc.model_id, mc.display_name, mc.visibility,
ac.provider, ac.name as provider_name
FROM model_configs mc
JOIN api_configs ac ON mc.api_config_id = ac.id
WHERE mc.visibility IN ('enabled', 'team')
AND ac.is_active = true AND ac.user_id IS NULL
ORDER BY ac.name, mc.model_id
`)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"})
return
}
defer rows.Close()
type availableModel struct {
ID string `json:"id"`
ModelID string `json:"model_id"`
DisplayName *string `json:"display_name"`
Visibility string `json:"visibility"`
Provider string `json:"provider"`
ProviderName string `json:"provider_name"`
}
models := make([]availableModel, 0)
for rows.Next() {
var m availableModel
if err := rows.Scan(&m.ID, &m.ModelID, &m.DisplayName, &m.Visibility,
&m.Provider, &m.ProviderName); err != nil {
continue
}
models = append(models, m)
}
c.JSON(http.StatusOK, gin.H{"models": models})
}
// ── Helpers ─────────────────────────────────
// getTeamID extracts team ID from either :id (admin routes) or :teamId (team-scoped routes).

View File

@@ -164,6 +164,7 @@ func main() {
teamScoped.POST("/members", teams.AddMember)
teamScoped.PUT("/members/:memberId", teams.UpdateMember)
teamScoped.DELETE("/members/:memberId", teams.RemoveMember)
teamScoped.GET("/models", teams.ListAvailableModels)
// Team presets
teamPresets := handlers.NewPresetHandler()