Proyecto acabado

This commit is contained in:
Luka 2026-02-27 12:21:17 +01:00
parent aa9919e714
commit cd15c68b4e
7 changed files with 467 additions and 26 deletions

Binary file not shown.

View File

@ -1,6 +1,7 @@
# config.py # config.py
import os import os
import psutil import psutil
import uuid # NUEVO: Para identificar cada instancia del chat
# --- Rutas y Archivos --- # --- Rutas y Archivos ---
SCRIPT_NAME = "backup_script.sh" SCRIPT_NAME = "backup_script.sh"
@ -9,25 +10,29 @@ SCRIPT_PATH = os.path.join(BASE_DIR, SCRIPT_NAME)
archivo_registro_csv = os.path.join(BASE_DIR, "data", "registro_recursos.csv") archivo_registro_csv = os.path.join(BASE_DIR, "data", "registro_recursos.csv")
PROGRESS_FILE = 'progress.tmp' PROGRESS_FILE = 'progress.tmp'
# NUEVO: Ruta de guardado de JSON y Carpeta de alarmas
ALARM_SAVE_FILE = os.path.join(BASE_DIR, "data", "alarmas.json") ALARM_SAVE_FILE = os.path.join(BASE_DIR, "data", "alarmas.json")
ALARM_FOLDER = os.path.join(BASE_DIR, "data", "alarmas") ALARM_FOLDER = os.path.join(BASE_DIR, "data", "alarmas")
ALERTA_SOUND_FILE = None # Almacenará la ruta del archivo de sonido seleccionado por el usuario ALERTA_SOUND_FILE = None
# Rutas de Scraping
SCRAPING_FOLDER = os.path.join(BASE_DIR, "data", "scraping") SCRAPING_FOLDER = os.path.join(BASE_DIR, "data", "scraping")
SCRAPING_CONFIG_FOLDER = os.path.join(BASE_DIR, "data", "tipo_scraping") SCRAPING_CONFIG_FOLDER = os.path.join(BASE_DIR, "data", "tipo_scraping")
# NUEVO: Carpeta específica para el Bloc de Notas
NOTES_FOLDER = os.path.join(BASE_DIR, "data", "notas") NOTES_FOLDER = os.path.join(BASE_DIR, "data", "notas")
# --- NUEVO: Configuración de Chat Local (IPC) ---
CHAT_FILE = os.path.join(BASE_DIR, "data", "local_chat.txt")
INSTANCE_ID = str(uuid.uuid4())[:5] # ID único corto para esta ventana/proceso
# --- NUEVO: Configuración de Servidor de Correo ---
EMAIL_SERVER_IP = "10.10.0.101"
EMAIL_SMTP_PORT = 25
EMAIL_IMAP_PORT = 143
EMAIL_POP_PORT = 110
# --- Variables de Monitoreo --- # --- Variables de Monitoreo ---
MAX_PUNTOS = 30 MAX_PUNTOS = 30
tiempos = list(range(-MAX_PUNTOS + 1, 1)) tiempos = list(range(-MAX_PUNTOS + 1, 1))
num_cores = psutil.cpu_count(logical=True) num_cores = psutil.cpu_count(logical=True)
datos_cores = [0] * num_cores datos_cores = [0] * num_cores
# --- Datos Dinámicos (Inicialización) ---
datos_cpu = [0] * MAX_PUNTOS datos_cpu = [0] * MAX_PUNTOS
datos_mem = [0] * MAX_PUNTOS datos_mem = [0] * MAX_PUNTOS
datos_net_sent = [0] * MAX_PUNTOS datos_net_sent = [0] * MAX_PUNTOS
@ -41,32 +46,26 @@ registro_csv_activo = False
system_log = None system_log = None
progress_bar = None progress_bar = None
editor_texto = None editor_texto = None
scraping_progress_bar = None # Barra de progreso de scraping scraping_progress_bar = None
scraping_output_text = None # Área de texto de salida de scraping scraping_output_text = None
scraping_url_input = None # Variable de control de la URL de scraping scraping_url_input = None
scraping_selector_input = None # Entrada para el selector CSS scraping_selector_input = None
scraping_attr_input = None # Entrada para el atributo CSS scraping_attr_input = None
scraping_config_file_label = None # Label para mostrar el archivo de configuración cargado scraping_config_file_label = None
scraping_config_data = {} # Diccionario para almacenar la configuración JSON de scraping scraping_config_data = {}
scraping_running = False # Bandera de estado de ejecución de scraping scraping_running = False
# Variables de Alarma
alarmas_programadas = {} alarmas_programadas = {}
alarma_counter = 0 alarma_counter = 0
# Control de Sonido
alarma_volumen = 0.5 alarma_volumen = 0.5
alarma_sonando = False alarma_sonando = False
# Control de Juegos (NUEVO) juego_window = None
juego_window = None # Referencia a la ventana Toplevel del juego juego_running = False
juego_running = False # Bandera de estado del juego
# Control de Música Adicional (NUEVO)
current_music_file = None current_music_file = None
music_sonando = False music_sonando = False
# Variables de UI
label_hostname = None label_hostname = None
label_os_info = None label_os_info = None
label_cpu_model = None label_cpu_model = None
@ -75,6 +74,6 @@ label_disk_total = None
label_net_info = None label_net_info = None
label_uptime = None label_uptime = None
label_1 = None # Estado Backup label_1 = None
label_2 = None # Estado Registro CSV label_2 = None
label_fecha_hora = None label_fecha_hora = None

