@@ -599,7 +786,7 @@ templ JobForm(ctx context.Context, data JobFormData) {
- Jobs will run according to their schedule and execute the selected transfer configuration
+ Jobs will run according to their schedule and execute the selected transfer configurations in the order specified
diff --git a/internal/db/db.go b/internal/db/db.go
index a8d9397..8678a91 100644
--- a/internal/db/db.go
+++ b/internal/db/db.go
@@ -2,6 +2,7 @@ package db
import (
"fmt"
+ "log"
"os"
"os/exec"
"path/filepath"
@@ -186,6 +187,9 @@ func (j *Job) SetConfigIDsList(ids []uint) {
// Join with commas
j.ConfigIDs = strings.Join(strIDs, ",")
+ // Debug log the final ConfigIDs string
+ log.Printf("SetConfigIDsList: Setting ConfigIDs to: %s (from %v)", j.ConfigIDs, ids)
+
// If there's at least one ID, set ConfigID to the first one for backward compatibility
if len(ids) > 0 {
j.ConfigID = ids[0]
@@ -376,8 +380,25 @@ func (db *DB) GetJob(id uint) (*Job, error) {
}
func (db *DB) UpdateJob(job *Job) error {
+ log.Printf("UpdateJob: Updating job ID: %d, ConfigIDs: %s", job.ID, job.ConfigIDs)
+
// Use Omit to prevent GORM from updating or creating a new config
- return db.Omit("Config").Save(job).Error
+ return db.Model(&Job{}).
+ Where("id = ?", job.ID).
+ Omit("Config").
+ Updates(map[string]interface{}{
+ "name": job.Name,
+ "config_id": job.ConfigID,
+ "config_ids": job.ConfigIDs, // Explicitly update config_ids
+ "schedule": job.Schedule,
+ "enabled": job.Enabled,
+ "webhook_enabled": job.WebhookEnabled,
+ "webhook_url": job.WebhookURL,
+ "webhook_secret": job.WebhookSecret,
+ "webhook_headers": job.WebhookHeaders,
+ "notify_on_success": job.NotifyOnSuccess,
+ "notify_on_failure": job.NotifyOnFailure,
+ }).Error
}
func (db *DB) DeleteJob(id uint) error {
@@ -965,7 +986,21 @@ func (db *DB) GetConfigsForJob(jobID uint) ([]TransferConfig, error) {
return nil, err
}
- return configs, nil
+ // Create a map for quick lookup
+ configMap := make(map[uint]TransferConfig)
+ for _, config := range configs {
+ configMap[config.ID] = config
+ }
+
+ // Create a new slice with configs in the correct order
+ orderedConfigs := make([]TransferConfig, 0, len(configs))
+ for _, configID := range configIDs {
+ if config, exists := configMap[configID]; exists {
+ orderedConfigs = append(orderedConfigs, config)
+ }
+ }
+
+ return orderedConfigs, nil
}
// GetSkipProcessedFiles returns the value of SkipProcessedFiles with a default if nil
diff --git a/internal/email/email.go b/internal/email/email.go
index 7d7b9da..146c47b 100644
--- a/internal/email/email.go
+++ b/internal/email/email.go
@@ -270,3 +270,182 @@ func (s *Service) sendEmail(toEmail, subject, htmlContent string) error {
return client.Quit()
}
}
+
+// SendTestEmail sends a test email to verify email configuration
+func (s *Service) SendTestEmail(toEmail, subject, message string) error {
+ if !s.Config.Email.Enabled {
+ return fmt.Errorf("email service is disabled")
+ }
+
+ // Use default subject if not provided
+ if subject == "" {
+ subject = "Test Email from GoMFT"
+ }
+
+ // Use default message if not provided
+ if message == "" {
+ message = "This is a test email from GoMFT to verify the email configuration is working correctly."
+ }
+
+ // Create email data for template
+ data := map[string]interface{}{
+ "Subject": subject,
+ "Message": message,
+ "AppName": "GoMFT",
+ "Year": time.Now().Year(),
+ "SMTPServer": s.Config.Email.Host,
+ "SMTPPort": s.Config.Email.Port,
+ "FromEmail": s.Config.Email.FromEmail,
+ "CurrentTime": time.Now().Format(time.RFC1123Z),
+ }
+
+ // Generate email content
+ htmlContent, err := s.generateTestEmailHTML(data)
+ if err != nil {
+ return err
+ }
+
+ // Send the email
+ return s.sendEmail(toEmail, subject, htmlContent)
+}
+
+// generateTestEmailHTML generates the HTML content for test emails
+func (s *Service) generateTestEmailHTML(data map[string]interface{}) (string, error) {
+ // HTML template for test email
+ tmpl, err := template.New("testEmail").Parse(`
+
+
+
+
+
+ {{.Subject}}
+
+
+
+
+
+
+
G
+
+
{{.Subject}}
+
+
+
{{.Message}}
+
+
+
+
SMTP Server:
+
{{.SMTPServer}}:{{.SMTPPort}}
+
+
+
From:
+
{{.FromEmail}}
+
+
+
Sent:
+
{{.CurrentTime}}
+
+
+
+
+
This is a test email sent from the GoMFT admin interface. If you've received this email, your email configuration is working correctly.
+
+
+
+
+
+
+`)
+ if err != nil {
+ return "", err
+ }
+
+ var result bytes.Buffer
+ if err := tmpl.Execute(&result, data); err != nil {
+ return "", err
+ }
+
+ return result.String(), nil
+}
diff --git a/internal/scheduler/scheduler.go b/internal/scheduler/scheduler.go
index 61da064..2a06228 100644
--- a/internal/scheduler/scheduler.go
+++ b/internal/scheduler/scheduler.go
@@ -173,6 +173,11 @@ func NewLogger() *Logger {
filepath.Join(logsDir, "scheduler.log"), maxSize, maxBackups, maxAge, compress, logLevel.String())
}
+ if logLevel >= LogLevelDebug {
+ logger.Debug.Printf("Log rotation details: file=%s, maxSize=%dMB, maxBackups=%d, maxAge=%d days, compress=%v",
+ filepath.Join(logsDir, "scheduler.log"), maxSize, maxBackups, maxAge, compress)
+ }
+
return logger
}
@@ -258,6 +263,8 @@ func (s *Scheduler) loadJobs() {
}
func (s *Scheduler) ScheduleJob(job *db.Job) error {
+ s.log.LogDebug("Attempting to schedule job ID %d: %+v", job.ID, job)
+
s.log.LogInfo("Scheduling job %d: %s with schedule %s", job.ID, job.Name, job.Schedule)
// Remove existing job if it exists
@@ -279,6 +286,8 @@ func (s *Scheduler) ScheduleJob(job *db.Job) error {
schedule = "0 " + schedule
}
+ s.log.LogDebug("Converted schedule from '%s' to '%s'", job.Schedule, schedule)
+
// Validate cron expression
parser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
_, err := parser.Parse(schedule)
@@ -286,6 +295,8 @@ func (s *Scheduler) ScheduleJob(job *db.Job) error {
return fmt.Errorf("invalid cron expression '%s': %w", job.Schedule, err)
}
+ s.log.LogDebug("Validated cron expression '%s' for job %d", schedule, job.ID)
+
// Schedule the job
entryID, err := s.cron.AddFunc(job.Schedule, func() {
s.executeJob(job.ID)
@@ -296,6 +307,8 @@ func (s *Scheduler) ScheduleJob(job *db.Job) error {
return err
}
+ s.log.LogDebug("Scheduled job %d with cron entry ID %d", job.ID, entryID)
+
// Store mapping of job ID to cron entry ID
s.jobMutex.Lock()
s.jobs[job.ID] = entryID
@@ -313,6 +326,9 @@ func (s *Scheduler) ScheduleJob(job *db.Job) error {
}
func (s *Scheduler) executeJob(jobID uint) {
+ s.log.LogDebug("Entering executeJob for job ID %d", jobID)
+ defer s.log.LogDebug("Exiting executeJob for job ID %d", jobID)
+
s.log.LogInfo("Starting execution of job %d", jobID)
// Get job details
@@ -322,6 +338,8 @@ func (s *Scheduler) executeJob(jobID uint) {
return
}
+ s.log.LogDebug("Loaded job details: %+v", job)
+
// Get all configurations associated with this job
configs, err := s.db.GetConfigsForJob(jobID)
if err != nil {
@@ -329,12 +347,45 @@ func (s *Scheduler) executeJob(jobID uint) {
return
}
+ s.log.LogDebug("Loaded %d configurations for job %d", len(configs), jobID)
+
if len(configs) == 0 {
s.log.LogError("Error: job %d has no associated configurations", jobID)
return
}
- s.log.LogInfo("Loaded job %d with %d configurations", jobID, len(configs))
+ // Get the ordered config IDs from the job
+ orderedConfigIDs := job.GetConfigIDsList()
+ s.log.LogDebug("Ordered config IDs for job %d: %v", jobID, orderedConfigIDs)
+
+ // Create a map of configs for easy lookup
+ configMap := make(map[uint]db.TransferConfig)
+ for _, config := range configs {
+ configMap[config.ID] = config
+ }
+
+ // Process configurations in the specified order
+ var orderedConfigs []db.TransferConfig
+
+ // First, add configs in the order specified in the job's ConfigIDs
+ for _, configID := range orderedConfigIDs {
+ if config, exists := configMap[configID]; exists {
+ orderedConfigs = append(orderedConfigs, config)
+ delete(configMap, configID) // Remove from map to avoid duplicates
+ }
+ }
+
+ // Add any remaining configs not in the ordered list (shouldn't happen, but just in case)
+ for _, config := range configMap {
+ orderedConfigs = append(orderedConfigs, config)
+ }
+
+ s.log.LogInfo("Processing job %d with %d configurations in specified order", jobID, len(orderedConfigs))
+
+ // Log the order of execution
+ for i, config := range orderedConfigs {
+ s.log.LogDebug("Execution order %d/%d: Config ID %d (%s)", i+1, len(orderedConfigs), config.ID, config.Name)
+ }
// Update job last run time
startTime := time.Now()
@@ -343,9 +394,9 @@ func (s *Scheduler) executeJob(jobID uint) {
s.log.LogError("Error updating job last run time for job %d: %v", jobID, err)
}
- // Process each configuration
- for i, config := range configs {
- s.processConfiguration(&job, &config, i+1, len(configs))
+ // Process each configuration in the specified order
+ for i, config := range orderedConfigs {
+ s.processConfiguration(&job, &config, i+1, len(orderedConfigs))
}
// Update next run time after execution
@@ -366,6 +417,8 @@ func (s *Scheduler) executeJob(jobID uint) {
// processConfiguration processes a single configuration for a job
func (s *Scheduler) processConfiguration(job *db.Job, config *db.TransferConfig, index int, totalConfigs int) {
+ s.log.LogDebug("Processing configuration %d: %+v", config.ID, config)
+
s.log.LogInfo("Processing configuration %d (%d/%d) for job %d: source=%s:%s, dest=%s:%s",
config.ID,
index,
@@ -392,12 +445,16 @@ func (s *Scheduler) processConfiguration(job *db.Job, config *db.TransferConfig,
return
}
+ s.log.LogDebug("Creating job history record: %+v", history)
+
// Execute the configuration transfer
s.executeConfigTransfer(*job, *config, history)
}
// executeConfigTransfer performs the actual file transfer for a single configuration
func (s *Scheduler) executeConfigTransfer(job db.Job, config db.TransferConfig, history *db.JobHistory) {
+ s.log.LogDebug("Starting transfer for config %d with params: %+v", config.ID, config)
+
// Track files already processed in this job execution to prevent duplicates
processedFiles := make(map[string]bool)
@@ -447,7 +504,7 @@ func (s *Scheduler) executeConfigTransfer(job db.Job, config db.TransferConfig,
listArgs = append(listArgs, sourceListPath)
// Execute lsjson command
- s.log.LogInfo("Listing files with metadata for job %d, config %d: rclone %s", job.ID, config.ID, strings.Join(listArgs, " "))
+ s.log.LogDebug("Full lsjson command: %s %v", os.Getenv("RCLONE_PATH"), listArgs)
rclonePath := os.Getenv("RCLONE_PATH")
if rclonePath == "" {
rclonePath = "rclone"
@@ -455,6 +512,19 @@ func (s *Scheduler) executeConfigTransfer(job db.Job, config db.TransferConfig,
listCmd := exec.Command(rclonePath, listArgs...)
listOutput, listErr := listCmd.CombinedOutput()
+ // Add debug logging of raw output
+ if listErr == nil {
+ s.log.LogDebug("Raw lsjson output for job %d config %d:\n%s",
+ job.ID,
+ config.ID,
+ string(listOutput))
+ } else {
+ s.log.LogDebug("Raw lsjson output (error case) for job %d config %d:\n%s",
+ job.ID,
+ config.ID,
+ string(listOutput))
+ }
+
if listErr != nil {
s.log.LogError("Error listing files for job %d, config %d: %v", job.ID, config.ID, listErr)
// s.log.Debug.Printf("Output: %s", string(listOutput))
@@ -550,7 +620,7 @@ func (s *Scheduler) executeConfigTransfer(job db.Job, config db.TransferConfig,
concurrencySemaphore := make(chan struct{}, maxConcurrent)
// Process each file individually
- for _, fileEntry := range files {
+ for i, fileEntry := range files {
fileName, ok := fileEntry["Path"].(string)
if !ok || fileName == "" {
continue
@@ -666,7 +736,8 @@ func (s *Scheduler) executeConfigTransfer(job db.Job, config db.TransferConfig,
currentModTime := modTime
// Log the file information that will be processed
- s.log.LogDebug("Processing file: %s, size: %d, hash: %s", currentFileName, currentFileSize, currentFileHash)
+ s.log.LogDebug("Processing file %d/%d: %s (Size: %d, Hash: %s)",
+ i+1, len(files), currentFileName, currentFileSize, currentFileHash)
// Start goroutine for concurrent processing
go func() {
@@ -740,13 +811,8 @@ func (s *Scheduler) executeConfigTransfer(job db.Job, config db.TransferConfig,
transferArgs = append(transferArgs, sourcePath, destPath)
// Execute transfer for this file
- s.log.LogInfo("Executing rclone transfer command for job %d, config %d, file %s: rclone %s",
- job.ID, config.ID, currentFileName, strings.Join(transferArgs, " "))
- // Get the rclone path from the environment variable or use the default path
- rclonePath := os.Getenv("RCLONE_PATH")
- if rclonePath == "" {
- rclonePath = "rclone"
- }
+ s.log.LogDebug("Full transfer command: %s %v", rclonePath, transferArgs)
+ s.log.LogDebug("Environment: RCLONE_PATH=%s", os.Getenv("RCLONE_PATH"))
cmd := exec.Command(rclonePath, transferArgs...)
fileOutput, fileErr := cmd.CombinedOutput()
@@ -1087,6 +1153,8 @@ func (s *Scheduler) sendWebhookNotification(job *db.Job, history *db.JobHistory,
return
}
+ s.log.LogDebug("Webhook payload: %s", string(jsonPayload))
+
// Create HTTP request
req, err := http.NewRequest("POST", job.WebhookURL, bytes.NewBuffer(jsonPayload))
if err != nil {
@@ -1116,6 +1184,8 @@ func (s *Scheduler) sendWebhookNotification(job *db.Job, history *db.JobHistory,
}
}
+ s.log.LogDebug("Webhook headers: %+v", req.Header)
+
// Send the request with a timeout
client := &http.Client{
Timeout: 10 * time.Second,
diff --git a/internal/web/handlers/admin_tools_handlers.go b/internal/web/handlers/admin_tools_handlers.go
index 3ef70a9..2654afe 100644
--- a/internal/web/handlers/admin_tools_handlers.go
+++ b/internal/web/handlers/admin_tools_handlers.go
@@ -63,17 +63,28 @@ func (h *Handlers) HandleAdminTools(c *gin.Context) {
data.TotalUsers = int(totalUsers)
}
- // Get last backup time and backup count
- data.LastBackupTime, data.BackupCount = h.getBackupInfo()
+ // Get backup info (last backup time and count)
+ lastBackup, backupCount := h.getBackupInfo()
+ data.LastBackupTime = lastBackup
+ data.BackupCount = backupCount
// Get list of backup files
data.BackupFiles = h.getBackupFiles()
- // Check for maintenance issues
+ // Get maintenance message if any
data.MaintenanceMessage = h.checkMaintenanceIssues()
- // Render the admin tools page
- components.AdminTools(components.CreateTemplateContext(c), data).Render(c, c.Writer)
+ // Add SMTP server info if available
+ if h.Email != nil && h.Email.Config != nil && h.Email.Config.Email.Host != "" {
+ smtpServer := h.Email.Config.Email.Host
+ if h.Email.Config.Email.Port != 0 {
+ data.SmtpServer = fmt.Sprintf("%s:%d", smtpServer, h.Email.Config.Email.Port)
+ } else {
+ data.SmtpServer = smtpServer
+ }
+ }
+
+ components.AdminTools(c.Request.Context(), data).Render(c.Request.Context(), c.Writer)
}
// HandleBackupDatabase handles the backup database request
@@ -1368,3 +1379,43 @@ func (h *Handlers) HandleImportConfigsFromFile(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("%d configs imported successfully", imported)})
}
+
+// HandleTestEmail handles the POST /admin/test-email route
+func (h *Handlers) HandleTestEmail(c *gin.Context) {
+ // Parse the form
+ recipient := c.PostForm("recipient")
+ subject := c.PostForm("subject")
+ message := c.PostForm("message")
+
+ // Validate required fields
+ if recipient == "" {
+ components.EmailTestToast(false, "Recipient email is required").Render(c.Request.Context(), c.Writer)
+ return
+ }
+
+ // Get SMTP server info for display
+ smtpServer := ""
+ if h.Email != nil && h.Email.Config != nil && h.Email.Config.Email.Host != "" {
+ smtpServer = h.Email.Config.Email.Host
+ if h.Email.Config.Email.Port != 0 {
+ smtpServer = fmt.Sprintf("%s:%d", smtpServer, h.Email.Config.Email.Port)
+ }
+ }
+
+ // Send the test email
+ if h.Email == nil {
+ components.EmailTestToast(false, "Email service is not configured").Render(c.Request.Context(), c.Writer)
+ return
+ }
+
+ err := h.Email.SendTestEmail(recipient, subject, message)
+ if err != nil {
+ // Failed to send email
+ components.EmailTestToast(false, fmt.Sprintf("Failed to send email: %v", err)).Render(c.Request.Context(), c.Writer)
+ return
+ }
+
+ // Email sent successfully
+ successMsg := fmt.Sprintf("Test email sent successfully to %s", recipient)
+ components.EmailTestToast(true, successMsg).Render(c.Request.Context(), c.Writer)
+}
diff --git a/internal/web/handlers/job_handlers.go b/internal/web/handlers/job_handlers.go
index 14e6aa8..47739fd 100644
--- a/internal/web/handlers/job_handlers.go
+++ b/internal/web/handlers/job_handlers.go
@@ -2,8 +2,10 @@ package handlers
import (
"fmt"
+ "log"
"net/http"
"strconv"
+ "strings"
"github.com/gin-gonic/gin"
"github.com/starfleetcptn/gomft/components"
@@ -141,6 +143,9 @@ func (h *Handlers) HandleEditJob(c *gin.Context) {
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 {
@@ -148,6 +153,9 @@ func (h *Handlers) HandleCreateJob(c *gin.Context) {
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 {
@@ -155,35 +163,83 @@ func (h *Handlers) HandleCreateJob(c *gin.Context) {
return
}
+ // Debug logging
+ log.Printf("HandleCreateJob: config_ids[]: %v", configIDs)
+
// Process config IDs
var configIDsList []uint
- 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 we have an explicit order specified
+ configOrder := c.PostForm("config_order")
+ log.Printf("HandleCreateJob: config_order: %s", configOrder)
- // 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")
+ 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
}
- }
- configIDsList = append(configIDsList, uint(configID))
+ // 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]
@@ -214,6 +270,10 @@ func (h *Handlers) HandleCreateJob(c *gin.Context) {
// 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"
@@ -239,10 +299,13 @@ func (h *Handlers) HandleCreateJob(c *gin.Context) {
// Create the job
if err := h.DB.CreateJob(&job); err != nil {
+ log.Printf("HandleCreateJob: Error creating job: %v", err)
c.String(http.StatusInternalServerError, "Failed to create job")
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())
@@ -257,8 +320,13 @@ 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
}
@@ -275,49 +343,102 @@ func (h *Handlers) HandleUpdateJob(c *gin.Context) {
// 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
- 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 we have an explicit order specified
+ configOrder := c.PostForm("config_order")
+ log.Printf("HandleUpdateJob: config_order: %s", configOrder)
- // 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")
+ 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
}
- }
- configIDsList = append(configIDsList, uint(configID))
+ // 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]
@@ -332,6 +453,10 @@ func (h *Handlers) HandleUpdateJob(c *gin.Context) {
// 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"
@@ -357,10 +482,13 @@ func (h *Handlers) HandleUpdateJob(c *gin.Context) {
job.Config = db.TransferConfig{}
if err := h.DB.UpdateJob(&job); err != nil {
+ log.Printf("HandleUpdateJob: Error updating job: %v", err)
c.String(http.StatusInternalServerError, "Failed to update job")
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())
diff --git a/internal/web/handlers/routes.go b/internal/web/handlers/routes.go
index a84cac9..448627e 100644
--- a/internal/web/handlers/routes.go
+++ b/internal/web/handlers/routes.go
@@ -110,6 +110,9 @@ func (h *Handlers) RegisterRoutes(router *gin.Engine) {
admin.GET("/logs/refresh", h.HandleRefreshLogs)
admin.GET("/logs/view/:fileName", h.HandleViewLog)
admin.GET("/logs/download/:fileName", h.HandleDownloadLog)
+
+ // Email test route
+ admin.POST("/test-email", h.HandleTestEmail)
}
// API routes