Files

818 lines
30 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import sys
import time
import socket
import platform
import threading
from pathlib import Path
from typing import Optional, List
import httpx
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QSize
from PyQt6.QtGui import QIcon, QFont, QColor, QAction
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QTabWidget, QLabel, QPushButton, QLineEdit, QTableWidget,
QTableWidgetItem, QHeaderView, QProgressBar, QFileDialog,
QMessageBox, QSystemTrayIcon, QMenu, QDialog, QFormLayout,
QComboBox, QSpinBox, QFrame, QCheckBox
)
# Add agent root to sys.path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from agent.config import load_config, save_config, AgentConfig, LocalFolderJob
from agent.service import AgentDaemon
from agent.uploader import ChunkUploader
from agent.chunker import compute_file_sha256
from agent.state_db import state_db
from create_icons import generate_app_icons
# --- Modern Dark QSS Stylesheet ---
DARK_QSS = """
QMainWindow, QWidget {
background-color: #0B0F19;
color: #F8FAFC;
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 13px;
}
QTabWidget::pane {
border: 1px solid rgba(255, 255, 255, 0.08);
background-color: #111827;
border-radius: 8px;
}
QTabBar::tab {
background: #1E293B;
color: #94A3B8;
padding: 10px 20px;
margin-right: 4px;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
font-weight: bold;
}
QTabBar::tab:selected {
background: #06B6D4;
color: #FFFFFF;
}
QTabBar::tab:hover:!selected {
background: #334155;
color: #FFFFFF;
}
QFrame.card {
background-color: #1E293B;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px;
padding: 16px;
}
QLineEdit, QComboBox, QSpinBox {
background-color: #0B0F19;
border: 1px solid #334155;
border-radius: 6px;
color: #FFFFFF;
padding: 8px 12px;
font-size: 13px;
}
QLineEdit:focus, QComboBox:focus, QSpinBox:focus {
border: 1px solid #06B6D4;
}
QCheckBox {
color: #E2E8F0;
font-size: 13px;
spacing: 8px;
}
QCheckBox::indicator {
width: 18px;
height: 18px;
border-radius: 4px;
border: 1px solid #475569;
background-color: #0B0F19;
}
QCheckBox::indicator:checked {
background-color: #06B6D4;
border-color: #06B6D4;
}
QPushButton {
background-color: #334155;
color: #FFFFFF;
border: none;
border-radius: 6px;
padding: 9px 18px;
font-weight: bold;
}
QPushButton:hover {
background-color: #475569;
}
QPushButton.primary {
background-color: #06B6D4;
color: #FFFFFF;
}
QPushButton.primary:hover {
background-color: #0891B2;
}
QPushButton.success {
background-color: #10B981;
color: #FFFFFF;
}
QPushButton.success:hover {
background-color: #059669;
}
QPushButton.danger {
background-color: rgba(244, 63, 94, 0.2);
color: #FDA4AF;
border: 1px solid rgba(244, 63, 94, 0.4);
}
QPushButton.danger:hover {
background-color: rgba(244, 63, 94, 0.35);
}
QProgressBar {
background-color: #1E293B;
border: 1px solid #334155;
border-radius: 6px;
text-align: center;
color: #FFFFFF;
font-weight: bold;
height: 18px;
}
QProgressBar::chunk {
background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #06B6D4, stop:1 #6366F1);
border-radius: 5px;
}
QTableWidget {
background-color: #0B0F19;
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 6px;
gridline-color: rgba(255, 255, 255, 0.04);
}
QTableWidget::item {
padding: 8px;
color: #F8FAFC;
}
QTableWidget::item:selected {
background-color: rgba(6, 182, 212, 0.2);
}
QHeaderView::section {
background-color: #1E293B;
color: #94A3B8;
padding: 8px;
font-weight: bold;
border: none;
border-bottom: 1px solid #334155;
}
QMenu {
background-color: #1E293B;
color: #FFFFFF;
border: 1px solid #334155;
}
QMenu::item:selected {
background-color: #06B6D4;
}
"""
class WorkerSignals(QThread):
started_signal = pyqtSignal(str, int)
progress_signal = pyqtSignal(str, int, int, float)
completed_signal = pyqtSignal(str, str, int)
error_signal = pyqtSignal(str, str)
status_signal = pyqtSignal(str, str)
class AddFolderDialog(QDialog):
"""Dialog for selecting and configuring a Windows backup folder."""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Añadir Carpeta de Backup — OnEver Drive")
self.resize(500, 320)
self.setStyleSheet(DARK_QSS)
layout = QVBoxLayout(self)
layout.setSpacing(14)
form = QFormLayout()
form.setSpacing(12)
self.txt_name = QLineEdit()
self.txt_name.setPlaceholderText("Ej: Base de Datos SQL Producción")
form.addRow("Nombre descriptivo:", self.txt_name)
path_layout = QHBoxLayout()
self.txt_path = QLineEdit()
self.txt_path.setPlaceholderText("C:\\SQLBackups")
btn_browse = QPushButton("Explorar...")
btn_browse.clicked.connect(self._browse_folder)
path_layout.addWidget(self.txt_path)
path_layout.addWidget(btn_browse)
form.addRow("Ruta en Windows:", path_layout)
self.txt_patterns = QLineEdit()
self.txt_patterns.setText("*.bak,*.mdf")
form.addRow("Filtros de archivo:", self.txt_patterns)
self.spin_interval = QSpinBox()
self.spin_interval.setRange(5, 1440)
self.spin_interval.setValue(60)
self.spin_interval.setSuffix(" min")
form.addRow("Frecuencia de sondeo:", self.spin_interval)
self.spin_stable = QSpinBox()
self.spin_stable.setRange(10, 600)
self.spin_stable.setValue(60)
self.spin_stable.setSuffix(" seg")
form.addRow("Estabilidad de archivo (Locks):", self.spin_stable)
layout.addLayout(form)
btn_layout = QHBoxLayout()
btn_layout.addStretch()
btn_cancel = QPushButton("Cancelar")
btn_cancel.clicked.connect(self.reject)
btn_save = QPushButton("Guardar Carpeta")
btn_save.setProperty("class", "primary")
btn_save.clicked.connect(self._validate_and_accept)
btn_layout.addWidget(btn_cancel)
btn_layout.addWidget(btn_save)
layout.addLayout(btn_layout)
def _browse_folder(self):
folder = QFileDialog.getExistingDirectory(self, "Seleccionar carpeta para backup")
if folder:
self.txt_path.setText(folder)
if not self.txt_name.text():
self.txt_name.setText(Path(folder).name)
def _validate_and_accept(self):
if not self.txt_path.text() or not os.path.exists(self.txt_path.text()):
QMessageBox.warning(self, "Ruta Inválida", "Por favor selecciona una carpeta existente en Windows.")
return
if not self.txt_name.text():
self.txt_name.setText(Path(self.txt_path.text()).name)
self.accept()
def get_data(self) -> LocalFolderJob:
return LocalFolderJob(
name=self.txt_name.text().strip(),
source_path=self.txt_path.text().strip(),
file_patterns=self.txt_patterns.text().strip() or "*.*",
schedule_interval_minutes=self.spin_interval.value(),
min_stable_seconds=self.spin_stable.value()
)
class OnEverDriveMainWindow(QMainWindow):
"""Main modern desktop GUI and tray controller for OnEver Drive Windows Agent."""
def __init__(self):
super().__init__()
self.setWindowTitle("OnEver Drive — Agente de Backup Windows")
self.resize(780, 580)
self.setStyleSheet(DARK_QSS)
self.config = load_config()
self.app_icon = self._load_app_icon()
self.setWindowIcon(self.app_icon)
# Background Worker Daemon
self.signals = WorkerSignals()
self.daemon = AgentDaemon(
config=self.config,
on_started=lambda f, b: self.signals.started_signal.emit(f, b),
on_progress=lambda f, d, t, p: self.signals.progress_signal.emit(f, d, t, p),
on_completed=lambda f, s, b: self.signals.completed_signal.emit(f, s, b),
on_error=lambda f, e: self.signals.error_signal.emit(f, e),
on_status=lambda s, m: self.signals.status_signal.emit(s, m)
)
self._connect_signals()
self._init_ui()
self._init_tray()
if self.config.device_id:
self.daemon.start()
def _load_app_icon(self) -> QIcon:
assets_dir = Path(__file__).resolve().parent / "assets"
png_path = assets_dir / "icon.png"
if not png_path.exists():
_, png_path = generate_app_icons()
return QIcon(str(png_path))
def _connect_signals(self):
self.signals.started_signal.connect(self._on_backup_started)
self.signals.progress_signal.connect(self._on_live_progress)
self.signals.completed_signal.connect(self._on_backup_completed)
self.signals.error_signal.connect(self._on_backup_error)
self.signals.status_signal.connect(self._on_daemon_status)
def _init_ui(self):
central = QWidget()
self.setCentralWidget(central)
main_layout = QVBoxLayout(central)
main_layout.setContentsMargins(20, 20, 20, 20)
main_layout.setSpacing(16)
# Header Bar
header = QHBoxLayout()
title_box = QVBoxLayout()
lbl_title = QLabel("OnEver Drive")
lbl_title.setFont(QFont("Segoe UI", 16, QFont.Weight.Bold))
lbl_sub = QLabel("Agente Empresarial de Sincronización y Backup para Windows")
lbl_sub.setStyleSheet("color: #94A3B8; font-size: 11px;")
title_box.addWidget(lbl_title)
title_box.addWidget(lbl_sub)
header.addLayout(title_box)
header.addStretch()
self.lbl_status_badge = QLabel("● Desconectado")
self.lbl_status_badge.setStyleSheet("background-color: #7F1D1D; color: #FCA5A5; font-weight: bold; border-radius: 4px; padding: 6px 12px;")
header.addWidget(self.lbl_status_badge)
main_layout.addLayout(header)
# Tabs
self.tabs = QTabWidget()
self.tab_dashboard = QWidget()
self.tab_folders = QWidget()
self.tab_config = QWidget()
self.tab_history = QWidget()
self.tabs.addTab(self.tab_dashboard, "Dashboard")
self.tabs.addTab(self.tab_folders, "Carpetas de Backup")
self.tabs.addTab(self.tab_config, "Servidor & Config")
self.tabs.addTab(self.tab_history, "Historial de Archivos")
main_layout.addWidget(self.tabs)
self._build_tab_dashboard()
self._build_tab_folders()
self._build_tab_config()
self._build_tab_history()
self._update_header_status()
# --- TAB 1: DASHBOARD ---
def _build_tab_dashboard(self):
layout = QVBoxLayout(self.tab_dashboard)
layout.setContentsMargins(16, 16, 16, 16)
layout.setSpacing(16)
card_tel = QFrame()
card_tel.setProperty("class", "card")
tel_layout = QVBoxLayout(card_tel)
lbl_sec = QLabel("Transferencia en Vivo (Motor de Chunks)")
lbl_sec.setFont(QFont("Segoe UI", 12, QFont.Weight.Bold))
tel_layout.addWidget(lbl_sec)
self.lbl_transfer_info = QLabel("Estado: En espera de cambios en carpetas monitoreadas...")
self.lbl_transfer_info.setStyleSheet("color: #94A3B8;")
tel_layout.addWidget(self.lbl_transfer_info)
self.pbar_transfer = QProgressBar()
self.pbar_transfer.setValue(0)
tel_layout.addWidget(self.pbar_transfer)
self.lbl_chunk_details = QLabel("Chunks: 0 / 0 | Motor por Bloques de 4MB | SHA-256: —")
self.lbl_chunk_details.setStyleSheet("color: #64748B; font-family: 'Consolas', monospace; font-size: 11px;")
tel_layout.addWidget(self.lbl_chunk_details)
layout.addWidget(card_tel)
card_info = QFrame()
card_info.setProperty("class", "card")
info_layout = QVBoxLayout(card_info)
lbl_info_title = QLabel("Información del Dispositivo")
lbl_info_title.setFont(QFont("Segoe UI", 11, QFont.Weight.Bold))
info_layout.addWidget(lbl_info_title)
self.lbl_dash_client = QLabel("Cliente ID: —")
self.lbl_dash_server = QLabel("Servidor Proxmox: —")
self.lbl_dash_folders = QLabel("Carpetas en Monitoreo: 0")
info_layout.addWidget(self.lbl_dash_client)
info_layout.addWidget(self.lbl_dash_server)
info_layout.addWidget(self.lbl_dash_folders)
layout.addWidget(card_info)
btn_box = QHBoxLayout()
btn_backup_all = QPushButton("▶ Iniciar Sincronización Manual Ahora")
btn_backup_all.setProperty("class", "primary")
btn_backup_all.clicked.connect(self._trigger_all_backups)
btn_box.addWidget(btn_backup_all)
layout.addLayout(btn_box)
layout.addStretch()
# --- TAB 2: CARPETAS DE BACKUP ---
def _build_tab_folders(self):
layout = QVBoxLayout(self.tab_folders)
layout.setContentsMargins(16, 16, 16, 16)
layout.setSpacing(12)
top_bar = QHBoxLayout()
lbl = QLabel("Carpetas de Windows Monitoreadas")
lbl.setFont(QFont("Segoe UI", 12, QFont.Weight.Bold))
top_bar.addWidget(lbl)
top_bar.addStretch()
btn_add = QPushButton(" Añadir Carpeta...")
btn_add.setProperty("class", "primary")
btn_add.clicked.connect(self._show_add_folder_dialog)
top_bar.addWidget(btn_add)
layout.addLayout(top_bar)
self.tbl_folders = QTableWidget(0, 5)
self.tbl_folders.setHorizontalHeaderLabels(["Nombre", "Ruta en Windows", "Filtros", "Intervalo", "Último Estado"])
self.tbl_folders.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
self.tbl_folders.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
layout.addWidget(self.tbl_folders)
btn_row = QHBoxLayout()
btn_delete = QPushButton("🗑️ Eliminar Carpeta")
btn_delete.setProperty("class", "danger")
btn_delete.clicked.connect(self._delete_selected_folder)
btn_row.addWidget(btn_delete)
btn_row.addStretch()
layout.addLayout(btn_row)
self._refresh_folders_table()
# --- TAB 3: CONFIGURACIÓN & NOTIFICACIONES ---
def _build_tab_config(self):
layout = QVBoxLayout(self.tab_config)
layout.setContentsMargins(16, 16, 16, 16)
layout.setSpacing(16)
# Server Card
card_srv = QFrame()
card_srv.setProperty("class", "card")
form = QFormLayout(card_srv)
form.setSpacing(12)
lbl = QLabel("Conexión con el Servidor Central Proxmox VE")
lbl.setFont(QFont("Segoe UI", 12, QFont.Weight.Bold))
form.addRow(lbl)
self.txt_server_url = QLineEdit()
self.txt_server_url.setText(self.config.server_url)
btn_ping = QPushButton("🔍 Probar Conexión")
btn_ping.clicked.connect(self._test_server_connection)
srv_box = QHBoxLayout()
srv_box.addWidget(self.txt_server_url)
srv_box.addWidget(btn_ping)
form.addRow("URL Servidor:", srv_box)
self.txt_reg_code = QLineEdit()
self.txt_reg_code.setPlaceholderText("Código generado en la Web (ej: OED-XXXX-XXXX)")
form.addRow("Código de Registro:", self.txt_reg_code)
btn_register = QPushButton("🚀 Registrar / Re-vincular Dispositivo")
btn_register.setProperty("class", "primary")
btn_register.clicked.connect(self._register_device_api)
form.addRow("", btn_register)
layout.addWidget(card_srv)
# Notifications Preferences Card (Clean & Non-invasive)
card_notif = QFrame()
card_notif.setProperty("class", "card")
notif_layout = QVBoxLayout(card_notif)
notif_layout.setSpacing(10)
lbl_notif = QLabel("Preferencias de Notificaciones en Windows")
lbl_notif.setFont(QFont("Segoe UI", 12, QFont.Weight.Bold))
notif_layout.addWidget(lbl_notif)
self.chk_notif_main = QCheckBox("Habilitar notificaciones en el Área de Notificaciones (System Tray)")
self.chk_notif_main.setChecked(self.config.enable_notifications)
self.chk_notif_main.stateChanged.connect(self._save_notif_settings)
notif_layout.addWidget(self.chk_notif_main)
self.chk_notif_start = QCheckBox("Notificar únicamente cuando INICIA un proceso de respaldo")
self.chk_notif_start.setChecked(self.config.notify_on_start)
self.chk_notif_start.stateChanged.connect(self._save_notif_settings)
notif_layout.addWidget(self.chk_notif_start)
self.chk_notif_complete = QCheckBox("Notificar únicamente cuando FINALIZA con éxito (Confirmación SHA-256)")
self.chk_notif_complete.setChecked(self.config.notify_on_complete)
self.chk_notif_complete.stateChanged.connect(self._save_notif_settings)
notif_layout.addWidget(self.chk_notif_complete)
self.chk_notif_error = QCheckBox("Notificar en caso de error o pérdida de conexión")
self.chk_notif_error.setChecked(self.config.notify_on_error)
self.chk_notif_error.stateChanged.connect(self._save_notif_settings)
notif_layout.addWidget(self.chk_notif_error)
layout.addWidget(card_notif)
layout.addStretch()
# --- TAB 4: HISTORIAL ---
def _build_tab_history(self):
layout = QVBoxLayout(self.tab_history)
layout.setContentsMargins(16, 16, 16, 16)
layout.setSpacing(12)
lbl = QLabel("Historial de Archivos Respaldados Localmente")
lbl.setFont(QFont("Segoe UI", 12, QFont.Weight.Bold))
layout.addWidget(lbl)
self.tbl_history = QTableWidget(0, 4)
self.tbl_history.setHorizontalHeaderLabels(["Archivo", "Tamaño", "Integridad SHA-256", "Fecha de Respaldo"])
self.tbl_history.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
layout.addWidget(self.tbl_history)
self._refresh_history_table()
# --- TRAY ICON & WINDOW CLOSE BEHAVIOR ---
def _init_tray(self):
self.tray = QSystemTrayIcon(self)
self.tray.setIcon(self.app_icon)
self.tray.setToolTip(f"OnEver Drive — {self.config.client_code or 'Sin Registrar'}")
self._build_tray_menu()
self.tray.activated.connect(self._on_tray_activated)
self.tray.show()
def _build_tray_menu(self):
menu = QMenu()
client_label = self.config.client_code or "Sin Registrar"
act_title = QAction(f"OnEver Drive ({client_label})", self)
act_title.setEnabled(False)
menu.addAction(act_title)
menu.addSeparator()
act_open = QAction("🖥️ Abrir Panel de Control", self)
act_open.triggered.connect(self.showNormal)
menu.addAction(act_open)
act_sync = QAction("▶ Respaldar Todo Ahora", self)
act_sync.triggered.connect(self._trigger_all_backups)
menu.addAction(act_sync)
menu.addSeparator()
# Notification direct toggle in tray
self.act_tray_notif = QAction("🔔 Notificaciones Activadas" if self.config.enable_notifications else "🔕 Notificaciones Silenciadas", self)
self.act_tray_notif.triggered.connect(self._toggle_tray_notifications)
menu.addAction(self.act_tray_notif)
menu.addSeparator()
act_exit = QAction("❌ Salir", self)
act_exit.triggered.connect(self._clean_exit)
menu.addAction(act_exit)
self.tray.setContextMenu(menu)
def _toggle_tray_notifications(self):
self.config = load_config()
self.config.enable_notifications = not self.config.enable_notifications
save_config(self.config)
self.chk_notif_main.setChecked(self.config.enable_notifications)
self._build_tray_menu()
def _save_notif_settings(self):
self.config = load_config()
self.config.enable_notifications = self.chk_notif_main.isChecked()
self.config.notify_on_start = self.chk_notif_start.isChecked()
self.config.notify_on_complete = self.chk_notif_complete.isChecked()
self.config.notify_on_error = self.chk_notif_error.isChecked()
save_config(self.config)
self._build_tray_menu()
def _on_tray_activated(self, reason):
if reason == QSystemTrayIcon.ActivationReason.DoubleClick or reason == QSystemTrayIcon.ActivationReason.Trigger:
self.showNormal()
self.activateWindow()
def closeEvent(self, event):
"""Minimize silently to system tray on close without showing intrusive popups."""
event.ignore()
self.hide()
def _clean_exit(self):
self.daemon.stop()
QApplication.quit()
# --- ACTIONS & NOTIFICATION TRIGGERS (Discrete: Start & Finish only) ---
def _update_header_status(self):
self.config = load_config()
if self.config.client_code:
self.lbl_status_badge.setText(f"● Conectado ({self.config.client_code})")
self.lbl_status_badge.setStyleSheet("background-color: #065F46; color: #34D399; font-weight: bold; border-radius: 4px; padding: 6px 12px;")
self.lbl_dash_client.setText(f"Cliente ID: {self.config.client_code} ({self.config.client_name or 'Local'})")
self.lbl_dash_server.setText(f"Servidor Proxmox: {self.config.server_url}")
self.lbl_dash_folders.setText(f"Carpetas en Monitoreo: {len(self.config.local_folders)}")
else:
self.lbl_status_badge.setText("● Sin Registrar")
self.lbl_status_badge.setStyleSheet("background-color: #7F1D1D; color: #FCA5A5; font-weight: bold; border-radius: 4px; padding: 6px 12px;")
def _show_add_folder_dialog(self):
dialog = AddFolderDialog(self)
if dialog.exec() == QDialog.DialogCode.Accepted:
new_job = dialog.get_data()
self.config = load_config()
self.config.local_folders.append(new_job)
save_config(self.config)
self._refresh_folders_table()
self._update_header_status()
QMessageBox.information(self, "Carpeta Añadida", f"La carpeta '{new_job.name}' ha sido configurada y está siendo monitoreada.")
def _refresh_folders_table(self):
self.config = load_config()
self.tbl_folders.setRowCount(len(self.config.local_folders))
for row, job in enumerate(self.config.local_folders):
self.tbl_folders.setItem(row, 0, QTableWidgetItem(job.name))
self.tbl_folders.setItem(row, 1, QTableWidgetItem(job.source_path))
self.tbl_folders.setItem(row, 2, QTableWidgetItem(job.file_patterns))
self.tbl_folders.setItem(row, 3, QTableWidgetItem(f"{job.schedule_interval_minutes} min"))
self.tbl_folders.setItem(row, 4, QTableWidgetItem(job.last_status or "En espera"))
def _delete_selected_folder(self):
row = self.tbl_folders.currentRow()
if row < 0:
QMessageBox.warning(self, "Selección", "Por favor selecciona una carpeta para eliminar.")
return
job_name = self.tbl_folders.item(row, 0).text()
reply = QMessageBox.question(self, "Confirmar", f"¿Eliminar el monitoreo de la carpeta '{job_name}'?")
if reply == QMessageBox.StandardButton.Yes:
self.config = load_config()
if row < len(self.config.local_folders):
self.config.local_folders.pop(row)
save_config(self.config)
self._refresh_folders_table()
self._update_header_status()
def _test_server_connection(self):
url = self.txt_server_url.text().strip().rstrip("/")
if not url:
QMessageBox.warning(self, "Error", "Ingresa una URL de servidor.")
return
try:
t0 = time.time()
with httpx.Client(timeout=5.0) as client:
r = client.get(f"{url}/health")
elapsed_ms = int((time.time() - t0) * 1000)
if r.status_code == 200:
QMessageBox.information(self, "Conexión Exitosa", f"✓ Servidor OnEver Drive alcanzable.\nLatencia: {elapsed_ms} ms\nRespuesta: {r.json()}")
else:
QMessageBox.warning(self, "Error", f"El servidor respondió con código {r.status_code}")
except Exception as ex:
QMessageBox.critical(self, "Error de Conexión", f"No se pudo contactar al servidor en {url}:\n{str(ex)}")
def _register_device_api(self):
server_url = self.txt_server_url.text().strip().rstrip("/")
code = self.txt_reg_code.text().strip().upper()
if not server_url or not code:
QMessageBox.warning(self, "Error", "Debes ingresar la URL del servidor y el código de registro.")
return
try:
hostname = socket.gethostname()
os_info = f"{platform.system()} {platform.release()} (Build {platform.version()})"
payload = {
"registration_code": code,
"name": hostname,
"hostname": hostname,
"os_info": os_info,
"agent_version": "1.0.0"
}
with httpx.Client(timeout=15.0) as client:
resp = client.post(f"{server_url}/api/clients/register", json=payload)
if resp.status_code != 200:
QMessageBox.warning(self, "Registro Fallido", f"El servidor denegó el registro: {resp.text}")
return
data = resp.json()
self.config.server_url = server_url
self.config.client_code = data["client_code"]
self.config.device_id = data["device_id"]
self.config.device_token = data["device_token"]
self.config.client_name = data["name"]
save_config(self.config)
self._update_header_status()
self._build_tray_menu()
self.daemon.start()
QMessageBox.information(self, "Registro Exitoso", f"¡Dispositivo vinculado con éxito!\nCliente: {data['client_code']}")
except Exception as ex:
QMessageBox.critical(self, "Error", f"Error durante el registro: {str(ex)}")
def _trigger_all_backups(self):
if not self.config.device_id:
QMessageBox.warning(self, "Sin Registro", "Primero vincula el dispositivo en la pestaña Servidor & Config.")
return
threading.Thread(target=self.daemon._run_backup_cycle, daemon=True).start()
self.lbl_transfer_info.setText("Iniciando escaneo de carpetas y comprobación de locks...")
# --- DISCRETE NOTIFICATION HANDLERS (START & FINISH ONLY) ---
def _on_backup_started(self, filename: str, file_size: int):
mb = file_size / (1024 * 1024)
self.lbl_transfer_info.setText(f"Iniciando respaldo de: {filename} ({mb:.2f} MB)")
# Single notification at START of backup (if enabled)
if self.config.enable_notifications and self.config.notify_on_start:
self.tray.showMessage(
"OnEver Drive — Inicio de Respaldo",
f"Iniciando transferencia de {filename} ({mb:.2f} MB)...",
QSystemTrayIcon.MessageIcon.Information,
2500
)
def _on_live_progress(self, filename: str, done: int, total: int, pct: float):
# Progress updates ONLY update the GUI progressbar silently (no popups)
self.pbar_transfer.setValue(int(pct))
self.lbl_transfer_info.setText(f"Subiendo {filename}...")
self.lbl_chunk_details.setText(f"Chunks: {done} / {total} ({pct:.1f}%) | Motor por Bloques de 4MB Activo")
def _on_backup_completed(self, filename: str, sha256: str, file_size: int):
self.pbar_transfer.setValue(100)
self.lbl_transfer_info.setText(f"✓ Backup verificado e íntegro: {filename}")
self.lbl_chunk_details.setText(f"SHA-256: {sha256[:16]}... | Tamaño: {file_size / (1024*1024):.2f} MB")
# Single notification at FINISH of backup (if enabled)
if self.config.enable_notifications and self.config.notify_on_complete:
self.tray.showMessage(
"OnEver Drive — Respaldo Exitoso ✓",
f"{filename} respaldado y verificado en el servidor central (SHA-256).",
QSystemTrayIcon.MessageIcon.Information,
3000
)
self._refresh_history_table()
self._refresh_folders_table()
def _on_backup_error(self, filename: str, err: str):
self.lbl_transfer_info.setText(f"✗ Error al respaldar {filename}")
self.lbl_chunk_details.setText(f"Detalle: {err}")
# Notification on ERROR (if enabled)
if self.config.enable_notifications and self.config.notify_on_error:
self.tray.showMessage(
"OnEver Drive — Error en Respaldo ✗",
f"Fallo al respaldar {filename}: {err}",
QSystemTrayIcon.MessageIcon.Warning,
4000
)
def _on_daemon_status(self, status: str, message: str):
if status == "ONLINE":
self._update_header_status()
def _refresh_history_table(self):
try:
with state_db._get_conn() as conn:
cursor = conn.cursor()
cursor.execute("SELECT filepath, file_size, sha256, last_backup_time FROM completed_files ORDER BY last_backup_time DESC LIMIT 50")
rows = cursor.fetchall()
self.tbl_history.setRowCount(len(rows))
for r_idx, row in enumerate(rows):
self.tbl_history.setItem(r_idx, 0, QTableWidgetItem(Path(row[0]).name))
self.tbl_history.setItem(r_idx, 1, QTableWidgetItem(f"{row[1] / (1024*1024):.2f} MB"))
self.tbl_history.setItem(r_idx, 2, QTableWidgetItem(row[2][:16] + "..."))
self.tbl_history.setItem(r_idx, 3, QTableWidgetItem(str(row[3])))
except Exception:
pass
def main():
app = QApplication(sys.argv)
app.setQuitOnLastWindowClosed(False)
window = OnEverDriveMainWindow()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()