{ "meta": { "title": "Arquitectura de ProxMenux Monitor — AppImage, Flask, SQLite, WebSocket | ProxMenux", "description": "Cómo está construido ProxMenux Monitor: estructura del AppImage, blueprints de Flask, hilos de fondo, fuentes de datos (psutil, pvesh, smartctl, journalctl), persistencia en SQLite, terminal WebSocket, proveedores de IA, canales de notificación, reverse proxy e integración opcional con Fail2Ban.", "ogTitle": "Arquitectura de ProxMenux Monitor", "ogDescription": "Dentro de ProxMenux Monitor — estructura del AppImage, blueprints de Flask, hilos de fondo, SQLite, WebSocket, proveedores de IA, canales de notificación.", "twitterTitle": "Arquitectura de ProxMenux Monitor", "twitterDescription": "AppImage, Flask, SQLite, WebSocket, proveedores de IA y canales de notificación — por dentro del Monitor." }, "header": { "title": "Arquitectura", "description": "Cómo se empaqueta ProxMenux Monitor, qué corre dentro del AppImage y cómo fluyen las peticiones desde el navegador a través del backend Flask hasta las herramientas del host y el almacén SQLite.", "section": "ProxMenux Monitor" }, "intro": { "title": "Un proceso, muchas responsabilidades", "body": "Un único proceso Python escucha en el puerto TCP 8008. Sirve el build estático de Next.js, expone la API REST, gestiona el terminal WebSocket, ejecuta el Monitor de salud periódico y despacha notificaciones. No hay un servidor web aparte, ni un broker de mensajes, ni una base de datos externa." }, "requestFlow": { "heading": "Flujo de petición", "intro": "Del navegador al kernel, cada vista del panel sigue el mismo camino:", "diagramCaption": "Cada petición se autentica por JWT (cuando la autenticación está activada), se enruta a un blueprint y se responde con datos recogidos bajo demanda desde las herramientas del host. Si Fail2Ban está instalado y el jail proxmenux está activo, el middleware también comprueba la petición contra la lista de IPs baneadas del jail. El reverse proxy opcional es transparente para Flask — reenvía las cabeceras X-Forwarded-* y la app recupera la IP real del cliente a partir de ellas. El estado que necesita sobrevivir a una petición vive en SQLite.", "diagramArrowLabel": "HTTP / WS", "nodes": { "clientLabel": "Cliente", "clientDetail": "Navegador o PWA\n+ proxy opcional\nNginx / Caddy /\nTraefik", "flaskLabel": "Flask :8008", "flaskDetail": "Blueprints\nMiddleware JWT\nHook Fail2Ban\n(si está instalado)", "hostLabel": "Herramientas del host", "hostDetail": "psutil\npvesh\nsmartctl\njournalctl", "stateLabel": "Estado local", "stateDetail": "DB SQLite\n+ auth.json" }, "threadsIntro": "El mismo proceso también ejecuta cuatro hilos de fondo arrancados al boot — no sirven HTTP, empujan estado a SQLite o a la cola de notificaciones mientras el host está activo:", "headerThread": "Hilo", "headerCadence": "Cadencia", "headerJob": "Tarea", "rows": [ { "thread": "_temperature_collector_loop", "cadence": "60 s", "job": "Registra la temperatura de la CPU y una muestra de latencia de red en la DB de historial para que las gráficas del panel tengan datos incluso cuando ningún cliente está conectado." }, { "thread": "_health_collector_loop", "cadence": "5 min", "job": "Ejecuta el ciclo completo del Monitor de salud (10 categorías), persiste errores activos, dismissals y observaciones de disco, y alimenta nuevos eventos al motor de notificaciones." }, { "thread": "_vital_signs_sampler", "cadence": "~1 s", "job": "Muestreador de alta frecuencia de CPU + temperatura usado por los widgets en vivo del panel Overview." }, { "thread": "notification_manager.start()", "cadence": "dirigido por eventos", "job": "Lanza los watchers de journal / task / hook (JournalWatcher, TaskWatcher, ProxmoxHookWatcher) y despacha a los canales configurados con reescritura opcional con IA." } ] }, "systemd": { "heading": "Unidad systemd", "intro": "El instalador deja una unidad en /etc/systemd/system/proxmenux-monitor.service. Contenido por defecto:", "items": [ "User=root — obligatorio: SMART, pvesh, scopes de journal, comandos ZFS y el terminal web requieren todos root.", "Restart=on-failure con back-off de 10 segundos — las salidas con código distinto de cero relanzan automáticamente.", "After=network.target — espera a que el stack de red del host esté online." ], "inspectTitle": "Inspeccionar la unidad en vivo" }, "appimage": { "heading": "Qué contiene el AppImage", "intro": "El AppImage es un sistema de archivos automontable. AppRun en la raíz prepara el entorno y ejecuta flask_server.py:", "consequencesIntro": "Dos consecuencias de esta estructura:", "consequences": [ "Sin polución del Python del host. El intérprete y los paquetes empaquetados están aislados dentro del AppImage — actualizar el Python del sistema host no afecta al Monitor y viceversa.", "Las herramientas de hardware también vienen incluidas. ipmitool, lm-sensors y upsc viajan dentro del AppImage para que el panel pueda leer sensores fuera de banda y el estado de la UPS sin forzar al usuario a instalar paquetes Debian." ] }, "flask": { "heading": "Estructura de la app Flask", "intro": "flask_server.py crea una única instancia Flask(__name__), habilita CORS y registra seis blueprints más un inicializador de WebSocket:", "headerBlueprint": "Blueprint / módulo", "headerPrefix": "Prefijo de rutas", "headerOwns": "Responsable de", "rows": [ { "blueprint": "flask_server.py", "prefix": [ "/api/system", "/api/storage", "/api/network", "/api/vms", "/api/hardware", "/api/logs", "/api/prometheus" ], "owns": "Endpoints de datos principales + servir el panel estático + comprobación opcional de Fail2Ban a nivel de aplicación (activa solo cuando Fail2Ban está instalado en el host con el jail proxmenux)." }, { "blueprint": "flask_auth_routes.py", "prefix": [ "/api/auth/*" ], "owns": "Login, emisión de JWT, configuración / verificación de TOTP, cambio de contraseña, generación de API tokens." }, { "blueprint": "flask_health_routes.py", "prefix": [ "/api/health/*" ], "owns": "Probe de salud público, estado detallado, errores activos / dismissed, ajustes de supresión." }, { "blueprint": "flask_terminal_routes.py", "prefix": [ "/api/terminal/* + WS" ], "owns": "Asignación de PTY por sesión y pipe WebSocket hacia xterm.js en el navegador." }, { "blueprint": "flask_notification_routes.py", "prefix": [ "/api/notifications/*" ], "owns": "CRUD de canales, envío de prueba, configuración del proveedor de IA, historial, envíos manuales." }, { "blueprint": "flask_security_routes.py", "prefix": [ "/api/security/*" ], "owns": "Fallos de autenticación y, cuando Fail2Ban está instalado, estado del jail, eventos de ban y desbaneo manual." }, { "blueprint": "flask_proxmenux_routes.py", "prefix": [ "/api/proxmenux/*" ], "owns": "Lee qué optimizaciones post-instalación de ProxMenux están instaladas en el host." }, { "blueprint": "flask_oci_routes.py", "prefix": [ "/api/oci/*" ], "owns": "Ayudantes de despliegue de apps OCI / container (Proxmox VE 9.1+)." } ], "endpointsLink": "La lista completa de endpoints con la forma de petición / respuesta está en API Reference." }, "dataSources": { "heading": "Fuentes de datos", "intro": "Nada se recoge a través de un agente propio — el Monitor lee los mismos archivos y ejecuta los mismos comandos que un administrador humano:", "headerSource": "Fuente", "headerUsedFor": "Usado para", "rows": [ { "source": "psutil", "usedFor": "Carga de CPU, memoria, swap, uso de puntos de montaje, contadores de NIC, lista de procesos." }, { "source": "pvesh / qm / pct", "usedFor": "Información del nodo Proxmox, inventario y configuración de VMs y CTs, pools de almacenamiento, historial de tareas." }, { "source": "smartctl", "usedFor": "Atributos SATA / NVMe, salud SMART, desgaste / vida útil, modelo y número de serie." }, { "source": "zpool / zfs", "usedFor": "Estado del pool (ONLINE / DEGRADED / FAULTED / UNAVAIL), progreso de scrub, uso de datasets." }, { "source": "journalctl", "usedFor": "Logs del sistema, OOM kills, errores ATA / NVMe / dm, eventos de seguridad, unidades de servicio propias." }, { "source": "ip / iproute2", "usedFor": "Interfaces, direcciones, bridges, bonds, dispositivos gestionados por OVS." }, { "source": "nvidia-smi · intel_gpu_top", "usedFor": "Utilización de GPU, VRAM, temperatura, carga de encoder / decoder." }, { "source": "lspci · lscpu · dmidecode", "usedFor": "Topología PCIe, modelo y topología de CPU, información de placa y BIOS." }, { "source": "ipmitool · sensors", "usedFor": "Sensores fuera de banda, velocidades de ventilador, temperaturas de placa (cuando son compatibles)." }, { "source": "upsc (NUT)", "usedFor": "Estado de batería de UPS, carga, autonomía — cuando hay un servidor NUT configurado en el host." } ], "cacheTitle": "La salida se cachea — no toda petición llega al host", "cacheBody": "Las fuentes costosas (smartctl -a, pvesh get) se envuelven en cachés temporizadas dentro del proceso Flask para que una pestaña ocupada del panel no martillee el disco o la API del cluster. Los TTLs de la caché están ajustados por fuente (unos pocos segundos para métricas en vivo, varios minutos para SMART)." }, "persistence": { "heading": "Persistencia", "intro": "Dos ubicaciones del sistema de archivos separan el estado por sensibilidad:", "headerPath": "Ruta", "headerOwner": "Propietario", "headerContents": "Contenido", "rows": [ { "path": "/usr/local/share/proxmenux/health_monitor.db", "owner": "root:root", "contents": "DB SQLite. Tablas: errors, events, disk_registry, disk_observations, user_settings, notification_history, excluded_storages, excluded_interfaces. Modo journal WAL." }, { "path": "/usr/local/share/proxmenux/.notification_key", "owner": "root 0600", "contents": "Clave XOR de 32 bytes usada para cifrar ajustes sensibles de notificaciones antes de guardarlos en la DB (tokens de Telegram, API keys de IA, etc.)." }, { "path": "/root/.config/proxmenux-monitor/auth.json", "owner": "root:root", "contents": "Estado de autenticación: flag de activación, nombre de usuario, hash SHA-256 de la contraseña, secret TOTP, códigos de backup, lista de API tokens emitidos, lista de hashes de tokens revocados." }, { "path": "/var/log/proxmenux-auth.log", "owner": "root:root", "contents": "Log de eventos de autenticación en texto plano. Siempre se escribe. Si Fail2Ban está instalado con el jail [proxmenux], el jail lee este archivo para banear intentos de fuerza bruta; si no, el archivo simplemente acumula las entradas del log." } ], "backupTitle": "Haz backup de auth.json antes de reinstalar", "backupBody": "Reinstalar el AppImage reemplaza el binario pero deja /root/.config/proxmenux-monitor/auth.json y /usr/local/share/proxmenux/health_monitor.db intactos. Si restauras desde un backup del host, mantén ambos archivos juntos — los API tokens guardados en auth.json se validan contra JWT_SECRET; si la DB y auth.json se desincronizan, los errores dismissed y los tokens guardados pueden comportarse mal." }, "health": { "heading": "Ciclo del Monitor de salud", "intro": "Cada 5 minutos health_monitor.py ejecuta un ciclo determinista a través de las diez categorías mostradas en el panel:", "items": [ "Servicios críticos de PVE (pveproxy, pvedaemon, pvestatd, pve-cluster).", "Pools de almacenamiento Proxmox (pvesh get /storage + disponibilidad por almacenamiento).", "Discos y sistemas de archivos: SMART, errores de I/O en dmesg, salud de pool ZFS, capacidad de puntos de montaje.", "VMs y CTs: arranques fallidos, guests caídos, errores QMP, fallos de apagado.", "Red: estado de bridge / bond, estado de enlace, latencia al gateway.", "Actualizaciones: actualizaciones de paquetes pendientes y parches de seguridad.", "Logs: detección de patrones persistentes / spike / cascada en el journal del sistema.", "Memoria: actividad del OOM killer, presión alta sostenida.", "Temperatura: sensores CPU / chasis contra umbrales del fabricante.", "Seguridad: fallos de autenticación, eventos de ban, estado del jail fail2ban." ], "afterIntro": "Cada hallazgo se normaliza a un error_key + categoría + severidad estable. La capa de persistencia deduplica contra la tabla errors existente — los eventos repetidos actualizan last_seen y el contador de ocurrencias sin spamear notificaciones.", "cycleEnd": "El ciclo también auto-resuelve errores obsoletos usando el ajuste de Suppression Duration por categoría, limpia errores de recursos que ya no existen (VMs eliminadas / discos retirados / almacenamientos desmontados) y poda el log events con más de 30 días. El catálogo completo de categorías y la vista del panel que las expone está documentado en Dashboard → Monitor de salud." }, "notifications": { "heading": "Motor de notificaciones", "intro": "notification_manager.py es el orquestador. Carga los canales configurados, posee la cola de entrega y expone tanto una API Python (para rutas Flask y el ciclo del Monitor de salud) como un punto de entrada CLI (para los scripts .sh de hooks que vienen con ProxMenux).", "items": [ "Los watchers empujan eventos: JournalWatcher sigue el journal del sistema, TaskWatcher hace polling de la lista de tareas Proxmox, ProxmoxHookWatcher reacciona a hooks de backup / replicación / snapshot y PollingCollector gestiona fuentes de datos lentas.", "Las templates convierten un evento en un par (título, cuerpo). La misma template puede pasar por el proveedor de IA configurado (OpenAI / Anthropic / Gemini / Groq / Ollama / OpenRouter) para producir una reescritura en lenguaje natural; ambas versiones se guardan en notification_history.", "Los canales entregan los mensajes: Telegram, Discord, Email, Gotify y Apprise (multicanal). Cada uno está implementado en notification_channels.py detrás de la misma interfaz create_channel() / send(), así que añadir un canal nuevo es una sola clase.", "Cifrado. Los ajustes sensibles (telegram.token, discord.webhook_url, ai_api_key_*, email.password) se cifran con XOR usando la clave en .notification_key antes de escribirse en la DB. El texto plano nunca toca disco." ], "linksFooter": "Los toggles por evento, los overrides por canal y la configuración de IA se exponen en Settings → Notifications y Settings → AI Assistant." }, "websocket": { "heading": "Terminal WebSocket", "intro": "La pestaña Terminal del panel es un cliente fino xterm.js cableado a un PTY del lado servidor a través de un WebSocket. Dos modos de transporte:", "items": [ "Modo HTTP (por defecto): el servidor de desarrollo de Flask con flask-sock gestiona las peticiones de upgrade. Suficiente para LAN / acceso directo.", "Modo HTTPS / WSS: cuando hay un certificado SSL configurado, el proceso cambia a gevent.pywsgi.WSGIServer con geventwebsocket.handler.WebSocketHandler, para que los WebSockets funcionen sobre TLS sin polyfills." ], "outro": "El PTY es un hijo del proceso Flask, así que hereda User=root de la unidad. Cada petición de terminal pasa por auth JWT; el usuario debe estar ya logueado en el panel antes de que se asigne un PTY.", "proxyNote": "Si accedes al Monitor a través de un reverse proxy, asegúrate de habilitar el reenvío de WebSocket (cabeceras Upgrade y Connection). Sin eso, el terminal no funcionará." }, "proxy": { "heading": "Reverse proxy y Fail2Ban", "intro": "Dos salvaguardas se aseguran de que la seguridad funcione igual tanto si el panel se accede directamente como a través de un reverse proxy:", "items": [ "Recuperación de la IP real del cliente. Un hook before_request lee X-Forwarded-For y X-Real-IP en ese orden, cayendo a request.remote_addr. La dirección recuperada es la que ven el log de autenticación y el rate limiting. Esto está siempre activo.", "Comprobación de Fail2Ban a nivel de aplicación (opcional). Cuando el panel está detrás de un proxy, el firewall del kernel no puede bloquear la IP real del atacante — la conexión siempre viene del proxy. Para tapar ese hueco, el mismo hook de arriba consulta el jail proxmenux de Fail2Ban cada 30 segundos, cachea el conjunto de IPs baneadas y corta las peticiones desde esas IPs con HTTP 403 dentro de Flask." ], "calloutTitle": "Fail2Ban no viene incluido", "calloutBody": "Fail2Ban no lo instala ProxMenux Monitor por sí mismo. La comprobación a nivel de aplicación es un no-op hasta que instales Fail2Ban en el host (p. ej. vía Seguridad → Fail2Ban en el menú de ProxMenux). Cuando el binario fail2ban-client o el jail proxmenux está ausente, la llamada falla silenciosamente y las peticiones no se filtran — la autenticación sigue aplicándose, pero no hay baneo a nivel de IP.", "outro": "Los snippets de reverse proxy (Nginx / Caddy / Traefik) y el walkthrough del jail Fail2Ban están en Access & Authentication y Seguridad → Fail2Ban." }, "whereNext": { "heading": "Por dónde seguir", "items": [ { "label": "Access & Authentication", "href": "/docs/monitor/access-auth", "tail": " — configuración de primer arranque, contraseña + TOTP 2FA, snippets de reverse proxy, jail Fail2Ban." }, { "label": "API Reference", "href": "/docs/monitor/api", "tail": " — cada endpoint, gestión de tokens, mejores prácticas de seguridad." }, { "label": "Settings → ProxMenux Monitor", "href": "/docs/settings/proxmenux-monitor", "tail": " — el toggle del servicio dentro del menú y el flujo de verificación del estado dentro de la TUI de ProxMenux." } ] } }