Compare commits

...

8 Commits

Author SHA1 Message Date
bf1722c316 Update bin/README.md for v3.42.72
All checks were successful
CI/CD / Test (push) Successful in 1m15s
CI/CD / Lint (push) Successful in 1m24s
CI/CD / Build & Release (push) Has been skipped
2026-01-18 21:25:19 +01:00
a759f4d3db v3.42.72: Fix Large DB Mode not applying when changing profiles
All checks were successful
CI/CD / Test (push) Successful in 1m15s
CI/CD / Lint (push) Successful in 1m25s
CI/CD / Build & Release (push) Successful in 3m13s
Critical fix: Large DB Mode settings (reduced parallelism, increased locks)
were not being reapplied when user changed resource profiles in Settings.

This caused cluster restores to fail with 'out of shared memory' errors on
low-resource VMs even when Large DB Mode was enabled.

Now ApplyResourceProfile() properly applies LargeDBMode modifiers after
setting base profile values, ensuring parallelism stays reduced.

Example: balanced profile with Large DB Mode:
- ClusterParallelism: 4 → 2 (halved)
- MaxLocksPerTxn: 2048 → 8192 (increased)

This allows successful cluster restores on low-spec VMs.
2026-01-18 21:23:35 +01:00
7cf1d6f85b Update bin/README.md for v3.42.71
Some checks failed
CI/CD / Test (push) Successful in 1m15s
CI/CD / Lint (push) Successful in 1m25s
CI/CD / Build & Release (push) Has been cancelled
2026-01-18 21:17:11 +01:00
b305d1342e v3.42.71: Fix error message formatting + code alignment
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 3m12s
- Fixed incorrect diagnostic message for shared memory exhaustion
  (was 'Cannot access file', now 'PostgreSQL lock table exhausted')
- Improved clarity of lock capacity formula display
- Code formatting: aligned struct field comments for consistency
- Files affected: restore_exec.go, backup_exec.go, persist.go
2026-01-18 21:13:35 +01:00
5456da7183 Update bin/README.md for v3.42.70
All checks were successful
CI/CD / Test (push) Successful in 1m15s
CI/CD / Lint (push) Successful in 1m26s
CI/CD / Build & Release (push) Has been skipped
2026-01-18 18:53:44 +01:00
f9ff45cf2a v3.42.70: TUI consistency improvements - unified backup/restore views
All checks were successful
CI/CD / Test (push) Successful in 1m15s
CI/CD / Lint (push) Successful in 1m23s
CI/CD / Build & Release (push) Successful in 3m11s
- Added phase constants (backupPhaseGlobals, backupPhaseDatabases, backupPhaseCompressing)
- Changed title from '[EXEC] Backup Execution' to '[EXEC] Cluster Backup'
- Made phase labels explicit with action verbs (Backing up Globals, Backing up Databases, Compressing Archive)
- Added realtime ETA tracking to backup phase 2 (databases) with phase2StartTime
- Moved duration display from top to bottom as 'Elapsed:' (consistent with restore)
- Standardized keys label to '[KEYS]' everywhere (was '[KEY]')
- Added timing fields: phase2StartTime, dbPhaseElapsed, dbAvgPerDB
- Created renderBackupDatabaseProgressBarWithTiming() with elapsed + ETA display
- Enhanced completion summary with archive info and throughput calculation
- Removed duplicate formatDuration() function (shared with restore_exec.go)

All 10 consistency improvements implemented (high/medium/low priority).
Backup and restore TUI views now provide unified professional UX.
2026-01-18 18:52:26 +01:00
72c06ba5c2 fix(tui): realtime ETA updates during phase 3 cluster restore
All checks were successful
CI/CD / Test (push) Successful in 1m14s
CI/CD / Lint (push) Successful in 1m24s
CI/CD / Build & Release (push) Successful in 3m10s
Previously, the ETA during phase 3 (database restores) would appear to
hang because dbPhaseElapsed was only updated when a new database started
restoring, not during the restore operation itself.

