Files
GoMFT/internal/web/handlers/job_handlers.go
T
StarFleetCPTN db6259f8a3 feat: Implement configuration and job duplication functionality
- Added duplication feature for configurations and jobs, allowing users to create copies of existing entries.
- Introduced new buttons in the UI for duplicating configurations and jobs, enhancing user experience.
- Implemented backend logic to handle duplication requests, ensuring proper ownership checks and data integrity.
- Added notifications for successful duplication actions to inform users of the outcome.
- Updated relevant templates and handlers to support the new duplication functionality.
2025-03-22 22:19:05 -07:00

820 lines
24 KiB
Go

package handlers
import (
"fmt"
"log"
"net/http"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/starfleetcptn/gomft/components"
"github.com/starfleetcptn/gomft/internal/db"
)
// HandleJobs handles the GET /jobs route
func (h *Handlers) HandleJobs(c *gin.Context) {
userID := c.GetUint("userID")
var jobs []db.Job
h.DB.Where("created_by = ?", userID).Preload("Config").Find(&jobs)
// Create a map to store config counts for each job
configCount := make(map[uint]int)
// Count configurations for each job
for _, job := range jobs {
// Get all configurations for this job
configs, err := h.DB.GetConfigsForJob(job.ID)
if err != nil {
c.Error(fmt.Errorf("error loading configurations for job %d: %v", job.ID, err))
configCount[job.ID] = 0
} else {
configCount[job.ID] = len(configs)
}
}
data := components.JobsData{
Jobs: jobs,
ConfigCount: configCount,
}
components.Jobs(c, data).Render(c, c.Writer)
}
// HandleJobRunDetails handles the GET /job/:id route
func (h *Handlers) HandleJobRunDetails(c *gin.Context) {
userID := c.GetUint("userID")
jobID := c.Param("id")
// Get job history
var jobHistory db.JobHistory
if err := h.DB.First(&jobHistory, jobID).Error; err != nil {
c.String(http.StatusNotFound, "Job not found")
return
}
// Get job
var job db.Job
if err := h.DB.First(&job, jobHistory.JobID).Error; err != nil {
c.String(http.StatusNotFound, "Job not found")
return
}
// Verify that the user owns this job
if job.CreatedBy != userID {
c.String(http.StatusForbidden, "You don't have permission to view this job run")
return
}
// Get the config
var config db.TransferConfig
// First try to get the specific config used in this job history record
configID := jobHistory.ConfigID
// If no ConfigID is set in the history, fall back to the job's primary ConfigID
if configID == 0 {
configID = job.ConfigID
}
if err := h.DB.First(&config, configID).Error; err != nil {
c.String(http.StatusNotFound, "Configuration not found")
return
}
data := components.JobRunDetailsData{
JobHistory: jobHistory,
Job: job,
Config: config,
}
components.JobRunDetails(c.Request.Context(), data).Render(c, c.Writer)
}
// HandleNewJob handles the GET /jobs/new route
func (h *Handlers) HandleNewJob(c *gin.Context) {
// Get available configs for the user
userID := c.GetUint("userID")
var configs []db.TransferConfig
h.DB.Where("created_by = ?", userID).Find(&configs)
data := components.JobFormData{
Job: &db.Job{},
Configs: configs,
IsNew: true,
}
components.JobForm(c.Request.Context(), data).Render(c, c.Writer)
}
// HandleEditJob handles the GET /jobs/:id/edit route
func (h *Handlers) HandleEditJob(c *gin.Context) {
id := c.Param("id")
userID := c.GetUint("userID")
var job db.Job
if err := h.DB.First(&job, id).Error; err != nil {
c.Redirect(http.StatusFound, "/jobs")
return
}
// Check if user owns this job
if job.CreatedBy != userID {
// Check if user is admin
isAdmin, exists := c.Get("isAdmin")
if !exists || isAdmin != true {
c.Redirect(http.StatusFound, "/jobs")
return
}
}
// Get available configs for the user
var configs []db.TransferConfig
h.DB.Where("created_by = ?", userID).Find(&configs)
data := components.JobFormData{
Job: &job,
Configs: configs,
IsNew: false,
}
components.JobForm(c.Request.Context(), data).Render(c, c.Writer)
}
// HandleCreateJob handles the POST /jobs route
func (h *Handlers) HandleCreateJob(c *gin.Context) {
userID := c.GetUint("userID")
// Debug logging
log.Printf("HandleCreateJob: Form data received: %v", c.Request.PostForm)
// Parse form data
var job db.Job
if err := c.ShouldBind(&job); err != nil {
c.String(http.StatusBadRequest, "Invalid form data")
return
}
// Debug logging
log.Printf("HandleCreateJob: Job after binding: %+v", job)
// Get multiple config IDs from form
configIDs := c.PostFormArray("config_ids[]")
if len(configIDs) == 0 {
c.String(http.StatusBadRequest, "At least one configuration must be selected")
return
}
// Debug logging
log.Printf("HandleCreateJob: config_ids[]: %v", configIDs)
// Process config IDs
var configIDsList []uint
// Check if we have an explicit order specified
configOrder := c.PostForm("config_order")
log.Printf("HandleCreateJob: config_order: %s", configOrder)
if configOrder != "" {
// Parse the ordered list
orderStrings := strings.Split(configOrder, ",")
log.Printf("HandleCreateJob: order strings: %v", orderStrings)
for _, configIDStr := range orderStrings {
configID, err := strconv.ParseUint(configIDStr, 10, 32)
if err != nil {
log.Printf("HandleCreateJob: Error parsing config ID: %v", err)
c.String(http.StatusBadRequest, "Invalid configuration ID format in order")
return
}
// Verify that the config exists and belongs to the user
var config db.TransferConfig
if err := h.DB.First(&config, configID).Error; err != nil {
log.Printf("HandleCreateJob: Invalid config ID: %d, error: %v", configID, err)
c.String(http.StatusBadRequest, "Invalid configuration selected")
return
}
// Check if the config belongs to the user
if config.CreatedBy != userID {
// Check if user is admin
isAdmin, exists := c.Get("isAdmin")
if !exists || isAdmin != true {
c.String(http.StatusForbidden, "You do not have permission to use this configuration")
return
}
}
configIDsList = append(configIDsList, uint(configID))
}
} else {
// Fall back to unordered config IDs
log.Printf("HandleCreateJob: No config_order found, using checkbox order")
for _, configIDStr := range configIDs {
configID, err := strconv.ParseUint(configIDStr, 10, 32)
if err != nil {
c.String(http.StatusBadRequest, "Invalid configuration ID format")
return
}
// Verify that the config exists and belongs to the user
var config db.TransferConfig
if err := h.DB.First(&config, configID).Error; err != nil {
c.String(http.StatusBadRequest, "Invalid configuration selected")
return
}
// Check if the config belongs to the user
if config.CreatedBy != userID {
// Check if user is admin
isAdmin, exists := c.Get("isAdmin")
if !exists || isAdmin != true {
c.String(http.StatusForbidden, "You do not have permission to use this configuration")
return
}
}
configIDsList = append(configIDsList, uint(configID))
}
}
// Debug logging
log.Printf("HandleCreateJob: Final configIDsList: %v", configIDsList)
// Set the first config ID for backward compatibility
if len(configIDsList) > 0 {
job.ConfigID = configIDsList[0]
// Verify that the config exists and belongs to the user (using the first config as primary)
var config db.TransferConfig
if err := h.DB.First(&config, job.ConfigID).Error; err != nil {
c.String(http.StatusBadRequest, "Invalid configuration selected")
return
}
// Check if the config belongs to the user
if config.CreatedBy != userID {
// Check if user is admin
isAdmin, exists := c.Get("isAdmin")
if !exists || isAdmin != true {
c.String(http.StatusForbidden, "You do not have permission to use this configuration")
return
}
}
// If job name is empty, use the primary config name
if job.Name == "" {
job.Name = config.Name
}
}
// Set the config IDs list
job.SetConfigIDsList(configIDsList)
// Debug logging
log.Printf("HandleCreateJob: Job after setting ConfigIDsList: %+v", job)
log.Printf("HandleCreateJob: Job.ConfigIDs: %s", job.ConfigIDs)
// Set the boolean fields - handle both "on" and "true" values for checkboxes
enabledVal := c.Request.FormValue("enabled")
jobEnabledValue := enabledVal == "on" || enabledVal == "true"
job.SetEnabled(jobEnabledValue)
webhookEnabledVal := c.Request.FormValue("webhook_enabled")
webhookEnabledValue := webhookEnabledVal == "on" || webhookEnabledVal == "true"
job.SetWebhookEnabled(webhookEnabledValue)
notifySuccessVal := c.Request.FormValue("notify_on_success")
notifyOnSuccessValue := notifySuccessVal == "on" || notifySuccessVal == "true"
job.SetNotifyOnSuccess(notifyOnSuccessValue)
notifyFailureVal := c.Request.FormValue("notify_on_failure")
notifyOnFailureValue := notifyFailureVal == "on" || notifyFailureVal == "true"
job.SetNotifyOnFailure(notifyOnFailureValue)
// Set created by user
job.CreatedBy = userID
// Clear the Config field to prevent GORM from creating a new config
job.Config = db.TransferConfig{}
// Start a transaction
tx := h.DB.Begin()
if tx.Error != nil {
log.Printf("HandleCreateJob: Failed to begin transaction: %v", tx.Error)
c.String(http.StatusInternalServerError, "Failed to begin transaction")
return
}
// Create the job
if err := tx.Create(&job).Error; err != nil {
tx.Rollback()
log.Printf("HandleCreateJob: Error creating job: %v", err)
c.String(http.StatusInternalServerError, "Failed to create job")
return
}
// Create audit log entry
auditDetails := map[string]interface{}{
"name": job.Name,
"schedule": job.Schedule,
"enabled": job.GetEnabled(),
"config_ids": configIDsList,
"webhook_enabled": job.GetWebhookEnabled(),
"notify_on_success": job.GetNotifyOnSuccess(),
"notify_on_failure": job.GetNotifyOnFailure(),
}
auditLog := db.AuditLog{
Action: "create",
EntityType: "job",
EntityID: job.ID,
UserID: userID,
Details: auditDetails,
Timestamp: time.Now(),
}
if err := tx.Create(&auditLog).Error; err != nil {
tx.Rollback()
log.Printf("HandleCreateJob: Error creating audit log: %v", err)
c.String(http.StatusInternalServerError, "Failed to create audit log")
return
}
// Commit the transaction
if err := tx.Commit().Error; err != nil {
log.Printf("HandleCreateJob: Error committing transaction: %v", err)
c.String(http.StatusInternalServerError, "Failed to commit transaction")
return
}
log.Printf("HandleCreateJob: Job successfully created with ID: %d", job.ID)
// Schedule the job with the scheduler
if err := h.Scheduler.ScheduleJob(&job); err != nil {
c.String(http.StatusInternalServerError, "Job created but scheduling failed: "+err.Error())
return
}
c.Redirect(http.StatusFound, "/jobs")
}
// HandleUpdateJob handles the PUT /jobs/:id route
func (h *Handlers) HandleUpdateJob(c *gin.Context) {
id := c.Param("id")
userID := c.GetUint("userID")
// Debug logging
log.Printf("HandleUpdateJob: Updating job ID: %s", id)
log.Printf("HandleUpdateJob: Form data received: %v", c.Request.PostForm)
var job db.Job
if err := h.DB.First(&job, id).Error; err != nil {
log.Printf("HandleUpdateJob: Job not found: %v", err)
c.String(http.StatusNotFound, "Job not found")
return
}
// Check if user owns this job
if job.CreatedBy != userID {
// Check if user is admin
isAdmin, exists := c.Get("isAdmin")
if !exists || isAdmin != true {
c.String(http.StatusForbidden, "You do not have permission to update this job")
return
}
}
// Get the old job values for comparison
oldJob := job
log.Printf("HandleUpdateJob: Original job: %+v", oldJob)
log.Printf("HandleUpdateJob: Original job ConfigIDs: %s", oldJob.ConfigIDs)
// Parse form data
if err := c.ShouldBind(&job); err != nil {
log.Printf("HandleUpdateJob: Error binding form data: %v", err)
c.String(http.StatusBadRequest, "Invalid form data")
return
}
log.Printf("HandleUpdateJob: Job after binding: %+v", job)
// Get multiple config IDs from form
configIDs := c.PostFormArray("config_ids[]")
if len(configIDs) == 0 {
log.Printf("HandleUpdateJob: No config_ids[] found in form data")
c.String(http.StatusBadRequest, "At least one configuration must be selected")
return
}
log.Printf("HandleUpdateJob: config_ids[]: %v", configIDs)
// Process config IDs
var configIDsList []uint
// Check if we have an explicit order specified
configOrder := c.PostForm("config_order")
log.Printf("HandleUpdateJob: config_order: %s", configOrder)
if configOrder != "" {
// Parse the ordered list
orderStrings := strings.Split(configOrder, ",")
log.Printf("HandleUpdateJob: order strings: %v", orderStrings)
for _, configIDStr := range orderStrings {
configID, err := strconv.ParseUint(configIDStr, 10, 32)
if err != nil {
log.Printf("HandleUpdateJob: Error parsing config ID: %v", err)
c.String(http.StatusBadRequest, "Invalid configuration ID format in order")
return
}
// Verify that the config exists
var config db.TransferConfig
if err := h.DB.First(&config, configID).Error; err != nil {
log.Printf("HandleUpdateJob: Invalid config ID: %d, error: %v", configID, err)
c.String(http.StatusBadRequest, "Invalid configuration selected")
return
}
// Check if the config belongs to the user
if config.CreatedBy != userID {
// Check if user is admin
isAdmin, exists := c.Get("isAdmin")
if !exists || isAdmin != true {
c.String(http.StatusForbidden, "You do not have permission to use this configuration")
return
}
}
configIDsList = append(configIDsList, uint(configID))
}
} else {
// Fall back to unordered config IDs
log.Printf("HandleUpdateJob: No config_order found, using checkbox order")
for _, configIDStr := range configIDs {
configID, err := strconv.ParseUint(configIDStr, 10, 32)
if err != nil {
c.String(http.StatusBadRequest, "Invalid configuration ID format")
return
}
// Verify that the config exists
var config db.TransferConfig
if err := h.DB.First(&config, configID).Error; err != nil {
c.String(http.StatusBadRequest, "Invalid configuration selected")
return
}
// Check if the config belongs to the user
if config.CreatedBy != userID {
// Check if user is admin
isAdmin, exists := c.Get("isAdmin")
if !exists || isAdmin != true {
c.String(http.StatusForbidden, "You do not have permission to use this configuration")
return
}
}
configIDsList = append(configIDsList, uint(configID))
}
}
// Debug logging
log.Printf("HandleUpdateJob: Final configIDsList: %v", configIDsList)
// Set the first config ID for backward compatibility
if len(configIDsList) > 0 {
job.ConfigID = configIDsList[0]
// If job name is empty, use the primary config name
var config db.TransferConfig
if err := h.DB.First(&config, job.ConfigID).Error; err == nil && job.Name == "" {
job.Name = config.Name
}
}
// Set the config IDs list
job.SetConfigIDsList(configIDsList)
// Debug logging
log.Printf("HandleUpdateJob: Job after setting ConfigIDsList: %+v", job)
log.Printf("HandleUpdateJob: Job.ConfigIDs: %s", job.ConfigIDs)
// Set the boolean fields - handle both "on" and "true" values for checkboxes
enabledVal := c.Request.FormValue("enabled")
jobEnabledValue := enabledVal == "on" || enabledVal == "true"
job.SetEnabled(jobEnabledValue)
webhookEnabledVal := c.Request.FormValue("webhook_enabled")
webhookEnabledValue := webhookEnabledVal == "on" || webhookEnabledVal == "true"
job.SetWebhookEnabled(webhookEnabledValue)
notifySuccessVal := c.Request.FormValue("notify_on_success")
notifyOnSuccessValue := notifySuccessVal == "on" || notifySuccessVal == "true"
job.SetNotifyOnSuccess(notifyOnSuccessValue)
notifyFailureVal := c.Request.FormValue("notify_on_failure")
notifyOnFailureValue := notifyFailureVal == "on" || notifyFailureVal == "true"
job.SetNotifyOnFailure(notifyOnFailureValue)
// Preserve fields that shouldn't be updated
job.CreatedBy = oldJob.CreatedBy
job.ID = oldJob.ID
// Clear the Config field to prevent GORM from updating or creating a new config
job.Config = db.TransferConfig{}
// Start a transaction
tx := h.DB.Begin()
if tx.Error != nil {
log.Printf("HandleUpdateJob: Failed to begin transaction: %v", tx.Error)
c.String(http.StatusInternalServerError, "Failed to begin transaction")
return
}
if err := tx.Save(&job).Error; err != nil {
tx.Rollback()
log.Printf("HandleUpdateJob: Error updating job: %v", err)
c.String(http.StatusInternalServerError, "Failed to update job")
return
}
// Create audit log entry
auditDetails := map[string]interface{}{
"name": job.Name,
"schedule": job.Schedule,
"enabled": job.GetEnabled(),
"config_ids": configIDsList,
"webhook_enabled": job.GetWebhookEnabled(),
"notify_on_success": job.GetNotifyOnSuccess(),
"notify_on_failure": job.GetNotifyOnFailure(),
"previous_state": map[string]interface{}{
"name": oldJob.Name,
"schedule": oldJob.Schedule,
"enabled": oldJob.GetEnabled(),
"config_ids": oldJob.GetConfigIDsList(),
"webhook_enabled": oldJob.GetWebhookEnabled(),
"notify_on_success": oldJob.GetNotifyOnSuccess(),
"notify_on_failure": oldJob.GetNotifyOnFailure(),
},
}
auditLog := db.AuditLog{
Action: "update",
EntityType: "job",
EntityID: job.ID,
UserID: userID,
Details: auditDetails,
Timestamp: time.Now(),
}
if err := tx.Create(&auditLog).Error; err != nil {
tx.Rollback()
log.Printf("HandleUpdateJob: Error creating audit log: %v", err)
c.String(http.StatusInternalServerError, "Failed to create audit log")
return
}
// Commit the transaction
if err := tx.Commit().Error; err != nil {
log.Printf("HandleUpdateJob: Error committing transaction: %v", err)
c.String(http.StatusInternalServerError, "Failed to commit transaction")
return
}
log.Printf("HandleUpdateJob: Job successfully updated")
// Reschedule the job with the scheduler
if err := h.Scheduler.ScheduleJob(&job); err != nil {
c.String(http.StatusInternalServerError, "Job updated but scheduling failed: "+err.Error())
return
}
c.Redirect(http.StatusFound, "/jobs")
}
// HandleDeleteJob handles the DELETE /jobs/:id route
func (h *Handlers) HandleDeleteJob(c *gin.Context) {
id := c.Param("id")
userID := c.GetUint("userID")
var job db.Job
if err := h.DB.First(&job, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Job not found"})
return
}
// Check if user owns this job
if job.CreatedBy != userID {
// Check if user is admin
isAdmin, exists := c.Get("isAdmin")
if !exists || isAdmin != true {
c.JSON(http.StatusForbidden, gin.H{"error": "You do not have permission to delete this job"})
return
}
}
// Start a transaction
tx := h.DB.Begin()
if tx.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to begin transaction"})
return
}
// Create audit log before deletion
auditDetails := map[string]interface{}{
"name": job.Name,
"schedule": job.Schedule,
"enabled": job.GetEnabled(),
"config_ids": job.GetConfigIDsList(),
"webhook_enabled": job.GetWebhookEnabled(),
"notify_on_success": job.GetNotifyOnSuccess(),
"notify_on_failure": job.GetNotifyOnFailure(),
}
auditLog := db.AuditLog{
Action: "delete",
EntityType: "job",
EntityID: job.ID,
UserID: userID,
Details: auditDetails,
Timestamp: time.Now(),
}
if err := tx.Create(&auditLog).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create audit log"})
return
}
// Delete job
if err := tx.Delete(&job).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete job"})
return
}
// Commit the transaction
if err := tx.Commit().Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to commit transaction"})
return
}
// Unschedule the job from the scheduler
h.Scheduler.UnscheduleJob(job.ID)
c.JSON(http.StatusOK, gin.H{"message": "Job deleted successfully"})
}
// HandleRunJob handles the POST /jobs/:id/run route
func (h *Handlers) HandleRunJob(c *gin.Context) {
id := c.Param("id")
userID := c.GetUint("userID")
var job db.Job
if err := h.DB.First(&job, id).Error; err != nil {
c.Header("Content-Type", "text/html")
c.String(http.StatusNotFound, "<script>window.notyfInstance.error('Job not found')</script>")
return
}
// Check if user owns this job
if job.CreatedBy != userID {
// Check if user is admin
isAdmin, exists := c.Get("isAdmin")
if !exists || isAdmin != true {
c.Header("Content-Type", "text/html")
c.String(http.StatusForbidden, "<script>window.notyfInstance.error('You do not have permission to run this job')</script>")
return
}
}
// Determine job name for response
jobName := job.Name
if jobName == "" {
// If job name is empty, try to get config name
var config db.TransferConfig
if err := h.DB.First(&config, job.ConfigID).Error; err == nil {
jobName = config.Name
} else {
jobName = fmt.Sprintf("Job #%d", job.ID)
}
}
// Create audit log for manual job run
auditLog := db.AuditLog{
Action: "run_manual",
EntityType: "job",
EntityID: job.ID,
UserID: userID,
Details: map[string]interface{}{"name": jobName, "job_id": job.ID},
Timestamp: time.Now(),
}
if err := h.DB.Create(&auditLog).Error; err != nil {
log.Printf("HandleRunJob: Warning - Failed to create audit log: %v", err)
// Continue anyway, as this is not critical to running the job
}
// Run the job immediately using the scheduler
if err := h.Scheduler.RunJobNow(job.ID); err != nil {
c.Header("Content-Type", "text/html")
errorMsg := fmt.Sprintf("<script>window.notyfInstance.error('Failed to run job: %s')</script>", err.Error())
c.String(http.StatusInternalServerError, errorMsg)
return
}
// Set custom header with job name for HTMX to use in the toast notification
c.Header("HX-Job-Name", jobName)
c.Header("Content-Type", "text/html")
// Return HTML with JavaScript to trigger the notification
successScript := fmt.Sprintf("<script>window.notyfInstance.success('Job \"%s\" has been started successfully')</script>", jobName)
c.String(http.StatusOK, successScript)
}
// HandleDuplicateJob handles duplication of a job
func (h *Handlers) HandleDuplicateJob(c *gin.Context) {
// Get the job ID from the URL
idParam := c.Param("id")
id, err := strconv.ParseUint(idParam, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid job ID"})
return
}
userID := c.GetUint("userID")
// Get the original job
var originalJob db.Job
if err := h.DB.First(&originalJob, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Job not found"})
return
}
// Create a new job as a copy of the original
newJob := originalJob
newJob.ID = 0 // Reset ID to create a new record
newJob.Name = originalJob.Name + " - Copy"
newJob.CreatedAt = time.Now()
newJob.UpdatedAt = time.Now()
// Reset execution specific fields
newJob.LastRun = nil
newJob.NextRun = nil
// Save the new job
if err := h.DB.Create(&newJob).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create duplicate job: " + err.Error()})
return
}
// If the job has associated configs, duplicate those associations
configIDs := originalJob.GetConfigIDsList()
if len(configIDs) > 0 {
// Set the new job's config IDs
newJob.SetConfigIDsList(configIDs)
if err := h.DB.Save(&newJob).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update config associations: " + err.Error()})
return
}
}
// Create audit log entry
auditDetails := map[string]interface{}{
"name": newJob.Name,
"original_job_id": originalJob.ID,
"new_job_id": newJob.ID,
"schedule": newJob.Schedule,
"enabled": newJob.GetEnabled(),
"config_ids": configIDs,
"webhook_enabled": newJob.GetWebhookEnabled(),
"notify_on_success": newJob.GetNotifyOnSuccess(),
"notify_on_failure": newJob.GetNotifyOnFailure(),
}
auditLog := db.AuditLog{
Action: "duplicate",
EntityType: "job",
EntityID: newJob.ID,
UserID: userID,
Details: auditDetails,
Timestamp: time.Now(),
}
if err := h.DB.Create(&auditLog).Error; err != nil {
log.Printf("Warning: Failed to create audit log for job duplication: %v", err)
// Continue anyway, as this is not critical
}
// Schedule the job with the scheduler
if err := h.Scheduler.ScheduleJob(&newJob); err != nil {
log.Printf("Warning: Failed to schedule duplicated job: %v", err)
// Continue anyway, as user can manually schedule later
}
// Redirect to jobs page
c.Redirect(http.StatusFound, "/jobs")
}