"""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 ..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) 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 _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", "CloudRestoreAS v1.0.0\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()