0
data/local_chat.txt Normal file
View File

View File

@ -15,6 +15,16 @@ import pygame
import json import json
import random import random
import socket
import sys
import poplib
import smtplib
import imaplib
import email
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import decode_header
# Importaciones directas de módulos (Acceso con el prefijo del módulo) # Importaciones directas de módulos (Acceso con el prefijo del módulo)
import config import config
import monitor_manager import monitor_manager
@ -961,3 +971,243 @@ def simular_juego_camellos(root):
log_event("Simulación de Carrera de Camellos iniciada.") log_event("Simulación de Carrera de Camellos iniciada.")
juego_window.after(100, avanzar_carrera) # Iniciar la simulación juego_window.after(100, avanzar_carrera) # Iniciar la simulación
# ===============================================
# Funcionalidades de Chat Local (IPC)
# ===============================================
def inicializar_chat():
"""Usa un socket local para garantizar con 100% de precisión si somos la primera instancia."""
try:
# Creamos un socket de red local
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Intentamos ocupar el puerto 54321 de forma exclusiva
s.bind(("127.0.0.1", 54321))
# Si llegamos aquí sin dar error, SOMOS LA ÚNICA VENTANA ABIERTA.
# Guardamos el socket en config para mantener la "puerta" bloqueada mientras la app viva.
config.instancia_socket = s
# Como somos la primera instancia, BORRAMOS EL CHAT SIN PIEDAD
if os.path.exists(config.CHAT_FILE):
os.remove(config.CHAT_FILE)
log_event("Primera instancia detectada: Historial de chat limpiado al 100%.")
except socket.error:
# Si el puerto ya está ocupado, significa que YA TIENES otra ventana abierta.
# Por lo tanto, NO borramos el chat, simplemente nos unimos.
log_event("Segunda instancia detectada: Uniéndose al chat existente.")
except Exception as e:
log_event(f"Error inicializando chat: {e}")
def enviar_mensaje_chat(entrada_widget, alias_var):
"""Escribe un nuevo mensaje en el archivo usando el alias personalizado."""
mensaje = entrada_widget.get().strip()
if not mensaje:
return
entrada_widget.delete(0, tk.END)
os.makedirs(os.path.dirname(config.CHAT_FILE), exist_ok=True)
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
# Usar el alias del usuario o un valor por defecto si lo deja en blanco
alias = alias_var.get().strip() or f"User-{config.INSTANCE_ID}"
linea = f"[{timestamp}] {alias}: {mensaje}\n"
try:
with open(config.CHAT_FILE, "a", encoding="utf-8") as f:
f.write(linea)
except Exception as e:
log_event(f"ERROR escribiendo en el chat: {e}")
def monitor_chat_file(text_widget, root):
"""Hilo que vigila cambios en el archivo de chat."""
last_size = 0
if not os.path.exists(config.CHAT_FILE):
os.makedirs(os.path.dirname(config.CHAT_FILE), exist_ok=True)
open(config.CHAT_FILE, 'a').close()
while config.monitor_running:
if not root.winfo_exists(): break
try:
current_size = os.path.getsize(config.CHAT_FILE)
if current_size > last_size:
with open(config.CHAT_FILE, "r", encoding="utf-8") as f:
f.seek(last_size)
nuevos_mensajes = f.read()
last_size = current_size
if nuevos_mensajes:
root.after(0, text_widget.config, {"state": tk.NORMAL})
root.after(0, text_widget.insert, tk.END, nuevos_mensajes)
root.after(0, text_widget.see, tk.END)
root.after(0, text_widget.config, {"state": tk.DISABLED})
elif current_size < last_size:
# Si el archivo se redujo (se reinició la app), reseteamos el lector
last_size = 0
root.after(0, text_widget.config, {"state": tk.NORMAL})
root.after(0, lambda: text_widget.delete('1.0', tk.END))
root.after(0, text_widget.config, {"state": tk.DISABLED})
except Exception:
pass
time.sleep(0.5)
def enviar_correo(usuario, password, destinatario, asunto, cuerpo, root):
"""Envía un correo manejando correctamente los servidores Relay locales."""
def tarea():
try:
msg = MIMEMultipart()
msg['From'] = usuario
msg['To'] = destinatario
msg['Subject'] = asunto
msg.attach(MIMEText(cuerpo, 'plain'))
server = smtplib.SMTP(config.EMAIL_SERVER_IP, config.EMAIL_SMTP_PORT, timeout=10)
try:
server.starttls()
except Exception:
pass
# --- EL TRUCO ESTÁ AQUÍ ---
try:
server.login(usuario, password)
except smtplib.SMTPAuthenticationError:
# El error 535 salta aquí. Lo silenciamos porque el puerto 25
# local seguramente nos deje enviar el mensaje sin login.
pass
except smtplib.SMTPNotSupportedError:
pass
# Enviamos el mensaje directamente
server.send_message(msg)
server.quit()
if root.winfo_exists():
root.after(0, lambda: messagebox.showinfo("Éxito", "Correo enviado correctamente."))
root.after(0, lambda: log_event(f"Correo enviado a {destinatario}"))
except Exception as e:
error_msg = str(e)
if root.winfo_exists():
root.after(0, lambda err=error_msg: messagebox.showerror("Error SMTP", f"No se pudo enviar: {err}"))
root.after(0, lambda err=error_msg: log_event(f"Error SMTP: {err}"))
threading.Thread(target=tarea, daemon=True).start()
def cargar_correos(usuario, password, treeview, root):
"""Descarga los correos mediante IMAP en un hilo separado, asegurando el cierre de conexión."""
def tarea():
mail = None
try:
if root.winfo_exists():
root.after(0, lambda: treeview.delete(*treeview.get_children()))
mail = imaplib.IMAP4(config.EMAIL_SERVER_IP, config.EMAIL_IMAP_PORT)
mail.login(usuario, password)
mail.select('inbox')
status, messages = mail.search(None, 'ALL')
if messages[0]: # Solo procesar si hay correos
email_ids = messages[0].split()
for e_id in email_ids[-10:]:
_, msg_data = mail.fetch(e_id, '(RFC822)')
for response_part in msg_data:
if isinstance(response_part, tuple):
msg = email.message_from_bytes(response_part[1])
asunto_header = msg.get("Subject", "Sin Asunto")
asunto, encoding = decode_header(asunto_header)[0]
if isinstance(asunto, bytes):
asunto = asunto.decode(encoding if encoding else 'utf-8')
remitente = msg.get("From", "Desconocido")
if root.winfo_exists():
root.after(0, lambda eid=e_id, rem=remitente, asu=asunto: treeview.insert('', 0, values=(eid.decode(), rem, asu)))
if root.winfo_exists():
root.after(0, lambda: log_event("Bandeja de entrada actualizada vía IMAP."))
except Exception as e:
error_msg = str(e)
if root.winfo_exists():
root.after(0, lambda err=error_msg: messagebox.showerror("Error IMAP", f"Fallo al conectar: {err}"))
finally:
# ESTO ES LO MÁS IMPORTANTE: Cierra la sesión pase lo que pase para evitar el Errno 111
if mail is not None:
try:
mail.logout()
except:
pass
threading.Thread(target=tarea, daemon=True).start()
def comprobar_login_correo(usuario, password, login_frame, mail_content_frame, root):
"""Verifica las credenciales por IMAP antes de mostrar la bandeja de correo."""
def tarea():
try:
# Prueba de conexión rápida
mail = imaplib.IMAP4(config.EMAIL_SERVER_IP, config.EMAIL_IMAP_PORT)
mail.login(usuario, password)
mail.logout()
# Si llega aquí, el usuario es válido. Cambiamos las pantallas.
if root.winfo_exists():
root.after(0, lambda: login_frame.pack_forget())
root.after(0, lambda: mail_content_frame.pack(fill=tk.BOTH, expand=True))
root.after(0, lambda: log_event(f"Sesión iniciada correctamente como {usuario}"))
except Exception as e:
error_msg = str(e)
if root.winfo_exists():
root.after(0, lambda err=error_msg: messagebox.showerror("Autenticación Fallida", f"Credenciales incorrectas o servidor caído:\n{err}"))
threading.Thread(target=tarea, daemon=True).start()
def limpiar_chat_al_cerrar():
"""Ya no hace falta borrar al cerrar, se limpiará automáticamente al volver a abrir la primera instancia."""
pass
def cargar_correos(usuario, password, treeview, root):
"""Descarga los correos mediante IMAP en un hilo separado."""
def tarea():
try:
if root.winfo_exists():
root.after(0, lambda: treeview.delete(*treeview.get_children()))
mail = imaplib.IMAP4(config.EMAIL_SERVER_IP, config.EMAIL_IMAP_PORT)
mail.login(usuario, password)
mail.select('inbox')
status, messages = mail.search(None, 'ALL')
email_ids = messages[0].split()
for e_id in email_ids[-10:]:
_, msg_data = mail.fetch(e_id, '(RFC822)')
for response_part in msg_data:
if isinstance(response_part, tuple):
msg = email.message_from_bytes(response_part[1])
asunto, encoding = decode_header(msg["Subject"])[0]
if isinstance(asunto, bytes):
asunto = asunto.decode(encoding if encoding else 'utf-8')
remitente = msg.get("From")
if root.winfo_exists():
# CORRECCIÓN: Envolvemos en un lambda para usar el keyword 'values' sin que Tkinter explote
root.after(0, lambda eid=e_id, rem=remitente, asu=asunto: treeview.insert('', 0, values=(eid.decode(), rem, asu)))
mail.logout()
if root.winfo_exists():
root.after(0, lambda: log_event("Bandeja de entrada actualizada vía IMAP."))
except Exception as e:
error_msg = str(e)
if root.winfo_exists():
root.after(0, lambda err=error_msg: messagebox.showerror("Error IMAP", f"Fallo al conectar: {err}"))
threading.Thread(target=tarea, daemon=True).start()

