{
"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."
}
]
}
}