Files
IPSExtractor/bots/bot_descarga/bot_descarga/ui/dashboard.py
T

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