package handlers import ( "fmt" "log" "net/http" "strconv" "time" "github.com/Fimeg/RedFlag/aggregator-server/internal/database/queries" "github.com/Fimeg/RedFlag/aggregator-server/internal/models" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/jmoiron/sqlx" ) // UnifiedUpdateHandler processes all update reports (metrics, package updates, etc.) type UnifiedUpdateHandler struct { db *sqlx.DB agentQueries *queries.AgentQueries updateQueries *queries.UpdateQueries subsystemQueries *queries.SubsystemQueries commandQueries *queries.CommandQueries agentHandler *AgentHandler logger *log.Logger } // NewUnifiedUpdateHandler creates a new update handler func NewUnifiedUpdateHandler(db *sqlx.DB, logger *log.Logger, ah *AgentHandler) *UnifiedUpdateHandler { return &UnifiedUpdateHandler{ db: db, agentQueries: queries.NewAgentQueries(db), updateQueries: queries.NewUpdateQueries(db), subsystemQueries: queries.NewSubsystemQueries(db), commandQueries: queries.NewCommandQueries(db), agentHandler: ah, logger: logger, } } // Report handles POST /api/v1/agents/:id/updates func (h *UnifiedUpdateHandler) Report(c *gin.Context) { agentIDStr := c.Param("id") agentID, err := uuid.Parse(agentIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"}) return } // Update last_seen timestamp if err := h.agentQueries.UpdateAgentLastSeen(agentID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update last seen"}) return } var report models.UpdateReportRequest if err := c.ShouldBindJSON(&report); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Validate report if err := h.validateReport(&report); err != nil { c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()}) return } // Route to appropriate handler based on report type if h.isPackageUpdateReport(&report) { if err := h.handlePackageUpdateReport(agentID, &report); err != nil { h.logger.Printf("Failed to handle package update report: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to process updates"}) return } } else { c.JSON(http.StatusBadRequest, gin.H{"error": "unknown report type"}) return } c.JSON(http.StatusOK, gin.H{ "status": "received", "count": len(report.Updates), "command_id": report.CommandID, }) } // validateReport validates the update report func (h *UnifiedUpdateHandler) validateReport(report *models.UpdateReportRequest) error { if report.Timestamp.IsZero() { return fmt.Errorf("timestamp is required") } // Validate updates if len(report.Updates) > 0 { for i, update := range report.Updates { if update.PackageName == "" { return fmt.Errorf("update[%d]: package name is required", i) } if update.PackageType == "" { return fmt.Errorf("update[%d]: package type is required", i) } if update.AvailableVersion == "" { return fmt.Errorf("update[%d]: available version is required", i) } } } return nil } // isPackageUpdateReport determines if the report contains package updates func (h *UnifiedUpdateHandler) isPackageUpdateReport(report *models.UpdateReportRequest) bool { return len(report.Updates) > 0 } // handlePackageUpdateReport processes package update data func (h *UnifiedUpdateHandler) handlePackageUpdateReport(agentID uuid.UUID, report *models.UpdateReportRequest) error { // Convert update report items to events events := make([]models.UpdateEvent, 0, len(report.Updates)) for _, item := range report.Updates { event := models.UpdateEvent{ ID: uuid.New(), AgentID: agentID, PackageType: item.PackageType, PackageName: item.PackageName, VersionFrom: item.CurrentVersion, VersionTo: item.AvailableVersion, Severity: item.Severity, RepositorySource: item.RepositorySource, Metadata: item.Metadata, EventType: "discovered", CreatedAt: report.Timestamp, } events = append(events, event) } // Store events in batch if err := h.updateQueries.CreateUpdateEventsBatch(events); err != nil { return fmt.Errorf("failed to create update events: %w", err) } return nil } // ListUpdates retrieves updates with filtering func (h *UnifiedUpdateHandler) ListUpdates(c *gin.Context) { filters := &models.UpdateFilters{ Status: c.Query("status"), Severity: c.Query("severity"), PackageType: c.Query("package_type"), } if agentIDStr := c.Query("agent_id"); agentIDStr != "" { agentID, err := uuid.Parse(agentIDStr) if err == nil { filters.AgentID = agentID } } page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "50")) filters.Page = page filters.PageSize = pageSize updates, total, err := h.updateQueries.ListUpdatesFromState(filters) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list updates"}) return } stats, err := h.updateQueries.GetAllUpdateStats() if err != nil { stats = &models.UpdateStats{} } c.JSON(http.StatusOK, gin.H{ "updates": updates, "total": total, "page": page, "page_size": pageSize, "stats": stats, }) } // GetUpdate retrieves a single update by ID func (h *UnifiedUpdateHandler) GetUpdate(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid update ID"}) return } update, err := h.updateQueries.GetUpdateByID(id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "update not found"}) return } c.JSON(http.StatusOK, update) } // ApproveUpdate marks an update as approved func (h *UnifiedUpdateHandler) ApproveUpdate(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid update ID"}) return } if err := h.updateQueries.ApproveUpdate(id, "admin"); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to approve update: %v", err)}) return } c.JSON(http.StatusOK, gin.H{"message": "update approved"}) } // isValidResult checks if the result value complies with the database constraint func isValidUpdateResult(result string) bool { validResults := map[string]bool{ "success": true, "failed": true, "partial": true, } return validResults[result] } // ReportLog handles update execution logs from agents func (h *UnifiedUpdateHandler) ReportLog(c *gin.Context) { agentID := c.MustGet("agent_id").(uuid.UUID) if err := h.agentQueries.UpdateAgentLastSeen(agentID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update last seen"}) return } var req models.UpdateLogRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } validResult := req.Result if !isValidUpdateResult(validResult) { if validResult == "timed_out" || validResult == "timeout" || validResult == "cancelled" { validResult = "failed" } else { validResult = "failed" } } logEntry := &models.UpdateLog{ ID: uuid.New(), AgentID: agentID, Action: req.Action, Result: validResult, Stdout: req.Stdout, Stderr: req.Stderr, ExitCode: req.ExitCode, DurationSeconds: req.DurationSeconds, ExecutedAt: time.Now(), } if err := h.updateQueries.CreateUpdateLog(logEntry); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save log"}) return } // Update command status if command_id is provided if req.CommandID != "" { commandID, err := uuid.Parse(req.CommandID) if err != nil { log.Printf("Warning: Invalid command ID format in log request: %s\n", req.CommandID) } else { result := models.JSONB{ "stdout": req.Stdout, "stderr": req.Stderr, "exit_code": req.ExitCode, "duration_seconds": req.DurationSeconds, "logged_at": time.Now(), } if req.Result == "success" || req.Result == "completed" { if err := h.commandQueries.MarkCommandCompleted(commandID, result); err != nil { log.Printf("Warning: Failed to mark command %s as completed: %v\n", commandID, err) } command, err := h.commandQueries.GetCommandByID(commandID) if err == nil && command.CommandType == models.CommandTypeConfirmDependencies { if packageName, ok := command.Params["package_name"].(string); ok { if packageType, ok := command.Params["package_type"].(string); ok { var completionTime *time.Time if loggedAtStr, ok := command.Result["logged_at"].(string); ok { if parsed, err := time.Parse(time.RFC3339Nano, loggedAtStr); err == nil { completionTime = &parsed } } if err := h.updateQueries.UpdatePackageStatus(agentID, packageType, packageName, "updated", nil, completionTime); err != nil { log.Printf("Warning: Failed to update package status for %s/%s: %v", packageType, packageName, err) } else { log.Printf("✅ Package %s (%s) marked as updated after successful installation", packageName, packageType) } } } } } else if req.Result == "failed" || req.Result == "dry_run_failed" { if err := h.commandQueries.MarkCommandFailed(commandID, result); err != nil { log.Printf("Warning: Failed to mark command %s as failed: %v\n", commandID, err) } } else { if err := h.commandQueries.UpdateCommandResult(commandID, result); err != nil { log.Printf("Warning: Failed to update command %s result: %v\n", commandID, err) } } } } c.JSON(http.StatusOK, gin.H{"message": "log recorded"}) } // GetPackageHistory returns version history for a specific package func (h *UnifiedUpdateHandler) GetPackageHistory(c *gin.Context) { agentIDStr := c.Param("agent_id") agentID, err := uuid.Parse(agentIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"}) return } packageType := c.Query("package_type") packageName := c.Query("package_name") if packageType == "" || packageName == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "package_type and package_name are required"}) return } limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10")) history, err := h.updateQueries.GetPackageHistory(agentID, packageType, packageName, limit) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get package history"}) return } c.JSON(http.StatusOK, gin.H{ "history": history, "package_type": packageType, "package_name": packageName, "count": len(history), }) } // UpdatePackageStatus updates the status of a package func (h *UnifiedUpdateHandler) UpdatePackageStatus(c *gin.Context) { agentIDStr := c.Param("agent_id") agentID, err := uuid.Parse(agentIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"}) return } var req struct { PackageType string `json:"package_type" binding:"required"` PackageName string `json:"package_name" binding:"required"` Status string `json:"status" binding:"required"` Metadata map[string]interface{} `json:"metadata"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := h.updateQueries.UpdatePackageStatus(agentID, req.PackageType, req.PackageName, req.Status, req.Metadata, nil); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update package status"}) return } c.JSON(http.StatusOK, gin.H{"message": "package status updated"}) } // shouldEnableHeartbeat checks if heartbeat is already active for an agent func (h *UnifiedUpdateHandler) shouldEnableHeartbeat(agentID uuid.UUID, durationMinutes int) (bool, error) { agent, err := h.agentQueries.GetAgentByID(agentID) if err != nil { log.Printf("Warning: Failed to get agent %s for heartbeat check: %v", agentID, err) return true, nil } if enabled, ok := agent.Metadata["rapid_polling_enabled"].(bool); ok && enabled { if untilStr, ok := agent.Metadata["rapid_polling_until"].(string); ok { until, err := time.Parse(time.RFC3339, untilStr) if err == nil && until.After(time.Now().Add(5*time.Minute)) { log.Printf("[Heartbeat] Agent %s already has active heartbeat until %s (skipping)", agentID, untilStr) return false, nil } } } return true, nil } // InstallUpdate marks an update as ready for installation func (h *UnifiedUpdateHandler) InstallUpdate(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid update ID"}) return } update, err := h.updateQueries.GetUpdateByID(id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get update details"}) return } command := &models.AgentCommand{ ID: uuid.New(), AgentID: update.AgentID, CommandType: models.CommandTypeDryRunUpdate, Params: map[string]interface{}{ "update_id": id.String(), "package_name": update.PackageName, "package_type": update.PackageType, }, Status: models.CommandStatusPending, Source: models.CommandSourceManual, CreatedAt: time.Now(), } if shouldEnable, err := h.shouldEnableHeartbeat(update.AgentID, 10); err == nil && shouldEnable { heartbeatCmd := &models.AgentCommand{ ID: uuid.New(), AgentID: update.AgentID, CommandType: models.CommandTypeEnableHeartbeat, Params: models.JSONB{ "duration_minutes": 10, }, Status: models.CommandStatusPending, Source: models.CommandSourceSystem, CreatedAt: time.Now(), } if err := h.commandQueries.CreateCommand(heartbeatCmd); err != nil { log.Printf("[Heartbeat] Warning: Failed to create heartbeat command for agent %s: %v", update.AgentID, err) } } if err := h.commandQueries.CreateCommand(command); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create dry run command"}) return } if err := h.updateQueries.SetCheckingDependencies(id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update package status"}) return } c.JSON(http.StatusOK, gin.H{ "message": "dry run command created for agent", "command_id": command.ID.String(), }) } // ReportDependencies handles dependency reporting from agents after dry run func (h *UnifiedUpdateHandler) ReportDependencies(c *gin.Context) { agentID := c.MustGet("agent_id").(uuid.UUID) if err := h.agentQueries.UpdateAgentLastSeen(agentID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update last seen"}) return } var req models.DependencyReportRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Auto-approve if no dependencies if len(req.Dependencies) == 0 { update, err := h.updateQueries.GetUpdateByPackage(agentID, req.PackageType, req.PackageName) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get update details"}) return } command := &models.AgentCommand{ ID: uuid.New(), AgentID: agentID, CommandType: models.CommandTypeConfirmDependencies, Params: map[string]interface{}{ "update_id": update.ID.String(), "package_name": req.PackageName, "package_type": req.PackageType, "dependencies": []string{}, }, Status: models.CommandStatusPending, Source: models.CommandSourceManual, CreatedAt: time.Now(), } if shouldEnable, err := h.shouldEnableHeartbeat(agentID, 10); err == nil && shouldEnable { heartbeatCmd := &models.AgentCommand{ ID: uuid.New(), AgentID: agentID, CommandType: models.CommandTypeEnableHeartbeat, Params: models.JSONB{ "duration_minutes": 10, }, Status: models.CommandStatusPending, Source: models.CommandSourceSystem, CreatedAt: time.Now(), } if err := h.commandQueries.CreateCommand(heartbeatCmd); err != nil { log.Printf("[Heartbeat] Warning: Failed to create heartbeat command for agent %s: %v", agentID, err) } } if err := h.commandQueries.CreateCommand(command); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create installation command"}) return } if err := h.updateQueries.SetInstallingWithNoDependencies(update.ID, req.Dependencies); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update package status to installing"}) return } c.JSON(http.StatusOK, gin.H{ "message": "no dependencies found - installation command created automatically", "command_id": command.ID.String(), }) return } // Require manual approval for dependencies if err := h.updateQueries.SetPendingDependencies(agentID, req.PackageType, req.PackageName, req.Dependencies); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update package status"}) return } c.JSON(http.StatusOK, gin.H{"message": "dependencies reported and status updated"}) } // ConfirmDependencies handles user confirmation to proceed with dependency installation func (h *UnifiedUpdateHandler) ConfirmDependencies(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid update ID"}) return } update, err := h.updateQueries.GetUpdateByID(id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "update not found"}) return } command := &models.AgentCommand{ ID: uuid.New(), AgentID: update.AgentID, CommandType: models.CommandTypeConfirmDependencies, Params: map[string]interface{}{ "update_id": id.String(), "package_name": update.PackageName, "package_type": update.PackageType, "dependencies": update.Metadata["dependencies"], }, Status: models.CommandStatusPending, Source: models.CommandSourceManual, CreatedAt: time.Now(), } if shouldEnable, err := h.shouldEnableHeartbeat(update.AgentID, 10); err == nil && shouldEnable { heartbeatCmd := &models.AgentCommand{ ID: uuid.New(), AgentID: update.AgentID, CommandType: models.CommandTypeEnableHeartbeat, Params: models.JSONB{ "duration_minutes": 10, }, Status: models.CommandStatusPending, Source: models.CommandSourceSystem, CreatedAt: time.Now(), } if err := h.commandQueries.CreateCommand(heartbeatCmd); err != nil { log.Printf("[Heartbeat] Warning: Failed to create heartbeat command for agent %s: %v", update.AgentID, err) } } if err := h.commandQueries.CreateCommand(command); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create confirmation command"}) return } if err := h.updateQueries.InstallUpdate(id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update package status"}) return } c.JSON(http.StatusOK, gin.H{ "message": "dependency installation confirmed and command created", "command_id": command.ID.String(), }) } // ApproveUpdates handles bulk approval of updates func (h *UnifiedUpdateHandler) ApproveUpdates(c *gin.Context) { var req struct { UpdateIDs []string `json:"update_ids" binding:"required"` ScheduledAt *string `json:"scheduled_at"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } updateIDs := make([]uuid.UUID, 0, len(req.UpdateIDs)) for _, idStr := range req.UpdateIDs { id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid update ID: " + idStr}) return } updateIDs = append(updateIDs, id) } if err := h.updateQueries.BulkApproveUpdates(updateIDs, "admin"); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to approve updates"}) return } c.JSON(http.StatusOK, gin.H{ "message": "updates approved", "count": len(updateIDs), }) } // RejectUpdate rejects a single update func (h *UnifiedUpdateHandler) RejectUpdate(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid update ID"}) return } if err := h.updateQueries.RejectUpdate(id, "admin"); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to reject update"}) return } c.JSON(http.StatusOK, gin.H{"message": "update rejected"}) } // GetAllLogs retrieves logs across all agents with filtering func (h *UnifiedUpdateHandler) GetAllLogs(c *gin.Context) { filters := &models.LogFilters{ Action: c.Query("action"), Result: c.Query("result"), } if agentIDStr := c.Query("agent_id"); agentIDStr != "" { agentID, err := uuid.Parse(agentIDStr) if err == nil { filters.AgentID = agentID } } if sinceStr := c.Query("since"); sinceStr != "" { sinceTime, err := time.Parse(time.RFC3339, sinceStr) if err == nil { filters.Since = &sinceTime } } page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "100")) filters.Page = page filters.PageSize = pageSize items, total, err := h.updateQueries.GetAllUnifiedHistory(filters) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve history"}) return } c.JSON(http.StatusOK, gin.H{ "logs": items, "total": total, "page": page, "page_size": pageSize, }) } // GetUpdateLogs retrieves installation logs for a specific update func (h *UnifiedUpdateHandler) GetUpdateLogs(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid update ID"}) return } limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50")) logs, err := h.updateQueries.GetUpdateLogs(id, limit) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve update logs"}) return } c.JSON(http.StatusOK, gin.H{ "logs": logs, "count": len(logs), }) } // RetryCommand retries a failed command func (h *UnifiedUpdateHandler) RetryCommand(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid command ID"}) return } newCommand, err := h.commandQueries.RetryCommand(id) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("failed to retry command: %v", err)}) return } c.JSON(http.StatusOK, gin.H{ "message": "command retry created", "command_id": newCommand.ID.String(), "new_id": newCommand.ID.String(), }) } // CancelCommand cancels a pending command func (h *UnifiedUpdateHandler) CancelCommand(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid command ID"}) return } if err := h.commandQueries.CancelCommand(id); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("failed to cancel command: %v", err)}) return } c.JSON(http.StatusOK, gin.H{"message": "command cancelled"}) } // GetActiveCommands retrieves currently active commands func (h *UnifiedUpdateHandler) GetActiveCommands(c *gin.Context) { commands, err := h.commandQueries.GetActiveCommands() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve active commands"}) return } c.JSON(http.StatusOK, gin.H{ "commands": commands, "count": len(commands), }) } // GetRecentCommands retrieves recent commands func (h *UnifiedUpdateHandler) GetRecentCommands(c *gin.Context) { limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50")) commands, err := h.commandQueries.GetRecentCommands(limit) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve recent commands"}) return } c.JSON(http.StatusOK, gin.H{ "commands": commands, "count": len(commands), "limit": limit, }) } // ClearFailedCommands removes failed commands func (h *UnifiedUpdateHandler) ClearFailedCommands(c *gin.Context) { olderThanDaysStr := c.Query("older_than_days") onlyRetriedStr := c.Query("only_retried") allFailedStr := c.Query("all_failed") var count int64 var err error olderThanDays := 7 if olderThanDaysStr != "" { if days, err := strconv.Atoi(olderThanDaysStr); err == nil && days > 0 { olderThanDays = days } } onlyRetried := onlyRetriedStr == "true" allFailed := allFailedStr == "true" if allFailed { count, err = h.commandQueries.ClearAllFailedCommandsRegardlessOfAge() } else if onlyRetried { count, err = h.commandQueries.ClearRetriedFailedCommands(olderThanDays) } else { count, err = h.commandQueries.ClearOldFailedCommands(olderThanDays) } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "failed to clear failed commands", "details": err.Error(), }) return } message := fmt.Sprintf("Archived %d failed commands", count) if count > 0 { message += ". WARNING: This shouldn't be necessary if the retry logic is working properly" message += " (History preserved - commands moved to archived status)" } c.JSON(http.StatusOK, gin.H{ "message": message, "count": count, }) } // GetBatchStatus returns recent batch processing status for an agent func (h *UnifiedUpdateHandler) GetBatchStatus(c *gin.Context) { agentIDStr := c.Param("agent_id") agentID, err := uuid.Parse(agentIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"}) return } limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10")) batches, err := h.updateQueries.GetBatchStatus(agentID, limit) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get batch status"}) return } c.JSON(http.StatusOK, gin.H{ "batches": batches, "count": len(batches), }) } // GetActiveOperations retrieves currently running operations func (h *UnifiedUpdateHandler) GetActiveOperations(c *gin.Context) { operations, err := h.updateQueries.GetActiveOperations() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve active operations"}) return } c.JSON(http.StatusOK, gin.H{ "operations": operations, "count": len(operations), }) }