302 lines
14 KiB
Python
302 lines
14 KiB
Python
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()
|