feature/integracion-cpanel-asrecovery

This commit is contained in:
2026-06-30 11:29:06 -06:00
parent 072be5b5db
commit 034a5ca5bb
35 changed files with 2416 additions and 320 deletions

View File

@@ -2,181 +2,207 @@
from PySide6.QtWidgets import (
QMainWindow, QWidget, QVBoxLayout, QTabWidget,
QSystemTrayIcon, QMenu
QSystemTrayIcon, QMenu, QMessageBox, QApplication,
)
from PySide6.QtCore import Qt, QTimer, Signal
from PySide6.QtGui import QIcon, QCloseEvent, QAction
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."""
# Señal para minimizar a tray
minimize_to_tray = Signal()
def __init__(self):
def __init__(
self,
minimized: bool = False,
start_engine: bool = False,
panel_configured: bool = True,
):
"""Inicializa la ventana principal."""
super().__init__()
# Motor de restauración
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()
# Configurar ventana
self.setWindowTitle("CloudRestoreAS - Restauración Automática SQL Server")
self.setMinimumSize(1200, 800)
# System tray
self.tray_icon: QSystemTrayIcon = None
self.tray_icon: QSystemTrayIcon | None = None
self._setup_tray()
# UI
self._setup_ui()
# Conectar señales del motor
self._connect_engine_signals()
# Timer para actualizar stats
self.stats_timer = QTimer()
self.stats_timer.timeout.connect(self._update_stats)
self.stats_timer.start(5000) # Cada 5 segundos
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)
if self._minimized_on_load:
QTimer.singleShot(100, self.hide)
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."""
# Widget central
central_widget = QWidget()
self.setCentralWidget(central_widget)
# Layout principal
layout = QVBoxLayout(central_widget)
layout.setContentsMargins(10, 10, 10, 10)
# Tabs
self.tabs = QTabWidget()
# Crear tabs
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)
# Agregar tabs
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)
# Menu bar
self._setup_menu()
def _setup_menu(self):
"""Configura el menú."""
menubar = self.menuBar()
# Menú Motor
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)
# Menú Ayuda
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):
"""Configura el icono de bandeja del sistema."""
# Crear icono (usar un icono por defecto o crear uno simple)
if not QSystemTrayIcon.isSystemTrayAvailable():
app_logger.warning("Bandeja del sistema no disponible en este entorno")
return
self.tray_icon = QSystemTrayIcon(self)
self.tray_icon.setIcon(load_tray_icon())
self.tray_icon.setToolTip("CloudRestoreAS")
# Menú del tray
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_action = QAction("Salir", self)
quit_action.triggered.connect(self._quit_application)
tray_menu.addAction(quit_action)
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)
# Doble clic en tray para mostrar/ocultar
self.tray_icon.activated.connect(self._on_tray_activated)
# Mostrar tray
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):
"""Maneja el evento de cierre de ventana (minimizar a tray)."""
if self.tray_icon.isVisible():
"""Minimiza a bandeja al cerrar la ventana (no sale de la aplicación)."""
if self._force_quit:
event.accept()
return
if self.tray_icon and self.tray_icon.isVisible():
self.hide()
event.ignore()
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:
event.accept()
def _on_tray_activated(self, reason):
"""Maneja la activación del icono de tray."""
if reason == QSystemTrayIcon.ActivationReason.DoubleClick:
@@ -184,35 +210,56 @@ class MainWindow(QMainWindow):
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()
@@ -220,48 +267,53 @@ class MainWindow(QMainWindow):
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."""
from PySide6.QtWidgets import QMessageBox
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"
"© 2026 Aduanasoft"
"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()
self.tray_icon.hide()
self.close()
from PySide6.QtWidgets import QApplication
if self.tray_icon:
self.tray_icon.hide()
QApplication.quit()