""" All Backups Dialog - Shows complete backup history with statistics """ from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QTableWidget, QTableWidgetItem, QLabel, QMessageBox, QGroupBox, QHeaderView, QAbstractItemView ) from PyQt6.QtCore import Qt from PyQt6.QtGui import QFont, QColor from loguru import logger from datetime import datetime from typing import Optional class AllBackupsDialog(QDialog): """Dialog showing all backup operations with statistics""" def __init__(self, parent, db): super().__init__(parent) self.db = db self.parent_widget = parent self.selected_operation = None self.setWindowTitle("All Backup Operations") self.resize(1000, 700) self._setup_ui() self._load_all_backups() def _setup_ui(self): """Setup dialog UI""" layout = QVBoxLayout(self) layout.setContentsMargins(15, 15, 15, 15) layout.setSpacing(15) # Header with statistics stats_group = QGroupBox("Backup Statistics") stats_layout = QVBoxLayout(stats_group) self.stats_label = QLabel("Loading statistics...") stats_font = QFont() stats_font.setPointSize(10) self.stats_label.setFont(stats_font) stats_layout.addWidget(self.stats_label) layout.addWidget(stats_group) # All backups table table_group = QGroupBox("All Backup Operations") table_layout = QVBoxLayout(table_group) self.backups_table = QTableWidget() self.backups_table.setColumnCount(8) self.backups_table.setHorizontalHeaderLabels([ "ID", "Profile", "Backup Name", "Type", "Status", "Start Time", "Duration", "Files" ]) self.backups_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive) self.backups_table.horizontalHeader().setStretchLastSection(True) self.backups_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.backups_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) self.backups_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) self.backups_table.itemSelectionChanged.connect(self._on_backup_selected) self.backups_table.doubleClicked.connect(self._restore_selected_backup) # Set column widths self.backups_table.setColumnWidth(0, 50) self.backups_table.setColumnWidth(1, 150) self.backups_table.setColumnWidth(2, 200) self.backups_table.setColumnWidth(3, 80) self.backups_table.setColumnWidth(4, 80) self.backups_table.setColumnWidth(5, 150) self.backups_table.setColumnWidth(6, 100) self.backups_table.setColumnWidth(7, 120) table_layout.addWidget(self.backups_table) layout.addWidget(table_group) # Buttons button_layout = QHBoxLayout() info_label = QLabel("💡 Tip: Double-click a backup to restore it") button_layout.addWidget(info_label) button_layout.addStretch() self.restore_btn = QPushButton("🔄 Restore Selected") self.restore_btn.clicked.connect(self._restore_selected_backup) self.restore_btn.setEnabled(False) button_layout.addWidget(self.restore_btn) close_btn = QPushButton("Close") close_btn.clicked.connect(self.accept) button_layout.addWidget(close_btn) layout.addLayout(button_layout) def _load_all_backups(self): """Load all backup operations and statistics""" try: # Get ALL backup operations (no limit) operations = self.db.get_recent_backup_operations(limit=999999) # Calculate statistics total_backups = len(operations) completed_backups = sum(1 for op in operations if op['status'] == 'completed') failed_backups = sum(1 for op in operations if op['status'] == 'failed') running_backups = sum(1 for op in operations if op['status'] == 'running') # Find oldest and newest if operations: oldest = min(operations, key=lambda x: x['start_time'])['start_time'] newest = max(operations, key=lambda x: x['start_time'])['start_time'] try: oldest_dt = datetime.fromisoformat(oldest) newest_dt = datetime.fromisoformat(newest) oldest_str = oldest_dt.strftime('%Y-%m-%d %H:%M') newest_str = newest_dt.strftime('%Y-%m-%d %H:%M') time_span_days = (newest_dt - oldest_dt).days except: oldest_str = oldest[:16] if len(oldest) > 16 else oldest newest_str = newest[:16] if len(newest) > 16 else newest time_span_days = "N/A" else: oldest_str = "N/A" newest_str = "N/A" time_span_days = 0 # Update statistics label stats_text = ( f"📊 Total Backups: {total_backups} | " f"✅ Completed: {completed_backups} | " f"❌ Failed: {failed_backups} | " f"⏳ Running: {running_backups}\n" f"📅 Date Range: {oldest_str} → {newest_str}" ) if isinstance(time_span_days, int): stats_text += f" ({time_span_days} days)" self.stats_label.setText(stats_text) # Load table self.backups_table.setRowCount(len(operations)) for row, op in enumerate(operations): # ID id_item = QTableWidgetItem(str(op['id'])) id_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) self.backups_table.setItem(row, 0, id_item) # Profile name profile = self.db.get_backup_profile(op['profile_id']) profile_name = profile['name'] if profile else f"Profile {op['profile_id']}" profile_item = QTableWidgetItem(profile_name) self.backups_table.setItem(row, 1, profile_item) # Backup name backup_name = op.get('backup_name', 'N/A') backup_item = QTableWidgetItem(backup_name) self.backups_table.setItem(row, 2, backup_item) # Type type_item = QTableWidgetItem(op['operation_type']) self.backups_table.setItem(row, 3, type_item) # Status status_text = op['status'] status_item = QTableWidgetItem(status_text) status_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) # Color code status if op['status'] == 'completed': status_item.setForeground(QColor('green')) elif op['status'] == 'failed': status_item.setForeground(QColor('red')) elif op['status'] == 'running': status_item.setForeground(QColor('orange')) self.backups_table.setItem(row, 4, status_item) # Start time start_time = op['start_time'] if start_time: try: dt = datetime.fromisoformat(start_time) time_str = dt.strftime('%Y-%m-%d %H:%M:%S') except: time_str = start_time[:19] if len(start_time) > 19 else start_time else: time_str = "N/A" time_item = QTableWidgetItem(time_str) self.backups_table.setItem(row, 5, time_item) # Duration if op.get('end_time') and op.get('start_time'): try: start = datetime.fromisoformat(op['start_time']) end = datetime.fromisoformat(op['end_time']) duration = (end - start).total_seconds() # Format duration nicely if duration < 60: duration_str = f"{duration:.1f}s" elif duration < 3600: duration_str = f"{duration/60:.1f}m" else: duration_str = f"{duration/3600:.1f}h" except: duration_str = "N/A" else: duration_str = "Running..." if op['status'] == 'running' else "N/A" duration_item = QTableWidgetItem(duration_str) duration_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) self.backups_table.setItem(row, 6, duration_item) # Files files_total = op.get('files_total', 0) files_linked = op.get('files_linked', 0) files_str = f"{files_total} ({files_linked} links)" if files_total > 0 else "N/A" files_item = QTableWidgetItem(files_str) files_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) self.backups_table.setItem(row, 7, files_item) except Exception as e: logger.error(f"Error loading all backups: {e}") QMessageBox.critical(self, "Error", f"Failed to load backups: {e}") def _on_backup_selected(self): """Handle backup selection""" selected = self.backups_table.selectedItems() if not selected: self.restore_btn.setEnabled(False) self.selected_operation = None return row = selected[0].row() op_id = int(self.backups_table.item(row, 0).text()) status = self.backups_table.item(row, 4).text() operation_type = self.backups_table.item(row, 3).text() # Enable restore only for completed backup operations can_restore = (status == 'completed' and operation_type == 'backup') self.restore_btn.setEnabled(can_restore) if can_restore: # Store selected operation try: operations = self.db.get_recent_backup_operations(limit=999999) for op in operations: if op['id'] == op_id: self.selected_operation = op break except Exception as e: logger.error(f"Error getting operation details: {e}") self.selected_operation = None def _restore_selected_backup(self): """Restore the selected backup""" if not self.selected_operation: QMessageBox.warning(self, "No Selection", "Please select a completed backup to restore") return backup_name = self.selected_operation.get('backup_name') if not backup_name: QMessageBox.warning(self, "Error", "Selected operation has no backup name") return # Get profile to find backup_root profile = self.db.get_backup_profile(self.selected_operation['profile_id']) if not profile: QMessageBox.critical(self, "Error", f"Profile {self.selected_operation['profile_id']} not found") return try: from gui.backup_restore_dialog import BackupRestoreDialog # Open selective restore dialog dialog = BackupRestoreDialog(self, profile['backup_root'], backup_name) dialog.exec() except Exception as e: logger.error(f"Error opening restore dialog: {e}") QMessageBox.critical( self, "Restore Failed", f"Failed to open restore dialog:\n{str(e)}" )