feat: Initial commit for OnEver Drive centralized backup system with Windows PyQt6 agent and Proxmox LXC deployment

This commit is contained in:
2026-08-13 19:33:44 -03:00
commit 1bfb808c79
77 changed files with 10675 additions and 0 deletions
+301
View File
@@ -0,0 +1,301 @@
import os
import sys
import threading
import time
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from pathlib import Path
import httpx
# Add agent root to sys.path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from agent.config import load_config, save_config, AgentConfig
from agent.uploader import ChunkUploader
from agent.chunker import compute_file_sha256
class AgentGuiApp:
"""Lightweight Windows GUI control panel for OnEver Drive."""
def __init__(self, root: tk.Tk):
self.root = root
self.root.title("OnEver Drive — Agente Windows")
self.root.geometry("640x520")
self.root.resizable(False, False)
# Style configuration
self.config = load_config()
self._apply_dark_theme()
# Build UI layout
self._build_header()
self._build_tabs()
self._refresh_status()
def _apply_dark_theme(self):
self.root.configure(bg="#0F172A")
style = ttk.Style()
style.theme_use("clam")
# Configure colors
style.configure("TNotebook", background="#0F172A", borderwidth=0)
style.configure("TNotebook.Tab", background="#1E293B", foreground="#94A3B8", padding=[16, 8], font=("Segoe UI", 9, "bold"))
style.map("TNotebook.Tab", background=[("selected", "#06B6D4")], foreground=[("selected", "#FFFFFF")])
style.configure("TFrame", background="#0F172A")
style.configure("Card.TFrame", background="#1E293B", relief="flat")
style.configure("TLabel", background="#0F172A", foreground="#F8FAFC", font=("Segoe UI", 9))
style.configure("Card.TLabel", background="#1E293B", foreground="#F8FAFC", font=("Segoe UI", 9))
style.configure("Dim.TLabel", background="#1E293B", foreground="#94A3B8", font=("Segoe UI", 8))
style.configure("Header.TLabel", background="#0F172A", foreground="#FFFFFF", font=("Segoe UI", 12, "bold"))
style.configure("Primary.TButton", background="#06B6D4", foreground="#FFFFFF", font=("Segoe UI", 9, "bold"), borderwidth=0, padding=6)
style.map("Primary.TButton", background=[("active", "#0891B2")])
style.configure("Secondary.TButton", background="#334155", foreground="#FFFFFF", font=("Segoe UI", 9), borderwidth=0, padding=6)
style.map("Secondary.TButton", background=[("active", "#475569")])
style.configure("TProgressbar", thickness=10, background="#06B6D4", troughcolor="#334155", borderwidth=0)
def _build_header(self):
header_frame = ttk.Frame(self.root, padding=16)
header_frame.pack(fill="x")
title_lbl = ttk.Label(header_frame, text="OnEver Drive — Agente Windows", style="Header.TLabel")
title_lbl.pack(side="left")
self.status_badge = tk.Label(header_frame, text="● En Línea", bg="#065F46", fg="#34D399", font=("Segoe UI", 8, "bold"), padx=8, pady=3)
self.status_badge.pack(side="right")
def _build_tabs(self):
self.notebook = ttk.Notebook(self.root)
self.notebook.pack(fill="both", expand=True, padx=16, pady=(0, 16))
# Tab 1: Estado y Dispositivo
self.tab_status = ttk.Frame(self.notebook, padding=16)
self.notebook.add(self.tab_status, text="Estado")
self._build_tab_status()
# Tab 2: Trabajos de Backup
self.tab_jobs = ttk.Frame(self.notebook, padding=16)
self.notebook.add(self.tab_jobs, text="Trabajos Asignados")
self._build_tab_jobs()
# Tab 3: Respaldo Manual
self.tab_manual = ttk.Frame(self.notebook, padding=16)
self.notebook.add(self.tab_manual, text="Backup Manual")
self._build_tab_manual()
# Tab 4: Registro / Configuración
self.tab_config = ttk.Frame(self.notebook, padding=16)
self.notebook.add(self.tab_config, text="Configuración")
self._build_tab_config()
def _build_tab_status(self):
card = ttk.Frame(self.tab_status, style="Card.TFrame", padding=16)
card.pack(fill="both", expand=True)
self.lbl_client_code = ttk.Label(card, text="Cliente ID: —", style="Card.TLabel", font=("Segoe UI", 10, "bold"))
self.lbl_client_code.pack(anchor="w", pady=(0, 4))
self.lbl_client_name = ttk.Label(card, text="Nombre: —", style="Dim.TLabel")
self.lbl_client_name.pack(anchor="w", pady=2)
self.lbl_server_url = ttk.Label(card, text="Servidor: —", style="Dim.TLabel")
self.lbl_server_url.pack(anchor="w", pady=2)
self.lbl_device_id = ttk.Label(card, text="Device ID: —", style="Dim.TLabel")
self.lbl_device_id.pack(anchor="w", pady=2)
ttk.Separator(card).pack(fill="x", pady=12)
ttk.Label(card, text="Motor de Transferencia:", style="Card.TLabel", font=("Segoe UI", 9, "bold")).pack(anchor="w")
ttk.Label(card, text="• Transferencia por bloques de 4 MB\n• Reanudación automática ante microcortes\n• Verificación estricta de integridad SHA-256\n• Detección de archivos en uso (Locks de SQL Server)", style="Dim.TLabel").pack(anchor="w", pady=6)
btn_refresh = ttk.Button(card, text="Actualizar Estado", style="Secondary.TButton", command=self._refresh_status)
btn_refresh.pack(anchor="e", pady=(12, 0))
def _build_tab_jobs(self):
self.jobs_container = ttk.Frame(self.tab_jobs, style="Card.TFrame", padding=12)
self.jobs_container.pack(fill="both", expand=True)
self.jobs_list_lbl = ttk.Label(self.jobs_container, text="Cargando trabajos asignados por el servidor...", style="Dim.TLabel")
self.jobs_list_lbl.pack(anchor="w", pady=10)
def _build_tab_manual(self):
card = ttk.Frame(self.tab_manual, style="Card.TFrame", padding=16)
card.pack(fill="both", expand=True)
ttk.Label(card, text="Selecciona un archivo local para realizar un backup por chunks:", style="Card.TLabel").pack(anchor="w", pady=(0, 8))
file_frame = ttk.Frame(card, style="Card.TFrame")
file_frame.pack(fill="x", pady=4)
self.entry_file = tk.Entry(file_frame, bg="#0F172A", fg="#FFFFFF", insertbackground="#FFFFFF", relief="flat", font=("Segoe UI", 9))
self.entry_file.pack(side="left", fill="x", expand=True, ipady=6, padx=(0, 8))
btn_browse = ttk.Button(file_frame, text="Explorar...", style="Secondary.TButton", command=self._browse_file)
btn_browse.pack(side="right")
self.btn_upload = ttk.Button(card, text="Iniciar Backup Inmediato", style="Primary.TButton", command=self._start_manual_backup)
self.btn_upload.pack(fill="x", pady=16)
self.lbl_progress = ttk.Label(card, text="Estado: En espera", style="Dim.TLabel")
self.lbl_progress.pack(anchor="w", pady=(0, 4))
self.progressbar = ttk.Progressbar(card, style="TProgressbar", mode="determinate")
self.progressbar.pack(fill="x", pady=(0, 8))
def _build_tab_config(self):
card = ttk.Frame(self.tab_config, style="Card.TFrame", padding=16)
card.pack(fill="both", expand=True)
ttk.Label(card, text="Servidor Central (URL):", style="Card.TLabel").pack(anchor="w")
self.entry_server = tk.Entry(card, bg="#0F172A", fg="#FFFFFF", insertbackground="#FFFFFF", relief="flat", font=("Segoe UI", 9))
self.entry_server.insert(0, self.config.server_url)
self.entry_server.pack(fill="x", ipady=6, pady=(4, 12))
ttk.Label(card, text="Código de Registro (generado en Web UI):", style="Card.TLabel").pack(anchor="w")
self.entry_reg_code = tk.Entry(card, bg="#0F172A", fg="#FFFFFF", insertbackground="#FFFFFF", relief="flat", font=("Segoe UI", 9))
self.entry_reg_code.pack(fill="x", ipady=6, pady=(4, 16))
btn_register = ttk.Button(card, text="Registrar Dispositivo", style="Primary.TButton", command=self._register_device)
btn_register.pack(fill="x")
def _browse_file(self):
filename = filedialog.askopenfilename(title="Seleccionar archivo para backup", filetypes=[("Archivos SQL Server / Todos", "*.bak;*.mdf;*.*")])
if filename:
self.entry_file.delete(0, tk.END)
self.entry_file.insert(0, filename)
def _start_manual_backup(self):
filepath_str = self.entry_file.get().strip()
if not filepath_str or not os.path.exists(filepath_str):
messagebox.showerror("Error", "Por favor selecciona un archivo existente.")
return
if not self.config.device_id:
messagebox.showerror("Error", "El agente no está registrado contra el servidor.")
return
self.btn_upload.configure(state="disabled")
self.lbl_progress.configure(text="Iniciando subida por bloques...")
self.progressbar["value"] = 0
def worker():
try:
filepath = Path(filepath_str)
uploader = ChunkUploader(self.config)
def on_progress(done, total, pct):
self.root.after(0, lambda: self._update_progress_ui(done, total, pct, filepath.name))
res = uploader.upload_file(filepath, progress_callback=on_progress)
self.root.after(0, lambda: self._on_backup_success(filepath.name, res))
except Exception as ex:
self.root.after(0, lambda: self._on_backup_error(str(ex)))
threading.Thread(target=worker, daemon=True).start()
def _update_progress_ui(self, done, total, pct, filename):
self.progressbar["value"] = pct
self.lbl_progress.configure(text=f"Subiendo {filename}: {done}/{total} chunks ({pct:.1f}%)")
def _on_backup_success(self, filename, res):
self.btn_upload.configure(state="normal")
self.progressbar["value"] = 100
self.lbl_progress.configure(text=f"✓ Backup completado y verificado: {filename}")
messagebox.showinfo("Éxito", f"¡Backup completado con éxito!\n\nArchivo: {filename}\nSHA-256: {res.get('sha256')}\nRuta remota: {res.get('relative_path')}")
def _on_backup_error(self, err_msg):
self.btn_upload.configure(state="normal")
self.lbl_progress.configure(text=f"✗ Error: {err_msg}")
messagebox.showerror("Error de Backup", f"Fallo al respaldar archivo:\n{err_msg}")
def _register_device(self):
server_url = self.entry_server.get().strip().rstrip("/")
code = self.entry_reg_code.get().strip().upper()
if not server_url or not code:
messagebox.showerror("Error", "Debes ingresar la URL del servidor y el código de registro.")
return
try:
import socket, platform
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:
messagebox.showerror("Error de Registro", f"El servidor respondió: {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)
messagebox.showinfo("Registro Exitoso", f"¡Dispositivo registrado!\nCliente: {data['client_code']}\nID: {data['device_id']}")
self._refresh_status()
except Exception as ex:
messagebox.showerror("Error de Conexión", f"No se pudo conectar al servidor:\n{str(ex)}")
def _refresh_status(self):
self.config = load_config()
if self.config.client_code:
self.lbl_client_code.configure(text=f"Cliente ID: {self.config.client_code}")
self.lbl_client_name.configure(text=f"Nombre: {self.config.client_name or ''}")
self.lbl_server_url.configure(text=f"Servidor: {self.config.server_url}")
self.lbl_device_id.configure(text=f"Device ID: {self.config.device_id}")
self.status_badge.configure(text="● Registrado", bg="#065F46", fg="#34D399")
self._fetch_jobs()
else:
self.lbl_client_code.configure(text="Cliente ID: No Registrado")
self.status_badge.configure(text="● Sin Registro", bg="#7F1D1D", fg="#FCA5A5")
def _fetch_jobs(self):
def worker():
try:
headers = {"X-Device-Id": self.config.device_id, "X-Device-Token": self.config.device_token}
with httpx.Client(base_url=self.config.server_url, headers=headers, timeout=10.0) as client:
resp = client.get("/api/jobs/agent/assigned")
if resp.status_code == 200:
jobs = resp.json()
self.root.after(0, lambda: self._render_jobs_list(jobs))
except Exception:
pass
threading.Thread(target=worker, daemon=True).start()
def _render_jobs_list(self, jobs):
for widget in self.jobs_container.winfo_children():
widget.destroy()
if not jobs:
ttk.Label(self.jobs_container, text="No hay trabajos programados asignados a este equipo.", style="Dim.TLabel").pack(pady=20)
return
for j in jobs:
item_frame = ttk.Frame(self.jobs_container, style="Card.TFrame", padding=8)
item_frame.pack(fill="x", pady=4)
ttk.Label(item_frame, text=f"[{j['job_code']}] {j['name']}", style="Card.TLabel", font=("Segoe UI", 9, "bold")).pack(anchor="w")
ttk.Label(item_frame, text=f"Ruta: {j['source_path']} | Filtros: {j['file_patterns']} | Cron: {j['schedule_cron']}", style="Dim.TLabel").pack(anchor="w")
def launch_gui():
root = tk.Tk()
app = AgentGuiApp(root)
root.mainloop()
if __name__ == "__main__":
launch_gui()