"""Ventana principal de la aplicación.""" from PySide6.QtWidgets import ( QMainWindow, QWidget, QVBoxLayout, QTabWidget, QSystemTrayIcon, QMenu, QMessageBox, QApplication, ) from PySide6.QtCore import Qt, QTimer, Signal from PySide6.QtGui import QCloseEvent, QAction from .dashboard_tab import DashboardTab from .jobs_tab import JobsTab from .nodes_tab import NodesTab from .config_tab import ConfigTab from .logs_tab import LogsTab from .tray_assets import load_tray_icon from .. import __version__ from ..constants import APP_ARCH, APP_PLATFORM from ..engine.engine import RestoreEngine from ..utils.logger import app_logger class MainWindow(QMainWindow): """Ventana principal de la aplicación.""" minimize_to_tray = Signal() def __init__( self, minimized: bool = False, start_engine: bool = False, panel_configured: bool = True, ): """Inicializa la ventana principal.""" super().__init__() self._start_on_load = start_engine self._minimized_on_load = minimized self._panel_configured = panel_configured self._force_quit = False self._tray_hint_shown = False self.engine = RestoreEngine() self.setWindowTitle("CloudRestoreAS - Restauración Automática SQL Server") self.setMinimumSize(1200, 800) self.setWindowIcon(load_tray_icon()) self.tray_icon: QSystemTrayIcon | None = None self._setup_tray() self._setup_ui() self._connect_engine_signals() self.stats_timer = QTimer() self.stats_timer.timeout.connect(self._update_stats) self.stats_timer.start(5000) if not self._panel_configured: self.dashboard_tab.set_panel_warning( "Configure la conexión al servicio de bases en config/.env " "(CLOUDRESTORE_PANEL_API_URL, TOKEN e INSTANCE_KEY)." ) if self._start_on_load: QTimer.singleShot(500, self._start_engine) self._update_tray_status() self.dashboard_tab.update_motor_status() app_logger.info("Ventana principal inicializada") def _setup_ui(self): """Configura la interfaz de usuario.""" central_widget = QWidget() self.setCentralWidget(central_widget) layout = QVBoxLayout(central_widget) layout.setContentsMargins(10, 10, 10, 10) self.tabs = QTabWidget() self.dashboard_tab = DashboardTab(self.engine) self.jobs_tab = JobsTab(self.engine) self.nodes_tab = NodesTab(self.engine) self.config_tab = ConfigTab(self.engine) self.logs_tab = LogsTab(self.engine) self.tabs.addTab(self.dashboard_tab, "📊 Dashboard") self.tabs.addTab(self.jobs_tab, "📋 Jobs") self.tabs.addTab(self.nodes_tab, "🔗 Nodos") self.tabs.addTab(self.config_tab, "⚙️ Configuración") self.tabs.addTab(self.logs_tab, "📝 Logs") layout.addWidget(self.tabs) self._setup_menu() def _setup_menu(self): """Configura el menú.""" menubar = self.menuBar() file_menu = menubar.addMenu("Archivo") quit_action = QAction("Salir", self) quit_action.triggered.connect(self._quit_application) file_menu.addAction(quit_action) motor_menu = menubar.addMenu("Motor") self.start_action = QAction("▶️ Iniciar", self) self.start_action.triggered.connect(self._start_engine) motor_menu.addAction(self.start_action) self.pause_action = QAction("⏸️ Pausar", self) self.pause_action.triggered.connect(self._pause_engine) self.pause_action.setEnabled(False) motor_menu.addAction(self.pause_action) self.resume_action = QAction("▶️ Continuar", self) self.resume_action.triggered.connect(self._resume_engine) self.resume_action.setEnabled(False) motor_menu.addAction(self.resume_action) self.stop_action = QAction("⏹️ Detener", self) self.stop_action.triggered.connect(self._stop_engine) self.stop_action.setEnabled(False) motor_menu.addAction(self.stop_action) motor_menu.addSeparator() self.scan_action = QAction("🔍 Escanear Ahora", self) self.scan_action.triggered.connect(self._scan_now) motor_menu.addAction(self.scan_action) self.cleanup_action = QAction("🧹 Limpiar Respaldos Ahora", self) self.cleanup_action.triggered.connect(self._run_cleanup_now) motor_menu.addAction(self.cleanup_action) help_menu = menubar.addMenu("Ayuda") about_action = QAction("Acerca de", self) about_action.triggered.connect(self._show_about) help_menu.addAction(about_action) def _setup_tray(self, _attempt: int = 0): """Configura el icono de bandeja del sistema (reintenta si aún no está listo).""" if not QSystemTrayIcon.isSystemTrayAvailable(): if _attempt < 20: # La bandeja puede no estar lista justo tras el login: reintentar ~10s. QTimer.singleShot(500, lambda: self._setup_tray(_attempt + 1)) else: app_logger.warning( "Bandeja del sistema no disponible tras varios reintentos" ) return self.tray_icon = QSystemTrayIcon(self) self.tray_icon.setIcon(load_tray_icon()) self.tray_icon.setToolTip("CloudRestoreAS") tray_menu = QMenu() show_action = QAction("Mostrar", self) show_action.triggered.connect(self._show_window) tray_menu.addAction(show_action) hide_action = QAction("Ocultar", self) hide_action.triggered.connect(self.hide) tray_menu.addAction(hide_action) tray_menu.addSeparator() pause_tray_action = QAction("Pausar", self) pause_tray_action.triggered.connect(self._pause_engine) tray_menu.addAction(pause_tray_action) resume_tray_action = QAction("Continuar", self) resume_tray_action.triggered.connect(self._resume_engine) tray_menu.addAction(resume_tray_action) tray_menu.addSeparator() quit_tray_action = QAction("Salir", self) quit_tray_action.triggered.connect(self._quit_application) tray_menu.addAction(quit_tray_action) self.tray_icon.setContextMenu(tray_menu) self.tray_icon.activated.connect(self._on_tray_activated) self.tray_icon.show() def _connect_engine_signals(self): """Conecta las señales del motor.""" self.engine.signals.job_created.connect(self._on_job_created) self.engine.signals.stats_updated.connect(self._on_stats_updated) self.engine.signals.config_loaded.connect(self._on_config_loaded) def closeEvent(self, event: QCloseEvent): """Nunca cierra la app: minimiza a la bandeja (o a la barra de tareas si no hay bandeja).""" if self._force_quit: event.accept() return event.ignore() if self.tray_icon and self.tray_icon.isVisible(): self.hide() app_logger.info("Ventana minimizada a bandeja del sistema") if not self._tray_hint_shown: self.tray_icon.showMessage( "CloudRestoreAS sigue en ejecución", "La aplicación está en la bandeja. Use clic derecho → Salir para cerrar.", QSystemTrayIcon.MessageIcon.Information, 4000, ) self._tray_hint_shown = True else: # Sin bandeja disponible: minimizar a la barra de tareas, nunca cerrar. self.showMinimized() app_logger.info("Ventana minimizada a la barra de tareas (sin bandeja)") def _on_tray_activated(self, reason): """Maneja la activación del icono de tray.""" if reason == QSystemTrayIcon.ActivationReason.DoubleClick: if self.isVisible(): self.hide() else: self._show_window() def _show_window(self): """Muestra la ventana.""" self.show() self.activateWindow() self.raise_() def _motor_status_text(self) -> str: if not self.engine.is_running(): return "Detenido" if self.engine.is_paused(): return "Pausado" return "Activo" def _update_tray_status(self): """Actualiza tooltip del icono de bandeja según estado del motor.""" if not self.tray_icon: return status = self._motor_status_text() self.tray_icon.setToolTip(f"CloudRestoreAS — Motor: {status}") def _start_engine(self): """Inicia el motor.""" self.engine.start() self.start_action.setEnabled(False) self.pause_action.setEnabled(True) self.stop_action.setEnabled(True) self.resume_action.setEnabled(False) self._update_tray_status() self.dashboard_tab.update_motor_status() app_logger.info("Motor iniciado desde UI") def _pause_engine(self): """Pausa el motor.""" self.engine.pause() self.pause_action.setEnabled(False) self.resume_action.setEnabled(True) self._update_tray_status() self.dashboard_tab.update_motor_status() app_logger.info("Motor pausado desde UI") def _resume_engine(self): """Reanuda el motor.""" self.engine.resume() self.resume_action.setEnabled(False) self.pause_action.setEnabled(True) self._update_tray_status() self.dashboard_tab.update_motor_status() app_logger.info("Motor reanudado desde UI") def _stop_engine(self): """Detiene el motor.""" self.engine.stop() self.stop_action.setEnabled(False) self.pause_action.setEnabled(False) self.resume_action.setEnabled(False) self.start_action.setEnabled(True) self._update_tray_status() self.dashboard_tab.update_motor_status() app_logger.info("Motor detenido desde UI") def _scan_now(self): """Fuerza un escaneo manual.""" self.engine.scan_now() app_logger.info("Escaneo manual solicitado desde UI") def _run_cleanup_now(self): """Dispara la retención de respaldos obsoletos (respeta el dry_run de la config).""" self.engine.trigger_maintenance_now() app_logger.info("Limpieza de respaldos solicitada desde UI") def _update_stats(self): """Actualiza las estadísticas.""" self._update_tray_status() self.dashboard_tab.update_motor_status() if self.engine.is_running(): stats = self.engine.get_stats() self.dashboard_tab.update_stats(stats) def _on_job_created(self, job_id: str): """Maneja la creación de un nuevo job.""" self.jobs_tab.refresh_jobs() self._update_stats() def _on_stats_updated(self, stats: dict): """Maneja la actualización de estadísticas.""" self.dashboard_tab.update_stats(stats) def _on_config_loaded(self, config: dict): """Maneja la carga de configuración.""" self.config_tab.load_config(config) def _show_about(self): """Muestra el diálogo Acerca de.""" QMessageBox.about( self, "Acerca de CloudRestoreAS", f"CloudRestoreAS v{__version__} ({APP_PLATFORM}/{APP_ARCH})\n\n" "Aplicación de restauración automática de bases de datos SQL Server.\n\n" "Al cerrar la ventana, la aplicación permanece en la bandeja del sistema.\n" "Use Archivo → Salir o la bandeja → Salir para cerrar por completo.\n\n" "© 2026 Aduanasoft", ) def _quit_application(self): """Sale de la aplicación completamente.""" app_logger.info("Saliendo de la aplicación") self._force_quit = True self.engine.stop() if self.tray_icon: self.tray_icon.hide() QApplication.quit()