Compare commits

...

3 Commits

Author SHA1 Message Date
e363e1937f fix: cluster restore database detection and TUI error display
All checks were successful
CI/CD / Test (push) Successful in 1m14s
CI/CD / Lint (push) Successful in 1m25s
CI/CD / Build & Release (push) Successful in 3m16s
- Fixed psql connection for database detection (always use -h flag)
- Use CombinedOutput() to capture stderr for better diagnostics
- Added existingDBError tracking in restore preview
- Show 'Unable to detect' instead of misleading 'None' when listing fails
- Disable cleanup toggle when database detection failed
2026-01-17 16:44:44 +01:00
df1ab2f55b feat: TUI improvements and consistency fixes
All checks were successful
CI/CD / Test (push) Successful in 1m14s
CI/CD / Lint (push) Successful in 1m23s
CI/CD / Build & Release (push) Successful in 3m10s
- Add product branding header to main menu (version + tagline)
- Fix backup success/error report formatting consistency
- Remove extra newline before error box in backup_exec
- Align backup and restore completion screens
2026-01-17 16:26:00 +01:00
0e050b2def fix: cluster backup TUI success report formatting consistency
All checks were successful
CI/CD / Test (push) Successful in 1m15s
CI/CD / Lint (push) Successful in 1m24s
CI/CD / Build & Release (push) Successful in 3m10s
- Aligned box width and indentation with restore success screen
- Removed inconsistent 2-space prefix from success/error boxes
- Standardized content indentation to 4 spaces
- Moved timing section outside else block (always shown)
- Updated footer style to match restore screen
2026-01-17 16:15:16 +01:00
5 changed files with 82 additions and 54 deletions

View File

@@ -4,8 +4,8 @@ This directory contains pre-compiled binaries for the DB Backup Tool across mult
## Build Information ## Build Information
- **Version**: 3.42.50 - **Version**: 3.42.50
- **Build Time**: 2026-01-17_12:25:20_UTC - **Build Time**: 2026-01-17_15:26:14_UTC
- **Git Commit**: c5be9bc - **Git Commit**: df1ab2f
## Recent Updates (v1.1.0) ## Recent Updates (v1.1.0)
- ✅ Fixed TUI progress display with line-by-line output - ✅ Fixed TUI progress display with line-by-line output

View File

@@ -334,10 +334,12 @@ func (s *Safety) checkPostgresDatabaseExists(ctx context.Context, dbName string)
"-tAc", fmt.Sprintf("SELECT 1 FROM pg_database WHERE datname='%s'", dbName), "-tAc", fmt.Sprintf("SELECT 1 FROM pg_database WHERE datname='%s'", dbName),
} }
// Only add -h flag if host is not localhost (to use Unix socket for peer auth) // Always add -h flag for explicit host connection (required for password auth)
if s.cfg.Host != "localhost" && s.cfg.Host != "127.0.0.1" && s.cfg.Host != "" { host := s.cfg.Host
args = append([]string{"-h", s.cfg.Host}, args...) if host == "" {
host = "localhost"
} }
args = append([]string{"-h", host}, args...)
cmd := exec.CommandContext(ctx, "psql", args...) cmd := exec.CommandContext(ctx, "psql", args...)
@@ -346,9 +348,9 @@ func (s *Safety) checkPostgresDatabaseExists(ctx context.Context, dbName string)
cmd.Env = append(os.Environ(), fmt.Sprintf("PGPASSWORD=%s", s.cfg.Password)) cmd.Env = append(os.Environ(), fmt.Sprintf("PGPASSWORD=%s", s.cfg.Password))
} }
output, err := cmd.Output() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
return false, fmt.Errorf("failed to check database existence: %w", err) return false, fmt.Errorf("failed to check database existence: %w (output: %s)", err, strings.TrimSpace(string(output)))
} }
return strings.TrimSpace(string(output)) == "1", nil return strings.TrimSpace(string(output)) == "1", nil
@@ -405,10 +407,13 @@ func (s *Safety) listPostgresUserDatabases(ctx context.Context) ([]string, error
"-c", query, "-c", query,
} }
// Only add -h flag if host is not localhost (to use Unix socket for peer auth) // Always add -h flag for explicit host connection (required for password auth)
if s.cfg.Host != "localhost" && s.cfg.Host != "127.0.0.1" && s.cfg.Host != "" { // Empty or unset host defaults to localhost
args = append([]string{"-h", s.cfg.Host}, args...) host := s.cfg.Host
if host == "" {
host = "localhost"
} }
args = append([]string{"-h", host}, args...)
cmd := exec.CommandContext(ctx, "psql", args...) cmd := exec.CommandContext(ctx, "psql", args...)
@@ -417,9 +422,10 @@ func (s *Safety) listPostgresUserDatabases(ctx context.Context) ([]string, error
cmd.Env = append(os.Environ(), fmt.Sprintf("PGPASSWORD=%s", s.cfg.Password)) cmd.Env = append(os.Environ(), fmt.Sprintf("PGPASSWORD=%s", s.cfg.Password))
} }
output, err := cmd.Output() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to list databases: %w", err) // Include psql output in error for debugging
return nil, fmt.Errorf("failed to list databases: %w (output: %s)", err, strings.TrimSpace(string(output)))
} }
// Parse output // Parse output

