diff --git a/dbbackup b/dbbackup index e0d2ee0..312a950 100755 Binary files a/dbbackup and b/dbbackup differ diff --git a/dbbackup_linux_amd64 b/dbbackup_linux_amd64 index e0d2ee0..312a950 100755 Binary files a/dbbackup_linux_amd64 and b/dbbackup_linux_amd64 differ diff --git a/internal/restore/engine.go b/internal/restore/engine.go index c3696e4..cb0914f 100644 --- a/internal/restore/engine.go +++ b/internal/restore/engine.go @@ -23,6 +23,7 @@ type Engine struct { progress progress.Indicator detailedReporter *progress.DetailedReporter dryRun bool + progressCallback func(phase, status string, percent int) // Callback for TUI progress } // New creates a new restore engine @@ -52,6 +53,19 @@ func NewSilent(cfg *config.Config, log logger.Logger, db database.Database) *Eng progress: progressIndicator, detailedReporter: detailedReporter, dryRun: false, + progressCallback: nil, + } +} + +// SetProgressCallback sets a callback function for progress updates (used by TUI) +func (e *Engine) SetProgressCallback(callback func(phase, status string, percent int)) { + e.progressCallback = callback +} + +// reportProgress calls the progress callback if set +func (e *Engine) reportProgress(phase, status string, percent int) { + if e.progressCallback != nil { + e.progressCallback(phase, status, percent) } } @@ -312,6 +326,7 @@ func (e *Engine) RestoreCluster(ctx context.Context, archivePath string) error { } e.progress.Start(fmt.Sprintf("Restoring cluster from %s", filepath.Base(archivePath))) + e.reportProgress("Extracting", "Extracting cluster archive...", 5) // Create temporary extraction directory tempDir := filepath.Join(e.cfg.BackupDir, fmt.Sprintf(".restore_%d", time.Now().Unix())) @@ -333,6 +348,7 @@ func (e *Engine) RestoreCluster(ctx context.Context, archivePath string) error { if _, err := os.Stat(globalsFile); err == nil { e.log.Info("Restoring global objects") e.progress.Update("Restoring global objects (roles, tablespaces)...") + e.reportProgress("Globals", "Restoring global objects...", 15) if err := e.restoreGlobals(ctx, globalsFile); err != nil { e.log.Warn("Failed to restore global objects", "error", err) // Continue anyway - global objects might already exist @@ -355,6 +371,14 @@ func (e *Engine) RestoreCluster(ctx context.Context, archivePath string) error { successCount := 0 failCount := 0 var failedDBs []string + totalDBs := 0 + + // Count total databases + for _, entry := range entries { + if !entry.IsDir() { + totalDBs++ + } + } for i, entry := range entries { if entry.IsDir() { @@ -364,7 +388,12 @@ func (e *Engine) RestoreCluster(ctx context.Context, archivePath string) error { dumpFile := filepath.Join(dumpsDir, entry.Name()) dbName := strings.TrimSuffix(entry.Name(), ".dump") - e.progress.Update(fmt.Sprintf("[%d/%d] Restoring database: %s", i+1, len(entries), dbName)) + // Calculate progress: 15% for extraction/globals, 85% for databases + dbProgress := 15 + int(float64(i)/float64(totalDBs)*85.0) + + statusMsg := fmt.Sprintf("⠋ [%d/%d] Restoring: %s", i+1, totalDBs, dbName) + e.progress.Update(statusMsg) + e.reportProgress("Restoring", statusMsg, dbProgress) e.log.Info("Restoring database", "name", dbName, "file", dumpFile) // Create database first if it doesn't exist diff --git a/internal/tui/restore_exec.go b/internal/tui/restore_exec.go index 2ab7d4f..4dd31f8 100644 --- a/internal/tui/restore_exec.go +++ b/internal/tui/restore_exec.go @@ -31,6 +31,8 @@ type RestoreExecutionModel struct { progress int details []string startTime time.Time + spinnerFrame int + spinnerFrames []string // Results done bool @@ -54,6 +56,8 @@ func NewRestoreExecution(cfg *config.Config, log logger.Logger, parent tea.Model phase: "Starting", startTime: time.Now(), details: []string{}, + spinnerFrames: []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}, + spinnerFrame: 0, } } @@ -139,7 +143,7 @@ func (m RestoreExecutionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case restoreTickMsg: if !m.done { - m.progress = (m.progress + 2) % 100 + m.spinnerFrame = (m.spinnerFrame + 1) % len(m.spinnerFrames) m.elapsed = time.Since(m.startTime) return m, restoreTickCmd() } @@ -228,7 +232,10 @@ func (m RestoreExecutionModel) View() string { } else { // Show progress s.WriteString(fmt.Sprintf("Phase: %s\n", m.phase)) - s.WriteString(fmt.Sprintf("Status: %s\n", m.status)) + + // Show status with rotating spinner + spinner := m.spinnerFrames[m.spinnerFrame] + s.WriteString(fmt.Sprintf("Status: %s %s\n", spinner, m.status)) s.WriteString("\n") // Progress bar