Compare commits

...

1 Commits

Author SHA1 Message Date
09a917766f TUI: Add database progress tracking to cluster backup
All checks were successful
CI/CD / Test (push) Successful in 1m27s
CI/CD / Lint (push) Successful in 1m33s
CI/CD / Build & Release (push) Successful in 3m28s
- Add ProgressCallback and DatabaseProgressCallback to backup engine
- Add SetProgressCallback() and SetDatabaseProgressCallback() methods
- Wire database progress callback in backup_exec.go
- Show progress bar (Database: [████░░░] X/Y) during cluster backups
- Hide spinner when progress bar is displayed
- Use shared progress state pattern for thread-safe TUI updates
2026-01-15 15:32:26 +01:00
3 changed files with 165 additions and 15 deletions

View File

@@ -4,8 +4,8 @@ This directory contains pre-compiled binaries for the DB Backup Tool across mult
## Build Information
- **Version**: 3.42.34
- **Build Time**: 2026-01-14_16:19:00_UTC
- **Git Commit**: 7711a20
- **Build Time**: 2026-01-15_14:16:33_UTC
- **Git Commit**: eeacbfa
## Recent Updates (v1.1.0)
- ✅ Fixed TUI progress display with line-by-line output

View File

@@ -28,6 +28,12 @@ import (
"dbbackup/internal/swap"
)
// ProgressCallback is called with byte-level progress updates during backup operations
type ProgressCallback func(current, total int64, description string)
// DatabaseProgressCallback is called with database count progress during cluster backup
type DatabaseProgressCallback func(done, total int, dbName string)
// Engine handles backup operations
type Engine struct {
cfg *config.Config
@@ -36,6 +42,8 @@ type Engine struct {
progress progress.Indicator
detailedReporter *progress.DetailedReporter
silent bool // Silent mode for TUI
progressCallback ProgressCallback
dbProgressCallback DatabaseProgressCallback
}
// New creates a new backup engine
@@ -86,6 +94,30 @@ func NewSilent(cfg *config.Config, log logger.Logger, db database.Database, prog
}
}
// SetProgressCallback sets a callback for detailed progress reporting (for TUI mode)
func (e *Engine) SetProgressCallback(cb ProgressCallback) {
e.progressCallback = cb
}
// SetDatabaseProgressCallback sets a callback for database count progress during cluster backup
func (e *Engine) SetDatabaseProgressCallback(cb DatabaseProgressCallback) {
e.dbProgressCallback = cb
}
// reportProgress reports progress to the callback if set
func (e *Engine) reportProgress(current, total int64, description string) {
if e.progressCallback != nil {
e.progressCallback(current, total, description)
}
}
// reportDatabaseProgress reports database count progress to the callback if set
func (e *Engine) reportDatabaseProgress(done, total int, dbName string) {
if e.dbProgressCallback != nil {
e.dbProgressCallback(done, total, dbName)
}
}
// loggerAdapter adapts our logger to the progress.Logger interface
type loggerAdapter struct {
logger logger.Logger
@@ -465,6 +497,8 @@ func (e *Engine) BackupCluster(ctx context.Context) error {
estimator.UpdateProgress(idx)
e.printf(" [%d/%d] Backing up database: %s\n", idx+1, len(databases), name)
quietProgress.Update(fmt.Sprintf("Backing up database %d/%d: %s", idx+1, len(databases), name))
// Report database progress to TUI callback
e.reportDatabaseProgress(idx+1, len(databases), name)
mu.Unlock()
// Check database size and warn if very large

124
internal/tui/backup_exec.go Executable file → Normal file
View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"strings"
"sync"
"time"
tea "github.com/charmbracelet/bubbletea"
@@ -33,6 +34,56 @@ type BackupExecutionModel struct {
startTime time.Time
details []string
spinnerFrame int
// Database count progress (for cluster backup)
dbTotal int
dbDone int
dbName string // Current database being backed up
}
// sharedBackupProgressState holds progress state that can be safely accessed from callbacks
type sharedBackupProgressState struct {
mu sync.Mutex
dbTotal int
dbDone int
dbName string
hasUpdate bool
}
// Package-level shared progress state for backup operations
var (
currentBackupProgressMu sync.Mutex
currentBackupProgressState *sharedBackupProgressState
)
func setCurrentBackupProgress(state *sharedBackupProgressState) {
currentBackupProgressMu.Lock()
defer currentBackupProgressMu.Unlock()
currentBackupProgressState = state
}
func clearCurrentBackupProgress() {
currentBackupProgressMu.Lock()
defer currentBackupProgressMu.Unlock()
currentBackupProgressState = nil
}
func getCurrentBackupProgress() (dbTotal, dbDone int, dbName string, hasUpdate bool) {
currentBackupProgressMu.Lock()
defer currentBackupProgressMu.Unlock()
if currentBackupProgressState == nil {
return 0, 0, "", false
}
currentBackupProgressState.mu.Lock()
defer currentBackupProgressState.mu.Unlock()
hasUpdate = currentBackupProgressState.hasUpdate
currentBackupProgressState.hasUpdate = false
return currentBackupProgressState.dbTotal, currentBackupProgressState.dbDone,
currentBackupProgressState.dbName, hasUpdate
}
func NewBackupExecution(cfg *config.Config, log logger.Logger, parent tea.Model, ctx context.Context, backupType, dbName string, ratio int) BackupExecutionModel {
@@ -55,7 +106,6 @@ func NewBackupExecution(cfg *config.Config, log logger.Logger, parent tea.Model,
}
func (m BackupExecutionModel) Init() tea.Cmd {
// TUI handles all display through View() - no progress callbacks needed
return tea.Batch(
executeBackupWithTUIProgress(m.ctx, m.config, m.logger, m.backupType, m.databaseName, m.ratio),
backupTickCmd(),
@@ -91,6 +141,11 @@ func executeBackupWithTUIProgress(parentCtx context.Context, cfg *config.Config,
start := time.Now()
// Setup shared progress state for TUI polling
progressState := &sharedBackupProgressState{}
setCurrentBackupProgress(progressState)
defer clearCurrentBackupProgress()
dbClient, err := database.New(cfg, log)
if err != nil {
return backupCompleteMsg{
@@ -110,6 +165,16 @@ func executeBackupWithTUIProgress(parentCtx context.Context, cfg *config.Config,
// Pass nil as indicator - TUI itself handles all display, no stdout printing
engine := backup.NewSilent(cfg, log, dbClient, nil)
// Set database progress callback for cluster backups
engine.SetDatabaseProgressCallback(func(done, total int, currentDB string) {
progressState.mu.Lock()
progressState.dbDone = done
progressState.dbTotal = total
progressState.dbName = currentDB
progressState.hasUpdate = true
progressState.mu.Unlock()
})
var backupErr error
switch backupType {
case "single":
@@ -157,10 +222,21 @@ func (m BackupExecutionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Increment spinner frame for smooth animation
m.spinnerFrame = (m.spinnerFrame + 1) % len(spinnerFrames)
// Update status based on elapsed time to show progress
// Poll for database progress updates from callbacks
dbTotal, dbDone, dbName, hasUpdate := getCurrentBackupProgress()
if hasUpdate {
m.dbTotal = dbTotal
m.dbDone = dbDone
m.dbName = dbName
}
// Update status based on progress and elapsed time
elapsedSec := int(time.Since(m.startTime).Seconds())
if elapsedSec < 2 {
if m.dbTotal > 0 && m.dbDone > 0 {
// We have real progress from cluster backup
m.status = fmt.Sprintf("Backing up database: %s", m.dbName)
} else if elapsedSec < 2 {
m.status = "Initializing backup..."
} else if elapsedSec < 5 {
if m.backupType == "cluster" {
@@ -234,6 +310,34 @@ func (m BackupExecutionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
// renderDatabaseProgressBar renders a progress bar for database count progress
func renderBackupDatabaseProgressBar(done, total int, dbName string, width int) string {
if total == 0 {
return ""
}
// Calculate progress percentage
percent := float64(done) / float64(total)
if percent > 1.0 {
percent = 1.0
}
// Calculate filled width
barWidth := width - 20 // Leave room for label and percentage
if barWidth < 10 {
barWidth = 10
}
filled := int(float64(barWidth) * percent)
if filled > barWidth {
filled = barWidth
}
// Build progress bar
bar := strings.Repeat("█", filled) + strings.Repeat("░", barWidth-filled)
return fmt.Sprintf(" Database: [%s] %d/%d", bar, done, total)
}
func (m BackupExecutionModel) View() string {
var s strings.Builder
s.Grow(512) // Pre-allocate estimated capacity for better performance
@@ -255,12 +359,24 @@ func (m BackupExecutionModel) View() string {
s.WriteString(fmt.Sprintf(" %-10s %s\n", "Duration:", time.Since(m.startTime).Round(time.Second)))
s.WriteString("\n")
// Status with spinner
// Status display
if !m.done {
// Show database progress bar if we have progress data (cluster backup)
if m.dbTotal > 0 && m.dbDone > 0 {
// Show progress bar instead of spinner when we have real progress
progressBar := renderBackupDatabaseProgressBar(m.dbDone, m.dbTotal, m.dbName, 50)
s.WriteString(progressBar + "\n")
s.WriteString(fmt.Sprintf(" %s\n", m.status))
} else {
// Show spinner during initial phases
if m.cancelling {
s.WriteString(fmt.Sprintf(" %s %s\n", spinnerFrames[m.spinnerFrame], m.status))
} else {
s.WriteString(fmt.Sprintf(" %s %s\n", spinnerFrames[m.spinnerFrame], m.status))
}
}
if !m.cancelling {
s.WriteString("\n [KEY] Press Ctrl+C or ESC to cancel\n")
}
} else {