""" Versions Widget for GUI - Manages project versioning """ from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QTableWidget, QTableWidgetItem, QLabel, QMessageBox, QGroupBox, QLineEdit, QHeaderView, QAbstractItemView, QFileDialog, QProgressDialog, QApplication ) from PyQt6.QtCore import Qt, pyqtSignal, QThread, pyqtSignal as Signal from PyQt6.QtGui import QFont, QColor from loguru import logger from datetime import datetime from pathlib import Path from core.database import Database from core.config import SchedulerConfig from core.version_manager import VersionManager class VersionWorker(QThread): """Worker thread for version operations with progress tracking""" # Signals progress_updated = Signal(str, int, int, str) # phase, current, total, filename operation_finished = Signal(dict) # result dictionary operation_failed = Signal(str) # error message def __init__(self, operation_type, version_manager, **kwargs): super().__init__() self.operation_type = operation_type # 'upload', 'download', 'restore' self.version_manager = version_manager self.kwargs = kwargs def run(self): """Execute the operation in background thread""" try: # Progress callback def progress_callback(phase, current, total, filename): self.progress_updated.emit(phase, current, total, filename) # Add progress callback to kwargs self.kwargs['progress_callback'] = progress_callback # Execute the appropriate operation if self.operation_type == 'upload': result = self.version_manager.upload_version(**self.kwargs) elif self.operation_type == 'download': result = self.version_manager.download_version(**self.kwargs) elif self.operation_type == 'restore': result = self.version_manager.restore_version(**self.kwargs) else: raise ValueError(f"Unknown operation type: {self.operation_type}") self.operation_finished.emit(result) except Exception as e: logger.error(f"Worker error in {self.operation_type}: {e}") self.operation_failed.emit(str(e)) class VersionsWidget(QWidget): """Widget for managing project versions""" project_changed = pyqtSignal() def __init__(self, db: Database, config: SchedulerConfig): super().__init__() self.db = db self.config = config self.version_manager = VersionManager(db) self.selected_project_id = None self._setup_ui() self.refresh() def _setup_ui(self): """Setup versions UI components""" layout = QVBoxLayout(self) layout.setContentsMargins(15, 15, 15, 15) layout.setSpacing(15) # Header header_layout = QHBoxLayout() title_label = QLabel("📦 Project Versioning") title_font = QFont() title_font.setPointSize(14) title_font.setBold(True) title_label.setFont(title_font) header_layout.addWidget(title_label) header_layout.addStretch() # Project action buttons self.new_project_btn = QPushButton("➕ New Project") self.new_project_btn.clicked.connect(self._create_project) header_layout.addWidget(self.new_project_btn) self.edit_project_btn = QPushButton("✏ Edit Project") self.edit_project_btn.clicked.connect(self._edit_project) self.edit_project_btn.setEnabled(False) header_layout.addWidget(self.edit_project_btn) self.delete_project_btn = QPushButton("🗑 Delete Project") self.delete_project_btn.clicked.connect(self._delete_project) self.delete_project_btn.setEnabled(False) header_layout.addWidget(self.delete_project_btn) self.refresh_btn = QPushButton("🔄 Refresh") self.refresh_btn.clicked.connect(self.refresh) header_layout.addWidget(self.refresh_btn) layout.addLayout(header_layout) # Projects table projects_group = QGroupBox("Projects") projects_layout = QVBoxLayout(projects_group) self.projects_table = QTableWidget() self.projects_table.setColumnCount(5) self.projects_table.setHorizontalHeaderLabels([ "ID", "Name", "Source Path", "Server Base Path", "Enabled" ]) self.projects_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive) self.projects_table.horizontalHeader().setStretchLastSection(True) self.projects_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.projects_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) self.projects_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) self.projects_table.itemSelectionChanged.connect(self._on_project_selected) self.projects_table.doubleClicked.connect(self._edit_project) # Set column widths self.projects_table.setColumnWidth(0, 50) self.projects_table.setColumnWidth(1, 150) self.projects_table.setColumnWidth(2, 250) self.projects_table.setColumnWidth(3, 250) self.projects_table.setColumnWidth(4, 80) projects_layout.addWidget(self.projects_table) layout.addWidget(projects_group, stretch=1) # Upload version section self.upload_group = QGroupBox("Upload New Version") upload_layout = QVBoxLayout(self.upload_group) form_layout = QHBoxLayout() form_layout.addWidget(QLabel("Version Name:")) self.version_name_edit = QLineEdit() self.version_name_edit.setPlaceholderText("e.g., v1.2.0") self.version_name_edit.setMaximumWidth(150) form_layout.addWidget(self.version_name_edit) form_layout.addWidget(QLabel("Description:")) self.version_desc_edit = QLineEdit() self.version_desc_edit.setPlaceholderText("Optional description of changes") form_layout.addWidget(self.version_desc_edit) self.upload_btn = QPushButton("⬆ Upload & Create Version") self.upload_btn.clicked.connect(self._upload_version) self.upload_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold; padding: 8px;") form_layout.addWidget(self.upload_btn) upload_layout.addLayout(form_layout) self.upload_group.setVisible(False) # Hidden until project selected layout.addWidget(self.upload_group) # Versions history table versions_group = QGroupBox("Version History") versions_layout = QVBoxLayout(versions_group) # Version action buttons version_actions_layout = QHBoxLayout() version_actions_layout.addStretch() self.download_btn = QPushButton("⬇ Download") self.download_btn.clicked.connect(self._download_version) self.download_btn.setEnabled(False) version_actions_layout.addWidget(self.download_btn) self.restore_btn = QPushButton("↩ Restore") self.restore_btn.clicked.connect(self._restore_version) self.restore_btn.setEnabled(False) self.restore_btn.setStyleSheet("background-color: #FF9800; color: white; font-weight: bold;") version_actions_layout.addWidget(self.restore_btn) self.delete_version_btn = QPushButton("🗑 Delete") self.delete_version_btn.clicked.connect(self._delete_version) self.delete_version_btn.setEnabled(False) version_actions_layout.addWidget(self.delete_version_btn) versions_layout.addLayout(version_actions_layout) self.versions_table = QTableWidget() self.versions_table.setColumnCount(6) self.versions_table.setHorizontalHeaderLabels([ "ID", "Version", "Description", "Created", "Files", "Size" ]) self.versions_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive) self.versions_table.horizontalHeader().setStretchLastSection(True) self.versions_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.versions_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) self.versions_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) self.versions_table.itemSelectionChanged.connect(self._on_version_selected) # Set column widths self.versions_table.setColumnWidth(0, 50) self.versions_table.setColumnWidth(1, 120) self.versions_table.setColumnWidth(2, 300) self.versions_table.setColumnWidth(3, 150) self.versions_table.setColumnWidth(4, 80) self.versions_table.setColumnWidth(5, 100) versions_layout.addWidget(self.versions_table) layout.addWidget(versions_group, stretch=2) def refresh(self): """Refresh all data""" self._load_projects() if self.selected_project_id: self._load_versions() def _load_projects(self): """Load projects from database""" try: projects = self.db.get_all_project_configs() self.projects_table.setRowCount(len(projects)) for row, project in enumerate(projects): # ID id_item = QTableWidgetItem(str(project['id'])) id_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) self.projects_table.setItem(row, 0, id_item) # Name name_item = QTableWidgetItem(project['name']) self.projects_table.setItem(row, 1, name_item) # Source source_item = QTableWidgetItem(project['source_path']) self.projects_table.setItem(row, 2, source_item) # Server server_item = QTableWidgetItem(project['server_base_path']) self.projects_table.setItem(row, 3, server_item) # Enabled enabled_text = "✓ Yes" if project['enabled'] else "✗ No" enabled_item = QTableWidgetItem(enabled_text) enabled_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) if project['enabled']: enabled_item.setForeground(QColor('green')) else: enabled_item.setForeground(QColor('red')) self.projects_table.setItem(row, 4, enabled_item) except Exception as e: logger.error(f"Error loading projects: {e}") QMessageBox.critical(self, "Error", f"Failed to load projects: {e}") def _load_versions(self): """Load versions for selected project""" if not self.selected_project_id: self.versions_table.setRowCount(0) return try: versions = self.db.get_project_versions(self.selected_project_id) self.versions_table.setRowCount(len(versions)) for row, version in enumerate(versions): # ID id_item = QTableWidgetItem(str(version['id'])) id_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) self.versions_table.setItem(row, 0, id_item) # Version name version_item = QTableWidgetItem(version['version_name']) self.versions_table.setItem(row, 1, version_item) # Description desc_item = QTableWidgetItem(version['description'] or '') self.versions_table.setItem(row, 2, desc_item) # Created created_time = version['created_at'] if created_time: try: dt = datetime.fromisoformat(created_time) time_str = dt.strftime('%Y-%m-%d %H:%M:%S') except: time_str = created_time[:19] if len(created_time) > 19 else created_time else: time_str = "N/A" created_item = QTableWidgetItem(time_str) self.versions_table.setItem(row, 3, created_item) # Files count files_item = QTableWidgetItem(str(version['files_count'])) files_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) self.versions_table.setItem(row, 4, files_item) # Size (MB) size_mb = version['size_bytes'] / (1024 * 1024) size_item = QTableWidgetItem(f"{size_mb:.2f} MB") size_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) self.versions_table.setItem(row, 5, size_item) except Exception as e: logger.error(f"Error loading versions: {e}") QMessageBox.critical(self, "Error", f"Failed to load versions: {e}") def _on_project_selected(self): """Handle project selection""" selected_rows = self.projects_table.selectedItems() has_selection = len(selected_rows) > 0 self.edit_project_btn.setEnabled(has_selection) self.delete_project_btn.setEnabled(has_selection) if has_selection: row = selected_rows[0].row() self.selected_project_id = int(self.projects_table.item(row, 0).text()) self.upload_group.setVisible(True) self._load_versions() else: self.selected_project_id = None self.upload_group.setVisible(False) self.versions_table.setRowCount(0) def _on_version_selected(self): """Handle version selection""" has_selection = len(self.versions_table.selectedItems()) > 0 self.download_btn.setEnabled(has_selection) self.restore_btn.setEnabled(has_selection) self.delete_version_btn.setEnabled(has_selection) def _get_selected_version_id(self): """Get selected version ID""" selected = self.versions_table.selectedItems() if not selected: return None row = selected[0].row() return int(self.versions_table.item(row, 0).text()) def _create_project(self): """Create new project configuration""" from gui.project_config_dialog import ProjectConfigDialog from PyQt6.QtWidgets import QDialog dialog = ProjectConfigDialog(self, self.db) if dialog.exec() == QDialog.DialogCode.Accepted: logger.info("New project config created") self.refresh() self.project_changed.emit() def _edit_project(self): """Edit selected project configuration""" from gui.project_config_dialog import ProjectConfigDialog from PyQt6.QtWidgets import QDialog if not self.selected_project_id: QMessageBox.warning(self, "No Selection", "Please select a project to edit") return dialog = ProjectConfigDialog(self, self.db, self.selected_project_id) if dialog.exec() == QDialog.DialogCode.Accepted: logger.info(f"Project config {self.selected_project_id} updated") self.refresh() self.project_changed.emit() def _delete_project(self): """Delete selected project configuration""" if not self.selected_project_id: return project = self.db.get_project_config(self.selected_project_id) if not project: return versions = self.db.get_project_versions(self.selected_project_id) version_count = len(versions) reply = QMessageBox.question( self, "Delete Project", f"Are you sure you want to delete project '{project['name']}'?\n\n" f"This will also delete {version_count} version(s).\n" f"Note: ZIP files on server will NOT be deleted automatically.", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) if reply == QMessageBox.StandardButton.Yes: try: self.db.delete_project_config(self.selected_project_id) logger.info(f"Deleted project config {self.selected_project_id}") self.selected_project_id = None self.refresh() self.project_changed.emit() except Exception as e: logger.error(f"Error deleting project: {e}") QMessageBox.critical(self, "Error", f"Failed to delete project: {e}") def _upload_version(self): """Upload new version using background thread""" if not self.selected_project_id: return version_name = self.version_name_edit.text().strip() description = self.version_desc_edit.text().strip() if not version_name: QMessageBox.warning(self, "Validation Error", "Version name is required") self.version_name_edit.setFocus() return project = self.db.get_project_config(self.selected_project_id) if not project: return reply = QMessageBox.question( self, "Upload Version", f"Upload new version '{version_name}' for project '{project['name']}'?\n\n" f"This will:\n" f"1. Create ZIP snapshot from source (~250 MB with Python embedded)\n" f"2. Extract to server active location\n" f"3. Save version metadata\n\n" f"Continue?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) if reply != QMessageBox.StandardButton.Yes: return # Create progress dialog with determinate progress self.upload_progress = QProgressDialog( "Initializing...", None, # No cancel button 0, 100, self ) self.upload_progress.setWindowTitle("Uploading Version") self.upload_progress.setWindowModality(Qt.WindowModality.WindowModal) self.upload_progress.setMinimumDuration(0) self.upload_progress.setAutoClose(False) self.upload_progress.setAutoReset(False) self.upload_progress.show() # Create and start worker thread self.upload_worker = VersionWorker( 'upload', self.version_manager, project_id=self.selected_project_id, version_name=version_name, description=description ) # Connect signals self.upload_worker.progress_updated.connect(self._on_upload_progress) self.upload_worker.operation_finished.connect(self._on_upload_finished) self.upload_worker.operation_failed.connect(self._on_upload_failed) # Store for later self._upload_version_name = version_name self._upload_project_name = project['name'] # Start the operation logger.info(f"Starting upload worker for version '{version_name}'") self.upload_worker.start() def _on_upload_progress(self, phase, current, total, filename): """Handle progress updates from upload worker""" if not hasattr(self, 'upload_progress'): return # Update label based on phase if phase == 'copy': phase_text = "Copying files to server (Robocopy)" icon = "📋" # Handle indeterminate progress during Robocopy (total=0) if total == 0: # Indeterminate mode - show busy indicator self.upload_progress.setRange(0, 0) # Indeterminate mode label_text = ( f"{icon} {phase_text}...\n\n" f"Multi-threaded copy in progress\n" f"Please wait..." ) else: # Normal progress mode (after Robocopy, during cleanup) percentage = int((current / total) * 100) if total > 0 else 0 self.upload_progress.setRange(0, 100) # Normal mode self.upload_progress.setValue(percentage) display_filename = filename if len(display_filename) > 50: display_filename = "..." + display_filename[-47:] label_text = ( f"{icon} {phase_text}...\n\n" f"File {current} of {total}\n" f"{display_filename}\n\n" f"Progress: {percentage}%" ) elif phase == 'zip': phase_text = "Creating ZIP snapshot (local)" icon = "📦" percentage = int((current / total) * 100) if total > 0 else 0 self.upload_progress.setRange(0, 100) self.upload_progress.setValue(percentage) display_filename = filename if len(display_filename) > 50: display_filename = "..." + display_filename[-47:] label_text = ( f"{icon} {phase_text}...\n\n" f"File {current} of {total}\n" f"{display_filename}\n\n" f"Progress: {percentage}%" ) elif phase == 'extract': phase_text = "Extracting to server" icon = "📂" percentage = int((current / total) * 100) if total > 0 else 0 self.upload_progress.setRange(0, 100) self.upload_progress.setValue(percentage) display_filename = filename if len(display_filename) > 50: display_filename = "..." + display_filename[-47:] label_text = ( f"{icon} {phase_text}...\n\n" f"File {current} of {total}\n" f"{display_filename}\n\n" f"Progress: {percentage}%" ) else: phase_text = "Processing" icon = "⚙️" label_text = f"{icon} {phase_text}..." self.upload_progress.setLabelText(label_text) QApplication.processEvents() def _on_upload_finished(self, result): """Handle upload completion""" if hasattr(self, 'upload_progress'): self.upload_progress.close() delattr(self, 'upload_progress') if result['success']: # Format elapsed time elapsed = result.get('elapsed_time', 0) elapsed_minutes = int(elapsed // 60) elapsed_seconds = int(elapsed % 60) time_str = f"{elapsed_minutes}m {elapsed_seconds}s" if elapsed_minutes > 0 else f"{elapsed_seconds}s" QMessageBox.information( self, "Success", f"Version '{self._upload_version_name}' uploaded successfully!\n\n" f"Files: {result['files_count']}\n" f"Size: {result['size_bytes'] / (1024*1024):.2f} MB\n" f"Time: {time_str}\n" f"ZIP: {result['zip_path']}" ) # Clear input fields self.version_name_edit.clear() self.version_desc_edit.clear() # Refresh versions list self.refresh() else: QMessageBox.critical(self, "Upload Failed", result['message']) def _on_upload_failed(self, error_msg): """Handle upload failure""" if hasattr(self, 'upload_progress'): self.upload_progress.close() delattr(self, 'upload_progress') logger.error(f"Upload failed: {error_msg}") QMessageBox.critical(self, "Error", f"Failed to upload version: {error_msg}") def _download_version(self): """Download selected version to local path""" version_id = self._get_selected_version_id() if not version_id: return version = self.db.get_project_version(version_id) if not version: return # Ask for destination destination = QFileDialog.getExistingDirectory( self, f"Choose destination for version '{version['version_name']}'", "" ) if not destination: return # Check if destination has files and warn user dest_path = Path(destination) if dest_path.exists() and any(dest_path.iterdir()): reply = QMessageBox.warning( self, "Destination Not Empty", f"⚠ WARNING ⚠\n\n" f"The destination directory is not empty.\n\n" f"Download behavior:\n" f"• Files in ZIP will OVERWRITE existing files\n" f"• Files NOT in ZIP will be KEPT\n" f"• This may leave obsolete files from previous versions\n\n" f"💡 Recommendation: Download to an empty directory\n" f" or create a subdirectory (e.g., 'v{version['version_name']}').\n\n" f"Continue anyway?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No ) if reply != QMessageBox.StandardButton.Yes: return # Create progress dialog self.download_progress = QProgressDialog( "Initializing download...", None, # No cancel button 0, 100, self ) self.download_progress.setWindowTitle("Downloading Version") self.download_progress.setWindowModality(Qt.WindowModality.WindowModal) self.download_progress.setMinimumDuration(0) self.download_progress.setAutoClose(False) self.download_progress.show() # Create and start worker thread self.download_worker = VersionWorker( 'download', self.version_manager, version_id=version_id, destination=destination ) # Connect signals self.download_worker.progress_updated.connect(self._on_download_progress) self.download_worker.operation_finished.connect(self._on_download_finished) self.download_worker.operation_failed.connect(self._on_download_failed) # Store for later self._download_version_name = version['version_name'] self._download_destination = destination # Start the operation logger.info(f"Starting download worker for version {version_id}") self.download_worker.start() def _on_download_progress(self, phase, current, total, filename): """Handle progress updates from download worker""" if not hasattr(self, 'download_progress'): return # Calculate percentage if total > 0: percentage = int((current / total) * 100) else: percentage = 0 self.download_progress.setValue(percentage) # Truncate long filenames display_filename = filename if len(display_filename) > 50: display_filename = "..." + display_filename[-47:] label_text = ( f"📥 Extracting files...\n\n" f"File {current} of {total}\n" f"{display_filename}\n\n" f"Destination: {self._download_destination}\n" f"Progress: {percentage}%" ) self.download_progress.setLabelText(label_text) QApplication.processEvents() def _on_download_finished(self, result): """Handle download completion""" if hasattr(self, 'download_progress'): self.download_progress.close() delattr(self, 'download_progress') if result['success']: QMessageBox.information( self, "Success", f"Version '{self._download_version_name}' downloaded successfully!\n\n" f"Files: {result['files_count']}\n" f"Location: {result['destination']}" ) else: QMessageBox.critical(self, "Download Failed", result['message']) def _on_download_failed(self, error_msg): """Handle download failure""" if hasattr(self, 'download_progress'): self.download_progress.close() delattr(self, 'download_progress') logger.error(f"Download failed: {error_msg}") QMessageBox.critical(self, "Error", f"Failed to download version: {error_msg}") def _restore_version(self): """Restore selected version to server""" version_id = self._get_selected_version_id() if not version_id: return version = self.db.get_project_version(version_id) if not version: return project = self.db.get_project_config(version['project_id']) if not project: return reply = QMessageBox.warning( self, "Restore Version", f"⚠ WARNING ⚠\n\n" f"This will restore version '{version['version_name']}' to the server.\n\n" f"Location: {project['server_base_path']}/{project['name']}/\n\n" f"Any existing files will be REPLACED.\n" f"A pre-restore backup will be created automatically (if files exist).\n\n" f"Continue?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No ) if reply != QMessageBox.StandardButton.Yes: return # Create progress dialog self.restore_progress = QProgressDialog( "Initializing restore...", None, 0, 100, self ) self.restore_progress.setWindowTitle("Restoring Version") self.restore_progress.setWindowModality(Qt.WindowModality.WindowModal) self.restore_progress.setMinimumDuration(0) self.restore_progress.setAutoClose(False) self.restore_progress.show() # Create and start worker thread self.restore_worker = VersionWorker( 'restore', self.version_manager, version_id=version_id ) # Connect signals self.restore_worker.progress_updated.connect(self._on_restore_progress) self.restore_worker.operation_finished.connect(self._on_restore_finished) self.restore_worker.operation_failed.connect(self._on_restore_failed) # Store for later self._restore_version_name = version['version_name'] # Start the operation logger.info(f"Starting restore worker for version {version_id}") self.restore_worker.start() def _on_restore_progress(self, phase, current, total, filename): """Handle progress updates from restore worker""" if not hasattr(self, 'restore_progress'): return # Calculate percentage if total > 0: percentage = int((current / total) * 100) else: percentage = 0 self.restore_progress.setValue(percentage) # Update label based on phase if phase == 'backup': phase_text = "Creating pre-restore backup" icon = "💾" elif phase == 'extract': phase_text = "Extracting to server" icon = "📂" else: phase_text = "Processing" icon = "⚙️" # Truncate long filenames display_filename = filename if len(display_filename) > 50: display_filename = "..." + display_filename[-47:] label_text = ( f"{icon} {phase_text}...\n\n" f"File {current} of {total}\n" f"{display_filename}\n\n" f"Progress: {percentage}%" ) self.restore_progress.setLabelText(label_text) QApplication.processEvents() def _on_restore_finished(self, result): """Handle restore completion""" if hasattr(self, 'restore_progress'): self.restore_progress.close() delattr(self, 'restore_progress') if result['success']: # Build message based on whether backup was created message = f"Version '{self._restore_version_name}' restored successfully!\n\n" message += f"Files: {result['files_count']}\n" if result.get('backup_version_name'): message += f"Backup created: '{result['backup_version_name']}'" else: message += "No backup needed (first restore)" QMessageBox.information(self, "Success", message) self.refresh() else: QMessageBox.critical(self, "Restore Failed", result['message']) def _on_restore_failed(self, error_msg): """Handle restore failure""" if hasattr(self, 'restore_progress'): self.restore_progress.close() delattr(self, 'restore_progress') logger.error(f"Restore failed: {error_msg}") QMessageBox.critical(self, "Error", f"Failed to restore version: {error_msg}") def _delete_version(self): """Delete selected version""" version_id = self._get_selected_version_id() if not version_id: return version = self.db.get_project_version(version_id) if not version: return reply = QMessageBox.question( self, "Delete Version", f"Delete version '{version['version_name']}'?\n\n" f"This will delete the ZIP file and database record.\n" f"This action cannot be undone.", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) if reply == QMessageBox.StandardButton.Yes: try: result = self.version_manager.delete_version(version_id) if result['success']: logger.info(f"Deleted version {version_id}") self.refresh() else: QMessageBox.critical(self, "Delete Failed", result['message']) except Exception as e: logger.error(f"Error deleting version: {e}") QMessageBox.critical(self, "Error", f"Failed to delete version: {e}")