diff --git a/bin/README.md b/bin/README.md index 7a715be..61ebf98 100644 --- a/bin/README.md +++ b/bin/README.md @@ -3,9 +3,9 @@ This directory contains pre-compiled binaries for the DB Backup Tool across multiple platforms and architectures. ## Build Information -- **Version**: 3.42.1 -- **Build Time**: 2026-01-08_05:03:53_UTC -- **Git Commit**: 9c65821 +- **Version**: 3.42.10 +- **Build Time**: 2026-01-08_08:48:48_UTC +- **Git Commit**: 24377ea ## Recent Updates (v1.1.0) - ✅ Fixed TUI progress display with line-by-line output diff --git a/cmd/restore.go b/cmd/restore.go index a4ff28b..4eabb6e 100755 --- a/cmd/restore.go +++ b/cmd/restore.go @@ -37,9 +37,9 @@ var ( restoreSaveDebugLog string // Path to save debug log on failure // Diagnose flags - diagnoseJSON bool - diagnoseDeep bool - diagnoseKeepTemp bool + diagnoseJSON bool + diagnoseDeep bool + diagnoseKeepTemp bool // Encryption flags restoreEncryptionKeyFile string @@ -565,7 +565,7 @@ func runRestoreSingle(cmd *cobra.Command, args []string) error { // Create restore engine engine := restore.New(cfg, log, db) - + // Enable debug logging if requested if restoreSaveDebugLog != "" { engine.SetDebugLogPath(restoreSaveDebugLog) @@ -589,15 +589,15 @@ func runRestoreSingle(cmd *cobra.Command, args []string) error { // Run pre-restore diagnosis if requested if restoreDiagnose { log.Info("[DIAG] Running pre-restore diagnosis...") - + diagnoser := restore.NewDiagnoser(log, restoreVerbose) result, err := diagnoser.DiagnoseFile(archivePath) if err != nil { return fmt.Errorf("diagnosis failed: %w", err) } - + diagnoser.PrintDiagnosis(result) - + if !result.IsValid { log.Error("[FAIL] Pre-restore diagnosis found issues") if result.IsTruncated { @@ -607,7 +607,7 @@ func runRestoreSingle(cmd *cobra.Command, args []string) error { log.Error(" The backup file appears to be CORRUPTED") } fmt.Println("\nUse --force to attempt restore anyway.") - + if !restoreForce { return fmt.Errorf("aborting restore due to backup file issues") } @@ -785,7 +785,7 @@ func runRestoreCluster(cmd *cobra.Command, args []string) error { // Create restore engine engine := restore.New(cfg, log, db) - + // Enable debug logging if requested if restoreSaveDebugLog != "" { engine.SetDebugLogPath(restoreSaveDebugLog) @@ -830,7 +830,7 @@ func runRestoreCluster(cmd *cobra.Command, args []string) error { // Run pre-restore diagnosis if requested if restoreDiagnose { log.Info("[DIAG] Running pre-restore diagnosis...") - + // Create temp directory for extraction in configured WorkDir workDir := cfg.GetEffectiveWorkDir() diagTempDir, err := os.MkdirTemp(workDir, "dbbackup-diagnose-*") @@ -838,13 +838,13 @@ func runRestoreCluster(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to create temp directory for diagnosis in %s: %w", workDir, err) } defer os.RemoveAll(diagTempDir) - + diagnoser := restore.NewDiagnoser(log, restoreVerbose) results, err := diagnoser.DiagnoseClusterDumps(archivePath, diagTempDir) if err != nil { return fmt.Errorf("diagnosis failed: %w", err) } - + // Check for any invalid dumps var invalidDumps []string for _, result := range results { @@ -853,7 +853,7 @@ func runRestoreCluster(cmd *cobra.Command, args []string) error { diagnoser.PrintDiagnosis(result) } } - + if len(invalidDumps) > 0 { log.Error("[FAIL] Pre-restore diagnosis found issues", "invalid_dumps", len(invalidDumps), @@ -864,7 +864,7 @@ func runRestoreCluster(cmd *cobra.Command, args []string) error { } fmt.Println("\nRun 'dbbackup restore diagnose --deep' for full details.") fmt.Println("Use --force to attempt restore anyway.") - + if !restoreForce { return fmt.Errorf("aborting restore due to %d invalid dump(s)", len(invalidDumps)) } diff --git a/internal/installer/installer.go b/internal/installer/installer.go index 05d7db0..d52c29d 100644 --- a/internal/installer/installer.go +++ b/internal/installer/installer.go @@ -53,16 +53,16 @@ type InstallOptions struct { // ServiceStatus contains information about installed services type ServiceStatus struct { - Installed bool - Enabled bool - Active bool - TimerEnabled bool - TimerActive bool - LastRun string - NextRun string - ServicePath string - TimerPath string - ExporterPath string + Installed bool + Enabled bool + Active bool + TimerEnabled bool + TimerActive bool + LastRun string + NextRun string + ServicePath string + TimerPath string + ExporterPath string } // NewInstaller creates a new Installer @@ -188,7 +188,7 @@ func (i *Installer) Uninstall(ctx context.Context, instance string, purge bool) if instance != "cluster" && instance != "" { templateService := filepath.Join(i.unitDir, "dbbackup@.service") templateTimer := filepath.Join(i.unitDir, "dbbackup@.timer") - + // Only remove templates if no other instances are using them if i.canRemoveTemplates() { if !i.dryRun { @@ -644,11 +644,11 @@ func (i *Installer) canRemoveTemplates() bool { // Check if any dbbackup@*.service instances exist pattern := filepath.Join(i.unitDir, "dbbackup@*.service") matches, _ := filepath.Glob(pattern) - + // Also check for running instances cmd := exec.Command("systemctl", "list-units", "--type=service", "--all", "dbbackup@*") output, _ := cmd.Output() - + return len(matches) == 0 && !strings.Contains(string(output), "dbbackup@") } diff --git a/internal/tui/backup_manager.go b/internal/tui/backup_manager.go index 6439e2c..0f980a6 100755 --- a/internal/tui/backup_manager.go +++ b/internal/tui/backup_manager.go @@ -180,11 +180,11 @@ func (m BackupManagerModel) View() string { return s.String() } - // Column headers - s.WriteString(archiveHeaderStyle.Render(fmt.Sprintf("%-35s %-25s %-12s %-20s", + // Column headers with better alignment + s.WriteString(archiveHeaderStyle.Render(fmt.Sprintf(" %-32s %-22s %10s %-16s", "FILENAME", "FORMAT", "SIZE", "MODIFIED"))) s.WriteString("\n") - s.WriteString(strings.Repeat("-", 95)) + s.WriteString(strings.Repeat("-", 90)) s.WriteString("\n") // Show archives (limit to visible area) @@ -199,27 +199,27 @@ func (m BackupManagerModel) View() string { for i := start; i < end; i++ { archive := m.archives[i] - cursor := " " + cursor := " " style := archiveNormalStyle if i == m.cursor { - cursor = ">" + cursor = "> " style = archiveSelectedStyle } - // Status icon - statusIcon := "[+]" + // Status icon - consistent 4-char width + statusIcon := " [+]" if !archive.Valid { - statusIcon = "[-]" + statusIcon = " [-]" style = archiveInvalidStyle } else if time.Since(archive.Modified) > 30*24*time.Hour { - statusIcon = "[WARN]" + statusIcon = " [!]" } - filename := truncate(archive.Name, 33) - format := truncate(archive.Format.String(), 23) + filename := truncate(archive.Name, 32) + format := truncate(archive.Format.String(), 22) - line := fmt.Sprintf("%s %s %-33s %-23s %-10s %-19s", + line := fmt.Sprintf("%s%s %-32s %-22s %10s %-16s", cursor, statusIcon, filename, @@ -239,8 +239,20 @@ func (m BackupManagerModel) View() string { } s.WriteString(infoStyle.Render(fmt.Sprintf("Selected: %d/%d", m.cursor+1, len(m.archives)))) + s.WriteString("\n\n") + + // Grouped keyboard shortcuts for better readability + s.WriteString(infoStyle.Render("NAVIGATE ACTIONS OTHER")) s.WriteString("\n") - s.WriteString(infoStyle.Render("[KEY] ↑/↓: Navigate | r: Restore | v: Verify | d: Delete | i: Info | R: Refresh | Esc: Back")) + s.WriteString(infoStyle.Render("-------- ------- -----")) + s.WriteString("\n") + s.WriteString(infoStyle.Render("Up/Down: Move r: Restore R: Refresh")) + s.WriteString("\n") + s.WriteString(infoStyle.Render(" v: Verify Esc: Back")) + s.WriteString("\n") + s.WriteString(infoStyle.Render(" d: Delete q: Quit")) + s.WriteString("\n") + s.WriteString(infoStyle.Render(" i: Info")) return s.String() } diff --git a/internal/tui/diagnose_view.go b/internal/tui/diagnose_view.go index 2683abc..1f5b99d 100644 --- a/internal/tui/diagnose_view.go +++ b/internal/tui/diagnose_view.go @@ -204,124 +204,132 @@ func (m DiagnoseViewModel) View() string { func (m DiagnoseViewModel) renderSingleResult(result *restore.DiagnoseResult) string { var s strings.Builder - // Status - s.WriteString(strings.Repeat("-", 60)) - s.WriteString("\n") + // Status Box + s.WriteString("+--[ VALIDATION STATUS ]" + strings.Repeat("-", 37) + "+\n") if result.IsValid { - s.WriteString(diagnosePassStyle.Render("[OK] STATUS: VALID")) + s.WriteString("| " + diagnosePassStyle.Render("[OK] VALID - Archive passed all checks") + strings.Repeat(" ", 18) + "|\n") } else { - s.WriteString(diagnoseFailStyle.Render("[FAIL] STATUS: INVALID")) + s.WriteString("| " + diagnoseFailStyle.Render("[FAIL] INVALID - Archive has problems") + strings.Repeat(" ", 19) + "|\n") } - s.WriteString("\n") if result.IsTruncated { - s.WriteString(diagnoseFailStyle.Render("[WARN] TRUNCATED: File appears incomplete")) - s.WriteString("\n") + s.WriteString("| " + diagnoseFailStyle.Render("[!] TRUNCATED - File is incomplete") + strings.Repeat(" ", 22) + "|\n") } if result.IsCorrupted { - s.WriteString(diagnoseFailStyle.Render("[WARN] CORRUPTED: File structure is damaged")) - s.WriteString("\n") + s.WriteString("| " + diagnoseFailStyle.Render("[!] CORRUPTED - File structure damaged") + strings.Repeat(" ", 18) + "|\n") } - s.WriteString(strings.Repeat("-", 60)) - s.WriteString("\n\n") + s.WriteString("+" + strings.Repeat("-", 60) + "+\n\n") - // Details + // Details Box if result.Details != nil { - s.WriteString(diagnoseHeaderStyle.Render("[STATS] DETAILS:")) - s.WriteString("\n") + s.WriteString("+--[ DETAILS ]" + strings.Repeat("-", 46) + "+\n") if result.Details.HasPGDMPSignature { - s.WriteString(diagnosePassStyle.Render(" [+] ")) - s.WriteString("Has PGDMP signature (custom format)\n") + s.WriteString("| " + diagnosePassStyle.Render("[+]") + " PostgreSQL custom format (PGDMP)" + strings.Repeat(" ", 20) + "|\n") } if result.Details.HasSQLHeader { - s.WriteString(diagnosePassStyle.Render(" [+] ")) - s.WriteString("Has PostgreSQL SQL header\n") + s.WriteString("| " + diagnosePassStyle.Render("[+]") + " PostgreSQL SQL header found" + strings.Repeat(" ", 25) + "|\n") } if result.Details.GzipValid { - s.WriteString(diagnosePassStyle.Render(" [+] ")) - s.WriteString("Gzip compression valid\n") + s.WriteString("| " + diagnosePassStyle.Render("[+]") + " Gzip compression valid" + strings.Repeat(" ", 30) + "|\n") } if result.Details.PgRestoreListable { - s.WriteString(diagnosePassStyle.Render(" [+] ")) - s.WriteString(fmt.Sprintf("pg_restore can list contents (%d tables)\n", result.Details.TableCount)) + tableInfo := fmt.Sprintf(" (%d tables)", result.Details.TableCount) + padding := 36 - len(tableInfo) + if padding < 0 { + padding = 0 + } + s.WriteString("| " + diagnosePassStyle.Render("[+]") + " pg_restore can list contents" + tableInfo + strings.Repeat(" ", padding) + "|\n") } if result.Details.CopyBlockCount > 0 { - s.WriteString(diagnoseInfoStyle.Render(" - ")) - s.WriteString(fmt.Sprintf("Contains %d COPY blocks\n", result.Details.CopyBlockCount)) + blockInfo := fmt.Sprintf("%d COPY blocks found", result.Details.CopyBlockCount) + padding := 50 - len(blockInfo) + if padding < 0 { + padding = 0 + } + s.WriteString("| [-] " + blockInfo + strings.Repeat(" ", padding) + "|\n") } if result.Details.UnterminatedCopy { - s.WriteString(diagnoseFailStyle.Render(" [-] ")) - s.WriteString(fmt.Sprintf("Unterminated COPY block: %s (line %d)\n", - result.Details.LastCopyTable, result.Details.LastCopyLineNumber)) + s.WriteString("| " + diagnoseFailStyle.Render("[-]") + " Unterminated COPY: " + truncate(result.Details.LastCopyTable, 30) + strings.Repeat(" ", 5) + "|\n") } if result.Details.ProperlyTerminated { - s.WriteString(diagnosePassStyle.Render(" [+] ")) - s.WriteString("All COPY blocks properly terminated\n") + s.WriteString("| " + diagnosePassStyle.Render("[+]") + " All COPY blocks properly terminated" + strings.Repeat(" ", 17) + "|\n") } if result.Details.ExpandedSize > 0 { - s.WriteString(diagnoseInfoStyle.Render(" - ")) - s.WriteString(fmt.Sprintf("Expanded size: %s (ratio: %.1fx)\n", - formatSize(result.Details.ExpandedSize), result.Details.CompressionRatio)) + sizeInfo := fmt.Sprintf("Expanded: %s (%.1fx)", formatSize(result.Details.ExpandedSize), result.Details.CompressionRatio) + padding := 50 - len(sizeInfo) + if padding < 0 { + padding = 0 + } + s.WriteString("| [-] " + sizeInfo + strings.Repeat(" ", padding) + "|\n") } + + s.WriteString("+" + strings.Repeat("-", 60) + "+\n") } - // Errors + // Errors Box if len(result.Errors) > 0 { - s.WriteString("\n") - s.WriteString(diagnoseFailStyle.Render("[FAIL] ERRORS:")) - s.WriteString("\n") + s.WriteString("\n+--[ ERRORS ]" + strings.Repeat("-", 47) + "+\n") for i, e := range result.Errors { if i >= 5 { - s.WriteString(diagnoseInfoStyle.Render(fmt.Sprintf(" ... and %d more\n", len(result.Errors)-5))) + remaining := fmt.Sprintf("... and %d more errors", len(result.Errors)-5) + padding := 56 - len(remaining) + s.WriteString("| " + remaining + strings.Repeat(" ", padding) + "|\n") break } - s.WriteString(diagnoseFailStyle.Render(" - ")) - s.WriteString(truncate(e, 70)) - s.WriteString("\n") + errText := truncate(e, 54) + padding := 56 - len(errText) + if padding < 0 { + padding = 0 + } + s.WriteString("| " + errText + strings.Repeat(" ", padding) + "|\n") } + s.WriteString("+" + strings.Repeat("-", 60) + "+\n") } - // Warnings + // Warnings Box if len(result.Warnings) > 0 { - s.WriteString("\n") - s.WriteString(diagnoseWarnStyle.Render("[WARN] WARNINGS:")) - s.WriteString("\n") + s.WriteString("\n+--[ WARNINGS ]" + strings.Repeat("-", 45) + "+\n") for i, w := range result.Warnings { if i >= 3 { - s.WriteString(diagnoseInfoStyle.Render(fmt.Sprintf(" ... and %d more\n", len(result.Warnings)-3))) + remaining := fmt.Sprintf("... and %d more warnings", len(result.Warnings)-3) + padding := 56 - len(remaining) + s.WriteString("| " + remaining + strings.Repeat(" ", padding) + "|\n") break } - s.WriteString(diagnoseWarnStyle.Render(" - ")) - s.WriteString(truncate(w, 70)) - s.WriteString("\n") + warnText := truncate(w, 54) + padding := 56 - len(warnText) + if padding < 0 { + padding = 0 + } + s.WriteString("| " + warnText + strings.Repeat(" ", padding) + "|\n") } + s.WriteString("+" + strings.Repeat("-", 60) + "+\n") } - // Recommendations + // Recommendations Box if !result.IsValid { - s.WriteString("\n") - s.WriteString(diagnoseHeaderStyle.Render("[HINT] RECOMMENDATIONS:")) - s.WriteString("\n") + s.WriteString("\n+--[ RECOMMENDATIONS ]" + strings.Repeat("-", 38) + "+\n") if result.IsTruncated { - s.WriteString(" 1. Re-run the backup process for this database\n") - s.WriteString(" 2. Check disk space on backup server\n") - s.WriteString(" 3. Verify network stability for remote backups\n") + s.WriteString("| 1. Re-run backup with current version (v3.42.12+) |\n") + s.WriteString("| 2. Check disk space on backup server |\n") + s.WriteString("| 3. Verify network stability for remote backups |\n") } if result.IsCorrupted { - s.WriteString(" 1. Verify backup was transferred completely\n") - s.WriteString(" 2. Try restoring from a previous backup\n") + s.WriteString("| 1. Verify backup was transferred completely |\n") + s.WriteString("| 2. Try restoring from a previous backup |\n") } + s.WriteString("+" + strings.Repeat("-", 60) + "+\n") } return s.String()