View File

@@ -454,65 +454,61 @@ func (m BackupExecutionModel) View() string {
} else { } else {
// Show completion summary with detailed stats // Show completion summary with detailed stats
if m.err != nil { if m.err != nil {
s.WriteString(errorStyle.Render("╔══════════════════════════════════════════════════════════════╗"))
s.WriteString("\n") s.WriteString("\n")
s.WriteString(errorStyle.Render(" ╔══════════════════════════════════════════════════════════╗")) s.WriteString(errorStyle.Render(" [FAIL] BACKUP FAILED ║"))
s.WriteString("\n") s.WriteString("\n")
s.WriteString(errorStyle.Render(" ║ [FAIL] BACKUP FAILED ║")) s.WriteString(errorStyle.Render("╚══════════════════════════════════════════════════════════════╝"))
s.WriteString("\n")
s.WriteString(errorStyle.Render(" ╚══════════════════════════════════════════════════════════╝"))
s.WriteString("\n\n") 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") s.WriteString("\n")
} else { } else {
s.WriteString(successStyle.Render("╔══════════════════════════════════════════════════════════════╗"))
s.WriteString("\n") s.WriteString("\n")
s.WriteString(successStyle.Render(" ╔══════════════════════════════════════════════════════════╗")) s.WriteString(successStyle.Render(" [OK] BACKUP COMPLETED SUCCESSFULLY ║"))
s.WriteString("\n") s.WriteString("\n")
s.WriteString(successStyle.Render(" ║ [OK] BACKUP COMPLETED SUCCESSFULLY ║")) s.WriteString(successStyle.Render("╚══════════════════════════════════════════════════════════════╝"))
s.WriteString("\n")
s.WriteString(successStyle.Render(" ╚══════════════════════════════════════════════════════════╝"))
s.WriteString("\n\n") s.WriteString("\n\n")
// Summary section // Summary section
s.WriteString(infoStyle.Render(" ─── Summary ─────────────────────────────────────────────")) s.WriteString(infoStyle.Render(" ─── Summary ───────────────────────────────────────────────"))
s.WriteString("\n\n") s.WriteString("\n\n")
// Backup type specific info // Backup type specific info
switch m.backupType { switch m.backupType {
case "cluster": case "cluster":
s.WriteString(" Type: Cluster Backup\n") s.WriteString(" Type: Cluster Backup\n")
if m.dbTotal > 0 { if m.dbTotal > 0 {
s.WriteString(fmt.Sprintf(" Databases: %d backed up\n", m.dbTotal)) s.WriteString(fmt.Sprintf(" Databases: %d backed up\n", m.dbTotal))
} }
case "single": case "single":
s.WriteString(" Type: Single Database Backup\n") s.WriteString(" Type: Single Database Backup\n")
s.WriteString(fmt.Sprintf(" Database: %s\n", m.databaseName)) s.WriteString(fmt.Sprintf(" Database: %s\n", m.databaseName))
case "sample": case "sample":
s.WriteString(" Type: Sample Backup\n") s.WriteString(" Type: Sample Backup\n")
s.WriteString(fmt.Sprintf(" Database: %s\n", m.databaseName)) s.WriteString(fmt.Sprintf(" Database: %s\n", m.databaseName))
s.WriteString(fmt.Sprintf(" Sample Ratio: %d\n", m.ratio)) s.WriteString(fmt.Sprintf(" Sample Ratio: %d\n", m.ratio))
} }
s.WriteString("\n") s.WriteString("\n")
}
// Timing section
s.WriteString(infoStyle.Render(" ─── Timing ──────────────────────────────────────────────")) // Timing section (always shown, consistent with restore)
s.WriteString("\n\n") s.WriteString(infoStyle.Render(" ─── Timing ────────────────────────────────────────────────"))
s.WriteString("\n\n")
elapsed := time.Since(m.startTime)
s.WriteString(fmt.Sprintf(" Total Time: %s\n", formatBackupDuration(elapsed))) 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) if m.backupType == "cluster" && m.dbTotal > 0 && m.err == nil {
s.WriteString(fmt.Sprintf(" Avg per DB: %s\n", formatBackupDuration(avgPerDB))) 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") s.WriteString("\n")
s.WriteString(" [KEY] Press Enter or ESC to return to menu\n") s.WriteString(infoStyle.Render(" ───────────────────────────────────────────────────────────"))
s.WriteString("\n\n")
s.WriteString(infoStyle.Render(" [KEYS] Press Enter to continue"))
} }
return s.String() return s.String()