View File

@ -52,6 +52,7 @@ def crear_ui_completa(root):
config.monitor_running = False config.monitor_running = False
system_utils.detener_sonido_alarma() system_utils.detener_sonido_alarma()
system_utils.detener_mp3() # Detener música al cerrar system_utils.detener_mp3() # Detener música al cerrar
system_utils.limpiar_chat_al_cerrar()
root.destroy() root.destroy()
root.protocol("WM_DELETE_WINDOW", on_closing) root.protocol("WM_DELETE_WINDOW", on_closing)
@ -120,6 +121,14 @@ def crear_ui_completa(root):
# --- PESTAÑA 6: MÚSICA (NUEVO) --- # --- PESTAÑA 6: MÚSICA (NUEVO) ---
music_tab = ttk.Frame(notebook) music_tab = ttk.Frame(notebook)
notebook.add(music_tab, text="Música 🎵") notebook.add(music_tab, text="Música 🎵")
# --- PESTAÑA 7: CHAT LOCAL (NUEVO) ---
chat_tab = ttk.Frame(notebook)
notebook.add(chat_tab, text="Chat Local 💬")
# --- PESTAÑA 8: CORREO (NUEVO) ---
email_tab = ttk.Frame(notebook)
notebook.add(email_tab, text="Correo 📧")
# --------------------------------------------- # ---------------------------------------------
@ -234,6 +243,9 @@ def crear_ui_completa(root):
# =============================================== # ===============================================
# CONTENIDO DE LA SOLAPA DE ALARMA # CONTENIDO DE LA SOLAPA DE ALARMA
# =============================================== # ===============================================
system_utils.inicializar_chat()
main_alarm_frame = tk.Frame(alarma_tab) main_alarm_frame = tk.Frame(alarma_tab)
main_alarm_frame.pack(fill="both", expand=True) main_alarm_frame.pack(fill="both", expand=True)
@ -674,6 +686,186 @@ def crear_ui_completa(root):
volumen_scale_music.set(config.alarma_volumen * 100) volumen_scale_music.set(config.alarma_volumen * 100)
volumen_scale_music.pack(pady=5) volumen_scale_music.pack(pady=5)
## ===============================================
# CONTENIDO DE LA SOLAPA DE CHAT LOCAL
# ===============================================
system_utils.inicializar_chat()
chat_frame = ttk.Frame(chat_tab, padding="25")
chat_frame.pack(fill=tk.BOTH, expand=True)
# Nombre de la aplicación en grande (Marko One)
tk.Label(
chat_frame,
text="Chat Multipípedo",
font=('Marko One', 24),
fg="#2C3E50"
).pack(pady=(0, 10))
# Marco para la configuración del usuario
user_config_frame = ttk.Frame(chat_frame)
user_config_frame.pack(fill=tk.X, pady=(0, 15))
# Variable para guardar el Alias
alias_var = tk.StringVar(value=f"Instancia_{config.INSTANCE_ID}")
# Títulos usando Trirong y texto normal en Nunito
tk.Label(user_config_frame, text="Tu Alias:", font=('Trirong', 12, 'bold'), fg="#34495E").pack(side=tk.LEFT, padx=(0, 10))
ttk.Entry(user_config_frame, textvariable=alias_var, font=('Nunito', 11), width=20).pack(side=tk.LEFT, ipady=3)
tk.Label(user_config_frame, text=f"(ID Sistema: {config.INSTANCE_ID})", font=('Nunito', 10, 'italic'), fg="#7F8C8D").pack(side=tk.RIGHT)
# Área de texto del chat (Texto normal en Nunito)
chat_text_area = ScrolledText(
chat_frame,
wrap='word',
state=tk.DISABLED,
font=('Nunito', 11),
bg='#F8F9FA',
fg='#2C3E50',
bd=1,
relief="solid",
padx=15,
pady=15
)
chat_text_area.pack(fill=tk.BOTH, expand=True, pady=(0, 20))
chat_input_frame = ttk.Frame(chat_frame)
chat_input_frame.pack(fill=tk.X)
# Título secundario en Trirong
tk.Label(chat_input_frame, text="Mensaje:", font=('Trirong', 12, 'bold'), fg="#34495E").pack(side=tk.LEFT, padx=(0, 10))
chat_entry = ttk.Entry(chat_input_frame, font=('Nunito', 12))
chat_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10), ipady=5)
# Vincular Enter y el Botón pasando el alias_var
chat_entry.bind("<Return>", lambda event: system_utils.enviar_mensaje_chat(chat_entry, alias_var))
ttk.Button(
chat_input_frame,
text="Enviar 🚀",
command=lambda: system_utils.enviar_mensaje_chat(chat_entry, alias_var)
).pack(side=tk.RIGHT, ipady=4)
threading.Thread(target=lambda: system_utils.monitor_chat_file(chat_text_area, root), daemon=True).start()
# ===============================================
# CONTENIDO DE LA SOLAPA DE CORREO CLIENTE
# ===============================================
mail_main_container = ttk.Frame(email_tab)
mail_main_container.pack(fill=tk.BOTH, expand=True)
# Variables de credenciales globales para esta pestaña
email_user_var = tk.StringVar()
email_pass_var = tk.StringVar()
# ---------------------------------------------------------
# PANTALLA 1: LOGIN (Visible por defecto)
# ---------------------------------------------------------
login_frame = ttk.Frame(mail_main_container, padding="40")
login_frame.pack(fill=tk.BOTH, expand=True)
tk.Label(
login_frame,
text="Gestor de Correo Interno",
font=('Marko One', 22),
fg="#2C3E50"
).pack(pady=(40, 30))
form_frame = ttk.Frame(login_frame)
form_frame.pack()
tk.Label(form_frame, text="Email:", font=('Trirong', 12, 'bold')).grid(row=0, column=0, padx=10, pady=15, sticky="e")
ttk.Entry(form_frame, textvariable=email_user_var, width=35, font=('Nunito', 11)).grid(row=0, column=1, padx=10, pady=15, ipady=5)
tk.Label(form_frame, text="Contraseña:", font=('Trirong', 12, 'bold')).grid(row=1, column=0, padx=10, pady=15, sticky="e")
ttk.Entry(form_frame, textvariable=email_pass_var, show="", width=35, font=('Nunito', 11)).grid(row=1, column=1, padx=10, pady=15, ipady=5)
# ---------------------------------------------------------
# PANTALLA 2: BANDEJA DE CORREO (Oculta hasta iniciar sesión)
# ---------------------------------------------------------
mail_content_frame = ttk.Frame(mail_main_container, padding="15")
# Dividir pantalla (Enviar vs Recibir)
paned_window = ttk.PanedWindow(mail_content_frame, orient=tk.HORIZONTAL)
paned_window.pack(fill=tk.BOTH, expand=True)
# --- Bandeja de Salida (SMTP) ---
send_frame = tk.LabelFrame(
paned_window,
text=" Bandeja de Salida (SMTP) ",
font=('Trirong', 11, 'bold'),
fg="#2980B9",
padx=15,
pady=15
)
paned_window.add(send_frame, weight=1)
tk.Label(send_frame, text="Destinatario:", font=('Trirong', 10, 'bold')).pack(anchor='w', pady=(0, 2))
to_var = tk.StringVar()
ttk.Entry(send_frame, textvariable=to_var, font=('Nunito', 11)).pack(fill=tk.X, pady=(0, 10), ipady=3)
tk.Label(send_frame, text="Asunto:", font=('Trirong', 10, 'bold')).pack(anchor='w', pady=(0, 2))
subject_var = tk.StringVar()
ttk.Entry(send_frame, textvariable=subject_var, font=('Nunito', 11)).pack(fill=tk.X, pady=(0, 10), ipady=3)
tk.Label(send_frame, text="Mensaje:", font=('Trirong', 10, 'bold')).pack(anchor='w', pady=(0, 2))
body_text = tk.Text(send_frame, height=8, font=('Nunito', 11), bd=1, relief="solid")
body_text.pack(fill=tk.BOTH, expand=True, pady=(0, 15))
ttk.Button(
send_frame,
text="📨 Enviar Email",
command=lambda: system_utils.enviar_correo(
email_user_var.get(), email_pass_var.get(), to_var.get(), subject_var.get(), body_text.get("1.0", tk.END), root
)
).pack(ipady=3)
# --- Bandeja de Entrada (IMAP) ---
recv_frame = tk.LabelFrame(
paned_window,
text=" Bandeja de Entrada (IMAP) ",
font=('Trirong', 11, 'bold'),
fg="#27AE60",
padx=15,
pady=15
)
paned_window.add(recv_frame, weight=1)
style = ttk.Style()
style.configure("Treeview", font=('Nunito', 10), rowheight=28)
style.configure("Treeview.Heading", font=('Trirong', 10, 'bold'))
cols = ('ID', 'De', 'Asunto')
treeview_emails = ttk.Treeview(recv_frame, columns=cols, show='headings')
treeview_emails.heading('ID', text='ID')
treeview_emails.heading('De', text='Remitente')
treeview_emails.heading('Asunto', text='Asunto')
treeview_emails.column('ID', width=50, anchor='center')
treeview_emails.column('De', width=160)
treeview_emails.column('Asunto', width=220)
treeview_emails.pack(fill=tk.BOTH, expand=True, pady=(0, 15))
ttk.Button(
recv_frame,
text="📥 Actualizar Bandeja",
command=lambda: system_utils.cargar_correos(
email_user_var.get(), email_pass_var.get(), treeview_emails, root
)
).pack(ipady=3)
# ---------------------------------------------------------
# BOTÓN DE INICIO DE SESIÓN (Llama a la validación)
# ---------------------------------------------------------
ttk.Button(
form_frame,
text="🔑 Autenticar e Iniciar",
command=lambda: system_utils.comprobar_login_correo(
email_user_var.get(), email_pass_var.get(), login_frame, mail_content_frame, root
)
).grid(row=2, column=0, columnspan=2, pady=25, ipady=5)
# --- Iniciar Hilos --- # --- Iniciar Hilos ---
system_utils.log_event("Monitor de sistema iniciado. Esperando la primera lectura de métricas...") system_utils.log_event("Monitor de sistema iniciado. Esperando la primera lectura de métricas...")
monitor_manager.iniciar_monitor_sistema(fig, canvas, ax_cpu, ax_mem, ax_net, ax_cores, ax_pie, ax_disk_io, treeview_processes, root) monitor_manager.iniciar_monitor_sistema(fig, canvas, ax_cpu, ax_mem, ax_net, ax_cores, ax_pie, ax_disk_io, treeview_processes, root)