Fixed by:
- Added phase3StartTime to track when phase 3 begins
- Calculate dbPhaseElapsed in realtime using time.Since(phase3StartTime)
- ETA now updates every 100ms tick instead of only on database transitions

This ensures the elapsed time and ETA display continuously update during
long-running database restores.
2026-01-18 18:36:48 +01:00
a0a401cab1 fix(tui): suppress preflight stdout output in TUI mode to prevent scrambled display
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 3m8s
- Add silentMode field to restore Engine struct
- Set silentMode=true in NewSilent() constructor for TUI mode
- Skip fmt.Println output in printPreflightSummary when in silent mode
- Log summary instead of printing to stdout in TUI mode
- Fixes scrambled output during cluster restore preflight checks
2026-01-18 18:17:00 +01:00
7 changed files with 185 additions and 61 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.50
- **Build Time**: 2026-01-18_11:19:47_UTC
- **Git Commit**: 490a12f
- **Build Time**: 2026-01-18_20:23:55_UTC
- **Git Commit**: a759f4d
## Recent Updates (v1.1.0)
- ✅ Fixed TUI progress display with line-by-line output

View File

@@ -445,6 +445,12 @@ func (c *Config) ApplyResourceProfile(profileName string) error {
// Apply profile settings
c.ResourceProfile = profile.Name
// If LargeDBMode is enabled, apply its modifiers
if c.LargeDBMode {
profile = cpu.ApplyLargeDBMode(profile)
}
c.ClusterParallelism = profile.ClusterParallelism
c.Jobs = profile.Jobs
c.DumpJobs = profile.DumpJobs

View File

@@ -30,7 +30,7 @@ type LocalConfig struct {
// Performance settings
CPUWorkload string
MaxCores int
ClusterTimeout int // Cluster operation timeout in minutes (default: 1440 = 24 hours)
ClusterTimeout int // Cluster operation timeout in minutes (default: 1440 = 24 hours)
ResourceProfile string
LargeDBMode bool // Enable large database mode (reduces parallelism, increases locks)

View File

@@ -50,6 +50,7 @@ type Engine struct {
progress progress.Indicator
detailedReporter *progress.DetailedReporter
dryRun bool
silentMode bool // Suppress stdout output (for TUI mode)
debugLogPath string // Path to save debug log on error
// TUI progress callback for detailed progress reporting
@@ -86,6 +87,7 @@ func NewSilent(cfg *config.Config, log logger.Logger, db database.Database) *Eng
progress: progressIndicator,
detailedReporter: detailedReporter,
dryRun: false,
silentMode: true, // Suppress stdout for TUI
}
}

View File

@@ -542,7 +542,20 @@ func (e *Engine) calculateRecommendedParallel(result *PreflightResult) int {
}
// printPreflightSummary prints a nice summary of all checks
// In silent mode (TUI), this is skipped and results are logged instead
func (e *Engine) printPreflightSummary(result *PreflightResult) {
// In TUI/silent mode, don't print to stdout - it causes scrambled output
if e.silentMode {
// Log summary instead for debugging
e.log.Info("Preflight checks complete",
"can_proceed", result.CanProceed,
"warnings", len(result.Warnings),
"errors", len(result.Errors),
"total_blobs", result.Archive.TotalBlobCount,
"recommended_locks", result.Archive.RecommendedLockBoost)
return
}
fmt.Println()
fmt.Println(strings.Repeat("─", 60))
fmt.Println(" PREFLIGHT CHECKS")

View File

@@ -13,6 +13,14 @@ import (
"dbbackup/internal/config"
"dbbackup/internal/database"
"dbbackup/internal/logger"
"path/filepath"
)
// Backup phase constants for consistency
const (
backupPhaseGlobals = 1
backupPhaseDatabases = 2
backupPhaseCompressing = 3
)
// BackupExecutionModel handles backup execution with progress
@@ -31,27 +39,36 @@ type BackupExecutionModel struct {
cancelling bool // True when user has requested cancellation
err error
result string
archivePath string // Path to created archive (for summary)
archiveSize int64 // Size of created archive (for summary)
startTime time.Time
elapsed time.Duration // Final elapsed time
details []string
spinnerFrame int
// Database count progress (for cluster backup)
dbTotal int
dbDone int
dbName string // Current database being backed up
overallPhase int // 1=globals, 2=databases, 3=compressing
phaseDesc string // Description of current phase
dbTotal int
dbDone int
dbName string // Current database being backed up
overallPhase int // 1=globals, 2=databases, 3=compressing
phaseDesc string // Description of current phase
phase2StartTime time.Time // When phase 2 (databases) started (for realtime ETA)
dbPhaseElapsed time.Duration // Elapsed time since database backup phase started
dbAvgPerDB time.Duration // Average time per database backup
}
// sharedBackupProgressState holds progress state that can be safely accessed from callbacks
type sharedBackupProgressState struct {
mu sync.Mutex
dbTotal int
dbDone int
dbName string
overallPhase int // 1=globals, 2=databases, 3=compressing
phaseDesc string // Description of current phase
hasUpdate bool
mu sync.Mutex
dbTotal int
dbDone int
dbName string
overallPhase int // 1=globals, 2=databases, 3=compressing
phaseDesc string // Description of current phase
hasUpdate bool
phase2StartTime time.Time // When phase 2 started (for realtime ETA calculation)
dbPhaseElapsed time.Duration // Elapsed time since database backup phase started
dbAvgPerDB time.Duration // Average time per database backup
}
// Package-level shared progress state for backup operations
@@ -72,12 +89,12 @@ func clearCurrentBackupProgress() {
currentBackupProgressState = nil
}
func getCurrentBackupProgress() (dbTotal, dbDone int, dbName string, overallPhase int, phaseDesc string, hasUpdate bool) {
func getCurrentBackupProgress() (dbTotal, dbDone int, dbName string, overallPhase int, phaseDesc string, hasUpdate bool, dbPhaseElapsed, dbAvgPerDB time.Duration, phase2StartTime time.Time) {
currentBackupProgressMu.Lock()
defer currentBackupProgressMu.Unlock()
if currentBackupProgressState == nil {
return 0, 0, "", 0, "", false
return 0, 0, "", 0, "", false, 0, 0, time.Time{}
}
currentBackupProgressState.mu.Lock()
@@ -86,9 +103,17 @@ func getCurrentBackupProgress() (dbTotal, dbDone int, dbName string, overallPhas
hasUpdate = currentBackupProgressState.hasUpdate
currentBackupProgressState.hasUpdate = false
// Calculate realtime phase elapsed if we have a phase 2 start time
dbPhaseElapsed = currentBackupProgressState.dbPhaseElapsed
if !currentBackupProgressState.phase2StartTime.IsZero() {
dbPhaseElapsed = time.Since(currentBackupProgressState.phase2StartTime)
}
return currentBackupProgressState.dbTotal, currentBackupProgressState.dbDone,
currentBackupProgressState.dbName, currentBackupProgressState.overallPhase,
currentBackupProgressState.phaseDesc, hasUpdate
currentBackupProgressState.phaseDesc, hasUpdate,
dbPhaseElapsed, currentBackupProgressState.dbAvgPerDB,
currentBackupProgressState.phase2StartTime
}
func NewBackupExecution(cfg *config.Config, log logger.Logger, parent tea.Model, ctx context.Context, backupType, dbName string, ratio int) BackupExecutionModel {
@@ -132,8 +157,11 @@ type backupProgressMsg struct {
}
type backupCompleteMsg struct {
result string
err error
result string
err error
archivePath string
archiveSize int64
elapsed time.Duration
}
func executeBackupWithTUIProgress(parentCtx context.Context, cfg *config.Config, log logger.Logger, backupType, dbName string, ratio int) tea.Cmd {
@@ -176,9 +204,13 @@ func executeBackupWithTUIProgress(parentCtx context.Context, cfg *config.Config,
progressState.dbDone = done
progressState.dbTotal = total
progressState.dbName = currentDB
progressState.overallPhase = 2 // Phase 2: Backing up databases
progressState.phaseDesc = fmt.Sprintf("Phase 2/3: Databases (%d/%d)", done, total)
progressState.overallPhase = backupPhaseDatabases
progressState.phaseDesc = fmt.Sprintf("Phase 2/3: Backing up Databases (%d/%d)", done, total)
progressState.hasUpdate = true
// Set phase 2 start time on first callback (for realtime ETA calculation)
if progressState.phase2StartTime.IsZero() {
progressState.phase2StartTime = time.Now()
}
progressState.mu.Unlock()
})
@@ -216,8 +248,9 @@ func executeBackupWithTUIProgress(parentCtx context.Context, cfg *config.Config,
}
return backupCompleteMsg{
result: result,
err: nil,
result: result,
err: nil,
elapsed: elapsed,
}
}
}
@@ -230,13 +263,15 @@ func (m BackupExecutionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.spinnerFrame = (m.spinnerFrame + 1) % len(spinnerFrames)
// Poll for database progress updates from callbacks
dbTotal, dbDone, dbName, overallPhase, phaseDesc, hasUpdate := getCurrentBackupProgress()
dbTotal, dbDone, dbName, overallPhase, phaseDesc, hasUpdate, dbPhaseElapsed, dbAvgPerDB, _ := getCurrentBackupProgress()
if hasUpdate {
m.dbTotal = dbTotal
m.dbDone = dbDone
m.dbName = dbName
m.overallPhase = overallPhase
m.phaseDesc = phaseDesc
m.dbPhaseElapsed = dbPhaseElapsed
m.dbAvgPerDB = dbAvgPerDB
}
// Update status based on progress and elapsed time
@@ -284,6 +319,7 @@ func (m BackupExecutionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.done = true
m.err = msg.err
m.result = msg.result
m.elapsed = msg.elapsed
if m.err == nil {
m.status = "[OK] Backup completed successfully!"
} else {
@@ -361,14 +397,52 @@ func renderBackupDatabaseProgressBar(done, total int, dbName string, width int)
return fmt.Sprintf(" Database: [%s] %d/%d", bar, done, total)
}
// renderBackupDatabaseProgressBarWithTiming renders database backup progress with ETA
func renderBackupDatabaseProgressBarWithTiming(done, total int, dbPhaseElapsed, dbAvgPerDB time.Duration) string {
if total == 0 {
return ""
}
// Calculate progress percentage
percent := float64(done) / float64(total)
if percent > 1.0 {
percent = 1.0
}
// Build progress bar
barWidth := 50
filled := int(float64(barWidth) * percent)
if filled > barWidth {
filled = barWidth
}
bar := strings.Repeat("█", filled) + strings.Repeat("░", barWidth-filled)
// Calculate ETA similar to restore
var etaStr string
if done > 0 && done < total {
avgPerDB := dbPhaseElapsed / time.Duration(done)
remaining := total - done
eta := avgPerDB * time.Duration(remaining)
etaStr = fmt.Sprintf(" | ETA: %s", formatDuration(eta))
} else if done == total {
etaStr = " | Complete"
}
return fmt.Sprintf(" Databases: [%s] %d/%d | Elapsed: %s%s\n",
bar, done, total, formatDuration(dbPhaseElapsed), etaStr)
}
func (m BackupExecutionModel) View() string {
var s strings.Builder
s.Grow(512) // Pre-allocate estimated capacity for better performance
// Clear screen with newlines and render header
s.WriteString("\n\n")
header := titleStyle.Render("[EXEC] Backup Execution")
s.WriteString(header)
header := "[EXEC] Backing up Database"
if m.backupType == "cluster" {
header = "[EXEC] Cluster Backup"
}
s.WriteString(titleStyle.Render(header))
s.WriteString("\n\n")
// Backup details - properly aligned
@@ -379,7 +453,6 @@ func (m BackupExecutionModel) View() string {
if m.ratio > 0 {
s.WriteString(fmt.Sprintf(" %-10s %d\n", "Sample:", m.ratio))
}
s.WriteString(fmt.Sprintf(" %-10s %s\n", "Duration:", time.Since(m.startTime).Round(time.Second)))
s.WriteString("\n")
// Status display
@@ -395,11 +468,15 @@ func (m BackupExecutionModel) View() string {
elapsedSec := int(time.Since(m.startTime).Seconds())
if m.overallPhase == 2 && m.dbTotal > 0 {
if m.overallPhase == backupPhaseDatabases && m.dbTotal > 0 {
// Phase 2: Database backups - contributes 15-90%
dbPct := int((int64(m.dbDone) * 100) / int64(m.dbTotal))
overallProgress = 15 + (dbPct * 75 / 100)
phaseLabel = m.phaseDesc
} else if m.overallPhase == backupPhaseCompressing {
// Phase 3: Compressing archive
overallProgress = 92
phaseLabel = "Phase 3/3: Compressing Archive"
} else if elapsedSec < 5 {
// Initial setup
overallProgress = 2
@@ -430,9 +507,9 @@ func (m BackupExecutionModel) View() string {
}
s.WriteString("\n")
// Database progress bar
progressBar := renderBackupDatabaseProgressBar(m.dbDone, m.dbTotal, m.dbName, 50)
s.WriteString(progressBar + "\n")
// Database progress bar with timing
s.WriteString(renderBackupDatabaseProgressBarWithTiming(m.dbDone, m.dbTotal, m.dbPhaseElapsed, m.dbAvgPerDB))
s.WriteString("\n")
} else {
// Intermediate phase (globals)
spinner := spinnerFrames[m.spinnerFrame]
@@ -449,7 +526,10 @@ func (m BackupExecutionModel) View() string {
}
if !m.cancelling {
s.WriteString("\n [KEY] Press Ctrl+C or ESC to cancel\n")
// Elapsed time
s.WriteString(fmt.Sprintf("Elapsed: %s\n", formatDuration(time.Since(m.startTime))))
s.WriteString("\n")
s.WriteString(infoStyle.Render("[KEYS] Press Ctrl+C or ESC to cancel"))
}
} else {
// Show completion summary with detailed stats
@@ -474,6 +554,14 @@ func (m BackupExecutionModel) View() string {
s.WriteString(infoStyle.Render(" ─── Summary ───────────────────────────────────────────────"))
s.WriteString("\n\n")
// Archive info (if available)
if m.archivePath != "" {
s.WriteString(fmt.Sprintf(" Archive: %s\n", filepath.Base(m.archivePath)))
}
if m.archiveSize > 0 {
s.WriteString(fmt.Sprintf(" Archive Size: %s\n", FormatBytes(m.archiveSize)))
}
// Backup type specific info
switch m.backupType {
case "cluster":
@@ -497,12 +585,21 @@ func (m BackupExecutionModel) View() string {
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 := m.elapsed
if elapsed == 0 {
elapsed = time.Since(m.startTime)
}
s.WriteString(fmt.Sprintf(" Total Time: %s\n", formatDuration(elapsed)))
// Calculate and show throughput if we have size info
if m.archiveSize > 0 && elapsed.Seconds() > 0 {
throughput := float64(m.archiveSize) / elapsed.Seconds()
s.WriteString(fmt.Sprintf(" Throughput: %s/s (average)\n", FormatBytes(int64(throughput))))
}
if m.backupType == "cluster" && m.dbTotal > 0 && m.err == nil {
avgPerDB := elapsed / time.Duration(m.dbTotal)
s.WriteString(fmt.Sprintf(" Avg per DB: %s\n", formatBackupDuration(avgPerDB)))
s.WriteString(fmt.Sprintf(" Avg per DB: %s\n", formatDuration(avgPerDB)))
}
s.WriteString("\n")
@@ -513,18 +610,3 @@ func (m BackupExecutionModel) View() string {
return s.String()
}
// formatBackupDuration formats duration in human readable format
func formatBackupDuration(d time.Duration) string {
if d < time.Minute {
return fmt.Sprintf("%.1fs", d.Seconds())
}
if d < time.Hour {
minutes := int(d.Minutes())
seconds := int(d.Seconds()) % 60
return fmt.Sprintf("%dm %ds", minutes, seconds)
}
hours := int(d.Hours())
minutes := int(d.Minutes()) % 60
return fmt.Sprintf("%dh %dm", hours, minutes)
}

View File

@@ -152,8 +152,9 @@ type sharedProgressState struct {
currentDB string
// Timing info for database restore phase
dbPhaseElapsed time.Duration // Elapsed time since restore phase started
dbAvgPerDB time.Duration // Average time per database restore
dbPhaseElapsed time.Duration // Elapsed time since restore phase started
dbAvgPerDB time.Duration // Average time per database restore
phase3StartTime time.Time // When phase 3 started (for realtime ETA calculation)
// Overall phase tracking (1=Extract, 2=Globals, 3=Databases)
overallPhase int
@@ -190,12 +191,12 @@ func clearCurrentRestoreProgress() {
currentRestoreProgressState = nil
}
func getCurrentRestoreProgress() (bytesTotal, bytesDone int64, description string, hasUpdate bool, dbTotal, dbDone int, speed float64, dbPhaseElapsed, dbAvgPerDB time.Duration, currentDB string, overallPhase int, extractionDone bool, dbBytesTotal, dbBytesDone int64) {
func getCurrentRestoreProgress() (bytesTotal, bytesDone int64, description string, hasUpdate bool, dbTotal, dbDone int, speed float64, dbPhaseElapsed, dbAvgPerDB time.Duration, currentDB string, overallPhase int, extractionDone bool, dbBytesTotal, dbBytesDone int64, phase3StartTime time.Time) {
currentRestoreProgressMu.Lock()
defer currentRestoreProgressMu.Unlock()
if currentRestoreProgressState == nil {
return 0, 0, "", false, 0, 0, 0, 0, 0, "", 0, false, 0, 0
return 0, 0, "", false, 0, 0, 0, 0, 0, "", 0, false, 0, 0, time.Time{}
}
currentRestoreProgressState.mu.Lock()
@@ -204,13 +205,20 @@ func getCurrentRestoreProgress() (bytesTotal, bytesDone int64, description strin
// Calculate rolling window speed
speed = calculateRollingSpeed(currentRestoreProgressState.speedSamples)
// Calculate realtime phase elapsed if we have a phase 3 start time
dbPhaseElapsed = currentRestoreProgressState.dbPhaseElapsed
if !currentRestoreProgressState.phase3StartTime.IsZero() {
dbPhaseElapsed = time.Since(currentRestoreProgressState.phase3StartTime)
}
return currentRestoreProgressState.bytesTotal, currentRestoreProgressState.bytesDone,
currentRestoreProgressState.description, currentRestoreProgressState.hasUpdate,
currentRestoreProgressState.dbTotal, currentRestoreProgressState.dbDone, speed,
currentRestoreProgressState.dbPhaseElapsed, currentRestoreProgressState.dbAvgPerDB,
dbPhaseElapsed, currentRestoreProgressState.dbAvgPerDB,
currentRestoreProgressState.currentDB, currentRestoreProgressState.overallPhase,
currentRestoreProgressState.extractionDone,
currentRestoreProgressState.dbBytesTotal, currentRestoreProgressState.dbBytesDone
currentRestoreProgressState.dbBytesTotal, currentRestoreProgressState.dbBytesDone,
currentRestoreProgressState.phase3StartTime
}
// calculateRollingSpeed calculates speed from recent samples (last 5 seconds)
@@ -357,6 +365,10 @@ func executeRestoreWithTUIProgress(parentCtx context.Context, cfg *config.Config
progressState.overallPhase = 3
progressState.extractionDone = true
progressState.hasUpdate = true
// Set phase 3 start time on first callback (for realtime ETA calculation)
if progressState.phase3StartTime.IsZero() {
progressState.phase3StartTime = time.Now()
}
// Clear byte progress when switching to db progress
progressState.bytesTotal = 0
progressState.bytesDone = 0
@@ -375,6 +387,10 @@ func executeRestoreWithTUIProgress(parentCtx context.Context, cfg *config.Config
progressState.dbPhaseElapsed = phaseElapsed
progressState.dbAvgPerDB = avgPerDB
progressState.hasUpdate = true
// Set phase 3 start time on first callback (for realtime ETA calculation)
if progressState.phase3StartTime.IsZero() {
progressState.phase3StartTime = time.Now()
}
// Clear byte progress when switching to db progress
progressState.bytesTotal = 0
progressState.bytesDone = 0
@@ -392,6 +408,10 @@ func executeRestoreWithTUIProgress(parentCtx context.Context, cfg *config.Config
progressState.overallPhase = 3
progressState.extractionDone = true
progressState.hasUpdate = true
// Set phase 3 start time on first callback (for realtime ETA calculation)
if progressState.phase3StartTime.IsZero() {
progressState.phase3StartTime = time.Now()
}
})
// Store progress state in a package-level variable for the ticker to access
@@ -447,7 +467,8 @@ func (m RestoreExecutionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.elapsed = time.Since(m.startTime)
// Poll shared progress state for real-time updates
bytesTotal, bytesDone, description, hasUpdate, dbTotal, dbDone, speed, dbPhaseElapsed, dbAvgPerDB, currentDB, overallPhase, extractionDone, dbBytesTotal, dbBytesDone := getCurrentRestoreProgress()
// Note: dbPhaseElapsed is now calculated in realtime inside getCurrentRestoreProgress()
bytesTotal, bytesDone, description, hasUpdate, dbTotal, dbDone, speed, dbPhaseElapsed, dbAvgPerDB, currentDB, overallPhase, extractionDone, dbBytesTotal, dbBytesDone, _ := getCurrentRestoreProgress()
if hasUpdate && bytesTotal > 0 && !extractionDone {
// Phase 1: Extraction
m.bytesTotal = bytesTotal
@@ -1150,12 +1171,12 @@ func formatRestoreError(errStr string) string {
// Provide specific recommendations based on error
if strings.Contains(errStr, "out of shared memory") || strings.Contains(errStr, "max_locks_per_transaction") {
s.WriteString(errorStyle.Render(" • Cannot access file: stat : no such file or directory\n"))
s.WriteString(errorStyle.Render(" • PostgreSQL lock table exhausted\n"))
s.WriteString("\n")
s.WriteString(infoStyle.Render(" ─── [HINT] Recommendations ────────────────────────────────"))
s.WriteString("\n\n")
s.WriteString(" Lock table exhausted. Total capacity = max_locks_per_transaction\n")
s.WriteString(" × (max_connections + max_prepared_transactions).\n\n")
s.WriteString(" Lock capacity = max_locks_per_transaction\n")
s.WriteString(" × (max_connections + max_prepared_transactions)\n\n")
s.WriteString(" If you reduced VM size or max_connections, you need higher\n")
s.WriteString(" max_locks_per_transaction to compensate.\n\n")
s.WriteString(successStyle.Render(" FIX OPTIONS:\n"))