diff --git a/bin/README.md b/bin/README.md index 72e171b..5dea344 100644 --- a/bin/README.md +++ b/bin/README.md @@ -4,8 +4,8 @@ This directory contains pre-compiled binaries for the DB Backup Tool across mult ## Build Information - **Version**: 3.42.34 -- **Build Time**: 2026-01-15_17:50:35_UTC -- **Git Commit**: 4938dc1 +- **Build Time**: 2026-01-16_08:42:47_UTC +- **Git Commit**: a85ad0c ## Recent Updates (v1.1.0) - ✅ Fixed TUI progress display with line-by-line output diff --git a/internal/restore/engine.go b/internal/restore/engine.go index 841f6f0..7759082 100755 --- a/internal/restore/engine.go +++ b/internal/restore/engine.go @@ -49,9 +49,9 @@ type Engine struct { debugLogPath string // Path to save debug log on error // TUI progress callback for detailed progress reporting - progressCallback ProgressCallback - dbProgressCallback DatabaseProgressCallback - dbProgressTimingCallback DatabaseProgressWithTimingCallback + progressCallback ProgressCallback + dbProgressCallback DatabaseProgressCallback + dbProgressTimingCallback DatabaseProgressWithTimingCallback } // New creates a new restore engine diff --git a/internal/tui/backup_exec.go b/internal/tui/backup_exec.go index baab868..963e969 100644 --- a/internal/tui/backup_exec.go +++ b/internal/tui/backup_exec.go @@ -380,22 +380,83 @@ func (m BackupExecutionModel) View() string { s.WriteString("\n [KEY] Press Ctrl+C or ESC to cancel\n") } } else { - s.WriteString(fmt.Sprintf(" %s\n\n", m.status)) - + // Show completion summary with detailed stats if m.err != nil { - s.WriteString(fmt.Sprintf(" [FAIL] Error: %v\n", m.err)) - } else if m.result != "" { - // Parse and display result cleanly - lines := strings.Split(m.result, "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - if line != "" { - s.WriteString(" " + line + "\n") + s.WriteString("\n") + s.WriteString(errorStyle.Render(" ╔══════════════════════════════════════════════════════════╗")) + s.WriteString("\n") + s.WriteString(errorStyle.Render(" ║ [FAIL] BACKUP FAILED ║")) + s.WriteString("\n") + s.WriteString(errorStyle.Render(" ╚══════════════════════════════════════════════════════════╝")) + s.WriteString("\n\n") + s.WriteString(errorStyle.Render(fmt.Sprintf(" Error: %v", m.err))) + s.WriteString("\n") + } else { + s.WriteString("\n") + s.WriteString(successStyle.Render(" ╔══════════════════════════════════════════════════════════╗")) + s.WriteString("\n") + s.WriteString(successStyle.Render(" ║ [OK] BACKUP COMPLETED SUCCESSFULLY ║")) + s.WriteString("\n") + s.WriteString(successStyle.Render(" ╚══════════════════════════════════════════════════════════╝")) + s.WriteString("\n\n") + + // Summary section + s.WriteString(infoStyle.Render(" ─── Summary ─────────────────────────────────────────────")) + s.WriteString("\n\n") + + // Backup type specific info + switch m.backupType { + case "cluster": + s.WriteString(" Type: Cluster Backup\n") + if m.dbTotal > 0 { + s.WriteString(fmt.Sprintf(" Databases: %d backed up\n", m.dbTotal)) } + case "single": + s.WriteString(" Type: Single Database Backup\n") + s.WriteString(fmt.Sprintf(" Database: %s\n", m.databaseName)) + case "sample": + s.WriteString(" Type: Sample Backup\n") + s.WriteString(fmt.Sprintf(" Database: %s\n", m.databaseName)) + s.WriteString(fmt.Sprintf(" Sample Ratio: %d\n", m.ratio)) } + + s.WriteString("\n") + + // Timing section + s.WriteString(infoStyle.Render(" ─── Timing ──────────────────────────────────────────────")) + s.WriteString("\n\n") + + elapsed := time.Since(m.startTime) + s.WriteString(fmt.Sprintf(" Total Time: %s\n", formatBackupDuration(elapsed))) + + if m.backupType == "cluster" && m.dbTotal > 0 { + avgPerDB := elapsed / time.Duration(m.dbTotal) + s.WriteString(fmt.Sprintf(" Avg per DB: %s\n", formatBackupDuration(avgPerDB))) + } + + s.WriteString("\n") + s.WriteString(infoStyle.Render(" ─────────────────────────────────────────────────────────")) + s.WriteString("\n") } - s.WriteString("\n [KEY] Press Enter or ESC to return to menu\n") + + s.WriteString("\n") + s.WriteString(" [KEY] Press Enter or ESC to return to menu\n") } return s.String() } + +// formatBackupDuration formats duration in human readable format +func formatBackupDuration(d time.Duration) string { + if d < time.Minute { + return fmt.Sprintf("%.1fs", d.Seconds()) + } + if d < time.Hour { + minutes := int(d.Minutes()) + seconds := int(d.Seconds()) % 60 + return fmt.Sprintf("%dm %ds", minutes, seconds) + } + hours := int(d.Hours()) + minutes := int(d.Minutes()) % 60 + return fmt.Sprintf("%dh %dm", hours, minutes) +} diff --git a/internal/tui/restore_exec.go b/internal/tui/restore_exec.go index d3c7888..a6c3f37 100755 --- a/internal/tui/restore_exec.go +++ b/internal/tui/restore_exec.go @@ -544,22 +544,71 @@ func (m RestoreExecutionModel) View() string { s.WriteString("\n") if m.done { - // Show result + // Show result with comprehensive summary if m.err != nil { - s.WriteString(errorStyle.Render("[FAIL] Restore Failed")) + s.WriteString(errorStyle.Render("╔══════════════════════════════════════════════════════════════╗")) + s.WriteString("\n") + s.WriteString(errorStyle.Render("║ [FAIL] RESTORE FAILED ║")) + s.WriteString("\n") + s.WriteString(errorStyle.Render("╚══════════════════════════════════════════════════════════════╝")) s.WriteString("\n\n") - s.WriteString(errorStyle.Render(fmt.Sprintf("Error: %v", m.err))) + s.WriteString(errorStyle.Render(fmt.Sprintf(" Error: %v", m.err))) s.WriteString("\n") } else { - s.WriteString(successStyle.Render("[OK] Restore Completed Successfully")) + s.WriteString(successStyle.Render("╔══════════════════════════════════════════════════════════════╗")) + s.WriteString("\n") + s.WriteString(successStyle.Render("║ [OK] RESTORE COMPLETED SUCCESSFULLY ║")) + s.WriteString("\n") + s.WriteString(successStyle.Render("╚══════════════════════════════════════════════════════════════╝")) s.WriteString("\n\n") - s.WriteString(successStyle.Render(m.result)) + + // Summary section + s.WriteString(infoStyle.Render(" ─── Summary ───────────────────────────────────────────────")) + s.WriteString("\n\n") + + // Archive info + s.WriteString(fmt.Sprintf(" Archive: %s\n", m.archive.Name)) + if m.archive.Size > 0 { + s.WriteString(fmt.Sprintf(" Archive Size: %s\n", FormatBytes(m.archive.Size))) + } + + // Restore type specific info + if m.restoreType == "restore-cluster" { + s.WriteString(fmt.Sprintf(" Type: Cluster Restore\n")) + if m.dbTotal > 0 { + s.WriteString(fmt.Sprintf(" Databases: %d restored\n", m.dbTotal)) + } + if m.cleanClusterFirst && len(m.existingDBs) > 0 { + s.WriteString(fmt.Sprintf(" Cleaned: %d existing database(s) dropped\n", len(m.existingDBs))) + } + } else { + s.WriteString(fmt.Sprintf(" Type: Single Database Restore\n")) + s.WriteString(fmt.Sprintf(" Target DB: %s\n", m.targetDB)) + } + s.WriteString("\n") } - s.WriteString(fmt.Sprintf("\nElapsed Time: %s\n", formatDuration(m.elapsed))) + // Timing section + s.WriteString(infoStyle.Render(" ─── Timing ────────────────────────────────────────────────")) + s.WriteString("\n\n") + s.WriteString(fmt.Sprintf(" Total Time: %s\n", formatDuration(m.elapsed))) + + // Calculate and show throughput if we have size info + if m.archive.Size > 0 && m.elapsed.Seconds() > 0 { + throughput := float64(m.archive.Size) / m.elapsed.Seconds() + s.WriteString(fmt.Sprintf(" Throughput: %s/s (average)\n", FormatBytes(int64(throughput)))) + } + + if m.dbTotal > 0 && m.err == nil { + avgPerDB := m.elapsed / time.Duration(m.dbTotal) + s.WriteString(fmt.Sprintf(" Avg per DB: %s\n", formatDuration(avgPerDB))) + } + s.WriteString("\n") - s.WriteString(infoStyle.Render("[KEYS] Press Enter to continue")) + s.WriteString(infoStyle.Render(" ───────────────────────────────────────────────────────────")) + s.WriteString("\n\n") + s.WriteString(infoStyle.Render(" [KEYS] Press Enter to continue")) } else { // Show progress s.WriteString(fmt.Sprintf("Phase: %s\n", m.phase))