357 lines
12 KiB
Python
357 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
import socket
|
|
import sys
|
|
import threading
|
|
|
|
import customtkinter as ctk
|
|
|
|
from bot_descarga.ui.settings_store import (
|
|
DEFAULT_API_URL,
|
|
load_gui_settings,
|
|
normalize_api_url,
|
|
save_gui_settings,
|
|
)
|
|
from bot_descarga.ui.windows_autostart import (
|
|
disable_autostart,
|
|
enable_autostart,
|
|
is_autostart_enabled,
|
|
)
|
|
from bot_descarga.worker.agent import WorkerAgent
|
|
|
|
ctk.set_appearance_mode("dark")
|
|
ctk.set_default_color_theme("blue")
|
|
|
|
|
|
class BotDashboard(ctk.CTk):
|
|
"""GUI del agent: configura API remota y conecta como worker idle."""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
self.title("Bot de Descargas PDF — Agent")
|
|
self.geometry("920x720")
|
|
self.resizable(False, False)
|
|
|
|
self._agent: WorkerAgent | None = None
|
|
self._agent_thread: threading.Thread | None = None
|
|
self._connected = False
|
|
|
|
self._construir_ui()
|
|
self._cargar_settings()
|
|
|
|
self.protocol("WM_DELETE_WINDOW", self._on_close)
|
|
|
|
# Tras login de Windows: si está marcado, conectar solo
|
|
if bool(load_gui_settings().get("auto_connect")):
|
|
self.after(800, self._maybe_auto_connect)
|
|
|
|
def _construir_ui(self):
|
|
root = ctk.CTkFrame(self, corner_radius=18)
|
|
root.pack(fill="both", expand=True, padx=18, pady=18)
|
|
|
|
ctk.CTkLabel(
|
|
root,
|
|
text="Bot de Descargas PDF",
|
|
font=ctk.CTkFont(size=26, weight="bold"),
|
|
).pack(pady=(18, 4))
|
|
|
|
ctk.CTkLabel(
|
|
root,
|
|
text="Agent remoto — idle hasta que el desktop asigne un Excel a este bot",
|
|
text_color="gray",
|
|
font=ctk.CTkFont(size=13),
|
|
).pack(pady=(0, 18))
|
|
|
|
config = ctk.CTkFrame(root, corner_radius=14)
|
|
config.pack(fill="x", padx=24, pady=(0, 14))
|
|
|
|
ctk.CTkLabel(
|
|
config,
|
|
text="API remota (IPSExtractor)",
|
|
font=ctk.CTkFont(size=16, weight="bold"),
|
|
).pack(anchor="w", padx=18, pady=(14, 8))
|
|
|
|
row_api = ctk.CTkFrame(config, fg_color="transparent")
|
|
row_api.pack(fill="x", padx=18, pady=(0, 8))
|
|
ctk.CTkLabel(row_api, text="URL API", width=90, anchor="w").pack(side="left")
|
|
self.entry_api = ctk.CTkEntry(
|
|
row_api,
|
|
placeholder_text=DEFAULT_API_URL,
|
|
)
|
|
self.entry_api.pack(side="left", fill="x", expand=True)
|
|
|
|
row_id = ctk.CTkFrame(config, fg_color="transparent")
|
|
row_id.pack(fill="x", padx=18, pady=(0, 8))
|
|
ctk.CTkLabel(row_id, text="Worker ID", width=90, anchor="w").pack(side="left")
|
|
self.entry_worker = ctk.CTkEntry(
|
|
row_id,
|
|
placeholder_text=socket.gethostname() or "PC-01",
|
|
)
|
|
self.entry_worker.pack(side="left", fill="x", expand=True)
|
|
|
|
row_token = ctk.CTkFrame(config, fg_color="transparent")
|
|
row_token.pack(fill="x", padx=18, pady=(0, 8))
|
|
ctk.CTkLabel(row_token, text="Token", width=90, anchor="w").pack(side="left")
|
|
self.entry_token = ctk.CTkEntry(
|
|
row_token,
|
|
placeholder_text="BOT_API_TOKEN",
|
|
show="•",
|
|
)
|
|
self.entry_token.pack(side="left", fill="x", expand=True, padx=(0, 8))
|
|
self._token_visible = False
|
|
ctk.CTkButton(
|
|
row_token,
|
|
text="👁",
|
|
width=40,
|
|
command=self._toggle_token,
|
|
).pack(side="left")
|
|
|
|
row_auto = ctk.CTkFrame(config, fg_color="transparent")
|
|
row_auto.pack(fill="x", padx=18, pady=(0, 14))
|
|
self.chk_autostart = ctk.CTkCheckBox(
|
|
row_auto,
|
|
text="Iniciar con Windows",
|
|
command=self._on_autostart_toggle,
|
|
)
|
|
self.chk_autostart.pack(side="left", padx=(0, 18))
|
|
self.chk_auto_connect = ctk.CTkCheckBox(
|
|
row_auto,
|
|
text="Conectar al abrir",
|
|
command=self._on_auto_connect_toggle,
|
|
)
|
|
self.chk_auto_connect.pack(side="left")
|
|
if sys.platform != "win32":
|
|
self.chk_autostart.configure(state="disabled")
|
|
|
|
status = ctk.CTkFrame(root, corner_radius=14)
|
|
status.pack(fill="x", padx=24, pady=(0, 14))
|
|
|
|
self.lbl_estado = ctk.CTkLabel(
|
|
status,
|
|
text="Estado: desconectado",
|
|
font=ctk.CTkFont(size=15, weight="bold"),
|
|
)
|
|
self.lbl_estado.pack(anchor="w", padx=18, pady=(14, 2))
|
|
|
|
self.lbl_hint = ctk.CTkLabel(
|
|
status,
|
|
text="Configura la API y pulsa Conectar. Este PC no expone puertos.",
|
|
text_color="gray",
|
|
font=ctk.CTkFont(size=13),
|
|
)
|
|
self.lbl_hint.pack(anchor="w", padx=18, pady=(0, 14))
|
|
|
|
botones = ctk.CTkFrame(root, fg_color="transparent")
|
|
botones.pack(fill="x", padx=24, pady=(0, 14))
|
|
|
|
self.btn_connect = ctk.CTkButton(
|
|
botones,
|
|
text="▶ Conectar agent",
|
|
height=42,
|
|
width=180,
|
|
font=ctk.CTkFont(size=14, weight="bold"),
|
|
command=self._toggle_connect,
|
|
)
|
|
self.btn_connect.pack(side="left", padx=(0, 10))
|
|
|
|
ctk.CTkButton(
|
|
botones,
|
|
text="Guardar config",
|
|
height=42,
|
|
width=140,
|
|
command=self._guardar_settings,
|
|
).pack(side="left", padx=(0, 10))
|
|
|
|
ctk.CTkButton(
|
|
botones,
|
|
text="Limpiar logs",
|
|
height=42,
|
|
width=130,
|
|
command=self._limpiar_logs,
|
|
).pack(side="left")
|
|
|
|
logs_frame = ctk.CTkFrame(root, corner_radius=14)
|
|
logs_frame.pack(fill="both", expand=True, padx=24, pady=(0, 18))
|
|
|
|
ctk.CTkLabel(
|
|
logs_frame,
|
|
text="Detalle del agent",
|
|
font=ctk.CTkFont(size=15, weight="bold"),
|
|
).pack(anchor="w", padx=18, pady=(12, 6))
|
|
|
|
self.text_logs = ctk.CTkTextbox(
|
|
logs_frame, font=ctk.CTkFont(family="Courier", size=12)
|
|
)
|
|
self.text_logs.pack(fill="both", expand=True, padx=18, pady=(0, 16))
|
|
|
|
def _persist_flags(self):
|
|
save_gui_settings(
|
|
normalize_api_url(self.entry_api.get()),
|
|
self.entry_worker.get(),
|
|
self.entry_token.get(),
|
|
autostart_windows=bool(self.chk_autostart.get()),
|
|
auto_connect=bool(self.chk_auto_connect.get()),
|
|
)
|
|
|
|
def _cargar_settings(self):
|
|
s = load_gui_settings()
|
|
self.entry_api.insert(0, s.get("api_url") or DEFAULT_API_URL)
|
|
self.entry_worker.insert(0, s.get("worker_id") or socket.gethostname())
|
|
self.entry_token.insert(0, s.get("token") or "")
|
|
|
|
want_autostart = bool(s.get("autostart_windows"))
|
|
if want_autostart and sys.platform == "win32":
|
|
try:
|
|
if not is_autostart_enabled():
|
|
enable_autostart()
|
|
self.chk_autostart.select()
|
|
except Exception as e:
|
|
self._log(f"No se pudo restaurar autoarranque: {e}")
|
|
elif is_autostart_enabled():
|
|
# Script presente aunque el flag esté off → marcar checkbox
|
|
self.chk_autostart.select()
|
|
|
|
if bool(s.get("auto_connect")):
|
|
self.chk_auto_connect.select()
|
|
|
|
def _guardar_settings(self):
|
|
api = normalize_api_url(self.entry_api.get())
|
|
self.entry_api.delete(0, "end")
|
|
self.entry_api.insert(0, api)
|
|
self._persist_flags()
|
|
self._log("Configuración guardada en data/gui_settings.json")
|
|
|
|
def _on_autostart_toggle(self):
|
|
if sys.platform != "win32":
|
|
return
|
|
enabled = bool(self.chk_autostart.get())
|
|
try:
|
|
if enabled:
|
|
path = enable_autostart()
|
|
self._log(f"Autoarranque activado: {path}")
|
|
# Tiene sentido conectar al abrir si arranca con Windows
|
|
if not self.chk_auto_connect.get():
|
|
self.chk_auto_connect.select()
|
|
else:
|
|
disable_autostart()
|
|
self._log("Autoarranque desactivado.")
|
|
self._persist_flags()
|
|
except Exception as e:
|
|
self._log(f"Error configurando autoarranque: {e}")
|
|
if enabled:
|
|
self.chk_autostart.deselect()
|
|
else:
|
|
self.chk_autostart.select()
|
|
|
|
def _on_auto_connect_toggle(self):
|
|
self._persist_flags()
|
|
|
|
def _maybe_auto_connect(self):
|
|
if self._connected:
|
|
return
|
|
if not self.entry_worker.get().strip() or not self.entry_token.get().strip():
|
|
self._log("Auto-conectar omitido: falta Worker ID o token.")
|
|
return
|
|
self._log("Conectando automáticamente…")
|
|
self._conectar()
|
|
|
|
def _toggle_token(self):
|
|
self._token_visible = not self._token_visible
|
|
self.entry_token.configure(show="" if self._token_visible else "•")
|
|
|
|
def _toggle_connect(self):
|
|
if self._connected:
|
|
self._desconectar()
|
|
else:
|
|
self._conectar()
|
|
|
|
def _conectar(self):
|
|
api = normalize_api_url(self.entry_api.get())
|
|
worker_id = self.entry_worker.get().strip()
|
|
token = self.entry_token.get().strip()
|
|
|
|
if not worker_id:
|
|
self._log("Debes indicar un Worker ID.")
|
|
return
|
|
if not token:
|
|
self._log("Debes indicar el token (BOT_API_TOKEN).")
|
|
return
|
|
|
|
self.entry_api.delete(0, "end")
|
|
self.entry_api.insert(0, api)
|
|
self._persist_flags()
|
|
|
|
self._agent = WorkerAgent(
|
|
api_url=api,
|
|
worker_id=worker_id,
|
|
token=token,
|
|
log_func=self._log,
|
|
)
|
|
self._connected = True
|
|
self.btn_connect.configure(text="⏹ Desconectar", fg_color="#c0392b", hover_color="#a93226")
|
|
self.lbl_estado.configure(text=f"Estado: conectado como {worker_id}")
|
|
self.lbl_hint.configure(text=f"API: {api} — esperando jobs del desktop…")
|
|
self._set_config_enabled(False)
|
|
|
|
self._agent_thread = threading.Thread(
|
|
target=self._run_agent,
|
|
daemon=True,
|
|
)
|
|
self._agent_thread.start()
|
|
self._log(f"Conectando a {api}…")
|
|
|
|
def _run_agent(self):
|
|
try:
|
|
assert self._agent is not None
|
|
self._agent.run_forever()
|
|
except Exception as e:
|
|
self._log(f"Error del agent: {e}")
|
|
finally:
|
|
self.after(0, self._on_agent_stopped)
|
|
|
|
def _desconectar(self):
|
|
if self._agent:
|
|
self._agent.stop()
|
|
self._log("Solicitando desconexión…")
|
|
self.btn_connect.configure(state="disabled", text="Desconectando…")
|
|
|
|
def _on_agent_stopped(self):
|
|
self._connected = False
|
|
self._agent = None
|
|
self.btn_connect.configure(
|
|
state="normal",
|
|
text="▶ Conectar agent",
|
|
fg_color=["#3B8ED0", "#1F6AA5"],
|
|
hover_color=["#36719F", "#144870"],
|
|
)
|
|
self.lbl_estado.configure(text="Estado: desconectado")
|
|
self.lbl_hint.configure(
|
|
text="Configura la API y pulsa Conectar. Este PC no expone puertos."
|
|
)
|
|
self._set_config_enabled(True)
|
|
|
|
def _set_config_enabled(self, enabled: bool):
|
|
state = "normal" if enabled else "disabled"
|
|
self.entry_api.configure(state=state)
|
|
self.entry_worker.configure(state=state)
|
|
self.entry_token.configure(state=state)
|
|
if sys.platform == "win32":
|
|
self.chk_autostart.configure(state="normal")
|
|
self.chk_auto_connect.configure(state="normal")
|
|
|
|
def _limpiar_logs(self):
|
|
self.text_logs.delete("1.0", "end")
|
|
|
|
def _log(self, mensaje: str):
|
|
def _escribir():
|
|
self.text_logs.insert("end", mensaje + "\n")
|
|
self.text_logs.see("end")
|
|
|
|
self.after(0, _escribir)
|
|
|
|
def _on_close(self):
|
|
if self._agent:
|
|
self._agent.stop()
|
|
self.destroy()
|