package handlers import ( "context" "fmt" "net/http" "github.com/gin-gonic/gin" "github.com/tradarr/backend/internal/ai" "github.com/tradarr/backend/internal/httputil" "github.com/tradarr/backend/internal/models" ) // ── Credentials ──────────────────────────────────────────────────────────── type credentialsRequest struct { SourceID string `json:"source_id" binding:"required"` Username string `json:"username" binding:"required"` Password string `json:"password"` } func (h *Handler) GetCredentials(c *gin.Context) { sources, err := h.repo.ListSources() if err != nil { httputil.InternalError(c, err) return } type credResponse struct { SourceID string `json:"source_id"` SourceName string `json:"source_name"` Username string `json:"username"` HasPassword bool `json:"has_password"` } var result []credResponse for _, src := range sources { if src.Type != "bloomberg" { continue } cred, _ := h.repo.GetCredentials(src.ID) r := credResponse{SourceID: src.ID, SourceName: src.Name} if cred != nil { r.Username = cred.Username r.HasPassword = cred.PasswordEncrypted != "" } result = append(result, r) } httputil.OK(c, result) } func (h *Handler) UpsertCredentials(c *gin.Context) { var req credentialsRequest if err := c.ShouldBindJSON(&req); err != nil { httputil.BadRequest(c, err) return } encPwd := "" if req.Password != "" { var err error encPwd, err = h.enc.Encrypt(req.Password) if err != nil { httputil.InternalError(c, err) return } } if err := h.repo.UpsertCredentials(req.SourceID, req.Username, encPwd); err != nil { httputil.InternalError(c, err) return } httputil.OK(c, gin.H{"ok": true}) } // ── AI Providers ─────────────────────────────────────────────────────────── type aiProviderRequest struct { Name string `json:"name" binding:"required"` APIKey string `json:"api_key"` Model string `json:"model"` Endpoint string `json:"endpoint"` } func (h *Handler) ListAIProviders(c *gin.Context) { providers, err := h.repo.ListAIProviders() if err != nil { httputil.InternalError(c, err) return } // Ne pas exposer les clés chiffrées — juste indiquer si elle existe type safeProvider struct { ID string `json:"id"` Name string `json:"name"` Model string `json:"model"` Endpoint string `json:"endpoint"` IsActive bool `json:"is_active"` HasKey bool `json:"has_key"` } var result []safeProvider for _, p := range providers { result = append(result, safeProvider{ ID: p.ID, Name: p.Name, Model: p.Model, Endpoint: p.Endpoint, IsActive: p.IsActive, HasKey: p.APIKeyEncrypted != "", }) } httputil.OK(c, result) } func (h *Handler) CreateAIProvider(c *gin.Context) { var req aiProviderRequest if err := c.ShouldBindJSON(&req); err != nil { httputil.BadRequest(c, err) return } encKey := "" if req.APIKey != "" { var err error encKey, err = h.enc.Encrypt(req.APIKey) if err != nil { httputil.InternalError(c, err) return } } p, err := h.repo.CreateAIProvider(req.Name, encKey, req.Model, req.Endpoint) if err != nil { httputil.InternalError(c, err) return } httputil.Created(c, p) } func (h *Handler) UpdateAIProvider(c *gin.Context) { id := c.Param("id") var req aiProviderRequest if err := c.ShouldBindJSON(&req); err != nil { httputil.BadRequest(c, err) return } existing, err := h.repo.GetAIProviderByID(id) if err != nil || existing == nil { httputil.NotFound(c) return } encKey := existing.APIKeyEncrypted if req.APIKey != "" { encKey, err = h.enc.Encrypt(req.APIKey) if err != nil { httputil.InternalError(c, err) return } } if err := h.repo.UpdateAIProvider(id, encKey, req.Model, req.Endpoint); err != nil { httputil.InternalError(c, err) return } httputil.OK(c, gin.H{"ok": true}) } func (h *Handler) SetActiveAIProvider(c *gin.Context) { id := c.Param("id") if err := h.repo.SetActiveAIProvider(id); err != nil { httputil.InternalError(c, err) return } httputil.OK(c, gin.H{"ok": true}) } func (h *Handler) DeleteAIProvider(c *gin.Context) { if err := h.repo.DeleteAIProvider(c.Param("id")); err != nil { httputil.InternalError(c, err) return } httputil.NoContent(c) } func (h *Handler) ProbeAIModels(c *gin.Context) { var req aiProviderRequest if err := c.ShouldBindJSON(&req); err != nil { httputil.BadRequest(c, err) return } provider, err := h.pipeline.BuildProvider(req.Name, req.APIKey, req.Endpoint) if err != nil { httputil.InternalError(c, err) return } models, err := provider.ListModels(c.Request.Context()) if err != nil { httputil.InternalError(c, err) return } httputil.OK(c, models) } func (h *Handler) ListAIModels(c *gin.Context) { id := c.Param("id") p, err := h.repo.GetAIProviderByID(id) if err != nil || p == nil { httputil.NotFound(c) return } apiKey := "" if p.APIKeyEncrypted != "" { apiKey, err = h.enc.Decrypt(p.APIKeyEncrypted) if err != nil { httputil.InternalError(c, err) return } } provider, err := h.pipeline.BuildProvider(p.Name, apiKey, p.Endpoint) if err != nil { httputil.InternalError(c, err) return } models, err := provider.ListModels(c.Request.Context()) if err != nil { httputil.InternalError(c, err) return } httputil.OK(c, models) } // ── Sources ──────────────────────────────────────────────────────────────── func (h *Handler) ListSources(c *gin.Context) { sources, err := h.repo.ListSources() if err != nil { httputil.InternalError(c, err) return } httputil.OK(c, sources) } type updateSourceRequest struct { Enabled bool `json:"enabled"` } func (h *Handler) UpdateSource(c *gin.Context) { var req updateSourceRequest if err := c.ShouldBindJSON(&req); err != nil { httputil.BadRequest(c, err) return } if err := h.repo.UpdateSource(c.Param("id"), req.Enabled); err != nil { httputil.InternalError(c, err) return } httputil.OK(c, gin.H{"ok": true}) } // ── Scrape Jobs ──────────────────────────────────────────────────────────── func (h *Handler) ListScrapeJobs(c *gin.Context) { jobs, err := h.repo.ListScrapeJobs(100) if err != nil { httputil.InternalError(c, err) return } httputil.OK(c, jobs) } func (h *Handler) TriggerScrapeJob(c *gin.Context) { type triggerRequest struct { SourceID string `json:"source_id" binding:"required"` } var req triggerRequest if err := c.ShouldBindJSON(&req); err != nil { httputil.BadRequest(c, err) return } go func() { if err := h.registry.Run(req.SourceID); err != nil { fmt.Printf("scrape job error: %v\n", err) } }() c.JSON(http.StatusAccepted, gin.H{"ok": true, "message": "job started"}) } // ── Settings ─────────────────────────────────────────────────────────────── func (h *Handler) ListSettings(c *gin.Context) { settings, err := h.repo.ListSettings() if err != nil { httputil.InternalError(c, err) return } httputil.OK(c, settings) } type updateSettingsRequest struct { Settings []models.Setting `json:"settings"` } func (h *Handler) UpdateSettings(c *gin.Context) { var req updateSettingsRequest if err := c.ShouldBindJSON(&req); err != nil { httputil.BadRequest(c, err) return } for _, s := range req.Settings { if err := h.repo.SetSetting(s.Key, s.Value); err != nil { httputil.InternalError(c, err) return } } httputil.OK(c, gin.H{"ok": true}) } // ── Schedule ─────────────────────────────────────────────────────────────── func (h *Handler) GetSchedule(c *gin.Context) { slots, err := h.repo.ListScheduleSlots() if err != nil { httputil.InternalError(c, err) return } httputil.OK(c, slots) } type scheduleRequest struct { Slots []struct { DayOfWeek int `json:"day_of_week"` Hour int `json:"hour"` Minute int `json:"minute"` } `json:"slots"` } func (h *Handler) UpdateSchedule(c *gin.Context) { var req scheduleRequest if err := c.ShouldBindJSON(&req); err != nil { httputil.BadRequest(c, err) return } slots := make([]models.ScheduleSlot, len(req.Slots)) for i, s := range req.Slots { slots[i] = models.ScheduleSlot{DayOfWeek: s.DayOfWeek, Hour: s.Hour, Minute: s.Minute} } if err := h.repo.ReplaceSchedule(slots); err != nil { httputil.InternalError(c, err) return } if err := h.scheduler.Reload(); err != nil { fmt.Printf("schedule reload: %v\n", err) } httputil.OK(c, gin.H{"ok": true}) } func (h *Handler) GetDefaultSystemPrompt(c *gin.Context) { httputil.OK(c, gin.H{"prompt": ai.DefaultSystemPrompt}) } // ── AI Roles ─────────────────────────────────────────────────────────────── func (h *Handler) GetAIRoles(c *gin.Context) { roles := []string{"summary", "report", "filter"} resp := gin.H{} for _, role := range roles { providerID, _ := h.repo.GetSetting("ai_role_" + role + "_provider") model, _ := h.repo.GetSetting("ai_role_" + role + "_model") resp[role] = gin.H{"provider_id": providerID, "model": model} } httputil.OK(c, resp) } type updateAIRoleRequest struct { ProviderID string `json:"provider_id"` Model string `json:"model"` } func (h *Handler) UpdateAIRole(c *gin.Context) { role := c.Param("role") if role != "summary" && role != "report" && role != "filter" { httputil.BadRequest(c, fmt.Errorf("invalid role: must be summary, report, or filter")) return } var req updateAIRoleRequest if err := c.ShouldBindJSON(&req); err != nil { httputil.BadRequest(c, err) return } if err := h.repo.SetRoleProvider(role, req.ProviderID, req.Model); err != nil { httputil.InternalError(c, err) return } httputil.OK(c, gin.H{"ok": true}) } // ── Ollama Model Management ──────────────────────────────────────────────── func (h *Handler) getOllamaManager() (*ai.OllamaProvider, error) { providers, err := h.repo.ListAIProviders() if err != nil { return nil, err } endpoint := "" for _, p := range providers { if p.Name == "ollama" { endpoint = p.Endpoint break } } if endpoint == "" { endpoint = "http://ollama:11434" } return ai.NewOllamaManager(endpoint), nil } func (h *Handler) ListOllamaModels(c *gin.Context) { mgr, err := h.getOllamaManager() if err != nil { httputil.InternalError(c, err) return } models, err := mgr.ListModelsDetailed(c.Request.Context()) if err != nil { httputil.InternalError(c, err) return } if models == nil { models = []ai.OllamaModelInfo{} } httputil.OK(c, models) } type pullModelRequest struct { Name string `json:"name" binding:"required"` } func (h *Handler) PullOllamaModel(c *gin.Context) { var req pullModelRequest if err := c.ShouldBindJSON(&req); err != nil { httputil.BadRequest(c, err) return } mgr, err := h.getOllamaManager() if err != nil { httputil.InternalError(c, err) return } go func() { if err := mgr.PullModel(context.Background(), req.Name); err != nil { fmt.Printf("[ollama] pull %s failed: %v\n", req.Name, err) } else { fmt.Printf("[ollama] pull %s completed\n", req.Name) } }() c.JSON(202, gin.H{"ok": true, "message": "pull started"}) } func (h *Handler) DeleteOllamaModel(c *gin.Context) { name := c.Param("name") mgr, err := h.getOllamaManager() if err != nil { httputil.InternalError(c, err) return } if err := mgr.DeleteModel(c.Request.Context(), name); err != nil { httputil.InternalError(c, err) return } httputil.NoContent(c) } // ── Admin Users ──────────────────────────────────────────────────────────── func (h *Handler) ListUsers(c *gin.Context) { users, err := h.repo.ListUsers() if err != nil { httputil.InternalError(c, err) return } httputil.OK(c, users) } type updateUserRequest struct { Email string `json:"email" binding:"required,email"` Role string `json:"role" binding:"required"` } func (h *Handler) UpdateAdminUser(c *gin.Context) { id := c.Param("id") var req updateUserRequest if err := c.ShouldBindJSON(&req); err != nil { httputil.BadRequest(c, err) return } if req.Role != "admin" && req.Role != "user" { httputil.BadRequest(c, fmt.Errorf("role must be admin or user")) return } user, err := h.repo.UpdateUser(id, req.Email, models.Role(req.Role)) if err != nil { httputil.InternalError(c, err) return } httputil.OK(c, user) } func (h *Handler) DeleteAdminUser(c *gin.Context) { id := c.Param("id") // Empêcher la suppression du dernier admin user, err := h.repo.GetUserByID(id) if err != nil || user == nil { httputil.NotFound(c) return } if user.Role == "admin" { count, _ := h.repo.CountAdmins() if count <= 1 { c.JSON(http.StatusBadRequest, gin.H{"error": "cannot delete the last admin"}) return } } if err := h.repo.DeleteUser(id); err != nil { httputil.InternalError(c, err) return } httputil.NoContent(c) }