View File

@@ -299,9 +299,13 @@ func (m *MenuModel) View() string {
var s string var s string
// Product branding header
brandLine := fmt.Sprintf("dbbackup v%s • Enterprise Database Backup & Recovery", m.config.Version)
s += "\n" + infoStyle.Render(brandLine) + "\n"
// Header // Header
header := titleStyle.Render("Database Backup Tool - Interactive Menu") header := titleStyle.Render("Interactive Menu")
s += fmt.Sprintf("\n%s\n\n", header) s += fmt.Sprintf("%s\n\n", header)
if len(m.dbTypes) > 0 { if len(m.dbTypes) > 0 {
options := make([]string, len(m.dbTypes)) options := make([]string, len(m.dbTypes))

View File

@@ -55,6 +55,7 @@ type RestorePreviewModel struct {
cleanClusterFirst bool // For cluster restore: drop all user databases first cleanClusterFirst bool // For cluster restore: drop all user databases first
existingDBCount int // Number of existing user databases existingDBCount int // Number of existing user databases
existingDBs []string // List of existing user databases existingDBs []string // List of existing user databases
existingDBError string // Error message if database listing failed
safetyChecks []SafetyCheck safetyChecks []SafetyCheck
checking bool checking bool
canProceed bool canProceed bool
@@ -102,6 +103,7 @@ type safetyCheckCompleteMsg struct {
canProceed bool canProceed bool
existingDBCount int existingDBCount int
existingDBs []string existingDBs []string
existingDBError string
} }
func runSafetyChecks(cfg *config.Config, log logger.Logger, archive ArchiveInfo, targetDB string) tea.Cmd { func runSafetyChecks(cfg *config.Config, log logger.Logger, archive ArchiveInfo, targetDB string) tea.Cmd {
@@ -221,10 +223,12 @@ func runSafetyChecks(cfg *config.Config, log logger.Logger, archive ArchiveInfo,
check = SafetyCheck{Name: "Existing databases", Status: "checking", Critical: false} check = SafetyCheck{Name: "Existing databases", Status: "checking", Critical: false}
// Get list of existing user databases (exclude templates and system DBs) // Get list of existing user databases (exclude templates and system DBs)
var existingDBError string
dbList, err := safety.ListUserDatabases(ctx) dbList, err := safety.ListUserDatabases(ctx)
if err != nil { if err != nil {
check.Status = "warning" check.Status = "warning"
check.Message = fmt.Sprintf("Cannot list databases: %v", err) check.Message = fmt.Sprintf("Cannot list databases: %v", err)
existingDBError = err.Error()
} else { } else {
existingDBCount = len(dbList) existingDBCount = len(dbList)
existingDBs = dbList existingDBs = dbList
@@ -238,6 +242,14 @@ func runSafetyChecks(cfg *config.Config, log logger.Logger, archive ArchiveInfo,
} }
} }
checks = append(checks, check) checks = append(checks, check)
return safetyCheckCompleteMsg{
checks: checks,
canProceed: canProceed,
existingDBCount: existingDBCount,
existingDBs: existingDBs,
existingDBError: existingDBError,
}
} }
return safetyCheckCompleteMsg{ return safetyCheckCompleteMsg{
@@ -257,6 +269,7 @@ func (m RestorePreviewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.canProceed = msg.canProceed m.canProceed = msg.canProceed
m.existingDBCount = msg.existingDBCount m.existingDBCount = msg.existingDBCount
m.existingDBs = msg.existingDBs m.existingDBs = msg.existingDBs
m.existingDBError = msg.existingDBError
// Auto-forward in auto-confirm mode // Auto-forward in auto-confirm mode
if m.config.TUIAutoConfirm { if m.config.TUIAutoConfirm {
return m.parent, tea.Quit return m.parent, tea.Quit
@@ -275,12 +288,17 @@ func (m RestorePreviewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "c": case "c":
if m.mode == "restore-cluster" { if m.mode == "restore-cluster" {
// Toggle cluster cleanup // Prevent toggle if we couldn't detect existing databases
m.cleanClusterFirst = !m.cleanClusterFirst if m.existingDBError != "" {
if m.cleanClusterFirst { m.message = checkWarningStyle.Render("[WARN] Cannot enable cleanup - database detection failed")
m.message = checkWarningStyle.Render(fmt.Sprintf("[WARN] Will drop %d existing database(s) before restore", m.existingDBCount))
} else { } else {
m.message = fmt.Sprintf("Clean cluster first: disabled") // Toggle cluster cleanup
m.cleanClusterFirst = !m.cleanClusterFirst
if m.cleanClusterFirst {
m.message = checkWarningStyle.Render(fmt.Sprintf("[WARN] Will drop %d existing database(s) before restore", m.existingDBCount))
} else {
m.message = fmt.Sprintf("Clean cluster first: disabled")
}
} }
} else { } else {
// Toggle create if missing // Toggle create if missing
@@ -382,7 +400,11 @@ func (m RestorePreviewModel) View() string {
s.WriteString("\n") s.WriteString("\n")
s.WriteString(fmt.Sprintf(" Host: %s:%d\n", m.config.Host, m.config.Port)) s.WriteString(fmt.Sprintf(" Host: %s:%d\n", m.config.Host, m.config.Port))
if m.existingDBCount > 0 { if m.existingDBError != "" {
// Show error when database listing failed
s.WriteString(checkWarningStyle.Render(fmt.Sprintf(" Existing Databases: Unable to detect (%s)\n", m.existingDBError)))
s.WriteString(infoStyle.Render(" (Cleanup option disabled - cannot verify database status)\n"))
} else if m.existingDBCount > 0 {
s.WriteString(fmt.Sprintf(" Existing Databases: %d found\n", m.existingDBCount)) s.WriteString(fmt.Sprintf(" Existing Databases: %d found\n", m.existingDBCount))
// Show first few database names // Show first few database names