Compare commits

..

1 Commits

Author SHA1 Message Date
780beaadfb feat(tui): unified cluster restore progress display
All checks were successful
CI/CD / Test (push) Successful in 1m19s
CI/CD / Lint (push) Successful in 1m26s
CI/CD / Build & Release (push) Successful in 3m27s
- Add combined overall progress bar showing all phases (0-100%)
- Phase 1/3: Extracting Archive (0-60% of overall)
- Phase 2/3: Restoring Globals (60-65% of overall)
- Phase 3/3: Restoring Databases (65-100% of overall)
- Show current database name during restore
- Phase-aware progress tracking with overallPhase, currentDB, extractionDone
- Dual progress bars: overall + phase-specific (bytes or db count)
- Better visual feedback during entire cluster restore operation

v3.42.48
2026-01-16 15:32:24 +01:00
4 changed files with 163 additions and 35 deletions

View File

@@ -5,6 +5,33 @@ All notable changes to dbbackup will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [3.42.48] - 2026-01-15 "Unified Cluster Restore Progress"
### Added - Unified Progress Display for Cluster Restore
- **Combined overall progress bar** showing progress across all restore phases:
- Phase 1/3: Extracting Archive (0-60% of overall)
- Phase 2/3: Restoring Globals (60-65% of overall)
- Phase 3/3: Restoring Databases (65-100% of overall)
- **Current database indicator** - Shows which database is currently being restored
- **Phase-aware progress tracking** - New fields in progress state:
- `overallPhase` - Current phase (1=extraction, 2=globals, 3=databases)
- `currentDB` - Name of database currently being restored
- `extractionDone` - Boolean flag for phase transition
- **Dual progress bars** for cluster restore:
- Overall progress bar showing combined operation progress
- Phase-specific progress bar (extraction bytes or database count)
### Changed
- Cluster restore TUI now shows unified progress display
- Progress callbacks now set phase and current database information
- Extraction completion triggers automatic transition to globals phase
- Database restore phase shows current database name with spinner
### Improved
- Better visual feedback during entire cluster restore operation
- Clear phase indicators help users understand restore progress
- Overall progress percentage gives better time estimates
## [3.42.35] - 2026-01-15 "TUI Detailed Progress" ## [3.42.35] - 2026-01-15 "TUI Detailed Progress"
### Added - Enhanced TUI Progress Display ### Added - Enhanced TUI Progress Display

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.34 - **Version**: 3.42.34
- **Build Time**: 2026-01-16_13:17:19_UTC - **Build Time**: 2026-01-16_13:36:18_UTC
- **Git Commit**: 9d95a19 - **Git Commit**: 838c5b8
## 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

