From 670c9af2e7d4d2d4e854d062b6b0489d144b83d1 Mon Sep 17 00:00:00 2001 From: Alexander Renz Date: Sun, 18 Jan 2026 11:38:24 +0100 Subject: [PATCH] feat(tui): add resource profiles for backup/restore operations - Add memory detection for Linux, macOS, Windows - Add 5 predefined profiles: conservative, balanced, performance, max-performance, large-db - Add Resource Profile and Cluster Parallelism settings in TUI - Add quick hotkeys: 'l' for large-db, 'c' for conservative, 'p' for recommendations - Display system resources (CPU cores, memory) and recommended profile - Auto-detect and recommend profile based on system resources Fixes 'out of shared memory' errors when restoring large databases on small VMs. Use 'large-db' or 'conservative' profile for large databases on constrained systems. --- bin/README.md | 4 +- internal/config/config.go | 54 ++++- internal/cpu/profiles.go | 445 ++++++++++++++++++++++++++++++++++++++ internal/tui/settings.go | 133 +++++++++++- 4 files changed, 629 insertions(+), 7 deletions(-) create mode 100644 internal/cpu/profiles.go diff --git a/bin/README.md b/bin/README.md index d1475fe..4a5ef26 100644 --- a/bin/README.md +++ b/bin/README.md @@ -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:00:43_UTC -- **Git Commit**: 29e089f +- **Build Time**: 2026-01-17_16:07:42_UTC +- **Git Commit**: e2cf9ad ## Recent Updates (v1.1.0) - ✅ Fixed TUI progress display with line-by-line output diff --git a/internal/config/config.go b/internal/config/config.go index c517b1e..b533fb4 100755 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -36,9 +36,13 @@ type Config struct { AutoDetectCores bool CPUWorkloadType string // "cpu-intensive", "io-intensive", "balanced" + // Resource profile for backup/restore operations + ResourceProfile string // "conservative", "balanced", "performance", "max-performance", "large-db" + // CPU detection CPUDetector *cpu.Detector CPUInfo *cpu.CPUInfo + MemoryInfo *cpu.MemoryInfo // System memory information // Sample backup options SampleStrategy string // "ratio", "percent", "count" @@ -178,6 +182,13 @@ func New() *Config { sslMode = "" } + // Detect memory information + memInfo, _ := cpu.DetectMemory() + + // Determine recommended resource profile + recommendedProfile := cpu.RecommendProfile(cpuInfo, memInfo, false) + defaultProfile := getEnvString("RESOURCE_PROFILE", recommendedProfile.Name) + cfg := &Config{ // Database defaults Host: host, @@ -197,10 +208,12 @@ func New() *Config { MaxCores: getEnvInt("MAX_CORES", getDefaultMaxCores(cpuInfo)), AutoDetectCores: getEnvBool("AUTO_DETECT_CORES", true), CPUWorkloadType: getEnvString("CPU_WORKLOAD_TYPE", "balanced"), + ResourceProfile: defaultProfile, - // CPU detection + // CPU and memory detection CPUDetector: cpuDetector, CPUInfo: cpuInfo, + MemoryInfo: memInfo, // Sample backup defaults SampleStrategy: getEnvString("SAMPLE_STRATEGY", "ratio"), @@ -409,6 +422,45 @@ func (c *Config) OptimizeForCPU() error { return nil } +// ApplyResourceProfile applies a resource profile to the configuration +// This adjusts parallelism settings based on the chosen profile +func (c *Config) ApplyResourceProfile(profileName string) error { + profile := cpu.GetProfileByName(profileName) + if profile == nil { + return &ConfigError{ + Field: "resource_profile", + Value: profileName, + Message: "unknown profile. Valid profiles: conservative, balanced, performance, max-performance, large-db", + } + } + + // Validate profile against current system + isValid, warnings := cpu.ValidateProfileForSystem(profile, c.CPUInfo, c.MemoryInfo) + if !isValid { + // Log warnings but don't block - user may know what they're doing + _ = warnings // In production, log these warnings + } + + // Apply profile settings + c.ResourceProfile = profile.Name + c.ClusterParallelism = profile.ClusterParallelism + c.Jobs = profile.Jobs + c.DumpJobs = profile.DumpJobs + + return nil +} + +// GetResourceProfileRecommendation returns the recommended profile and reason +func (c *Config) GetResourceProfileRecommendation(isLargeDB bool) (string, string) { + profile, reason := cpu.RecommendProfileWithReason(c.CPUInfo, c.MemoryInfo, isLargeDB) + return profile.Name, reason +} + +// GetCurrentProfile returns the current resource profile details +func (c *Config) GetCurrentProfile() *cpu.ResourceProfile { + return cpu.GetProfileByName(c.ResourceProfile) +} + // GetCPUInfo returns CPU information, detecting if necessary func (c *Config) GetCPUInfo() (*cpu.CPUInfo, error) { if c.CPUInfo != nil { diff --git a/internal/cpu/profiles.go b/internal/cpu/profiles.go new file mode 100644 index 0000000..db9f9d1 --- /dev/null +++ b/internal/cpu/profiles.go @@ -0,0 +1,445 @@ +package cpu + +import ( + "bufio" + "fmt" + "os" + "os/exec" + "runtime" + "strconv" + "strings" +) + +// MemoryInfo holds system memory information +type MemoryInfo struct { + TotalBytes int64 `json:"total_bytes"` + AvailableBytes int64 `json:"available_bytes"` + FreeBytes int64 `json:"free_bytes"` + UsedBytes int64 `json:"used_bytes"` + SwapTotalBytes int64 `json:"swap_total_bytes"` + SwapFreeBytes int64 `json:"swap_free_bytes"` + TotalGB int `json:"total_gb"` + AvailableGB int `json:"available_gb"` + Platform string `json:"platform"` +} + +// ResourceProfile defines a resource allocation profile for backup/restore operations +type ResourceProfile struct { + Name string `json:"name"` + Description string `json:"description"` + ClusterParallelism int `json:"cluster_parallelism"` // Concurrent databases + Jobs int `json:"jobs"` // Parallel jobs within pg_restore + DumpJobs int `json:"dump_jobs"` // Parallel jobs for pg_dump + MaintenanceWorkMem string `json:"maintenance_work_mem"` // PostgreSQL recommendation + MaxLocksPerTxn int `json:"max_locks_per_txn"` // PostgreSQL recommendation + RecommendedForLarge bool `json:"recommended_for_large"` // Suitable for large DBs? + MinMemoryGB int `json:"min_memory_gb"` // Minimum memory for this profile + MinCores int `json:"min_cores"` // Minimum cores for this profile +} + +// Predefined resource profiles +var ( + // ProfileConservative - Safe for constrained VMs, avoids shared memory issues + ProfileConservative = ResourceProfile{ + Name: "conservative", + Description: "Safe for small VMs (2-4 cores, <16GB). Sequential operations, minimal memory pressure. Best for large DBs on limited hardware.", + ClusterParallelism: 1, + Jobs: 1, + DumpJobs: 2, + MaintenanceWorkMem: "256MB", + MaxLocksPerTxn: 4096, + RecommendedForLarge: true, + MinMemoryGB: 4, + MinCores: 2, + } + + // ProfileBalanced - Default profile, works for most scenarios + ProfileBalanced = ResourceProfile{ + Name: "balanced", + Description: "Balanced for medium VMs (4-8 cores, 16-32GB). Moderate parallelism with good safety margin.", + ClusterParallelism: 2, + Jobs: 2, + DumpJobs: 4, + MaintenanceWorkMem: "512MB", + MaxLocksPerTxn: 2048, + RecommendedForLarge: true, + MinMemoryGB: 16, + MinCores: 4, + } + + // ProfilePerformance - Aggressive parallelism for powerful servers + ProfilePerformance = ResourceProfile{ + Name: "performance", + Description: "Aggressive for powerful servers (8+ cores, 32GB+). Maximum parallelism for fast operations.", + ClusterParallelism: 4, + Jobs: 4, + DumpJobs: 8, + MaintenanceWorkMem: "1GB", + MaxLocksPerTxn: 1024, + RecommendedForLarge: false, // Large DBs may still need conservative + MinMemoryGB: 32, + MinCores: 8, + } + + // ProfileMaxPerformance - Maximum parallelism for high-end servers + ProfileMaxPerformance = ResourceProfile{ + Name: "max-performance", + Description: "Maximum for high-end servers (16+ cores, 64GB+). Full CPU utilization.", + ClusterParallelism: 8, + Jobs: 8, + DumpJobs: 16, + MaintenanceWorkMem: "2GB", + MaxLocksPerTxn: 512, + RecommendedForLarge: false, // Large DBs should use conservative + MinMemoryGB: 64, + MinCores: 16, + } + + // ProfileLargeDB - Optimized specifically for large databases + ProfileLargeDB = ResourceProfile{ + Name: "large-db", + Description: "Optimized for large databases with many tables/BLOBs. Prevents 'out of shared memory' errors.", + ClusterParallelism: 1, + Jobs: 2, + DumpJobs: 2, + MaintenanceWorkMem: "1GB", + MaxLocksPerTxn: 8192, + RecommendedForLarge: true, + MinMemoryGB: 8, + MinCores: 2, + } + + // AllProfiles contains all available profiles + AllProfiles = []ResourceProfile{ + ProfileConservative, + ProfileBalanced, + ProfilePerformance, + ProfileMaxPerformance, + ProfileLargeDB, + } +) + +// GetProfileByName returns a profile by its name +func GetProfileByName(name string) *ResourceProfile { + for _, p := range AllProfiles { + if strings.EqualFold(p.Name, name) { + return &p + } + } + return nil +} + +// DetectMemory detects system memory information +func DetectMemory() (*MemoryInfo, error) { + info := &MemoryInfo{ + Platform: runtime.GOOS, + } + + switch runtime.GOOS { + case "linux": + if err := detectLinuxMemory(info); err != nil { + return info, fmt.Errorf("linux memory detection failed: %w", err) + } + case "darwin": + if err := detectDarwinMemory(info); err != nil { + return info, fmt.Errorf("darwin memory detection failed: %w", err) + } + case "windows": + if err := detectWindowsMemory(info); err != nil { + return info, fmt.Errorf("windows memory detection failed: %w", err) + } + default: + // Fallback: use Go runtime memory stats + var memStats runtime.MemStats + runtime.ReadMemStats(&memStats) + info.TotalBytes = int64(memStats.Sys) + info.AvailableBytes = int64(memStats.Sys - memStats.Alloc) + } + + // Calculate GB values + info.TotalGB = int(info.TotalBytes / (1024 * 1024 * 1024)) + info.AvailableGB = int(info.AvailableBytes / (1024 * 1024 * 1024)) + + return info, nil +} + +// detectLinuxMemory reads memory info from /proc/meminfo +func detectLinuxMemory(info *MemoryInfo) error { + file, err := os.Open("/proc/meminfo") + if err != nil { + return err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + parts := strings.Fields(line) + if len(parts) < 2 { + continue + } + + key := strings.TrimSuffix(parts[0], ":") + value, err := strconv.ParseInt(parts[1], 10, 64) + if err != nil { + continue + } + + // Values are in kB + valueBytes := value * 1024 + + switch key { + case "MemTotal": + info.TotalBytes = valueBytes + case "MemAvailable": + info.AvailableBytes = valueBytes + case "MemFree": + info.FreeBytes = valueBytes + case "SwapTotal": + info.SwapTotalBytes = valueBytes + case "SwapFree": + info.SwapFreeBytes = valueBytes + } + } + + info.UsedBytes = info.TotalBytes - info.AvailableBytes + + return scanner.Err() +} + +// detectDarwinMemory detects memory on macOS +func detectDarwinMemory(info *MemoryInfo) error { + // Use sysctl for total memory + if output, err := runCommand("sysctl", "-n", "hw.memsize"); err == nil { + if val, err := strconv.ParseInt(strings.TrimSpace(output), 10, 64); err == nil { + info.TotalBytes = val + } + } + + // Use vm_stat for available memory (more complex parsing required) + if output, err := runCommand("vm_stat"); err == nil { + pageSize := int64(4096) // Default page size + var freePages, inactivePages int64 + + lines := strings.Split(output, "\n") + for _, line := range lines { + if strings.Contains(line, "page size of") { + parts := strings.Fields(line) + for i, p := range parts { + if p == "of" && i+1 < len(parts) { + if ps, err := strconv.ParseInt(parts[i+1], 10, 64); err == nil { + pageSize = ps + } + } + } + } else if strings.Contains(line, "Pages free:") { + val := extractNumberFromLine(line) + freePages = val + } else if strings.Contains(line, "Pages inactive:") { + val := extractNumberFromLine(line) + inactivePages = val + } + } + + info.FreeBytes = freePages * pageSize + info.AvailableBytes = (freePages + inactivePages) * pageSize + } + + info.UsedBytes = info.TotalBytes - info.AvailableBytes + return nil +} + +// detectWindowsMemory detects memory on Windows +func detectWindowsMemory(info *MemoryInfo) error { + // Use wmic for memory info + if output, err := runCommand("wmic", "OS", "get", "TotalVisibleMemorySize,FreePhysicalMemory", "/format:list"); err == nil { + lines := strings.Split(output, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "TotalVisibleMemorySize=") { + val := strings.TrimPrefix(line, "TotalVisibleMemorySize=") + if v, err := strconv.ParseInt(strings.TrimSpace(val), 10, 64); err == nil { + info.TotalBytes = v * 1024 // KB to bytes + } + } else if strings.HasPrefix(line, "FreePhysicalMemory=") { + val := strings.TrimPrefix(line, "FreePhysicalMemory=") + if v, err := strconv.ParseInt(strings.TrimSpace(val), 10, 64); err == nil { + info.FreeBytes = v * 1024 + info.AvailableBytes = v * 1024 + } + } + } + } + + info.UsedBytes = info.TotalBytes - info.AvailableBytes + return nil +} + +// RecommendProfile recommends a resource profile based on system resources and workload +func RecommendProfile(cpuInfo *CPUInfo, memInfo *MemoryInfo, isLargeDB bool) *ResourceProfile { + cores := 0 + if cpuInfo != nil { + cores = cpuInfo.PhysicalCores + if cores == 0 { + cores = cpuInfo.LogicalCores + } + } + if cores == 0 { + cores = runtime.NumCPU() + } + + memGB := 0 + if memInfo != nil { + memGB = memInfo.TotalGB + } + + // Special case: large databases should always use conservative/large-db profile + if isLargeDB { + if memGB >= 32 && cores >= 8 { + return &ProfileLargeDB // Still conservative but with more memory for maintenance + } + return &ProfileConservative + } + + // Resource-based selection + if cores >= 16 && memGB >= 64 { + return &ProfileMaxPerformance + } else if cores >= 8 && memGB >= 32 { + return &ProfilePerformance + } else if cores >= 4 && memGB >= 16 { + return &ProfileBalanced + } + + // Default to conservative for constrained systems + return &ProfileConservative +} + +// RecommendProfileWithReason returns a profile recommendation with explanation +func RecommendProfileWithReason(cpuInfo *CPUInfo, memInfo *MemoryInfo, isLargeDB bool) (*ResourceProfile, string) { + cores := 0 + if cpuInfo != nil { + cores = cpuInfo.PhysicalCores + if cores == 0 { + cores = cpuInfo.LogicalCores + } + } + if cores == 0 { + cores = runtime.NumCPU() + } + + memGB := 0 + if memInfo != nil { + memGB = memInfo.TotalGB + } + + // Build reason string + var reason strings.Builder + reason.WriteString(fmt.Sprintf("System: %d cores, %dGB RAM. ", cores, memGB)) + + profile := RecommendProfile(cpuInfo, memInfo, isLargeDB) + + if isLargeDB { + reason.WriteString("Large database detected - using conservative settings to avoid 'out of shared memory' errors.") + } else if profile.Name == "conservative" { + reason.WriteString("Limited resources detected - using conservative profile for stability.") + } else if profile.Name == "max-performance" { + reason.WriteString("High-end server detected - using maximum parallelism.") + } else if profile.Name == "performance" { + reason.WriteString("Good resources detected - using performance profile.") + } else { + reason.WriteString("Using balanced profile for optimal performance/stability trade-off.") + } + + return profile, reason.String() +} + +// ValidateProfileForSystem checks if a profile is suitable for the current system +func ValidateProfileForSystem(profile *ResourceProfile, cpuInfo *CPUInfo, memInfo *MemoryInfo) (bool, []string) { + var warnings []string + + cores := 0 + if cpuInfo != nil { + cores = cpuInfo.PhysicalCores + if cores == 0 { + cores = cpuInfo.LogicalCores + } + } + if cores == 0 { + cores = runtime.NumCPU() + } + + memGB := 0 + if memInfo != nil { + memGB = memInfo.TotalGB + } + + // Check minimum requirements + if cores < profile.MinCores { + warnings = append(warnings, + fmt.Sprintf("Profile '%s' recommends %d+ cores (system has %d)", profile.Name, profile.MinCores, cores)) + } + + if memGB < profile.MinMemoryGB { + warnings = append(warnings, + fmt.Sprintf("Profile '%s' recommends %dGB+ RAM (system has %dGB)", profile.Name, profile.MinMemoryGB, memGB)) + } + + // Check for potential issues + if profile.ClusterParallelism > cores { + warnings = append(warnings, + fmt.Sprintf("Cluster parallelism (%d) exceeds CPU cores (%d) - may cause contention", + profile.ClusterParallelism, cores)) + } + + // Memory pressure warning + memPerWorker := 2 // Rough estimate: 2GB per parallel worker for large DB operations + requiredMem := profile.ClusterParallelism * profile.Jobs * memPerWorker + if memGB > 0 && requiredMem > memGB { + warnings = append(warnings, + fmt.Sprintf("High parallelism may require ~%dGB RAM (system has %dGB) - risk of OOM", + requiredMem, memGB)) + } + + return len(warnings) == 0, warnings +} + +// FormatProfileSummary returns a formatted summary of a profile +func (p *ResourceProfile) FormatProfileSummary() string { + return fmt.Sprintf("[%s] Parallel: %d DBs, %d jobs | Recommended for large DBs: %v", + strings.ToUpper(p.Name), + p.ClusterParallelism, + p.Jobs, + p.RecommendedForLarge) +} + +// PostgreSQLRecommendations returns PostgreSQL configuration recommendations for this profile +func (p *ResourceProfile) PostgreSQLRecommendations() []string { + return []string{ + fmt.Sprintf("ALTER SYSTEM SET max_locks_per_transaction = %d;", p.MaxLocksPerTxn), + fmt.Sprintf("ALTER SYSTEM SET maintenance_work_mem = '%s';", p.MaintenanceWorkMem), + "-- Restart PostgreSQL after changes to max_locks_per_transaction", + } +} + +// Helper functions + +func runCommand(name string, args ...string) (string, error) { + cmd := exec.Command(name, args...) + output, err := cmd.Output() + if err != nil { + return "", err + } + return string(output), nil +} + +func extractNumberFromLine(line string) int64 { + // Extract number before the period at end (e.g., "Pages free: 123456.") + parts := strings.Fields(line) + for _, p := range parts { + p = strings.TrimSuffix(p, ".") + if val, err := strconv.ParseInt(p, 10, 64); err == nil && val > 0 { + return val + } + } + return 0 +} diff --git a/internal/tui/settings.go b/internal/tui/settings.go index e79505f..9b1ed3d 100755 --- a/internal/tui/settings.go +++ b/internal/tui/settings.go @@ -10,6 +10,7 @@ import ( "github.com/charmbracelet/lipgloss" "dbbackup/internal/config" + "dbbackup/internal/cpu" "dbbackup/internal/logger" ) @@ -101,6 +102,49 @@ func NewSettingsModel(cfg *config.Config, log logger.Logger, parent tea.Model) S Type: "selector", Description: "CPU workload profile (press Enter to cycle: Balanced → CPU-Intensive → I/O-Intensive)", }, + { + Key: "resource_profile", + DisplayName: "Resource Profile", + Value: func(c *config.Config) string { + profile := c.GetCurrentProfile() + if profile != nil { + return fmt.Sprintf("%s (P:%d J:%d)", profile.Name, profile.ClusterParallelism, profile.Jobs) + } + return c.ResourceProfile + }, + Update: func(c *config.Config, v string) error { + profiles := []string{"conservative", "balanced", "performance", "max-performance", "large-db"} + currentIdx := 0 + for i, p := range profiles { + if c.ResourceProfile == p { + currentIdx = i + break + } + } + nextIdx := (currentIdx + 1) % len(profiles) + return c.ApplyResourceProfile(profiles[nextIdx]) + }, + Type: "selector", + Description: "Resource profile for backup/restore. Use 'conservative' or 'large-db' for large databases on small VMs.", + }, + { + Key: "cluster_parallelism", + DisplayName: "Cluster Parallelism", + Value: func(c *config.Config) string { return fmt.Sprintf("%d", c.ClusterParallelism) }, + Update: func(c *config.Config, v string) error { + val, err := strconv.Atoi(v) + if err != nil { + return fmt.Errorf("cluster parallelism must be a number") + } + if val < 1 { + return fmt.Errorf("cluster parallelism must be at least 1") + } + c.ClusterParallelism = val + return nil + }, + Type: "int", + Description: "Concurrent databases during cluster backup/restore (1=sequential, safer for large DBs)", + }, { Key: "backup_dir", DisplayName: "Backup Directory", @@ -528,12 +572,58 @@ func (m SettingsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case "s": return m.saveSettings() + + case "l": + // Quick shortcut: Apply "large-db" profile for large databases + return m.applyLargeDBProfile() + + case "c": + // Quick shortcut: Apply "conservative" profile for constrained VMs + return m.applyConservativeProfile() + + case "p": + // Show profile recommendation + return m.showProfileRecommendation() } } return m, nil } +// applyLargeDBProfile applies the large-db profile optimized for large databases +func (m SettingsModel) applyLargeDBProfile() (tea.Model, tea.Cmd) { + if err := m.config.ApplyResourceProfile("large-db"); err != nil { + m.message = errorStyle.Render(fmt.Sprintf("[FAIL] %s", err.Error())) + return m, nil + } + m.message = successStyle.Render("[OK] Applied 'large-db' profile: Cluster=1, Jobs=2. Optimized for large DBs to avoid 'out of shared memory' errors.") + return m, nil +} + +// applyConservativeProfile applies the conservative profile for constrained VMs +func (m SettingsModel) applyConservativeProfile() (tea.Model, tea.Cmd) { + if err := m.config.ApplyResourceProfile("conservative"); err != nil { + m.message = errorStyle.Render(fmt.Sprintf("[FAIL] %s", err.Error())) + return m, nil + } + m.message = successStyle.Render("[OK] Applied 'conservative' profile: Cluster=1, Jobs=1. Safe for small VMs with limited memory.") + return m, nil +} + +// showProfileRecommendation displays the recommended profile based on system resources +func (m SettingsModel) showProfileRecommendation() (tea.Model, tea.Cmd) { + profileName, reason := m.config.GetResourceProfileRecommendation(false) + largeDBProfile, largeDBReason := m.config.GetResourceProfileRecommendation(true) + + m.message = infoStyle.Render(fmt.Sprintf( + "[RECOMMEND] Default: %s | For Large DBs: %s\n"+ + " → %s\n"+ + " → Large DB: %s\n"+ + " Press 'l' for large-db profile, 'c' for conservative", + profileName, largeDBProfile, reason, largeDBReason)) + return m, nil +} + // handleEditingInput handles input when editing a setting func (m SettingsModel) handleEditingInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) { switch msg.String() { @@ -747,7 +837,32 @@ func (m SettingsModel) View() string { // Current configuration summary if !m.editing { b.WriteString("\n") - b.WriteString(infoStyle.Render("[INFO] Current Configuration")) + b.WriteString(infoStyle.Render("[INFO] System Resources & Configuration")) + b.WriteString("\n") + + // System resources + var sysInfo []string + if m.config.CPUInfo != nil { + sysInfo = append(sysInfo, fmt.Sprintf("CPU: %d cores (physical), %d logical", + m.config.CPUInfo.PhysicalCores, m.config.CPUInfo.LogicalCores)) + } + if m.config.MemoryInfo != nil { + sysInfo = append(sysInfo, fmt.Sprintf("Memory: %dGB total, %dGB available", + m.config.MemoryInfo.TotalGB, m.config.MemoryInfo.AvailableGB)) + } + + // Recommended profile + recommendedProfile, reason := m.config.GetResourceProfileRecommendation(false) + sysInfo = append(sysInfo, fmt.Sprintf("Recommended Profile: %s", recommendedProfile)) + sysInfo = append(sysInfo, fmt.Sprintf(" → %s", reason)) + + for _, line := range sysInfo { + b.WriteString(detailStyle.Render(fmt.Sprintf(" %s", line))) + b.WriteString("\n") + } + + b.WriteString("\n") + b.WriteString(infoStyle.Render("[CONFIG] Current Settings")) b.WriteString("\n") summary := []string{ @@ -755,7 +870,17 @@ func (m SettingsModel) View() string { fmt.Sprintf("Database: %s@%s:%d", m.config.User, m.config.Host, m.config.Port), fmt.Sprintf("Backup Dir: %s", m.config.BackupDir), fmt.Sprintf("Compression: Level %d", m.config.CompressionLevel), - fmt.Sprintf("Jobs: %d parallel, %d dump", m.config.Jobs, m.config.DumpJobs), + fmt.Sprintf("Profile: %s | Cluster: %d parallel | Jobs: %d", + m.config.ResourceProfile, m.config.ClusterParallelism, m.config.Jobs), + } + + // Show profile warnings if applicable + profile := m.config.GetCurrentProfile() + if profile != nil { + isValid, warnings := cpu.ValidateProfileForSystem(profile, m.config.CPUInfo, m.config.MemoryInfo) + if !isValid && len(warnings) > 0 { + summary = append(summary, fmt.Sprintf("⚠️ Warning: %s", warnings[0])) + } } if m.config.CloudEnabled { @@ -782,9 +907,9 @@ func (m SettingsModel) View() string { } else { // Show different help based on current selection if m.cursor >= 0 && m.cursor < len(m.settings) && m.settings[m.cursor].Type == "path" { - footer = infoStyle.Render("\n[KEYS] Up/Down navigate | Enter edit | Tab browse directories | 's' save | 'r' reset | 'q' menu") + footer = infoStyle.Render("\n[KEYS] ↑↓ navigate | Enter edit | Tab dirs | 'l' large-db | 'c' conservative | 'p' recommend | 's' save | 'q' menu") } else { - footer = infoStyle.Render("\n[KEYS] Up/Down navigate | Enter edit | 's' save | 'r' reset | 'q' menu | Tab=dirs on path fields only") + footer = infoStyle.Render("\n[KEYS] ↑↓ navigate | Enter edit | 'l' large-db profile | 'c' conservative | 'p' recommend | 's' save | 'r' reset | 'q' menu") } } }