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()