@@ -57,10 +57,18 @@ type RestoreExecutionModel struct {
dbTotal int dbTotal int
dbDone int dbDone int
// Current database being restored (for detailed display)
currentDB string
// Timing info for database restore phase (ETA calculation) // Timing info for database restore phase (ETA calculation)
dbPhaseElapsed time.Duration // Elapsed time since restore phase started dbPhaseElapsed time.Duration // Elapsed time since restore phase started
dbAvgPerDB time.Duration // Average time per database restore dbAvgPerDB time.Duration // Average time per database restore
// Overall progress tracking for unified display
overallPhase int // 1=Extracting, 2=Globals, 3=Databases
extractionDone bool
extractionTime time.Duration // How long extraction took (for ETA calc)
// Results // Results
done bool done bool
cancelling bool // True when user has requested cancellation cancelling bool // True when user has requested cancellation
@@ -140,10 +148,17 @@ type sharedProgressState struct {
dbTotal int dbTotal int
dbDone int dbDone int
// Current database being restored
currentDB string
// Timing info for database restore phase // Timing info for database restore phase
dbPhaseElapsed time.Duration // Elapsed time since restore phase started dbPhaseElapsed time.Duration // Elapsed time since restore phase started
dbAvgPerDB time.Duration // Average time per database restore dbAvgPerDB time.Duration // Average time per database restore
// Overall phase tracking (1=Extract, 2=Globals, 3=Databases)
overallPhase int
extractionDone bool
// Rolling window for speed calculation // Rolling window for speed calculation
speedSamples []restoreSpeedSample speedSamples []restoreSpeedSample
} }
@@ -171,12 +186,12 @@ func clearCurrentRestoreProgress() {
currentRestoreProgressState = nil currentRestoreProgressState = nil
} }
func getCurrentRestoreProgress() (bytesTotal, bytesDone int64, description string, hasUpdate bool, dbTotal, dbDone int, speed float64, dbPhaseElapsed, dbAvgPerDB time.Duration) { func getCurrentRestoreProgress() (bytesTotal, bytesDone int64, description string, hasUpdate bool, dbTotal, dbDone int, speed float64, dbPhaseElapsed, dbAvgPerDB time.Duration, currentDB string, overallPhase int, extractionDone bool) {
currentRestoreProgressMu.Lock() currentRestoreProgressMu.Lock()
defer currentRestoreProgressMu.Unlock() defer currentRestoreProgressMu.Unlock()
if currentRestoreProgressState == nil { if currentRestoreProgressState == nil {
return 0, 0, "", false, 0, 0, 0, 0, 0 return 0, 0, "", false, 0, 0, 0, 0, 0, "", 0, false
} }
currentRestoreProgressState.mu.Lock() currentRestoreProgressState.mu.Lock()
@@ -188,7 +203,9 @@ func getCurrentRestoreProgress() (bytesTotal, bytesDone int64, description strin
return currentRestoreProgressState.bytesTotal, currentRestoreProgressState.bytesDone, return currentRestoreProgressState.bytesTotal, currentRestoreProgressState.bytesDone,
currentRestoreProgressState.description, currentRestoreProgressState.hasUpdate, currentRestoreProgressState.description, currentRestoreProgressState.hasUpdate,
currentRestoreProgressState.dbTotal, currentRestoreProgressState.dbDone, speed, currentRestoreProgressState.dbTotal, currentRestoreProgressState.dbDone, speed,
currentRestoreProgressState.dbPhaseElapsed, currentRestoreProgressState.dbAvgPerDB currentRestoreProgressState.dbPhaseElapsed, currentRestoreProgressState.dbAvgPerDB,
currentRestoreProgressState.currentDB, currentRestoreProgressState.overallPhase,
currentRestoreProgressState.extractionDone
} }
// calculateRollingSpeed calculates speed from recent samples (last 5 seconds) // calculateRollingSpeed calculates speed from recent samples (last 5 seconds)
@@ -288,6 +305,14 @@ func executeRestoreWithTUIProgress(parentCtx context.Context, cfg *config.Config
progressState.bytesTotal = total progressState.bytesTotal = total
progressState.description = description progressState.description = description
progressState.hasUpdate = true progressState.hasUpdate = true
progressState.overallPhase = 1
progressState.extractionDone = false
// Check if extraction is complete
if current >= total && total > 0 {
progressState.extractionDone = true
progressState.overallPhase = 2
}
// Add speed sample for rolling window calculation // Add speed sample for rolling window calculation
progressState.speedSamples = append(progressState.speedSamples, restoreSpeedSample{ progressState.speedSamples = append(progressState.speedSamples, restoreSpeedSample{
@@ -307,6 +332,9 @@ func executeRestoreWithTUIProgress(parentCtx context.Context, cfg *config.Config
progressState.dbDone = done progressState.dbDone = done
progressState.dbTotal = total progressState.dbTotal = total
progressState.description = fmt.Sprintf("Restoring %s", dbName) progressState.description = fmt.Sprintf("Restoring %s", dbName)
progressState.currentDB = dbName
progressState.overallPhase = 3
progressState.extractionDone = true
progressState.hasUpdate = true progressState.hasUpdate = true
// Clear byte progress when switching to db progress // Clear byte progress when switching to db progress
progressState.bytesTotal = 0 progressState.bytesTotal = 0
@@ -320,6 +348,9 @@ func executeRestoreWithTUIProgress(parentCtx context.Context, cfg *config.Config
progressState.dbDone = done progressState.dbDone = done
progressState.dbTotal = total progressState.dbTotal = total
progressState.description = fmt.Sprintf("Restoring %s", dbName) progressState.description = fmt.Sprintf("Restoring %s", dbName)
progressState.currentDB = dbName
progressState.overallPhase = 3
progressState.extractionDone = true
progressState.dbPhaseElapsed = phaseElapsed progressState.dbPhaseElapsed = phaseElapsed
progressState.dbAvgPerDB = avgPerDB progressState.dbAvgPerDB = avgPerDB
progressState.hasUpdate = true progressState.hasUpdate = true
@@ -381,28 +412,46 @@ func (m RestoreExecutionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.elapsed = time.Since(m.startTime) m.elapsed = time.Since(m.startTime)
// Poll shared progress state for real-time updates // Poll shared progress state for real-time updates
bytesTotal, bytesDone, description, hasUpdate, dbTotal, dbDone, speed, dbPhaseElapsed, dbAvgPerDB := getCurrentRestoreProgress() bytesTotal, bytesDone, description, hasUpdate, dbTotal, dbDone, speed, dbPhaseElapsed, dbAvgPerDB, currentDB, overallPhase, extractionDone := getCurrentRestoreProgress()
if hasUpdate && bytesTotal > 0 { if hasUpdate && bytesTotal > 0 && !extractionDone {
// Phase 1: Extraction
m.bytesTotal = bytesTotal m.bytesTotal = bytesTotal
m.bytesDone = bytesDone m.bytesDone = bytesDone
m.description = description m.description = description
m.showBytes = true m.showBytes = true
m.speed = speed m.speed = speed
m.overallPhase = 1
m.extractionDone = false
// Update status to reflect actual progress // Update status to reflect actual progress
m.status = description m.status = description
m.phase = "Extracting" m.phase = "Phase 1/3: Extracting Archive"
m.progress = int((bytesDone * 100) / bytesTotal) m.progress = int((bytesDone * 100) / bytesTotal)
} else if hasUpdate && dbTotal > 0 { } else if hasUpdate && dbTotal > 0 {
// Database count progress for cluster restore with timing // Phase 3: Database restores
m.dbTotal = dbTotal m.dbTotal = dbTotal
m.dbDone = dbDone m.dbDone = dbDone
m.dbPhaseElapsed = dbPhaseElapsed m.dbPhaseElapsed = dbPhaseElapsed
m.dbAvgPerDB = dbAvgPerDB m.dbAvgPerDB = dbAvgPerDB
m.currentDB = currentDB
m.overallPhase = overallPhase
m.extractionDone = extractionDone
m.showBytes = false m.showBytes = false
m.status = fmt.Sprintf("Restoring database %d of %d...", dbDone+1, dbTotal)
m.phase = "Restore" if dbDone < dbTotal {
m.status = fmt.Sprintf("Restoring: %s", currentDB)
} else {
m.status = "Finalizing..."
}
m.phase = fmt.Sprintf("Phase 3/3: Databases (%d/%d)", dbDone, dbTotal)
m.progress = int((dbDone * 100) / dbTotal) m.progress = int((dbDone * 100) / dbTotal)
} else if hasUpdate && extractionDone && dbTotal == 0 {
// Phase 2: Globals restore (brief phase between extraction and databases)
m.overallPhase = 2
m.extractionDone = true
m.showBytes = false
m.status = "Restoring global objects (roles, tablespaces)..."
m.phase = "Phase 2/3: Restoring Globals"
} else { } else {
// Fallback: Update status based on elapsed time to show progress // Fallback: Update status based on elapsed time to show progress
// This provides visual feedback even though we don't have real-time progress // This provides visual feedback even though we don't have real-time progress
@@ -610,36 +659,88 @@ func (m RestoreExecutionModel) View() string {
s.WriteString("\n\n") s.WriteString("\n\n")
s.WriteString(infoStyle.Render(" [KEYS] Press Enter to continue")) s.WriteString(infoStyle.Render(" [KEYS] Press Enter to continue"))
} else { } else {
// Show progress // Show unified progress for cluster restore
s.WriteString(fmt.Sprintf("Phase: %s\n", m.phase)) if m.restoreType == "restore-cluster" {
// Calculate overall progress across all phases
// Phase 1: Extraction (0-60%)
// Phase 2: Globals (60-65%)
// Phase 3: Databases (65-100%)
overallProgress := 0
phaseLabel := "Starting..."
// Show detailed progress bar when we have byte-level information if m.showBytes && m.bytesTotal > 0 {
// In this case, hide the spinner for cleaner display // Phase 1: Extraction - contributes 0-60%
if m.showBytes && m.bytesTotal > 0 { extractPct := int((m.bytesDone * 100) / m.bytesTotal)
// Status line without spinner (progress bar provides activity indication) overallProgress = (extractPct * 60) / 100
s.WriteString(fmt.Sprintf("Status: %s\n", m.status)) phaseLabel = "Phase 1/3: Extracting Archive"
s.WriteString("\n") } else if m.extractionDone && m.dbTotal == 0 {
// Phase 2: Globals restore
overallProgress = 62
phaseLabel = "Phase 2/3: Restoring Globals"
} else if m.dbTotal > 0 {
// Phase 3: Database restores - contributes 65-100%
dbPct := int((int64(m.dbDone) * 100) / int64(m.dbTotal))
overallProgress = 65 + (dbPct * 35 / 100)
phaseLabel = fmt.Sprintf("Phase 3/3: Databases (%d/%d)", m.dbDone, m.dbTotal)
}
// Render schollz-style progress bar with bytes, rolling speed, ETA // Header with phase and overall progress
s.WriteString(renderDetailedProgressBarWithSpeed(m.bytesDone, m.bytesTotal, m.speed)) s.WriteString(infoStyle.Render(" ─── Cluster Restore Progress ─────────────────────────────"))
s.WriteString("\n\n") s.WriteString("\n\n")
} else if m.dbTotal > 0 { s.WriteString(fmt.Sprintf(" %s\n\n", phaseLabel))
// Database count progress for cluster restore with timing
spinner := m.spinnerFrames[m.spinnerFrame]
s.WriteString(fmt.Sprintf("Status: %s %s\n", spinner, m.status))
s.WriteString("\n")
// Show database progress bar with timing and ETA // Overall progress bar
s.WriteString(renderDatabaseProgressBarWithTiming(m.dbDone, m.dbTotal, m.dbPhaseElapsed, m.dbAvgPerDB)) s.WriteString(" Overall: ")
s.WriteString(renderProgressBar(overallProgress))
s.WriteString(fmt.Sprintf(" %d%%\n", overallProgress))
// Phase-specific details
if m.showBytes && m.bytesTotal > 0 {
// Show extraction details
s.WriteString("\n")
s.WriteString(fmt.Sprintf(" %s\n", m.status))
s.WriteString("\n")
s.WriteString(renderDetailedProgressBarWithSpeed(m.bytesDone, m.bytesTotal, m.speed))
s.WriteString("\n")
} else if m.dbTotal > 0 {
// Show current database being restored
s.WriteString("\n")
spinner := m.spinnerFrames[m.spinnerFrame]
if m.currentDB != "" && m.dbDone < m.dbTotal {
s.WriteString(fmt.Sprintf(" Current: %s %s\n", spinner, m.currentDB))
} else if m.dbDone >= m.dbTotal {
s.WriteString(fmt.Sprintf(" %s Finalizing...\n", spinner))
}
s.WriteString("\n")
// Database progress bar with timing
s.WriteString(renderDatabaseProgressBarWithTiming(m.dbDone, m.dbTotal, m.dbPhaseElapsed, m.dbAvgPerDB))
s.WriteString("\n")
} else {
// Intermediate phase (globals)
spinner := m.spinnerFrames[m.spinnerFrame]
s.WriteString(fmt.Sprintf("\n %s %s\n\n", spinner, m.status))
}
s.WriteString("\n")
s.WriteString(infoStyle.Render(" ───────────────────────────────────────────────────────────"))
s.WriteString("\n\n") s.WriteString("\n\n")
} else { } else {
// Show status with rotating spinner (for phases without detailed progress) // Single database restore - simpler display
spinner := m.spinnerFrames[m.spinnerFrame] s.WriteString(fmt.Sprintf("Phase: %s\n", m.phase))
s.WriteString(fmt.Sprintf("Status: %s %s\n", spinner, m.status))
s.WriteString("\n")
if m.restoreType == "restore-single" { // Show detailed progress bar when we have byte-level information
// Fallback to simple progress bar for single database restore if m.showBytes && m.bytesTotal > 0 {
s.WriteString(fmt.Sprintf("Status: %s\n", m.status))
s.WriteString("\n")
s.WriteString(renderDetailedProgressBarWithSpeed(m.bytesDone, m.bytesTotal, m.speed))
s.WriteString("\n\n")
} else {
spinner := m.spinnerFrames[m.spinnerFrame]
s.WriteString(fmt.Sprintf("Status: %s %s\n", spinner, m.status))
s.WriteString("\n")
// Fallback to simple progress bar
progressBar := renderProgressBar(m.progress) progressBar := renderProgressBar(m.progress)
s.WriteString(progressBar) s.WriteString(progressBar)
s.WriteString(fmt.Sprintf(" %d%%\n", m.progress)) s.WriteString(fmt.Sprintf(" %d%%\n", m.progress))

View File

@@ -16,7 +16,7 @@ import (
// Build information (set by ldflags) // Build information (set by ldflags)
var ( var (
version = "3.42.34" version = "3.42.48"
buildTime = "unknown" buildTime = "unknown"
gitCommit = "unknown" gitCommit = "unknown"
) )