Compare commits

..

4 Commits

Author SHA1 Message Date
490a12f858 feat(tui): show resource profile and CPU workload on cluster restore preview
All checks were successful
CI/CD / Test (push) Successful in 1m18s
CI/CD / Lint (push) Successful in 1m26s
CI/CD / Build & Release (push) Successful in 3m15s
- Displays current Resource Profile with parallelism settings
- Shows CPU Workload type (balanced/cpu-intensive/io-intensive)
- Shows Cluster Parallelism (number of concurrent database restores)

This helps users understand what performance settings will be used
before starting a cluster restore operation.
2026-01-18 12:19:28 +01:00
ea4337e298 fix(config): use resource profile defaults for Jobs, DumpJobs, ClusterParallelism
All checks were successful
CI/CD / Test (push) Successful in 1m16s
CI/CD / Lint (push) Successful in 1m25s
CI/CD / Build & Release (push) Successful in 3m17s
- ClusterParallelism now defaults to recommended profile (1 for small VMs)
- Jobs and DumpJobs now use profile values instead of raw CPU counts
- Small VMs (4 cores, 32GB) will now get 'conservative' or 'balanced' profile
  with lower parallelism to avoid 'out of shared memory' errors

This fixes the issue where small VMs defaulted to ClusterParallelism=2
regardless of detected resources, causing restore failures on large DBs.
2026-01-18 12:04:11 +01:00
bbd4f0ceac docs: update TUI screenshots with resource profiles and system info
All checks were successful
CI/CD / Test (push) Successful in 1m17s
CI/CD / Lint (push) Successful in 1m27s
CI/CD / Build & Release (push) Has been skipped
- Added system detection display (CPU cores, memory, recommended profile)
- Added Resource Profile and Cluster Parallelism settings
- Updated hotkeys: 'l' large-db, 'c' conservative, 'p' recommend
- Added resource profiles table for large database operations
- Updated example values to reflect typical PostgreSQL setup
2026-01-18 11:55:30 +01:00
f6f8b04785 fix(tui): improve restore error display formatting
All checks were successful
CI/CD / Test (push) Successful in 1m17s
CI/CD / Lint (push) Successful in 1m27s
CI/CD / Build & Release (push) Successful in 3m16s
- Parse and structure error messages for clean TUI display
- Extract error type, message, hint, and failed databases
- Show specific recommendations based on error type
- Fix for 'out of shared memory' - suggest profile settings
- Limit line width to prevent scrambled display
- Add structured sections: Error Details, Diagnosis, Recommendations
2026-01-18 11:48:07 +01:00
6 changed files with 245 additions and 15 deletions

View File

