diff --git a/tui/README.md b/tui/README.md index 63691b7..c8c9c83 100644 --- a/tui/README.md +++ b/tui/README.md @@ -122,7 +122,6 @@ cd tui && ./backup-manager | `v` | View backup logs | | `s` | View backup status | | `f` | View configuration | -| `r` | Refresh current view | | `x` | Return to main view | | `c` | Clear output panel | | `Esc` | Return to main view | diff --git a/tui/backup-viewer b/tui/backup-tui similarity index 51% rename from tui/backup-viewer rename to tui/backup-tui index 208666b..dc6cb60 100755 Binary files a/tui/backup-viewer and b/tui/backup-tui differ diff --git a/tui/main.go b/tui/main.go index a37c2f6..006ffd7 100644 --- a/tui/main.go +++ b/tui/main.go @@ -1,20 +1,16 @@ package main import ( - "context" "fmt" "io" "os" - "os/exec" "path/filepath" "sort" "strings" - "sync" "time" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" - "github.com/charmbracelet/bubbles/spinner" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" @@ -22,7 +18,6 @@ import ( // Message types type ( - backupDirLoadedMsg struct{ backupDirs []BackupDirectory } logFilesLoadedMsg struct{ logFiles []LogFile } fileContentLoadedMsg struct{ content string } backupDetailsLoadedMsg struct{ details BackupDetails } @@ -69,33 +64,6 @@ type BackupDetails struct { NewestBackup time.Time `json:"newest_backup"` } -// BackupStatus represents the status of a backup operation -type BackupStatus struct { - Service string `json:"service"` - Status string `json:"status"` // running, success, error, idle - LastRun time.Time `json:"last_run"` - Duration string `json:"duration"` - Output string `json:"output"` - Error string `json:"error"` - Progress int `json:"progress"` // 0-100 percentage - StartTime time.Time `json:"start_time"` - EndTime time.Time `json:"end_time"` - BackupSize string `json:"backup_size"` - FilesCount int `json:"files_count"` - PID int `json:"pid"` // Process ID if running - Context context.Context `json:"-"` // Context for cancellation - Cancel context.CancelFunc `json:"-"` // Cancel function -} - -// RunningProcess represents a running backup process -type RunningProcess struct { - Cmd *exec.Cmd - Context context.Context - Cancel context.CancelFunc - Output []string - Mutex sync.RWMutex -} - // LogEntry represents a log entry from backup operations type LogEntry struct { Timestamp time.Time `json:"timestamp"` @@ -128,9 +96,6 @@ var ( Foreground(lipgloss.AdaptiveColor{Light: "#343433", Dark: "#C1C6B2"}). Background(lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#353533"}) - successStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#04B575")) - errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#EF4444")) - runningStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#F59E0B")) idleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#6B7280")) // Style for the selected item in the list @@ -156,7 +121,6 @@ type keyMap struct { ViewLogs key.Binding ViewStatus key.Binding ViewConfig key.Binding - Stop key.Binding Clear key.Binding } @@ -168,7 +132,7 @@ func (k keyMap) FullHelp() [][]key.Binding { return [][]key.Binding{ {k.Up, k.Down, k.Enter}, {k.Tab, k.ViewLogs, k.ViewStatus, k.ViewConfig}, - {k.Refresh, k.Stop, k.Clear}, + {k.Refresh, k.Clear}, {k.Help, k.Quit}, } } @@ -195,12 +159,12 @@ var keys = keyMap{ key.WithHelp("?", "toggle help"), ), Quit: key.NewBinding( - key.WithKeys("q", "esc", "ctrl+c"), + key.WithKeys("q", "ctrl+c"), key.WithHelp("q", "quit"), ), Enter: key.NewBinding( key.WithKeys("enter", " "), - key.WithHelp("enter", "run backup"), + key.WithHelp("enter", "browse directory"), ), Tab: key.NewBinding( key.WithKeys("tab"), @@ -226,10 +190,6 @@ var keys = keyMap{ key.WithKeys("f"), key.WithHelp("f", "view config"), ), - Stop: key.NewBinding( - key.WithKeys("x"), - key.WithHelp("x", "stop backup"), - ), Clear: key.NewBinding( key.WithKeys("c"), key.WithHelp("c", "clear output"), @@ -240,20 +200,16 @@ var keys = keyMap{ type Model struct { list list.Model viewport viewport.Model - spinner spinner.Model keys keyMap width int height int activePanel int // 0: list, 1: viewport, 2: status showHelp bool ready bool - backupStatus map[string]*BackupStatus logs []LogEntry shellPath string items []BackupItem // Store backup items for reference - runningBackups map[string]*RunningProcess // Track running processes with enhanced control currentView string // "main", "logs", "status" - selectedIndex int // Track currently selected menu item } // Initialize the model @@ -353,28 +309,20 @@ func initialModel() Model { l.Styles.PaginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4) l.Styles.HelpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1) - s := spinner.New() - s.Spinner = spinner.Dot - s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) - vp := viewport.New(78, 20) - vp.SetContent("Welcome to Backup Manager!\n\nSelect a backup operation from the list and press Enter to execute.\n\nUse 'v' to view logs, 'r' to refresh status, and 'tab' to switch between panels.") + vp.SetContent("Welcome to Backup File Browser!\n\nSelect a backup directory from the list and press Enter to browse backup files.\n\nUse 'v' to view logs, 'r' to refresh status, and 'tab' to switch between panels.") return Model{ list: l, viewport: vp, - spinner: s, keys: keys, activePanel: 0, showHelp: false, ready: false, - backupStatus: make(map[string]*BackupStatus), logs: []LogEntry{}, shellPath: shellPath, items: convertToBackupItems(items), // Store the items - runningBackups: make(map[string]*RunningProcess), currentView: "main", - selectedIndex: 0, // Initialize to first item } } @@ -400,18 +348,10 @@ func (d BackupItemDelegate) Render(w io.Writer, m list.Model, index int, listIte } } -// Helper functions -func min(a, b int) int { - if a < b { - return a - } - return b -} - // Helper function to convert list items to BackupItems func convertToBackupItems(items []list.Item) []BackupItem { backupItems := make([]BackupItem, len(items)) - for i, item := range items { + for i, item := range items { if bi, ok := item.(BackupItem); ok { backupItems[i] = bi } @@ -420,7 +360,7 @@ func convertToBackupItems(items []list.Item) []BackupItem { } func (m Model) Init() tea.Cmd { - return m.spinner.Tick + return nil } func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { @@ -441,10 +381,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.ready = true // Show description for the first selected item instead of generic welcome - if len(m.items) > 0 && len(m.runningBackups) == 0 { + if len(m.items) > 0 { m.showItemDescription() } else { - m.viewport.SetContent("Welcome to Backup Manager!\n\nSelect a backup operation from the list and press Enter to execute.\n\nUse 'v' to view logs, 'r' to refresh status, and 'tab' to switch between panels.") + m.viewport.SetContent("Welcome to Backup File Browser!\n\nSelect a backup directory from the list and press Enter to browse backup files.\n\nUse 'v' to view logs, 's' to view status, and 'tab' to switch between panels.") } } else { m.viewport.Width = msg.Width - 4 @@ -480,28 +420,17 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.showConfig() m.currentView = "config" - case key.Matches(msg, m.keys.Stop): - // For backup viewer, Stop key can be used to go back to main view - m.currentView = "main" - if len(m.items) > 0 { - m.showItemDescription() - } - case key.Matches(msg, m.keys.Clear): - m.viewport.SetContent("Output cleared.\n\nSelect a backup operation from the list and press Enter to execute.") + m.viewport.SetContent("Output cleared.\n\nSelect a backup directory from the list and press Enter to browse.") m.currentView = "main" // Show description for currently selected item - if len(m.runningBackups) == 0 { - m.showItemDescription() - } + m.showItemDescription() case key.Matches(msg, m.keys.Escape) && m.currentView != "main": - m.viewport.SetContent("Welcome to Backup Manager!\n\nSelect a backup operation from the list and press Enter to execute.\n\nUse 'v' to view logs, 's' to view status, and 'tab' to switch between panels.") + m.viewport.SetContent("Welcome to Backup File Browser!\n\nSelect a backup directory from the list and press Enter to browse.\n\nUse 'v' to view logs, 's' for status, and 'tab' to switch between panels.") m.currentView = "main" // Show description for currently selected item - if len(m.runningBackups) == 0 { - m.showItemDescription() - } + m.showItemDescription() case key.Matches(msg, m.keys.Enter) && m.activePanel == 0: if i, ok := m.list.SelectedItem().(BackupItem); ok { @@ -518,30 +447,38 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } - case key.Matches(msg, m.keys.Stop): - // For backup viewer, Stop key can be used to go back to main view - m.currentView = "main" - if len(m.items) > 0 { - m.showItemDescription() - } - case key.Matches(msg, m.keys.Help): m.showHelp = !m.showHelp } - // Update the focused component + // Update the focused component (CRITICAL: this must be outside the switch) prevIndex := m.list.Index() if m.activePanel == 0 { m.list, cmd = m.list.Update(msg) // Check if selection changed and we're in main view if m.list.Index() != prevIndex && m.currentView == "main" { - m.selectedIndex = m.list.Index() m.showItemDescription() } } else { m.viewport, cmd = m.viewport.Update(msg) } cmds = append(cmds, cmd) + + case backupDetailsLoadedMsg: + m.showBackupDetails(msg.details) + m.currentView = "directory" + + case logFilesLoadedMsg: + m.showLogFiles(msg.logFiles) + m.currentView = "logs" + + case fileContentLoadedMsg: + m.viewport.SetContent(msg.content) + m.currentView = "file" + + case errorMsg: + m.viewport.SetContent(fmt.Sprintf("Error: %s", msg.error)) + m.currentView = "error" } return m, tea.Batch(cmds...) @@ -630,37 +567,8 @@ func (m Model) createStatusBar() string { viewIndicator := fmt.Sprintf("View: %s", strings.ToUpper(m.currentView)) parts = append(parts, idleStyle.Render(viewIndicator)) - // Add running backups count - runningCount := len(m.runningBackups) - if runningCount > 0 { - parts = append(parts, runningStyle.Render(fmt.Sprintf("Running: %d", runningCount))) - } - - // Add service statuses - for service, status := range m.backupStatus { - var statusText string - switch status.Status { - case "running": - progressText := "" - if status.Progress > 0 { - progressText = fmt.Sprintf(" (%d%%)", status.Progress) - } - statusText = runningStyle.Render(fmt.Sprintf("%s: %s%s %s", service, status.Status, progressText, m.spinner.View())) - case "success": - statusText = successStyle.Render(fmt.Sprintf("%s: āœ“ %s", service, status.Duration)) - case "error": - statusText = errorStyle.Render(fmt.Sprintf("%s: āœ— failed", service)) - case "stopped": - statusText = idleStyle.Render(fmt.Sprintf("%s: šŸ›‘ stopped", service)) - default: - statusText = idleStyle.Render(fmt.Sprintf("%s: idle", service)) - } - parts = append(parts, statusText) - } - - if len(m.backupStatus) == 0 { - parts = append(parts, idleStyle.Render("No backups running")) - } + // Add file browser status + parts = append(parts, idleStyle.Render("Mode: Browse Only")) // Add navigation help if m.activePanel == 0 { @@ -696,82 +604,30 @@ func (m *Model) showLogs() { func (m *Model) showStatus() { var content strings.Builder - content.WriteString("=== BACKUP STATUS ===\n\n") - - if len(m.backupStatus) == 0 { - content.WriteString("No backup status available.\n\nRun some backups to see their status here.") - } else { - // Sort services alphabetically - var services []string - for service := range m.backupStatus { - services = append(services, service) - } - sort.Strings(services) - - for _, service := range services { - status := m.backupStatus[service] - content.WriteString(fmt.Sprintf("šŸ”§ Service: %s\n", strings.ToUpper(service))) - - // Status with emoji - statusIcon := "ā­•" - switch status.Status { - case "running": - statusIcon = "šŸ”„" - case "success": - statusIcon = "āœ…" - case "error": - statusIcon = "āŒ" - case "stopped": - statusIcon = "šŸ›‘" - } - content.WriteString(fmt.Sprintf(" Status: %s %s\n", statusIcon, status.Status)) - - if !status.LastRun.IsZero() { - content.WriteString(fmt.Sprintf(" Last Run: %s\n", status.LastRun.Format("2006-01-02 15:04:05"))) - } - - if status.Duration != "" { - content.WriteString(fmt.Sprintf(" Duration: %s\n", status.Duration)) - } - - if status.Progress > 0 && status.Status == "running" { - content.WriteString(fmt.Sprintf(" Progress: %d%%\n", status.Progress)) - } - - if status.BackupSize != "" { - content.WriteString(fmt.Sprintf(" Size: %s\n", status.BackupSize)) - } - - if status.FilesCount > 0 { - content.WriteString(fmt.Sprintf(" Files: %d\n", status.FilesCount)) - } - - if status.Error != "" { - content.WriteString(fmt.Sprintf(" Error: %s\n", status.Error[:min(100, len(status.Error))])) - } + content.WriteString("=== BACKUP DIRECTORY STATUS ===\n\n") + content.WriteString("šŸ“‚ Available Backup Directories:\n\n") + + for _, item := range m.items { + if item.itemType == "directory" { + content.WriteString(fmt.Sprintf("šŸ”§ Service: %s\n", strings.ToUpper(item.service))) + content.WriteString(fmt.Sprintf(" Path: %s\n", item.path)) + content.WriteString(" Type: Backup Directory\n") + content.WriteString(" Status: Available for browsing\n") content.WriteString("\n") } - - // Add summary - running := 0 - success := 0 - failed := 0 - for _, status := range m.backupStatus { - switch status.Status { - case "running": - running++ - case "success": - success++ - case "error": - failed++ - } - } - - content.WriteString("šŸ“Š SUMMARY:\n") - content.WriteString(fmt.Sprintf(" Running: %d | Success: %d | Failed: %d\n", running, success, failed)) } + content.WriteString("šŸ“Š SUMMARY:\n") + directoryCount := 0 + for _, item := range m.items { + if item.itemType == "directory" { + directoryCount++ + } + } + content.WriteString(fmt.Sprintf(" Available Directories: %d\n", directoryCount)) + content.WriteString(" Mode: Read-Only Browser\n") + m.viewport.SetContent(content.String()) } @@ -812,7 +668,6 @@ func (m *Model) showConfig() { // Performance Tips content.WriteString("šŸ’” TIPS:\n") - content.WriteString(" • Use 'r' to refresh current view\n") content.WriteString(" • Browse backup directories with Enter\n") content.WriteString(" • Check 's' for detailed status information\n") content.WriteString(" • Use 'x' to go back to main view\n") @@ -822,11 +677,13 @@ func (m *Model) showConfig() { // showItemDescription displays detailed information about the currently selected menu item func (m *Model) showItemDescription() { - if m.selectedIndex < 0 || m.selectedIndex >= len(m.items) { + // Use the list's actual selection index instead of a separate field + selectedIndex := m.list.Index() + if selectedIndex < 0 || selectedIndex >= len(m.items) { return } - item := m.items[m.selectedIndex] + item := m.items[selectedIndex] var content strings.Builder content.WriteString(fmt.Sprintf("šŸ“‹ %s\n", item.title)) @@ -943,9 +800,9 @@ func (m *Model) showItemDescription() { content.WriteString("\nšŸ’” Navigation:\n") content.WriteString("• Tab: Switch between panels\n") - content.WriteString("• R: Refresh current view\n") + content.WriteString("• r: Refresh current view\n") content.WriteString("• ?: Toggle help\n") - content.WriteString("• Q: Quit application\n") + content.WriteString("• q: Quit application\n") // Add path information content.WriteString(fmt.Sprintf("\nšŸ“ Path: %s\n", item.path)) @@ -953,7 +810,103 @@ func (m *Model) showItemDescription() { m.viewport.SetContent(content.String()) } +// showBackupDetails displays detailed information about backup files in a directory +func (m *Model) showBackupDetails(details BackupDetails) { + var content strings.Builder + content.WriteString(fmt.Sprintf("šŸ“ %s Backup Directory\n", details.Directory.Name)) + content.WriteString(strings.Repeat("=", len(details.Directory.Name)+20) + "\n\n") + + // Directory summary + content.WriteString("šŸ“Š Directory Summary:\n") + content.WriteString(fmt.Sprintf(" šŸ“ Path: %s\n", details.Directory.Path)) + content.WriteString(fmt.Sprintf(" šŸ“„ Files: %d\n", details.TotalFiles)) + content.WriteString(fmt.Sprintf(" šŸ’¾ Total Size: %s\n", formatSize(details.TotalSize))) + + if !details.NewestBackup.IsZero() { + content.WriteString(fmt.Sprintf(" šŸ•’ Newest: %s\n", details.NewestBackup.Format("2006-01-02 15:04:05"))) + } + if !details.OldestBackup.IsZero() { + content.WriteString(fmt.Sprintf(" šŸ“… Oldest: %s\n", details.OldestBackup.Format("2006-01-02 15:04:05"))) + } + content.WriteString("\n") + + // File listings + if len(details.Files) > 0 { + content.WriteString("šŸ“„ Backup Files:\n") + content.WriteString(" Name Size Modified\n") + content.WriteString(" " + strings.Repeat("-", 50) + "\n") + + for _, file := range details.Files { + // Truncate filename if too long + displayName := file.Name + if len(displayName) > 25 { + displayName = displayName[:22] + "..." + } + + compressionIndicator := "" + if file.IsCompressed { + compressionIndicator = "šŸ“¦ " + } + + content.WriteString(fmt.Sprintf(" %s%-25s %8s %s\n", + compressionIndicator, + displayName, + formatSize(file.Size), + file.ModifiedTime.Format("2006-01-02 15:04"))) + } + } else { + content.WriteString("šŸ“‚ No backup files found in this directory.\n") + } + + content.WriteString("\nšŸ’” Navigation:\n") + content.WriteString(" • x: Return to main view\n") + content.WriteString(" • r: Refresh directory listing\n") + content.WriteString(" • ?: Toggle help\n") + m.viewport.SetContent(content.String()) +} + +// showLogFiles displays log files for browsing +func (m *Model) showLogFiles(logFiles []LogFile) { + var content strings.Builder + content.WriteString("šŸ“‹ Backup Operation Logs\n") + content.WriteString("=========================\n\n") + + if len(logFiles) == 0 { + content.WriteString("šŸ“‚ No log files found.\n") + content.WriteString("Log files are typically stored in /mnt/share/media/backups/logs/\n") + } else { + content.WriteString("šŸ“„ Available Log Files:\n") + content.WriteString(" Filename Size Date\n") + content.WriteString(" " + strings.Repeat("-", 45) + "\n") + + for _, logFile := range logFiles { + displayName := logFile.Name + if len(displayName) > 25 { + displayName = displayName[:22] + "..." + } + + dateStr := "Unknown" + if !logFile.Date.IsZero() { + dateStr = logFile.Date.Format("2006-01-02 15:04") + } + + content.WriteString(fmt.Sprintf(" %-25s %8s %s\n", + displayName, + formatSize(logFile.Size), + dateStr)) + } + + content.WriteString(fmt.Sprintf("\nšŸ“Š Total: %d log files\n", len(logFiles))) + } + + content.WriteString("\nšŸ’” Navigation:\n") + content.WriteString(" • Enter: View log content (when a specific log is selected)\n") + content.WriteString(" • x: Return to main view\n") + content.WriteString(" • r: Refresh log listing\n") + + m.viewport.SetContent(content.String()) +} // formatSize converts bytes to human readable format func formatSize(bytes int64) string { @@ -1097,21 +1050,11 @@ func (m *Model) loadLogFiles() tea.Cmd { } } -// loadFileContent loads content of a specific file -func (m *Model) loadFileContent(filePath string) tea.Cmd { - return func() tea.Msg { - content, err := os.ReadFile(filePath) - if err != nil { - return errorMsg{error: fmt.Sprintf("Failed to read file %s: %v", filePath, err)} - } - - // Limit content size for display - maxSize := 50000 // 50KB limit for display - if len(content) > maxSize { - content = content[:maxSize] - content = append(content, []byte("\n\n... (file truncated for display)")...) - } - - return fileContentLoadedMsg{content: string(content)} +// main function to initialize and run the TUI application +func main() { + p := tea.NewProgram(initialModel(), tea.WithAltScreen()) + if _, err := p.Run(); err != nil { + fmt.Printf("Alas, there's been an error: %v", err) + os.Exit(1) } } diff --git a/tui/test-enhanced-features.sh b/tui/test-enhanced-features.sh deleted file mode 100755 index 1ca3cc1..0000000 --- a/tui/test-enhanced-features.sh +++ /dev/null @@ -1,147 +0,0 @@ -#!/bin/bash - -# Test script for Enhanced Backup TUI Features -# This script validates all the enhanced functionality - -set -e - -YELLOW='\033[1;33m' -GREEN='\033[0;32m' -RED='\033[0;31m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -echo -e "${BLUE}=== Enhanced Backup TUI Feature Validation ===${NC}" -echo "" - -# Test 1: Check if the TUI compiles and runs -echo -e "${YELLOW}Test 1: Compilation and Basic Startup${NC}" -cd /home/acedanger/shell/tui - -if go build -o backup-manager-test main.go; then - echo -e "${GREEN}āœ“ TUI compiles successfully${NC}" -else - echo -e "${RED}āœ— TUI compilation failed${NC}" - exit 1 -fi - -# Test 2: Check if the TUI starts without errors -echo -e "${YELLOW}Test 2: Basic TUI Startup${NC}" -timeout 3s ./backup-manager-test > /dev/null 2>&1 & -TUI_PID=$! -sleep 1 - -if kill -0 $TUI_PID 2>/dev/null; then - echo -e "${GREEN}āœ“ TUI starts successfully${NC}" - kill $TUI_PID 2>/dev/null || true -else - echo -e "${RED}āœ— TUI failed to start${NC}" -fi - -# Test 3: Check for enhanced features in the code -echo -e "${YELLOW}Test 3: Enhanced Feature Code Validation${NC}" - -# Check for progress tracking -if grep -q "backupProgressMsg" main.go; then - echo -e "${GREEN}āœ“ Real-time progress tracking implemented${NC}" -else - echo -e "${RED}āœ— Progress tracking not found${NC}" -fi - -# Check for context cancellation -if grep -q "context.WithCancel" main.go; then - echo -e "${GREEN}āœ“ Context cancellation implemented${NC}" -else - echo -e "${RED}āœ— Context cancellation not found${NC}" -fi - -# Check for process management -if grep -q "BackupStatus" main.go; then - echo -e "${GREEN}āœ“ Enhanced process management implemented${NC}" -else - echo -e "${RED}āœ— Process management not found${NC}" -fi - -# Check for output streaming -if grep -q "streamBackupOutput" main.go; then - echo -e "${GREEN}āœ“ Output streaming implemented${NC}" -else - echo -e "${RED}āœ— Output streaming not found${NC}" -fi - -# Check for error handling -if grep -q "analyzeError" main.go; then - echo -e "${GREEN}āœ“ Intelligent error analysis implemented${NC}" -else - echo -e "${RED}āœ— Error analysis not found${NC}" -fi - -# Test 4: Check for key bindings and UI enhancements -echo -e "${YELLOW}Test 4: UI Enhancement Validation${NC}" - -if grep -q "tea.KeyCancel" main.go; then - echo -e "${GREEN}āœ“ Cancel key binding implemented${NC}" -else - echo -e "${RED}āœ— Cancel key binding not found${NC}" -fi - -if grep -q "viewport" main.go; then - echo -e "${GREEN}āœ“ Enhanced viewport system implemented${NC}" -else - echo -e "${RED}āœ— Viewport system not found${NC}" -fi - -# Test 5: Validate backup script integration -echo -e "${YELLOW}Test 5: Backup Script Integration${NC}" - -# Check if backup scripts exist -BACKUP_SCRIPTS=( - "/home/acedanger/shell/plex/backup-plex.sh" - "/home/acedanger/shell/immich/backup-immich.sh" - "/home/acedanger/shell/backup-media.sh" -) - -for script in "${BACKUP_SCRIPTS[@]}"; do - if [[ -f "$script" ]]; then - echo -e "${GREEN}āœ“ Backup script found: $(basename "$script")${NC}" - else - echo -e "${RED}āœ— Backup script missing: $(basename "$script")${NC}" - fi -done - -# Test 6: Check dependencies -echo -e "${YELLOW}Test 6: Dependency Validation${NC}" - -if go mod verify > /dev/null 2>&1; then - echo -e "${GREEN}āœ“ Go module dependencies verified${NC}" -else - echo -e "${RED}āœ— Go module dependency issues${NC}" -fi - -# Test 7: Memory and performance validation -echo -e "${YELLOW}Test 7: Performance Validation${NC}" - -# Check for potential memory leaks or inefficiencies -if grep -q "sync.Mutex" main.go; then - echo -e "${GREEN}āœ“ Thread-safe operations implemented${NC}" -else - echo -e "${YELLOW}⚠ Thread safety not explicitly implemented${NC}" -fi - -# Test 8: Documentation validation -echo -e "${YELLOW}Test 8: Documentation Validation${NC}" - -if [[ -f "README.md" ]]; then - echo -e "${GREEN}āœ“ README.md exists${NC}" -else - echo -e "${RED}āœ— README.md missing${NC}" -fi - -echo "" -echo -e "${BLUE}=== Feature Validation Complete ===${NC}" -echo -e "${GREEN}Enhanced Backup TUI is ready for production use!${NC}" - -# Cleanup -rm -f backup-manager-test - -exit 0