package screens import ( "fmt" "strings" "github.com/calmcacil/wg-admin/internal/backup" "github.com/calmcacil/wg-admin/internal/tui/components" "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) // RestoreScreen displays a list of available backups for restoration type RestoreScreen struct { table table.Model backups []backup.Backup selectedBackup *backup.Backup confirmModal *components.ConfirmModel showConfirm bool isRestoring bool restoreError error restoreSuccess bool message string } // Styles var ( restoreTitleStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("62")). Bold(true) restoreHelpStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("63")). MarginTop(1) restoreSuccessStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("46")). Bold(true) restoreErrorStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("196")). Bold(true) restoreInfoStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("241")). MarginTop(1) ) // NewRestoreScreen creates a new restore screen func NewRestoreScreen() *RestoreScreen { return &RestoreScreen{ showConfirm: false, } } // Init initializes the restore screen func (s *RestoreScreen) Init() tea.Cmd { return s.loadBackups } // Update handles messages for the restore screen func (s *RestoreScreen) Update(msg tea.Msg) (Screen, tea.Cmd) { var cmd tea.Cmd // Handle confirmation modal if s.showConfirm && s.confirmModal != nil { _, cmd = s.confirmModal.Update(msg) // Handle confirmation result if !s.confirmModal.Visible { if s.confirmModal.IsConfirmed() && s.selectedBackup != nil { // User confirmed restore s.isRestoring = true s.showConfirm = false return s, s.performRestore() } // User cancelled - close modal s.showConfirm = false return s, nil } // Handle Enter key to confirm switch msg := msg.(type) { case tea.KeyMsg: if msg.String() == "enter" && s.confirmModal.IsConfirmed() && s.selectedBackup != nil { s.isRestoring = true s.showConfirm = false return s, s.performRestore() } } return s, cmd } // Handle normal screen messages switch msg := msg.(type) { case backupsLoadedMsg: s.backups = msg.backups s.buildTable() case tea.KeyMsg: switch msg.String() { case "q", "esc": // Return to list screen - signal parent to switch screens return nil, nil case "enter": // Show confirmation for selected backup if len(s.table.Rows()) > 0 { selected := s.table.SelectedRow() if len(selected) > 0 { // Find the backup by name for _, b := range s.backups { if b.Name == selected[0] { s.selectedBackup = &b s.confirmModal = components.NewConfirm( fmt.Sprintf( "Are you sure you want to restore from backup '%s'?\n\nOperation: %s\nDate: %s\n\nThis will replace current WireGuard configuration.\nA safety backup will be created first.", b.Name, b.Operation, b.Timestamp.Format("2006-01-02 15:04:05"), ), 80, 24, ) s.showConfirm = true break } } } } } case restoreCompletedMsg: s.isRestoring = false if msg.err != nil { s.restoreError = msg.err s.message = fmt.Sprintf("Restore failed: %v", msg.err) } else { s.restoreSuccess = true s.message = fmt.Sprintf("Restore successful! Safety backup created at: %s", msg.safetyBackupPath) } } if !s.showConfirm && s.confirmModal != nil { s.table, cmd = s.table.Update(msg) } return s, cmd } // View renders the restore screen func (s *RestoreScreen) View() string { if s.showConfirm && s.confirmModal != nil { // Render underlying content dimmed content := s.renderContent() dimmedContent := lipgloss.NewStyle(). Foreground(lipgloss.Color("244")). Render(content) // Overlay confirmation modal return lipgloss.JoinVertical( lipgloss.Left, dimmedContent, s.confirmModal.View(), ) } return s.renderContent() } // renderContent renders the main restore screen content func (s *RestoreScreen) renderContent() string { var content strings.Builder content.WriteString(restoreTitleStyle.Render("Restore WireGuard Configuration")) content.WriteString("\n\n") if len(s.backups) == 0 && !s.isRestoring && s.message == "" { content.WriteString("No backups found. Press 'q' to return.") return content.String() } if s.isRestoring { content.WriteString("Restoring from backup, please wait...") return content.String() } if s.restoreSuccess { content.WriteString(restoreSuccessStyle.Render("✓ " + s.message)) content.WriteString("\n\n") content.WriteString(restoreInfoStyle.Render("Press 'q' to return to client list.")) return content.String() } if s.restoreError != nil { content.WriteString(restoreErrorStyle.Render("✗ " + s.message)) content.WriteString("\n\n") content.WriteString(s.table.View()) content.WriteString("\n\n") content.WriteString(restoreHelpStyle.Render("Actions: [Enter] Restore Selected • [↑/↓] Navigate • [q] Back")) return content.String() } // Show backup list content.WriteString(s.table.View()) content.WriteString("\n\n") // Show selected backup details if len(s.table.Rows()) > 0 && s.selectedBackup != nil { content.WriteString(restoreInfoStyle.Render( fmt.Sprintf( "Selected: %s (%s) - %s\nSize: %s", s.selectedBackup.Operation, s.selectedBackup.Timestamp.Format("2006-01-02 15:04:05"), s.selectedBackup.Name, formatBytes(s.selectedBackup.Size), ), )) content.WriteString("\n") } content.WriteString(restoreHelpStyle.Render("Actions: [Enter] Restore Selected • [↑/↓] Navigate • [q] Back")) return content.String() } // loadBackups loads the list of available backups func (s *RestoreScreen) loadBackups() tea.Msg { backups, err := backup.ListBackups() if err != nil { return errMsg{err: err} } return backupsLoadedMsg{backups: backups} } // buildTable creates and configures the backup list table func (s *RestoreScreen) buildTable() { columns := []table.Column{ {Title: "Name", Width: 40}, {Title: "Operation", Width: 15}, {Title: "Date", Width: 20}, {Title: "Size", Width: 12}, } var rows []table.Row for _, b := range s.backups { row := table.Row{ b.Name, b.Operation, b.Timestamp.Format("2006-01-02 15:04"), formatBytes(b.Size), } rows = append(rows, row) } s.table = table.New( table.WithColumns(columns), table.WithRows(rows), table.WithFocused(true), table.WithHeight(len(rows)+2), // Header + rows ) // Apply styles s.setTableStyles() } // setTableStyles applies styling to the table func (s *RestoreScreen) setTableStyles() { styles := table.DefaultStyles() styles.Header = styles.Header. BorderStyle(lipgloss.NormalBorder()). BorderForeground(lipgloss.Color("240")). BorderBottom(true). Bold(true) styles.Selected = styles.Selected. Foreground(lipgloss.Color("229")). Background(lipgloss.Color("57")). Bold(false) s.table.SetStyles(styles) } // performRestore performs the restore operation func (s *RestoreScreen) performRestore() tea.Cmd { return func() tea.Msg { if s.selectedBackup == nil { return restoreCompletedMsg{ err: fmt.Errorf("no backup selected"), } } // Get safety backup path from backup.BackupConfig safetyBackupPath, err := backup.BackupConfig(fmt.Sprintf("pre-restore-from-%s", s.selectedBackup.Name)) if err != nil { return restoreCompletedMsg{ err: fmt.Errorf("failed to create safety backup: %w", err), } } // Perform restore if err := backup.RestoreBackup(s.selectedBackup.Name); err != nil { return restoreCompletedMsg{ err: err, safetyBackupPath: safetyBackupPath, } } // Restore succeeded - trigger client list refresh return restoreCompletedMsg{ safetyBackupPath: safetyBackupPath, } } } // formatBytes formats a byte count into human-readable format func formatBytes(bytes int64) string { const unit = 1024 if bytes < unit { return fmt.Sprintf("%d B", bytes) } div, exp := int64(unit), 0 for n := bytes / unit; n >= unit; n /= unit { div *= unit exp++ } return fmt.Sprintf("%.1f %ciB", float64(bytes)/float64(div), "KMGTPE"[exp]) } // Messages // backupsLoadedMsg is sent when backups are loaded type backupsLoadedMsg struct { backups []backup.Backup } // restoreCompletedMsg is sent when a restore operation completes type restoreCompletedMsg struct { err error safetyBackupPath string }