@@ -194,21 +194,49 @@ r: Restore | v: Verify | i: Info | d: Diagnose | D: Delete | R: Refresh | Esc: B
```
Configuration Settings
[SYSTEM] Detected Resources
CPU: 8 physical cores, 16 logical cores
Memory: 32GB total, 28GB available
Recommended Profile: balanced
→ 8 cores and 32GB RAM supports moderate parallelism
[CONFIG] Current Settings
Target DB: PostgreSQL (postgres)
Database: postgres@localhost:5432
Backup Dir: /var/backups/postgres
Compression: Level 6
Profile: balanced | Cluster: 2 parallel | Jobs: 4
> Database Type: postgres
CPU Workload Type: balanced
Backup Directory: /root/db_backups
Work Directory: /tmp
Resource Profile: balanced (P:2 J:4)
Cluster Parallelism: 2
Backup Directory: /var/backups/postgres
Work Directory: (system temp)
Compression Level: 6
Parallel Jobs: 16
Dump Jobs: 8
Parallel Jobs: 4
Dump Jobs: 4
Database Host: localhost
Database Port: 5432
Database User: root
Database User: postgres
SSL Mode: prefer
s: Save | r: Reset | q: Menu
[KEYS] ↑↓ navigate | Enter edit | 'l' large-db | 'c' conservative | 'p' recommend | 's' save | 'q' menu
```
**Resource Profiles for Large Databases:**
When restoring large databases on VMs with limited resources, use the resource profile settings to prevent "out of shared memory" errors:
| Profile | Cluster Parallel | Jobs | Best For |
|---------|------------------|------|----------|
| conservative | 1 | 1 | Small VMs (<16GB RAM) |
| balanced | 2 | 2-4 | Medium VMs (16-32GB RAM) |
| performance | 4 | 4-8 | Large servers (32GB+ RAM) |
| large-db | 1 | 2 | Large databases on any hardware |
**Quick shortcuts:** Press `l` to apply large-db profile, `c` for conservative, `p` to show recommendation.
**Database Status:**
```
Database Status & Health Check

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-17_16:07:42_UTC
- **Git Commit**: e2cf9ad
- **Build Time**: 2026-01-18_11:06:11_UTC
- **Git Commit**: ea4337e
## Recent Updates (v1.1.0)
- ✅ Fixed TUI progress display with line-by-line output

View File

@@ -200,11 +200,11 @@ func New() *Config {
SSLMode: sslMode,
Insecure: getEnvBool("INSECURE", false),
// Backup defaults
// Backup defaults - use recommended profile's settings for small VMs
BackupDir: backupDir,
CompressionLevel: getEnvInt("COMPRESS_LEVEL", 6),
Jobs: getEnvInt("JOBS", getDefaultJobs(cpuInfo)),
DumpJobs: getEnvInt("DUMP_JOBS", getDefaultDumpJobs(cpuInfo)),
Jobs: getEnvInt("JOBS", recommendedProfile.Jobs),
DumpJobs: getEnvInt("DUMP_JOBS", recommendedProfile.DumpJobs),
MaxCores: getEnvInt("MAX_CORES", getDefaultMaxCores(cpuInfo)),
AutoDetectCores: getEnvBool("AUTO_DETECT_CORES", true),
CPUWorkloadType: getEnvString("CPU_WORKLOAD_TYPE", "balanced"),
@@ -233,8 +233,8 @@ func New() *Config {
// Timeouts - default 24 hours (1440 min) to handle very large databases with large objects
ClusterTimeoutMinutes: getEnvInt("CLUSTER_TIMEOUT_MIN", 1440),
// Cluster parallelism (default: 2 concurrent operations for faster cluster backup/restore)
ClusterParallelism: getEnvInt("CLUSTER_PARALLELISM", 2),
// Cluster parallelism - use recommended profile's setting for small VMs
ClusterParallelism: getEnvInt("CLUSTER_PARALLELISM", recommendedProfile.ClusterParallelism),
// Working directory for large operations (default: system temp)
WorkDir: getEnvString("WORK_DIR", ""),

View File

@@ -659,7 +659,13 @@ func (m RestoreExecutionModel) View() string {
s.WriteString("\n")
s.WriteString(errorStyle.Render("╚══════════════════════════════════════════════════════════════╝"))
s.WriteString("\n\n")
s.WriteString(errorStyle.Render(fmt.Sprintf(" Error: %v", m.err)))
// Parse and display error in a clean, structured format
errStr := m.err.Error()
// Extract key parts from the error message
errDisplay := formatRestoreError(errStr)
s.WriteString(errDisplay)
s.WriteString("\n")
} else {
s.WriteString(successStyle.Render("╔══════════════════════════════════════════════════════════════╗"))
@@ -1005,3 +1011,188 @@ func dropDatabaseCLI(ctx context.Context, cfg *config.Config, dbName string) err
return nil
}
// formatRestoreError formats a restore error message for clean TUI display
func formatRestoreError(errStr string) string {
var s strings.Builder
maxLineWidth := 60
// Common patterns to extract
patterns := []struct {
key string
pattern string
}{
{"Error Type", "ERROR:"},
{"Hint", "HINT:"},
{"Last Error", "last error:"},
{"Total Errors", "total errors:"},
}
// First, try to extract a clean error summary
errLines := strings.Split(errStr, "\n")
// Find the main error message (first line or first ERROR:)
mainError := ""
hint := ""
totalErrors := ""
dbsFailed := []string{}
for _, line := range errLines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
// Extract ERROR messages
if strings.Contains(line, "ERROR:") {
if mainError == "" {
// Get just the ERROR part
idx := strings.Index(line, "ERROR:")
if idx >= 0 {
mainError = strings.TrimSpace(line[idx:])
// Truncate if too long
if len(mainError) > maxLineWidth {
mainError = mainError[:maxLineWidth-3] + "..."
}
}
}
}
// Extract HINT
if strings.Contains(line, "HINT:") {
idx := strings.Index(line, "HINT:")
if idx >= 0 {
hint = strings.TrimSpace(line[idx+5:])
if len(hint) > maxLineWidth {
hint = hint[:maxLineWidth-3] + "..."
}
}
}
// Extract total errors count
if strings.Contains(line, "total errors:") {
idx := strings.Index(line, "total errors:")
if idx >= 0 {
totalErrors = strings.TrimSpace(line[idx+13:])
// Just extract the number
parts := strings.Fields(totalErrors)
if len(parts) > 0 {
totalErrors = parts[0]
}
}
}
// Extract failed database names (for cluster restore)
if strings.Contains(line, ": restore failed:") {
parts := strings.SplitN(line, ":", 2)
if len(parts) > 0 {
dbName := strings.TrimSpace(parts[0])
if dbName != "" && !strings.HasPrefix(dbName, "Error") {
dbsFailed = append(dbsFailed, dbName)
}
}
}
}
// If no structured error found, use the first line
if mainError == "" {
firstLine := errStr
if idx := strings.Index(errStr, "\n"); idx > 0 {
firstLine = errStr[:idx]
}
if len(firstLine) > maxLineWidth*2 {
firstLine = firstLine[:maxLineWidth*2-3] + "..."
}
mainError = firstLine
}
// Build structured error display
s.WriteString(infoStyle.Render(" ─── Error Details ─────────────────────────────────────────"))
s.WriteString("\n\n")
// Error type detection
errorType := "critical"
if strings.Contains(errStr, "out of shared memory") || strings.Contains(errStr, "max_locks_per_transaction") {
errorType = "critical"
} else if strings.Contains(errStr, "connection") {
errorType = "connection"
} else if strings.Contains(errStr, "permission") || strings.Contains(errStr, "access") {
errorType = "permission"
}
s.WriteString(fmt.Sprintf(" Type: %s\n", errorType))
s.WriteString(fmt.Sprintf(" Message: %s\n", mainError))
if hint != "" {
s.WriteString(fmt.Sprintf(" Hint: %s\n", hint))
}
if totalErrors != "" {
s.WriteString(fmt.Sprintf(" Total Errors: %s\n", totalErrors))
}
// Show failed databases (max 5)
if len(dbsFailed) > 0 {
s.WriteString("\n")
s.WriteString(" Failed Databases:\n")
for i, db := range dbsFailed {
if i >= 5 {
s.WriteString(fmt.Sprintf(" ... and %d more\n", len(dbsFailed)-5))
break
}
s.WriteString(fmt.Sprintf(" • %s\n", db))
}
}
s.WriteString("\n")
s.WriteString(infoStyle.Render(" ─── Diagnosis ─────────────────────────────────────────────"))
s.WriteString("\n\n")
// 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("\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(" 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"))
s.WriteString(" 1. Use 'conservative' or 'large-db' profile in Settings\n")
s.WriteString(" (press 'l' for large-db, 'c' for conservative)\n\n")
s.WriteString(" 2. Increase PostgreSQL locks:\n")
s.WriteString(" ALTER SYSTEM SET max_locks_per_transaction = 4096;\n")
s.WriteString(" Then RESTART PostgreSQL.\n\n")
s.WriteString(" 3. Reduce parallel jobs:\n")
s.WriteString(" Set Cluster Parallelism = 1 in Settings\n")
} else if strings.Contains(errStr, "connection") || strings.Contains(errStr, "refused") {
s.WriteString(" • Database connection failed\n\n")
s.WriteString(infoStyle.Render(" ─── [HINT] Recommendations ────────────────────────────────"))
s.WriteString("\n\n")
s.WriteString(" 1. Check database is running\n")
s.WriteString(" 2. Verify host, port, and credentials in Settings\n")
s.WriteString(" 3. Check firewall/network connectivity\n")
} else if strings.Contains(errStr, "permission") || strings.Contains(errStr, "denied") {
s.WriteString(" • Permission denied\n\n")
s.WriteString(infoStyle.Render(" ─── [HINT] Recommendations ────────────────────────────────"))
s.WriteString("\n\n")
s.WriteString(" 1. Verify database user has sufficient privileges\n")
s.WriteString(" 2. Grant CREATE/DROP DATABASE permissions if restoring cluster\n")
s.WriteString(" 3. Check file system permissions on backup directory\n")
} else {
s.WriteString(" See error message above for details.\n\n")
s.WriteString(infoStyle.Render(" ─── [HINT] General Recommendations ────────────────────────"))
s.WriteString("\n\n")
s.WriteString(" 1. Check the full error log for details\n")
s.WriteString(" 2. Try restoring with 'conservative' profile (press 'c')\n")
s.WriteString(" 3. For large databases, use 'large-db' profile (press 'l')\n")
}
s.WriteString("\n")
// Suppress the pattern variable since we don't use it but defined it
_ = patterns
return s.String()
}

View File

@@ -402,6 +402,17 @@ func (m RestorePreviewModel) View() string {
s.WriteString("\n")
s.WriteString(fmt.Sprintf(" Host: %s:%d\n", m.config.Host, m.config.Port))
// Show Resource Profile and CPU Workload settings
profile := m.config.GetCurrentProfile()
if profile != nil {
s.WriteString(fmt.Sprintf(" Resource Profile: %s (Parallel:%d, Jobs:%d)\n",
profile.Name, profile.ClusterParallelism, profile.Jobs))
} else {
s.WriteString(fmt.Sprintf(" Resource Profile: %s\n", m.config.ResourceProfile))
}
s.WriteString(fmt.Sprintf(" CPU Workload: %s\n", m.config.CPUWorkloadType))
s.WriteString(fmt.Sprintf(" Cluster Parallelism: %d databases\n", m.config.ClusterParallelism))
if m.existingDBError != "" {
// Show warning when database listing failed - but still allow cleanup toggle
s.WriteString(checkWarningStyle.Render(" Existing Databases: Detection failed\n"))