Pulse/internal/api/config_profiles.go
rcourtman 743ef17b79 Fix AI and config profile handlers broken in v5 single-tenant mode
The single-tenant lockdown (499ab812e) set mtPersistence to nil but
only patched AISettingsHandler with a legacy fallback. AIHandler (chat
service) and ConfigProfileHandler were missed, so AI features (Patrol,
Chat) failed with "chat service not available" and config profiles
would panic on nil dereference. Wire legacy persistence into both
handlers and add the same fallback to ProfileSuggestionHandler.

Fixes #1322
2026-03-06 11:05:01 +00:00

1004 lines
29 KiB
Go

package api
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"sync"
"time"
"github.com/google/uuid"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/rs/zerolog/log"
)
// ConfigProfileHandler handles configuration profile operations
type ConfigProfileHandler struct {
mtPersistence *config.MultiTenantPersistence
legacyPersistence *config.ConfigPersistence
validator *models.ProfileValidator
mu sync.RWMutex
suggestionHandler *ProfileSuggestionHandler
}
// NewConfigProfileHandler creates a new handler
func NewConfigProfileHandler(mtp *config.MultiTenantPersistence) *ConfigProfileHandler {
return &ConfigProfileHandler{
mtPersistence: mtp,
validator: models.NewProfileValidator(),
}
}
// SetLegacyPersistence sets the single-tenant persistence fallback.
func (h *ConfigProfileHandler) SetLegacyPersistence(p *config.ConfigPersistence) {
h.legacyPersistence = p
}
// getPersistence resolves the persistence instance for the current tenant
func (h *ConfigProfileHandler) getPersistence(ctx context.Context) (*config.ConfigPersistence, error) {
if h.mtPersistence != nil {
orgID := GetOrgID(ctx)
return h.mtPersistence.GetPersistence(orgID)
}
if h.legacyPersistence != nil {
return h.legacyPersistence, nil
}
return nil, fmt.Errorf("no persistence available")
}
// SetAIHandler sets the AI handler for profile suggestions
func (h *ConfigProfileHandler) SetAIHandler(aiHandler *AIHandler) {
h.suggestionHandler = NewProfileSuggestionHandler(h.mtPersistence, h.legacyPersistence, aiHandler)
}
// ServeHTTP implements the http.Handler interface
func (h *ConfigProfileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Simple routing
path := strings.TrimSuffix(r.URL.Path, "/")
if path == "" || path == "/" {
if r.Method == http.MethodGet {
h.ListProfiles(w, r)
return
} else if r.Method == http.MethodPost {
h.CreateProfile(w, r)
return
}
} else if path == "/assignments" {
if r.Method == http.MethodGet {
h.ListAssignments(w, r)
return
} else if r.Method == http.MethodPost {
h.AssignProfile(w, r)
return
}
} else if strings.HasPrefix(path, "/assignments/") {
if r.Method == http.MethodDelete {
agentID := strings.TrimPrefix(path, "/assignments/")
h.UnassignProfile(w, r, agentID)
return
}
} else if path == "/schema" {
// GET /schema - Return config key definitions
if r.Method == http.MethodGet {
h.GetConfigSchema(w, r)
return
}
} else if path == "/validate" {
// POST /validate - Validate a config without saving
if r.Method == http.MethodPost {
h.ValidateConfig(w, r)
return
}
} else if path == "/suggestions" {
// POST /suggestions - AI-assisted profile suggestion
if r.Method == http.MethodPost {
if h.suggestionHandler != nil {
h.suggestionHandler.HandleSuggestProfile(w, r)
} else {
http.Error(w, "Pulse Assistant service not configured", http.StatusServiceUnavailable)
}
return
}
} else if path == "/changelog" {
// GET /changelog - Return profile change history
if r.Method == http.MethodGet {
h.GetChangeLog(w, r)
return
}
} else if path == "/deployments" {
// GET /deployments - Return deployment status
// POST /deployments - Update deployment status from agent
if r.Method == http.MethodGet {
h.GetDeploymentStatus(w, r)
return
} else if r.Method == http.MethodPost {
h.UpdateDeploymentStatus(w, r)
return
}
} else if strings.HasSuffix(path, "/versions") {
// GET /{id}/versions - Get version history for a profile
id := strings.TrimSuffix(path, "/versions")
id = strings.TrimPrefix(id, "/")
if r.Method == http.MethodGet {
h.GetProfileVersions(w, r, id)
return
}
} else if strings.Contains(path, "/rollback/") {
// POST /{id}/rollback/{version} - Rollback to a specific version
parts := strings.Split(path, "/")
if len(parts) >= 3 && r.Method == http.MethodPost {
// parts: ["", "id", "rollback", "version"]
id := parts[1]
version := parts[len(parts)-1]
h.RollbackProfile(w, r, id, version)
return
}
} else {
// ID parameters
// Expecting /{id}
id := strings.TrimPrefix(path, "/")
if r.Method == http.MethodGet {
h.GetProfile(w, r, id)
return
} else if r.Method == http.MethodPut {
h.UpdateProfile(w, r, id)
return
} else if r.Method == http.MethodDelete {
h.DeleteProfile(w, r, id)
return
}
}
http.Error(w, "Not found", http.StatusNotFound)
}
// ListProfiles returns all profiles
func (h *ConfigProfileHandler) ListProfiles(w http.ResponseWriter, r *http.Request) {
persistence, err := h.getPersistence(r.Context())
if err != nil {
log.Error().Err(err).Msg("Failed to get persistence for tenant")
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
return
}
profiles, err := persistence.LoadAgentProfiles()
if err != nil {
log.Error().Err(err).Msg("Failed to load profiles")
http.Error(w, "Failed to load profiles", http.StatusInternalServerError)
return
}
// Return empty array instead of null
if profiles == nil {
profiles = []models.AgentProfile{}
}
json.NewEncoder(w).Encode(profiles)
}
// CreateProfile creates a new profile
func (h *ConfigProfileHandler) CreateProfile(w http.ResponseWriter, r *http.Request) {
var input struct {
models.AgentProfile
ChangeNote string `json:"change_note,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if input.Name == "" {
http.Error(w, "Name is required", http.StatusBadRequest)
return
}
// Validate configuration
if input.Config != nil {
result := h.validator.Validate(input.Config)
if !result.Valid {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": "validation_failed",
"message": "Configuration validation failed",
"errors": result.Errors,
"warnings": result.Warnings,
})
return
}
}
h.mu.Lock()
defer h.mu.Unlock()
persistence, err := h.getPersistence(r.Context())
if err != nil {
log.Error().Err(err).Msg("Failed to get persistence for tenant")
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
return
}
profiles, err := persistence.LoadAgentProfiles()
if err != nil {
http.Error(w, "Failed to load profiles", http.StatusInternalServerError)
return
}
// Get username from context
username := getUsernameFromRequest(r)
input.ID = uuid.New().String()
input.Version = 1
input.CreatedAt = time.Now()
input.UpdatedAt = time.Now()
input.CreatedBy = username
input.UpdatedBy = username
profiles = append(profiles, input.AgentProfile)
if err := persistence.SaveAgentProfiles(profiles); err != nil {
log.Error().Err(err).Msg("Failed to save profiles")
http.Error(w, "Failed to save profile", http.StatusInternalServerError)
return
}
// Save initial version to history
version := models.AgentProfileVersion{
ProfileID: input.ID,
Version: 1,
Name: input.Name,
Description: input.Description,
Config: input.Config,
ParentID: input.ParentID,
CreatedAt: input.CreatedAt,
CreatedBy: username,
ChangeNote: input.ChangeNote,
}
h.saveVersionHistory(persistence, version)
// Log change
h.logChange(persistence, models.ProfileChangeLog{
ID: uuid.New().String(),
ProfileID: input.ID,
ProfileName: input.Name,
Action: "create",
NewVersion: 1,
User: username,
Timestamp: time.Now(),
Details: input.ChangeNote,
})
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(input.AgentProfile)
}
// getUsernameFromRequest extracts the username from the request context
func getUsernameFromRequest(r *http.Request) string {
if username, ok := r.Context().Value("username").(string); ok {
return username
}
return ""
}
// UpdateProfile updates an existing profile
func (h *ConfigProfileHandler) UpdateProfile(w http.ResponseWriter, r *http.Request, id string) {
if id == "" {
http.Error(w, "ID is required", http.StatusBadRequest)
return
}
var input struct {
models.AgentProfile
ChangeNote string `json:"change_note,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
// Validate configuration
if input.Config != nil {
result := h.validator.Validate(input.Config)
if !result.Valid {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": "validation_failed",
"message": "Configuration validation failed",
"errors": result.Errors,
"warnings": result.Warnings,
})
return
}
}
h.mu.Lock()
defer h.mu.Unlock()
persistence, err := h.getPersistence(r.Context())
if err != nil {
log.Error().Err(err).Msg("Failed to get persistence for tenant")
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
return
}
profiles, err := persistence.LoadAgentProfiles()
if err != nil {
http.Error(w, "Failed to load profiles", http.StatusInternalServerError)
return
}
username := getUsernameFromRequest(r)
found := false
var oldVersion int
var updatedProfile models.AgentProfile
for i, p := range profiles {
if p.ID == id {
oldVersion = p.Version
profiles[i].Name = input.Name
profiles[i].Description = input.Description
profiles[i].Config = input.Config
profiles[i].ParentID = input.ParentID
profiles[i].Version = p.Version + 1
profiles[i].UpdatedAt = time.Now()
profiles[i].UpdatedBy = username
updatedProfile = profiles[i]
found = true
break
}
}
if !found {
http.Error(w, "Profile not found", http.StatusNotFound)
return
}
if err := persistence.SaveAgentProfiles(profiles); err != nil {
log.Error().Err(err).Msg("Failed to save profiles")
http.Error(w, "Failed to save profile", http.StatusInternalServerError)
return
}
// Save new version to history
version := models.AgentProfileVersion{
ProfileID: id,
Version: updatedProfile.Version,
Name: updatedProfile.Name,
Description: updatedProfile.Description,
Config: updatedProfile.Config,
ParentID: updatedProfile.ParentID,
CreatedAt: updatedProfile.UpdatedAt,
CreatedBy: username,
ChangeNote: input.ChangeNote,
}
h.saveVersionHistory(persistence, version)
// Log change
h.logChange(persistence, models.ProfileChangeLog{
ID: uuid.New().String(),
ProfileID: id,
ProfileName: updatedProfile.Name,
Action: "update",
OldVersion: oldVersion,
NewVersion: updatedProfile.Version,
User: username,
Timestamp: time.Now(),
Details: input.ChangeNote,
})
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(updatedProfile)
}
// DeleteProfile deletes a profile
func (h *ConfigProfileHandler) DeleteProfile(w http.ResponseWriter, r *http.Request, id string) {
h.mu.Lock()
defer h.mu.Unlock()
persistence, err := h.getPersistence(r.Context())
if err != nil {
log.Error().Err(err).Msg("Failed to get persistence for tenant")
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
return
}
profiles, err := persistence.LoadAgentProfiles()
if err != nil {
http.Error(w, "Failed to load profiles", http.StatusInternalServerError)
return
}
var deletedProfile *models.AgentProfile
newProfiles := []models.AgentProfile{}
for _, p := range profiles {
if p.ID != id {
newProfiles = append(newProfiles, p)
} else {
deletedProfile = &p
}
}
if len(newProfiles) == len(profiles) {
http.Error(w, "Profile not found", http.StatusNotFound)
return
}
if err := persistence.SaveAgentProfiles(newProfiles); err != nil {
log.Error().Err(err).Msg("Failed to save profiles")
http.Error(w, "Failed to delete profile", http.StatusInternalServerError)
return
}
assignments, err := persistence.LoadAgentProfileAssignments()
if err != nil {
log.Error().Err(err).Msg("Failed to load assignments for profile cleanup")
http.Error(w, "Failed to delete profile assignments", http.StatusInternalServerError)
return
}
cleaned := assignments[:0]
for _, a := range assignments {
if a.ProfileID != id {
cleaned = append(cleaned, a)
}
}
if len(cleaned) != len(assignments) {
if err := persistence.SaveAgentProfileAssignments(cleaned); err != nil {
log.Error().Err(err).Msg("Failed to clean up assignments for deleted profile")
http.Error(w, "Failed to delete profile assignments", http.StatusInternalServerError)
return
}
}
// Log deletion
username := getUsernameFromRequest(r)
if deletedProfile != nil {
h.logChange(persistence, models.ProfileChangeLog{
ID: uuid.New().String(),
ProfileID: id,
ProfileName: deletedProfile.Name,
Action: "delete",
OldVersion: deletedProfile.Version,
User: username,
Timestamp: time.Now(),
})
}
w.WriteHeader(http.StatusOK)
}
// ListAssignments returns all assignments
func (h *ConfigProfileHandler) ListAssignments(w http.ResponseWriter, r *http.Request) {
persistence, err := h.getPersistence(r.Context())
if err != nil {
log.Error().Err(err).Msg("Failed to get persistence for tenant")
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
return
}
assignments, err := persistence.LoadAgentProfileAssignments()
if err != nil {
log.Error().Err(err).Msg("Failed to load assignments")
http.Error(w, "Failed to load assignments", http.StatusInternalServerError)
return
}
// Return empty array instead of null
if assignments == nil {
assignments = []models.AgentProfileAssignment{}
}
json.NewEncoder(w).Encode(assignments)
}
// AssignProfile assigns a profile to an agent
func (h *ConfigProfileHandler) AssignProfile(w http.ResponseWriter, r *http.Request) {
var input models.AgentProfileAssignment
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if input.AgentID == "" || input.ProfileID == "" {
http.Error(w, "AgentID and ProfileID are required", http.StatusBadRequest)
return
}
h.mu.Lock()
defer h.mu.Unlock()
persistence, err := h.getPersistence(r.Context())
if err != nil {
log.Error().Err(err).Msg("Failed to get persistence for tenant")
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
return
}
assignments, err := persistence.LoadAgentProfileAssignments()
if err != nil {
http.Error(w, "Failed to load assignments", http.StatusInternalServerError)
return
}
// Remove existing assignment for this agent if exists
newAssignments := []models.AgentProfileAssignment{}
for _, a := range assignments {
if a.AgentID != input.AgentID {
newAssignments = append(newAssignments, a)
}
}
username := getUsernameFromRequest(r)
input.UpdatedAt = time.Now()
input.AssignedBy = username
newAssignments = append(newAssignments, input)
if err := persistence.SaveAgentProfileAssignments(newAssignments); err != nil {
log.Error().Err(err).Msg("Failed to save assignments")
http.Error(w, "Failed to save assignment", http.StatusInternalServerError)
return
}
// Get profile name for logging
profiles, _ := persistence.LoadAgentProfiles()
var profileName string
for _, p := range profiles {
if p.ID == input.ProfileID {
profileName = p.Name
break
}
}
// Log assignment
h.logChange(persistence, models.ProfileChangeLog{
ID: uuid.New().String(),
ProfileID: input.ProfileID,
ProfileName: profileName,
Action: "assign",
AgentID: input.AgentID,
User: username,
Timestamp: time.Now(),
})
tokenID := ""
if record := getAPITokenRecordFromRequest(r); record != nil {
tokenID = record.ID
}
LogAuditEventForTenant(GetOrgID(r.Context()), "agent_profile_assigned", username, GetClientIP(r), r.URL.Path, true,
fmt.Sprintf("agent_id=%s profile_id=%s token_id=%s", input.AgentID, input.ProfileID, tokenID))
json.NewEncoder(w).Encode(input)
}
// UnassignProfile removes a profile assignment for an agent.
func (h *ConfigProfileHandler) UnassignProfile(w http.ResponseWriter, r *http.Request, agentID string) {
agentID = strings.TrimSpace(agentID)
if agentID == "" {
http.Error(w, "AgentID is required", http.StatusBadRequest)
return
}
h.mu.Lock()
defer h.mu.Unlock()
persistence, err := h.getPersistence(r.Context())
if err != nil {
log.Error().Err(err).Msg("Failed to get persistence for tenant")
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
return
}
assignments, err := persistence.LoadAgentProfileAssignments()
if err != nil {
http.Error(w, "Failed to load assignments", http.StatusInternalServerError)
return
}
var removedAssignment *models.AgentProfileAssignment
newAssignments := []models.AgentProfileAssignment{}
for _, a := range assignments {
if a.AgentID != agentID {
newAssignments = append(newAssignments, a)
} else {
removedAssignment = &a
}
}
if len(newAssignments) != len(assignments) {
if err := persistence.SaveAgentProfileAssignments(newAssignments); err != nil {
log.Error().Err(err).Msg("Failed to save assignments")
http.Error(w, "Failed to save assignment", http.StatusInternalServerError)
return
}
// Log unassignment
if removedAssignment != nil {
username := getUsernameFromRequest(r)
// Get profile name for logging
profiles, _ := persistence.LoadAgentProfiles()
var profileName string
for _, p := range profiles {
if p.ID == removedAssignment.ProfileID {
profileName = p.Name
break
}
}
h.logChange(persistence, models.ProfileChangeLog{
ID: uuid.New().String(),
ProfileID: removedAssignment.ProfileID,
ProfileName: profileName,
Action: "unassign",
AgentID: agentID,
User: username,
Timestamp: time.Now(),
})
tokenID := ""
if record := getAPITokenRecordFromRequest(r); record != nil {
tokenID = record.ID
}
LogAuditEventForTenant(GetOrgID(r.Context()), "agent_profile_unassigned", username, GetClientIP(r), r.URL.Path, true,
fmt.Sprintf("agent_id=%s profile_id=%s token_id=%s", agentID, removedAssignment.ProfileID, tokenID))
}
}
w.WriteHeader(http.StatusNoContent)
}
// GetProfile returns a single profile by ID
func (h *ConfigProfileHandler) GetProfile(w http.ResponseWriter, r *http.Request, id string) {
persistence, err := h.getPersistence(r.Context())
if err != nil {
log.Error().Err(err).Msg("Failed to get persistence for tenant")
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
return
}
profiles, err := persistence.LoadAgentProfiles()
if err != nil {
log.Error().Err(err).Msg("Failed to load profiles")
http.Error(w, "Failed to load profiles", http.StatusInternalServerError)
return
}
for _, p := range profiles {
if p.ID == id {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(p)
return
}
}
http.Error(w, "Profile not found", http.StatusNotFound)
}
// GetConfigSchema returns the configuration key definitions
func (h *ConfigProfileHandler) GetConfigSchema(w http.ResponseWriter, r *http.Request) {
definitions := models.GetConfigKeyDefinitions()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(definitions)
}
// ValidateConfig validates a configuration without saving
func (h *ConfigProfileHandler) ValidateConfig(w http.ResponseWriter, r *http.Request) {
var config models.AgentConfigMap
if err := json.NewDecoder(r.Body).Decode(&config); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
result := h.validator.Validate(config)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
}
// GetChangeLog returns profile change history
func (h *ConfigProfileHandler) GetChangeLog(w http.ResponseWriter, r *http.Request) {
persistence, err := h.getPersistence(r.Context())
if err != nil {
log.Error().Err(err).Msg("Failed to get persistence for tenant")
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
return
}
logs, err := persistence.LoadProfileChangeLogs()
if err != nil {
log.Error().Err(err).Msg("Failed to load change logs")
http.Error(w, "Failed to load change logs", http.StatusInternalServerError)
return
}
// Filter by profile_id if specified
profileID := r.URL.Query().Get("profile_id")
if profileID != "" {
filtered := []models.ProfileChangeLog{}
for _, entry := range logs {
if entry.ProfileID == profileID {
filtered = append(filtered, entry)
}
}
logs = filtered
}
// Return empty array instead of null
if logs == nil {
logs = []models.ProfileChangeLog{}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(logs)
}
// GetDeploymentStatus returns deployment status for all agents
func (h *ConfigProfileHandler) GetDeploymentStatus(w http.ResponseWriter, r *http.Request) {
persistence, err := h.getPersistence(r.Context())
if err != nil {
log.Error().Err(err).Msg("Failed to get persistence for tenant")
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
return
}
status, err := persistence.LoadProfileDeploymentStatus()
if err != nil {
log.Error().Err(err).Msg("Failed to load deployment status")
http.Error(w, "Failed to load deployment status", http.StatusInternalServerError)
return
}
// Filter by agent_id if specified
agentID := r.URL.Query().Get("agent_id")
if agentID != "" {
filtered := []models.ProfileDeploymentStatus{}
for _, s := range status {
if s.AgentID == agentID {
filtered = append(filtered, s)
}
}
status = filtered
}
// Return empty array instead of null
if status == nil {
status = []models.ProfileDeploymentStatus{}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(status)
}
// UpdateDeploymentStatus updates deployment status from an agent
func (h *ConfigProfileHandler) UpdateDeploymentStatus(w http.ResponseWriter, r *http.Request) {
var input models.ProfileDeploymentStatus
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if input.AgentID == "" || input.ProfileID == "" {
http.Error(w, "AgentID and ProfileID are required", http.StatusBadRequest)
return
}
// Validate deployment status
validStatuses := []string{"pending", "deployed", "failed"}
validStatus := false
for _, s := range validStatuses {
if input.DeploymentStatus == s {
validStatus = true
break
}
}
if !validStatus {
http.Error(w, "Invalid deployment status. Must be 'pending', 'deployed', or 'failed'", http.StatusBadRequest)
return
}
h.mu.Lock()
defer h.mu.Unlock()
persistence, err := h.getPersistence(r.Context())
if err != nil {
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
return
}
statuses, err := persistence.LoadProfileDeploymentStatus()
if err != nil {
http.Error(w, "Failed to load deployment status", http.StatusInternalServerError)
return
}
// Update or add the status
found := false
for i, s := range statuses {
if s.AgentID == input.AgentID && s.ProfileID == input.ProfileID {
statuses[i].DeployedVersion = input.DeployedVersion
statuses[i].DeploymentStatus = input.DeploymentStatus
statuses[i].ErrorMessage = input.ErrorMessage
statuses[i].LastDeployedAt = time.Now()
found = true
break
}
}
if !found {
input.LastDeployedAt = time.Now()
statuses = append(statuses, input)
}
if err := persistence.SaveProfileDeploymentStatus(statuses); err != nil {
log.Error().Err(err).Msg("Failed to save deployment status")
http.Error(w, "Failed to save deployment status", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(input)
}
// GetProfileVersions returns version history for a profile
func (h *ConfigProfileHandler) GetProfileVersions(w http.ResponseWriter, r *http.Request, profileID string) {
persistence, err := h.getPersistence(r.Context())
if err != nil {
log.Error().Err(err).Msg("Failed to get persistence for tenant")
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
return
}
versions, err := persistence.LoadAgentProfileVersions()
if err != nil {
log.Error().Err(err).Msg("Failed to load profile versions")
http.Error(w, "Failed to load profile versions", http.StatusInternalServerError)
return
}
// Filter by profile ID
filtered := []models.AgentProfileVersion{}
for _, v := range versions {
if v.ProfileID == profileID {
filtered = append(filtered, v)
}
}
// Return empty array instead of null
if filtered == nil {
filtered = []models.AgentProfileVersion{}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(filtered)
}
// RollbackProfile rolls back a profile to a specific version
func (h *ConfigProfileHandler) RollbackProfile(w http.ResponseWriter, r *http.Request, profileID string, versionStr string) {
var targetVersion int
if _, err := fmt.Sscanf(versionStr, "%d", &targetVersion); err != nil {
http.Error(w, "Invalid version number", http.StatusBadRequest)
return
}
h.mu.Lock()
defer h.mu.Unlock()
persistence, err := h.getPersistence(r.Context())
if err != nil {
log.Error().Err(err).Msg("Failed to get persistence for tenant")
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
return
}
// Load version history to find the target version
versions, err := persistence.LoadAgentProfileVersions()
if err != nil {
http.Error(w, "Failed to load profile versions", http.StatusInternalServerError)
return
}
var targetVersionData *models.AgentProfileVersion
for i, v := range versions {
if v.ProfileID == profileID && v.Version == targetVersion {
targetVersionData = &versions[i]
break
}
}
if targetVersionData == nil {
http.Error(w, "Version not found", http.StatusNotFound)
return
}
// Load current profiles
profiles, err := persistence.LoadAgentProfiles()
if err != nil {
http.Error(w, "Failed to load profiles", http.StatusInternalServerError)
return
}
username := getUsernameFromRequest(r)
found := false
var oldVersion int
var updatedProfile models.AgentProfile
for i, p := range profiles {
if p.ID == profileID {
oldVersion = p.Version
profiles[i].Name = targetVersionData.Name
profiles[i].Description = targetVersionData.Description
profiles[i].Config = targetVersionData.Config
profiles[i].ParentID = targetVersionData.ParentID
profiles[i].Version = p.Version + 1
profiles[i].UpdatedAt = time.Now()
profiles[i].UpdatedBy = username
updatedProfile = profiles[i]
found = true
break
}
}
if !found {
http.Error(w, "Profile not found", http.StatusNotFound)
return
}
if err := persistence.SaveAgentProfiles(profiles); err != nil {
log.Error().Err(err).Msg("Failed to save profiles after rollback")
http.Error(w, "Failed to rollback profile", http.StatusInternalServerError)
return
}
// Save new version to history
version := models.AgentProfileVersion{
ProfileID: profileID,
Version: updatedProfile.Version,
Name: updatedProfile.Name,
Description: updatedProfile.Description,
Config: updatedProfile.Config,
ParentID: updatedProfile.ParentID,
CreatedAt: updatedProfile.UpdatedAt,
CreatedBy: username,
ChangeNote: fmt.Sprintf("Rolled back to version %d", targetVersion),
}
h.saveVersionHistory(persistence, version)
// Log rollback
h.logChange(persistence, models.ProfileChangeLog{
ID: uuid.New().String(),
ProfileID: profileID,
ProfileName: updatedProfile.Name,
Action: "rollback",
OldVersion: oldVersion,
NewVersion: updatedProfile.Version,
User: username,
Timestamp: time.Now(),
Details: fmt.Sprintf("Rolled back from version %d to version %d", oldVersion, targetVersion),
})
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(updatedProfile)
}
// saveVersionHistory saves a version to the history
func (h *ConfigProfileHandler) saveVersionHistory(persistence *config.ConfigPersistence, version models.AgentProfileVersion) {
versions, err := persistence.LoadAgentProfileVersions()
if err != nil {
log.Error().Err(err).Msg("Failed to load version history")
return
}
versions = append(versions, version)
if err := persistence.SaveAgentProfileVersions(versions); err != nil {
log.Error().Err(err).Msg("Failed to save version history")
}
}
// logChange logs a profile change to the change log
func (h *ConfigProfileHandler) logChange(persistence *config.ConfigPersistence, entry models.ProfileChangeLog) {
if err := persistence.AppendProfileChangeLog(entry); err != nil {
log.Error().Err(err).Msg("Failed to log profile change")
}
}