diff --git a/tui/main.go b/tui/main.go index 19f3e3e..29007f8 100644 --- a/tui/main.go +++ b/tui/main.go @@ -479,10 +479,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.backupStatus[msg.service] == nil { m.backupStatus[msg.service] = &BackupStatus{} } - + // Create context for this backup ctx, cancel := context.WithCancel(context.Background()) - + m.backupStatus[msg.service].Service = msg.service m.backupStatus[msg.service].Status = "running" m.backupStatus[msg.service].LastRun = time.Now() @@ -490,9 +490,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.backupStatus[msg.service].Progress = 0 m.backupStatus[msg.service].Context = ctx m.backupStatus[msg.service].Cancel = cancel - + m.viewport.SetContent(fmt.Sprintf("šŸš€ Starting backup for %s...\n\nInitializing backup process...\nPress 'x' to cancel if needed.", msg.service)) - + // Start progress ticker for time-based progress estimation cmds = append(cmds, m.startProgressTicker(msg.service)) @@ -547,18 +547,18 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.backupStatus[msg.service].EndTime = time.Now() m.backupStatus[msg.service].Duration = m.backupStatus[msg.service].EndTime.Sub(m.backupStatus[msg.service].StartTime).String() m.backupStatus[msg.service].Progress = 100 - + // Clean up context if m.backupStatus[msg.service].Cancel != nil { m.backupStatus[msg.service].Cancel() } } delete(m.runningBackups, msg.service) - + // Parse output for additional info lines := strings.Split(msg.output, "\n") var summary strings.Builder - + // Extract meaningful summary information for _, line := range lines { if strings.Contains(line, "Total files:") || @@ -569,18 +569,18 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { summary.WriteString("šŸ“Š " + strings.TrimSpace(line) + "\n") } } - + summaryText := summary.String() if summaryText == "" { summaryText = "šŸ“Š Backup completed successfully" } - + duration := "" if m.backupStatus[msg.service] != nil { duration = m.backupStatus[msg.service].Duration } - - m.viewport.SetContent(fmt.Sprintf("āœ… Backup completed successfully for %s!\n\nDuration: %s\n\n%s\n\nšŸ“‹ Full Output:\n%s", + + m.viewport.SetContent(fmt.Sprintf("āœ… Backup completed successfully for %s!\n\nDuration: %s\n\n%s\n\nšŸ“‹ Full Output:\n%s", msg.service, duration, summaryText, msg.output)) case backupErrorMsg: @@ -589,19 +589,19 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.backupStatus[msg.service].Error = msg.error m.backupStatus[msg.service].EndTime = time.Now() m.backupStatus[msg.service].Duration = m.backupStatus[msg.service].EndTime.Sub(m.backupStatus[msg.service].StartTime).String() - + // Clean up context if m.backupStatus[msg.service].Cancel != nil { m.backupStatus[msg.service].Cancel() } } delete(m.runningBackups, msg.service) - + duration := "" if m.backupStatus[msg.service] != nil { duration = m.backupStatus[msg.service].Duration } - + // Provide helpful error analysis var errorAnalysis string if strings.Contains(msg.error, "permission denied") { @@ -613,8 +613,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } else if strings.Contains(msg.error, "space") { errorAnalysis = "\nšŸ’” This might be a disk space issue. Check available storage." } - - m.viewport.SetContent(fmt.Sprintf("āŒ Backup failed for %s!\n\nDuration: %s%s\n\n🚨 Error Details:\n%s", + + m.viewport.SetContent(fmt.Sprintf("āŒ Backup failed for %s!\n\nDuration: %s%s\n\n🚨 Error Details:\n%s", msg.service, duration, errorAnalysis, msg.error)) case spinner.TickMsg: @@ -663,11 +663,11 @@ func (m *Model) updateViewportForRunningService(service string, outputForDisplay totalEstimatedDuration := time.Duration(totalEstimatedRaw) remainingDuration := totalEstimatedDuration - elapsed if remainingDuration > 0 { - etaStr = fmt.Sprintf("\\nETA: %s", remainingDuration.Round(time.Second)) + etaStr = fmt.Sprintf("\nETA: %s", remainingDuration.Round(time.Second)) } } - m.viewport.SetContent(fmt.Sprintf("ā³ Backup in progress for %s...\\n\\n%s %d%%%%\\nElapsed: %s%s\\n\\nšŸ“‹ Latest output:\\n%s\\n\\nPress 'x' to stop the backup.", + m.viewport.SetContent(fmt.Sprintf("ā³ Backup in progress for %s...\n\n%s %d%%\nElapsed: %s%s\n\nšŸ“‹ Latest output:\n%s\n\nPress 'x' to stop the backup.", service, progressBar, progress, elapsed.Round(time.Second), etaStr, outputForDisplay)) } @@ -702,7 +702,7 @@ func (m Model) View() string { if m.activePanel == 1 { viewportStyle = viewportStyle.BorderStyle(lipgloss.ThickBorder()).BorderForeground(lipgloss.Color("#7D56F4")) } - + // Add view indicator to viewport title viewportTitle := "" switch m.currentView { @@ -715,7 +715,7 @@ func (m Model) View() string { default: viewportTitle = "šŸ’» Output" } - + // Layout based on active panel return lipgloss.JoinVertical(lipgloss.Left, titleStyle.Render("šŸ”§ Media & Plex Backup Manager"), @@ -833,7 +833,7 @@ func (m *Model) showStatus() { for _, service := range services { status := m.backupStatus[service] content.WriteString(fmt.Sprintf("šŸ”§ Service: %s\n", strings.ToUpper(service))) - + // Status with emoji statusIcon := "ā­•" switch status.Status { @@ -847,34 +847,34 @@ func (m *Model) showStatus() { statusIcon = "šŸ›‘" } content.WriteString(fmt.Sprintf(" Status: %s %s\n", statusIcon, status.Status)) - + if !status.LastRun.IsZero() { content.WriteString(fmt.Sprintf(" Last Run: %s\n", status.LastRun.Format("2006-01-02 15:04:05"))) } - + if status.Duration != "" { content.WriteString(fmt.Sprintf(" Duration: %s\n", status.Duration)) } - + if status.Progress > 0 && status.Status == "running" { content.WriteString(fmt.Sprintf(" Progress: %d%%\n", status.Progress)) } - + if status.BackupSize != "" { content.WriteString(fmt.Sprintf(" Size: %s\n", status.BackupSize)) } - + if status.FilesCount > 0 { content.WriteString(fmt.Sprintf(" Files: %d\n", status.FilesCount)) } - + if status.Error != "" { content.WriteString(fmt.Sprintf(" Error: %s\n", status.Error[:min(100, len(status.Error))])) } - + content.WriteString("\n") } - + // Add summary running := 0 success := 0 @@ -889,7 +889,7 @@ func (m *Model) showStatus() { failed++ } } - + content.WriteString("šŸ“Š SUMMARY:\n") content.WriteString(fmt.Sprintf(" Running: %d | Success: %d | Failed: %d\n", running, success, failed)) } @@ -916,7 +916,7 @@ func (m *Model) showConfig() { if len(item.args) > 0 { content.WriteString(fmt.Sprintf(" Args: %s\n", strings.Join(item.args, " "))) } - + // Check if script exists if _, err := os.Stat(item.script); os.IsNotExist(err) { content.WriteString(" āš ļø Status: Script not found\n") @@ -950,10 +950,10 @@ func (m *Model) stopBackup(service string, process *RunningProcess) tea.Cmd { if process.Cancel != nil { process.Cancel() } - + // Give process time to shut down gracefully time.Sleep(2 * time.Second) - + // Force kill if still running if process.Cmd != nil && process.Cmd.Process != nil { if process.Cmd.ProcessState == nil || !process.Cmd.ProcessState.Exited() { @@ -977,11 +977,11 @@ func (m Model) runBackupCommand(item BackupItem) tea.Cmd { // Create context for cancellation ctx, cancel := context.WithCancel(context.Background()) defer cancel() - + // Execute the backup script with context cmd := exec.CommandContext(ctx, item.script, item.args...) cmd.Dir = m.shellPath - + // Create pipes for stdout and stderr stdout, err := cmd.StdoutPipe() if err != nil { @@ -990,7 +990,7 @@ func (m Model) runBackupCommand(item BackupItem) tea.Cmd { error: fmt.Sprintf("Failed to create stdout pipe: %s", err.Error()), } } - + stderr, err := cmd.StderrPipe() if err != nil { return backupErrorMsg{ @@ -998,7 +998,7 @@ func (m Model) runBackupCommand(item BackupItem) tea.Cmd { error: fmt.Sprintf("Failed to create stderr pipe: %s", err.Error()), } } - + // Start the command if err := cmd.Start(); err != nil { return backupErrorMsg{ @@ -1006,14 +1006,14 @@ func (m Model) runBackupCommand(item BackupItem) tea.Cmd { error: fmt.Sprintf("Failed to start backup: %s", err.Error()), } } - + var outputBuilder strings.Builder var allOutput []string - + // Monitor stdout and stderr in goroutines var wg sync.WaitGroup wg.Add(2) - + // Read stdout go func() { defer wg.Done() @@ -1024,7 +1024,7 @@ func (m Model) runBackupCommand(item BackupItem) tea.Cmd { allOutput = append(allOutput, line) } }() - + // Read stderr go func() { defer wg.Done() @@ -1035,21 +1035,21 @@ func (m Model) runBackupCommand(item BackupItem) tea.Cmd { allOutput = append(allOutput, "[STDERR] "+line) } }() - + // Wait for the command to complete err = cmd.Wait() - + // Wait for output readers to finish wg.Wait() - + finalOutput := outputBuilder.String() - + if err != nil { // Check if it was cancelled if ctx.Err() == context.Canceled { return backupStopMsg{service: item.service} } - + return backupErrorMsg{ service: item.service, error: fmt.Sprintf("Backup failed: %s\n\nšŸ“‹ Output:\n%s", err.Error(), finalOutput), @@ -1068,7 +1068,7 @@ func estimateProgress(output []string, service string) int { if len(output) == 0 { return 0 } - + switch service { case "plex": // Plex backup stages: stop service (10%), backup files (60%), verify (20%), archive (10%) @@ -1080,7 +1080,7 @@ func estimateProgress(output []string, service string) int { "archiving": 90, "completed": 100, } - + for _, line := range output { line = strings.ToLower(line) for stage, progress := range stages { @@ -1089,7 +1089,7 @@ func estimateProgress(output []string, service string) int { } } } - + case "immich": // Immich stages: database dump (50%), uploads backup (40%), upload to B2 (10%) stages := map[string]int{ @@ -1099,7 +1099,7 @@ func estimateProgress(output []string, service string) int { "uploading to": 90, "completed": 100, } - + for _, line := range output { line = strings.ToLower(line) for stage, progress := range stages { @@ -1108,26 +1108,26 @@ func estimateProgress(output []string, service string) int { } } } - + case "media": // Media services backup: each service adds progress services := []string{"sonarr", "radarr", "prowlarr", "audiobookshelf", "tautulli", "sabnzbd", "jellyseerr"} completed := 0 - + for _, service := range services { for _, line := range output { - if strings.Contains(strings.ToLower(line), service) && - (strings.Contains(strings.ToLower(line), "success") || + if strings.Contains(strings.ToLower(line), service) && + (strings.Contains(strings.ToLower(line), "success") || strings.Contains(strings.ToLower(line), "completed")) { completed++ break } } } - + return (completed * 100) / len(services) } - + // Default progress estimation based on line count if len(output) < 10 { return 20 @@ -1136,7 +1136,7 @@ func estimateProgress(output []string, service string) int { } else if len(output) < 100 { return 80 } - + return 90 } @@ -1248,11 +1248,11 @@ func createProgressBar(progress int) string { if progress > 100 { progress = 100 } - + const barWidth = 30 filled := (progress * barWidth) / 100 empty := barWidth - filled - + bar := strings.Repeat("ā–ˆ", filled) + strings.Repeat("ā–‘", empty) return fmt.Sprintf("[%s]", bar) } @@ -1263,7 +1263,7 @@ func (m Model) startProgressTicker(service string) tea.Cmd { // Check if backup is still running if status, exists := m.backupStatus[service]; exists && status.Status == "running" { elapsed := time.Since(status.StartTime) - + // Estimate progress based on typical backup durations var estimatedDuration time.Duration switch service { @@ -1276,13 +1276,13 @@ func (m Model) startProgressTicker(service string) tea.Cmd { default: estimatedDuration = 5 * time.Minute } - + // Calculate progress (cap at 95% until actual completion) progress := int((elapsed.Seconds() / estimatedDuration.Seconds()) * 95) if progress > 95 { progress = 95 } - + return backupProgressMsg{ service: service, progress: progress,