Add rotating spinner to TUI status for visual progress feedback

This commit is contained in:
2025-11-07 11:20:36 +00:00
parent 91a41f043f
commit 2b868f859c
4 changed files with 39 additions and 3 deletions

BIN
dbbackup

Binary file not shown.

Binary file not shown.

View File

@ -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

View File

@ -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