Compare commits

...

11 Commits

Author SHA1 Message Date
058e4781e6 feat: Sistema de variables de entorno mejorado con EnvironmentFile
- Agregado campo 'environment' a MonitoredApp para almacenar variables ADICIONALES
- Solo se almacenan en JSON las variables agregadas manualmente desde el panel
- Las variables del .env del proyecto se cargan automáticamente con EnvironmentFile
- Modificado service_generator.rs para usar directiva EnvironmentFile en systemd
- Fix: Usuario ahora se lee correctamente del JSON sin fallback a 'root'
- Edit.html pre-carga variables adicionales del JSON al editar
- Separación clara: .env (proyecto) vs variables adicionales (JSON)
- Transparencia total con .env nativo del proyecto

Beneficios:
 No duplicación de variables (.env es la fuente de verdad)
 JSON solo guarda variables extras (pequeño y limpio)
 .env funciona igual que en desarrollo
 systemd lee .env con EnvironmentFile
 Variables adicionales se persisten en JSON
 Al editar, se pre-cargan variables adicionales guardadas
2026-01-21 21:48:59 -05:00
93d178b216 docs: Agregar Fase 5 - Script de inicialización de .env
- Nueva sección: Fase 5.1 - Script de Inicialización de .env
- Documentado el problema del .env en .gitignore
- Propuesta de solución: sync_env.sh desde servidor central
- 3 opciones: API Central, SCP, Vault/Secrets Manager
- Incluye script de ejemplo con curl + validación
- Fase 5.2: Templates de .env
- Fase 5.3: Gestión centralizada de secrets
- Actualizadas métricas del proyecto (25+ commits, 15 endpoints)
- Documentadas nuevas features implementadas
2026-01-21 20:59:51 -05:00
cd14cc5c06 feat: Agregar campo 'user' al JSON de configuración
- Agregado campo 'user: String' a MonitoredApp
- Función default_user() que obtiene usuario del sistema ($USER/$LOGNAME)
- Actualizado todos los lugares donde se crea MonitoredApp:
  * config.rs (add_app)
  * app_manager.rs (register_app) - usa config.user
  * discovery.rs (sync) - extrae de .service file
  * handlers.rs (update_app) - usa config.user
- El campo se guarda en monitored_apps.json
- Formulario de edición ahora carga el usuario correcto
- Resuelve problema: antes ponía 'pablinux' por defecto
- Ahora muestra el usuario real (ej: 'user_apps')
2026-01-21 18:08:40 -05:00
bb25004e67 fix: Cargar correctamente datos de la app en formulario de edición
- Construir script_path completo combinando path + entry_point
- Usar valores por defecto cuando campos no existen en JSON
- Agregar console.log para debug
- El campo user usa 'pablinux' por defecto (no está en JSON)
- Ahora los campos se llenan correctamente en lugar de mostrar solo placeholders
2026-01-21 18:00:56 -05:00
9e56490b05 fix: Agregar ruta /edit al router web (404 fix)
- Agregada ruta GET /edit al web_router
- Creado edit_handler() que sirve edit.html
- Resuelve error 404 al intentar acceder a /edit?app=NOMBRE
- Ahora el botón Editar del panel funciona correctamente
2026-01-21 17:57:34 -05:00
d2b8d0222c feat: Implementación completa de funcionalidad EDITAR apps (CRUD Update)
Backend:
- Endpoint PUT /api/apps/:name para actualizar configuración completa
- Endpoint GET /api/apps/:name para obtener datos de una app específica
- update_app_handler(): detiene servicio, regenera .service, daemon-reload, actualiza JSON, reinicia
- Soft delete de versión anterior al actualizar (mantiene historial)
- Logs detallados en cada paso del proceso de actualización
- Recarga automática de variables desde .env al actualizar

Frontend:
- Nueva página /edit?app=NOMBRE para editar apps
- Formulario pre-poblado con datos actuales de la app
- Nombre de app readonly (no se puede cambiar para evitar inconsistencias)
- Botón Editar (morado) en panel principal junto a logs/eliminar
- PUT en lugar de POST, mensaje de éxito actualizado
- Redirección automática al panel después de 2 segundos

Casos de uso resueltos:
 Cambiar usuario (ej: GatewaySIGMA con usuario incorrecto)
 Actualizar puerto
 Modificar variables de entorno
 Cambiar política de reinicio
 Actualizar ruta del script
 Recargar .env sin eliminar la app

Completa el patrón CRUD: Create, Read, Update, Delete 
2026-01-21 17:40:37 -05:00
d8b3214ede fix: Mejorar robustez del endpoint de eliminación de apps
- unregister_app_handler ahora intenta eliminar de 3 fuentes:
  1. AppManager (memoria) - puede fallar si app no se registró bien
  2. JSON (soft delete) - marca como eliminada en monitored_apps.json
  3. systemd (físico) - elimina .service, stop, disable, daemon-reload
- Logs detallados en cada paso (INFO/WARNING)
- Ya no falla con 'Aplicación no encontrada' si solo falta en memoria
- Resuelve problema de GatewaySIGMA que no se podía eliminar
- Operación best-effort: intenta todo sin fallar si un paso falla
2026-01-21 17:12:10 -05:00
2f867cb7ed feat: Auto-carga de variables de entorno desde archivo .env
- Agregada dependencia dotenvy para parsear archivos .env
- Implementada función read_env_file() que lee y parsea archivos .env
- Soporta comentarios (#), líneas vacías, y valores con/sin comillas
- Auto-detección: si existe .env en WorkingDirectory, se carga automáticamente
- Merge inteligente: .env primero, luego variables manuales (sobrescriben)
- Las apps ahora pueden usar su .env sin tener que copiar 17+ variables manualmente
- Logs claros: informa cuántas variables se cargaron desde .env
- Beneficio: registrar apps es mucho más rápido y menos propenso a errores
2026-01-21 17:04:53 -05:00
6fa7b5c86c chore: Actualizar .gitignore para excluir logs y config
- Agregar logs/*.log para ignorar archivos de logs del sistema
- Agregar config/monitored_apps.json para evitar commits de configuración local
- Mantener solo archivos de código fuente en el repositorio
2026-01-20 06:53:43 -05:00
fb3db3c713 fix: Mejorar claridad del modal de eliminación
- Mostrar nombre exacto del servicio (siax-app-NOMBRE.service) en lugar de comodín
- Actualizar texto para aclarar que es soft delete
- Agregar icono de archivo para indicar que se mantiene en historial
- Agregar mensaje informativo: 'Podrás restaurarla desde el historial'
- Cambiar colores para diferenciar eliminación física vs lógica
2026-01-20 06:49:31 -05:00
7a66f25150 fix: Evitar desbordamiento horizontal en logs
- Agregado overflow-x-auto a contenedores de logs
- Agregado break-words y overflow-wrap-anywhere a líneas de logs
- Aplicado tanto en logs de aplicaciones como en errores del sistema
- Las líneas largas ahora se ajustan correctamente sin scroll horizontal
2026-01-20 06:43:46 -05:00
16 changed files with 1263 additions and 39 deletions

2
.gitignore vendored
View File

@@ -1 +1,3 @@
/target /target
logs/*.log
config/monitored_apps.json

7
Cargo.lock generated
View File

@@ -281,6 +281,12 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "dotenvy"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]] [[package]]
name = "either" name = "either"
version = "1.15.0" version = "1.15.0"
@@ -1332,6 +1338,7 @@ dependencies = [
"axum", "axum",
"chrono", "chrono",
"dashmap", "dashmap",
"dotenvy",
"futures", "futures",
"regex", "regex",
"reqwest", "reqwest",

View File

@@ -17,6 +17,7 @@ tokio-stream = "0.1"
regex = "1.10" regex = "1.10"
thiserror = "1.0" thiserror = "1.0"
dashmap = "5.5" dashmap = "5.5"
dotenvy = "0.15"
[dev-dependencies] [dev-dependencies]
tempfile = "3.8" tempfile = "3.8"

View File

@@ -2,11 +2,27 @@
"apps": [ "apps": [
{ {
"name": "app_tareas", "name": "app_tareas",
"port": 3000 "service_name": "",
"path": "",
"port": 3000,
"entry_point": "",
"node_bin": "",
"mode": "production",
"service_file_path": "",
"deleted": true,
"deleted_at": "2026-01-21T18:01:42.273756980-05:00",
"deleted_reason": "Eliminada desde el panel de control"
}, },
{ {
"name": "fidelizacion", "name": "fidelizacion",
"port": 3001 "service_name": "",
"path": "",
"port": 3001,
"entry_point": "",
"node_bin": "",
"mode": "production",
"service_file_path": "",
"deleted": false
} }
] ]
} }

View File

@@ -2328,3 +2328,173 @@
[2026-01-19 08:27:21] [ERROR] [Monitor] Error sincronizando app_tareas | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"} [2026-01-19 08:27:21] [ERROR] [Monitor] Error sincronizando app_tareas | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-19 08:27:21] [INFO] [Monitor] App fidelizacion ya existe (ID: 4), actualizando... [2026-01-19 08:27:21] [INFO] [Monitor] App fidelizacion ya existe (ID: 4), actualizando...
[2026-01-19 08:27:21] [ERROR] [Monitor] Error sincronizando fidelizacion | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"} [2026-01-19 08:27:21] [ERROR] [Monitor] Error sincronizando fidelizacion | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-20 06:35:19] [INFO] [Sistema] Iniciando SIAX Agent
[2026-01-20 06:35:19] [INFO] [Sistema] Escaneando servicios systemd existentes...
[2026-01-20 06:35:19] [INFO] [Discovery] 🔍 Escaneando servicios systemd en: /etc/systemd/system
[2026-01-20 06:35:19] [INFO] [Discovery] ✅ Directorio /etc/systemd/system accesible
[2026-01-20 06:35:19] [INFO] [Discovery] 📊 Escaneados 131 archivos, 0 con prefijo 'siax-app-', 0 parseados exitosamente
[2026-01-20 06:35:19] [INFO] [Config] Usando archivo de configuración: config/monitored_apps.json
[2026-01-20 06:35:19] [INFO] [Config] ✅ Configuración cargada: 2 apps desde config/monitored_apps.json
[2026-01-20 06:35:19] [INFO] [Sistema] Servidor detectado: siax-intel
[2026-01-20 06:35:19] [INFO] [Sistema] Iniciando servidor unificado en puerto 8080
[2026-01-20 06:35:19] [INFO] [Sistema] Sistema SIAX completamente operativo en puerto 8080
[2026-01-20 06:35:19] [INFO] [Monitor] Vigilando procesos para siax-intel [SIAX-Agent/0.1.0 (linux/x86_64; Rust-Monitor)]
[2026-01-20 06:35:19] [INFO] [Monitor] Buscando app app_tareas en API central...
[2026-01-20 06:35:19] [INFO] [Monitor] App app_tareas encontrada en cloud (ID: 3)
[2026-01-20 06:35:19] [ERROR] [Monitor] Error sincronizando app_tareas | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-20 06:35:19] [INFO] [Monitor] Buscando app fidelizacion en API central...
[2026-01-20 06:35:19] [INFO] [Monitor] App fidelizacion encontrada en cloud (ID: 4)
[2026-01-20 06:35:19] [ERROR] [Monitor] Error sincronizando fidelizacion | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-20 06:36:20] [INFO] [Monitor] App app_tareas ya existe (ID: 3), actualizando...
[2026-01-20 06:36:20] [ERROR] [Monitor] Error sincronizando app_tareas | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-20 06:36:20] [INFO] [Monitor] App fidelizacion ya existe (ID: 4), actualizando...
[2026-01-20 06:36:20] [ERROR] [Monitor] Error sincronizando fidelizacion | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-20 06:37:20] [INFO] [Monitor] App app_tareas ya existe (ID: 3), actualizando...
[2026-01-20 06:37:20] [ERROR] [Monitor] Error sincronizando app_tareas | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-20 06:37:20] [INFO] [Monitor] App fidelizacion ya existe (ID: 4), actualizando...
[2026-01-20 06:37:20] [ERROR] [Monitor] Error sincronizando fidelizacion | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-20 06:38:20] [INFO] [Monitor] App app_tareas ya existe (ID: 3), actualizando...
[2026-01-20 06:38:20] [ERROR] [Monitor] Error sincronizando app_tareas | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-20 06:38:20] [INFO] [Monitor] App fidelizacion ya existe (ID: 4), actualizando...
[2026-01-20 06:38:20] [ERROR] [Monitor] Error sincronizando fidelizacion | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-20 06:39:20] [INFO] [Monitor] App app_tareas ya existe (ID: 3), actualizando...
[2026-01-20 06:39:20] [ERROR] [Monitor] Error sincronizando app_tareas | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-20 06:39:20] [INFO] [Monitor] App fidelizacion ya existe (ID: 4), actualizando...
[2026-01-20 06:39:20] [ERROR] [Monitor] Error sincronizando fidelizacion | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-20 06:40:20] [INFO] [Monitor] App app_tareas ya existe (ID: 3), actualizando...
[2026-01-20 06:40:21] [ERROR] [Monitor] Error sincronizando app_tareas | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-20 06:40:21] [INFO] [Monitor] App fidelizacion ya existe (ID: 4), actualizando...
[2026-01-20 06:40:21] [ERROR] [Monitor] Error sincronizando fidelizacion | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-20 06:41:21] [INFO] [Monitor] App app_tareas ya existe (ID: 3), actualizando...
[2026-01-20 06:41:21] [ERROR] [Monitor] Error sincronizando app_tareas | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-20 06:41:21] [INFO] [Monitor] App fidelizacion ya existe (ID: 4), actualizando...
[2026-01-20 06:41:21] [ERROR] [Monitor] Error sincronizando fidelizacion | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-20 06:42:21] [INFO] [Monitor] App app_tareas ya existe (ID: 3), actualizando...
[2026-01-20 06:42:21] [ERROR] [Monitor] Error sincronizando app_tareas | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-20 06:42:21] [INFO] [Monitor] App fidelizacion ya existe (ID: 4), actualizando...
[2026-01-20 06:42:21] [ERROR] [Monitor] Error sincronizando fidelizacion | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-20 06:43:21] [INFO] [Monitor] App app_tareas ya existe (ID: 3), actualizando...
[2026-01-20 06:43:21] [ERROR] [Monitor] Error sincronizando app_tareas | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-20 06:43:21] [INFO] [Monitor] App fidelizacion ya existe (ID: 4), actualizando...
[2026-01-20 06:43:21] [ERROR] [Monitor] Error sincronizando fidelizacion | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-20 06:44:21] [INFO] [Monitor] App app_tareas ya existe (ID: 3), actualizando...
[2026-01-20 06:44:21] [ERROR] [Monitor] Error sincronizando app_tareas | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-20 06:44:21] [INFO] [Monitor] App fidelizacion ya existe (ID: 4), actualizando...
[2026-01-20 06:44:21] [ERROR] [Monitor] Error sincronizando fidelizacion | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-20 06:44:46] [INFO] [Sistema] Iniciando SIAX Agent
[2026-01-20 06:44:46] [INFO] [Sistema] Escaneando servicios systemd existentes...
[2026-01-20 06:44:46] [INFO] [Discovery] 🔍 Escaneando servicios systemd en: /etc/systemd/system
[2026-01-20 06:44:46] [INFO] [Discovery] ✅ Directorio /etc/systemd/system accesible
[2026-01-20 06:44:46] [INFO] [Discovery] 📊 Escaneados 131 archivos, 0 con prefijo 'siax-app-', 0 parseados exitosamente
[2026-01-20 06:44:46] [INFO] [Config] Usando archivo de configuración: config/monitored_apps.json
[2026-01-20 06:44:46] [INFO] [Config] ✅ Configuración cargada: 2 apps desde config/monitored_apps.json
[2026-01-20 06:44:46] [INFO] [Sistema] Servidor detectado: siax-intel
[2026-01-20 06:44:46] [INFO] [Sistema] Iniciando servidor unificado en puerto 8080
[2026-01-20 06:44:46] [INFO] [Sistema] Sistema SIAX completamente operativo en puerto 8080
[2026-01-20 06:44:46] [INFO] [Monitor] Vigilando procesos para siax-intel [SIAX-Agent/0.1.0 (linux/x86_64; Rust-Monitor)]
[2026-01-20 06:44:46] [INFO] [Monitor] Buscando app app_tareas en API central...
[2026-01-20 06:44:46] [INFO] [Monitor] App app_tareas encontrada en cloud (ID: 3)
[2026-01-20 06:44:46] [ERROR] [Monitor] Error sincronizando app_tareas | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-20 06:44:46] [INFO] [Monitor] Buscando app fidelizacion en API central...
[2026-01-20 06:44:46] [INFO] [Monitor] App fidelizacion encontrada en cloud (ID: 4)
[2026-01-20 06:44:46] [ERROR] [Monitor] Error sincronizando fidelizacion | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-20 06:45:08] [INFO] [WebSocket] Nueva conexión para logs de: fidelizacion
[2026-01-20 06:45:09] [INFO] [WebSocket] Nueva conexión para logs de: app_tareas
[2026-01-20 06:45:09] [INFO] [WebSocket] Conexión cerrada para: fidelizacion
[2026-01-21 17:08:02] [INFO] [Sistema] Iniciando SIAX Agent
[2026-01-21 17:08:02] [INFO] [Sistema] Escaneando servicios systemd existentes...
[2026-01-21 17:08:02] [INFO] [Discovery] 🔍 Escaneando servicios systemd en: /etc/systemd/system
[2026-01-21 17:08:02] [INFO] [Discovery] ✅ Directorio /etc/systemd/system accesible
[2026-01-21 17:08:02] [INFO] [Discovery] 📊 Escaneados 131 archivos, 0 con prefijo 'siax-app-', 0 parseados exitosamente
[2026-01-21 17:08:02] [INFO] [Config] Usando archivo de configuración: config/monitored_apps.json
[2026-01-21 17:08:02] [INFO] [Config] ✅ Configuración cargada: 2 apps desde config/monitored_apps.json
[2026-01-21 17:08:02] [INFO] [Sistema] Servidor detectado: siax-intel
[2026-01-21 17:08:02] [INFO] [Sistema] Iniciando servidor unificado en puerto 8080
[2026-01-21 17:08:02] [INFO] [Sistema] Sistema SIAX completamente operativo en puerto 8080
[2026-01-21 17:08:02] [INFO] [Monitor] Vigilando procesos para siax-intel [SIAX-Agent/0.1.0 (linux/x86_64; Rust-Monitor)]
[2026-01-21 17:08:02] [INFO] [Monitor] Buscando app app_tareas en API central...
[2026-01-21 17:08:03] [INFO] [Monitor] App app_tareas encontrada en cloud (ID: 3)
[2026-01-21 17:08:03] [ERROR] [Monitor] Error sincronizando app_tareas | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-21 17:08:03] [INFO] [Monitor] Buscando app fidelizacion en API central...
[2026-01-21 17:08:03] [INFO] [Monitor] App fidelizacion encontrada en cloud (ID: 4)
[2026-01-21 17:08:04] [ERROR] [Monitor] Error sincronizando fidelizacion | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-21 17:55:31] [INFO] [Sistema] Iniciando SIAX Agent
[2026-01-21 17:55:31] [INFO] [Sistema] Escaneando servicios systemd existentes...
[2026-01-21 17:55:31] [INFO] [Discovery] 🔍 Escaneando servicios systemd en: /etc/systemd/system
[2026-01-21 17:55:31] [INFO] [Discovery] ✅ Directorio /etc/systemd/system accesible
[2026-01-21 17:55:31] [INFO] [Discovery] 📊 Escaneados 131 archivos, 0 con prefijo 'siax-app-', 0 parseados exitosamente
[2026-01-21 17:55:31] [INFO] [Config] Usando archivo de configuración: config/monitored_apps.json
[2026-01-21 17:55:31] [INFO] [Config] ✅ Configuración cargada: 2 apps desde config/monitored_apps.json
[2026-01-21 17:55:31] [INFO] [Sistema] Servidor detectado: siax-intel
[2026-01-21 17:55:31] [INFO] [Sistema] Iniciando servidor unificado en puerto 8080
[2026-01-21 17:55:31] [INFO] [Sistema] Sistema SIAX completamente operativo en puerto 8080
[2026-01-21 17:55:32] [INFO] [Monitor] Vigilando procesos para siax-intel [SIAX-Agent/0.1.0 (linux/x86_64; Rust-Monitor)]
[2026-01-21 17:55:32] [INFO] [Monitor] Buscando app app_tareas en API central...
[2026-01-21 17:55:32] [INFO] [Monitor] App app_tareas encontrada en cloud (ID: 3)
[2026-01-21 17:55:32] [ERROR] [Monitor] Error sincronizando app_tareas | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-21 17:55:32] [INFO] [Monitor] Buscando app fidelizacion en API central...
[2026-01-21 17:55:33] [INFO] [Monitor] App fidelizacion encontrada en cloud (ID: 4)
[2026-01-21 17:55:33] [ERROR] [Monitor] Error sincronizando fidelizacion | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-21 17:56:33] [INFO] [Monitor] App app_tareas ya existe (ID: 3), actualizando...
[2026-01-21 17:56:33] [ERROR] [Monitor] Error sincronizando app_tareas | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-21 17:56:33] [INFO] [Monitor] App fidelizacion ya existe (ID: 4), actualizando...
[2026-01-21 17:56:33] [ERROR] [Monitor] Error sincronizando fidelizacion | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-21 17:57:30] [INFO] [Sistema] Iniciando SIAX Agent
[2026-01-21 17:57:30] [INFO] [Sistema] Escaneando servicios systemd existentes...
[2026-01-21 17:57:30] [INFO] [Discovery] 🔍 Escaneando servicios systemd en: /etc/systemd/system
[2026-01-21 17:57:30] [INFO] [Discovery] ✅ Directorio /etc/systemd/system accesible
[2026-01-21 17:57:30] [INFO] [Discovery] 📊 Escaneados 131 archivos, 0 con prefijo 'siax-app-', 0 parseados exitosamente
[2026-01-21 17:57:30] [INFO] [Config] Usando archivo de configuración: config/monitored_apps.json
[2026-01-21 17:57:30] [INFO] [Config] ✅ Configuración cargada: 2 apps desde config/monitored_apps.json
[2026-01-21 17:57:30] [INFO] [Sistema] Servidor detectado: siax-intel
[2026-01-21 17:57:30] [INFO] [Sistema] Iniciando servidor unificado en puerto 8080
[2026-01-21 17:57:30] [INFO] [Sistema] Sistema SIAX completamente operativo en puerto 8080
[2026-01-21 17:57:30] [INFO] [Monitor] Vigilando procesos para siax-intel [SIAX-Agent/0.1.0 (linux/x86_64; Rust-Monitor)]
[2026-01-21 17:57:30] [INFO] [Monitor] Buscando app app_tareas en API central...
[2026-01-21 17:57:30] [INFO] [Monitor] App app_tareas encontrada en cloud (ID: 3)
[2026-01-21 17:57:30] [ERROR] [Monitor] Error sincronizando app_tareas | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-21 17:57:30] [INFO] [Monitor] Buscando app fidelizacion en API central...
[2026-01-21 17:57:31] [INFO] [Monitor] App fidelizacion encontrada en cloud (ID: 4)
[2026-01-21 17:57:31] [ERROR] [Monitor] Error sincronizando fidelizacion | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-21 17:58:31] [INFO] [Monitor] App app_tareas ya existe (ID: 3), actualizando...
[2026-01-21 17:58:31] [ERROR] [Monitor] Error sincronizando app_tareas | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-21 17:58:31] [INFO] [Monitor] App fidelizacion ya existe (ID: 4), actualizando...
[2026-01-21 17:58:31] [ERROR] [Monitor] Error sincronizando fidelizacion | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-21 17:59:32] [INFO] [Monitor] App app_tareas ya existe (ID: 3), actualizando...
[2026-01-21 17:59:32] [ERROR] [Monitor] Error sincronizando app_tareas | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-21 17:59:32] [INFO] [Monitor] App fidelizacion ya existe (ID: 4), actualizando...
[2026-01-21 17:59:32] [ERROR] [Monitor] Error sincronizando fidelizacion | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-21 18:00:32] [INFO] [Monitor] App app_tareas ya existe (ID: 3), actualizando...
[2026-01-21 18:00:33] [ERROR] [Monitor] Error sincronizando app_tareas | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-21 18:00:33] [INFO] [Monitor] App fidelizacion ya existe (ID: 4), actualizando...
[2026-01-21 18:00:33] [ERROR] [Monitor] Error sincronizando fidelizacion | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-21 18:01:28] [INFO] [Sistema] Iniciando SIAX Agent
[2026-01-21 18:01:28] [INFO] [Sistema] Escaneando servicios systemd existentes...
[2026-01-21 18:01:28] [INFO] [Discovery] 🔍 Escaneando servicios systemd en: /etc/systemd/system
[2026-01-21 18:01:28] [INFO] [Discovery] ✅ Directorio /etc/systemd/system accesible
[2026-01-21 18:01:28] [INFO] [Discovery] 📊 Escaneados 131 archivos, 0 con prefijo 'siax-app-', 0 parseados exitosamente
[2026-01-21 18:01:28] [INFO] [Config] Usando archivo de configuración: config/monitored_apps.json
[2026-01-21 18:01:28] [INFO] [Config] ✅ Configuración cargada: 2 apps desde config/monitored_apps.json
[2026-01-21 18:01:28] [INFO] [Sistema] Servidor detectado: siax-intel
[2026-01-21 18:01:28] [INFO] [Sistema] Iniciando servidor unificado en puerto 8080
[2026-01-21 18:01:28] [INFO] [Sistema] Sistema SIAX completamente operativo en puerto 8080
[2026-01-21 18:01:28] [INFO] [Monitor] Vigilando procesos para siax-intel [SIAX-Agent/0.1.0 (linux/x86_64; Rust-Monitor)]
[2026-01-21 18:01:29] [INFO] [Monitor] Buscando app app_tareas en API central...
[2026-01-21 18:01:29] [INFO] [Monitor] App app_tareas encontrada en cloud (ID: 3)
[2026-01-21 18:01:29] [ERROR] [Monitor] Error sincronizando app_tareas | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-21 18:01:29] [INFO] [Monitor] Buscando app fidelizacion en API central...
[2026-01-21 18:01:29] [INFO] [Monitor] App fidelizacion encontrada en cloud (ID: 4)
[2026-01-21 18:01:30] [ERROR] [Monitor] Error sincronizando fidelizacion | HTTP 500 Internal Server Error: {"success":false,"message":"Error al actualizar el estado","error":"Table 'webControl.app_service_history' doesn't exist"}
[2026-01-21 18:01:42] [INFO] [API] 🗑️ Solicitud de eliminación para: app_tareas
[2026-01-21 18:01:42] [INFO] [AppManager] Desregistrando aplicación (soft delete): app_tareas
[2026-01-21 18:01:42] [WARNING] [API] App no encontrada en AppManager: Aplicación no encontrada: app_tareas
[2026-01-21 18:01:42] [INFO] [API] ✅ Soft delete en JSON: app_tareas
[2026-01-21 18:01:42] [INFO] [SystemCtl] Deteniendo servicio: siax-app-app_tareas.service
[2026-01-21 18:01:50] [INFO] [API] Deteniendo servicio: siax-app-app_tareas.service
[2026-01-21 18:01:50] [INFO] [SystemCtl] Deshabilitando servicio: siax-app-app_tareas.service
[2026-01-21 18:01:56] [INFO] [API] Deshabilitando servicio: siax-app-app_tareas.service
[2026-01-21 18:01:56] [INFO] [ServiceGenerator] Eliminando servicio: /etc/systemd/system/siax-app-app_tareas.service
[2026-01-21 18:01:56] [WARNING] [ServiceGenerator] Servicio no encontrado | siax-app-app_tareas.service
[2026-01-21 18:01:56] [INFO] [API] ✅ Archivo .service eliminado: siax-app-app_tareas.service
[2026-01-21 18:01:56] [INFO] [SystemCtl] Recargando daemon de systemd
[2026-01-21 18:01:59] [INFO] [API] 🔄 daemon-reload ejecutado

View File

@@ -60,20 +60,216 @@ pub async fn register_app_handler(
} }
} }
pub async fn update_app_handler(
State(state): State<Arc<ApiState>>,
Path(app_name): Path<String>,
Json(payload): Json<RegisterAppRequest>,
) -> Result<Json<ApiResponse<OperationResponse>>, StatusCode> {
use crate::config::get_config_manager;
use crate::systemd::{SystemCtl, ServiceGenerator};
let logger = crate::logger::get_logger();
logger.info("API", &format!("✏️ Solicitud de actualización para: {}", app_name));
// Validar que el app_name coincida
if app_name != payload.app_name {
return Ok(Json(ApiResponse::error(
"El nombre de la app en la URL no coincide con el payload".to_string()
)));
}
// Parsear tipo de aplicación
let app_type = match payload.app_type.to_lowercase().as_str() {
"nodejs" | "node" => AppType::NodeJs,
"python" | "py" => AppType::Python,
_ => return Ok(Json(ApiResponse::error(
"Tipo de aplicación inválido. Use 'nodejs' o 'python'".to_string()
))),
};
// Parsear política de reinicio
let restart_policy = match payload.restart_policy.to_lowercase().as_str() {
"always" => RestartPolicy::Always,
"on-failure" | "onfailure" => RestartPolicy::OnFailure,
"no" | "never" => RestartPolicy::No,
_ => RestartPolicy::Always,
};
let config = ServiceConfig {
app_name: payload.app_name.clone(),
script_path: payload.script_path,
working_directory: payload.working_directory,
user: payload.user,
environment: payload.environment,
restart_policy,
app_type,
description: payload.description,
custom_executable: payload.custom_executable,
use_npm_start: payload.use_npm_start,
};
let service_name = format!("siax-app-{}.service", app_name);
// 1. Detener el servicio
logger.info("API", &format!("🛑 Deteniendo servicio: {}", service_name));
let _ = SystemCtl::stop(&service_name);
// 2. Regenerar el archivo .service
logger.info("API", "📝 Regenerando archivo .service con nueva configuración");
match ServiceGenerator::create_service(&config) {
Ok(service_content) => {
match ServiceGenerator::write_service_file(&config, &service_content) {
Ok(_) => {
logger.info("API", "✅ Archivo .service actualizado");
}
Err(e) => {
return Ok(Json(ApiResponse::error(
format!("Error escribiendo archivo .service: {}", e)
)));
}
}
}
Err(e) => {
return Ok(Json(ApiResponse::error(
format!("Error generando .service: {}", e)
)));
}
}
// 3. Recargar daemon
logger.info("API", "🔄 Ejecutando daemon-reload");
let _ = SystemCtl::daemon_reload();
// 4. Actualizar monitored_apps.json
let config_manager = get_config_manager();
let service_file_path = format!("/etc/systemd/system/{}", service_name);
let port = config.environment.get("PORT")
.and_then(|p| p.parse::<i32>().ok())
.unwrap_or(8080);
let entry_point = std::path::Path::new(&config.script_path)
.file_name()
.and_then(|f| f.to_str())
.unwrap_or("server.js")
.to_string();
let node_bin = config.custom_executable.clone().unwrap_or_default();
let mode = config.environment.get("NODE_ENV")
.cloned()
.unwrap_or_else(|| "production".to_string());
// Primero intentar hacer soft delete de la app anterior
let _ = config_manager.soft_delete_app(&app_name, Some("Actualizada - versión anterior".to_string()));
// Luego agregar la nueva configuración
let monitored_app = crate::config::MonitoredApp {
name: config.app_name.clone(),
service_name: service_name.clone(),
path: config.working_directory.clone(),
port,
entry_point,
node_bin,
mode,
user: config.user.clone(),
service_file_path,
registered_at: chrono::Local::now().to_rfc3339(),
deleted: false,
deleted_at: None,
deleted_reason: None,
environment: config.environment.clone(),
systemd_service: None,
created_at: None,
};
match config_manager.add_app_full(monitored_app) {
Ok(_) => {
logger.info("API", "✅ JSON actualizado");
}
Err(e) => {
logger.warning("API", &format!("No se pudo actualizar JSON: {}", e), None);
}
}
// 5. Iniciar el servicio nuevamente
logger.info("API", &format!("▶️ Iniciando servicio: {}", service_name));
match SystemCtl::start(&service_name) {
Ok(_) => {
logger.info("API", "✅ Servicio iniciado exitosamente");
}
Err(e) => {
logger.warning("API", &format!("Error al iniciar servicio: {}", e), None);
}
}
Ok(Json(ApiResponse::success(OperationResponse {
app_name: app_name.clone(),
operation: "update".to_string(),
success: true,
message: format!("Aplicación '{}' actualizada exitosamente", app_name),
})))
}
pub async fn unregister_app_handler( pub async fn unregister_app_handler(
State(state): State<Arc<ApiState>>, State(state): State<Arc<ApiState>>,
Path(app_name): Path<String>, Path(app_name): Path<String>,
) -> Result<Json<ApiResponse<OperationResponse>>, StatusCode> { ) -> Result<Json<ApiResponse<OperationResponse>>, StatusCode> {
use crate::config::get_config_manager;
use crate::systemd::{SystemCtl, ServiceGenerator};
let logger = crate::logger::get_logger();
let service_name = format!("siax-app-{}.service", app_name);
logger.info("API", &format!("🗑️ Solicitud de eliminación para: {}", app_name));
// Intentar 1: Eliminar desde AppManager (si está en memoria)
let mut deleted_from_memory = false;
match state.app_manager.unregister_app(&app_name) { match state.app_manager.unregister_app(&app_name) {
Ok(_) => Ok(Json(ApiResponse::success(OperationResponse { Ok(_) => {
app_name: app_name.clone(), logger.info("API", &format!("✅ Eliminado desde AppManager: {}", app_name));
operation: "unregister".to_string(), deleted_from_memory = true;
success: true, }
message: "Aplicación eliminada exitosamente".to_string(), Err(e) => {
}))), logger.warning("API", &format!("App no encontrada en AppManager: {}", e), None);
Err(e) => Ok(Json(ApiResponse::error(e.to_string()))), }
} }
// Intentar 2: Soft delete en JSON (siempre intentar)
let config_manager = get_config_manager();
let delete_reason = Some("Eliminada desde el panel de control".to_string());
match config_manager.soft_delete_app(&app_name, delete_reason) {
Ok(_) => {
logger.info("API", &format!("✅ Soft delete en JSON: {}", app_name));
}
Err(e) => {
logger.warning("API", &format!("No se pudo hacer soft delete en JSON: {}", e), None);
}
}
// Intentar 3: Eliminar servicio systemd físicamente (siempre intentar)
let _ = SystemCtl::stop(&service_name);
logger.info("API", &format!("Deteniendo servicio: {}", service_name));
let _ = SystemCtl::disable(&service_name);
logger.info("API", &format!("Deshabilitando servicio: {}", service_name));
match ServiceGenerator::delete_service_file(&service_name) {
Ok(_) => {
logger.info("API", &format!("✅ Archivo .service eliminado: {}", service_name));
}
Err(e) => {
logger.warning("API", &format!("No se pudo eliminar .service: {}", e), None);
}
}
let _ = SystemCtl::daemon_reload();
logger.info("API", "🔄 daemon-reload ejecutado");
Ok(Json(ApiResponse::success(OperationResponse {
app_name: app_name.clone(),
operation: "unregister".to_string(),
success: true,
message: format!("Aplicación '{}' eliminada exitosamente (servicio systemd + soft delete en JSON)", app_name),
})))
} }
pub async fn start_app_handler( pub async fn start_app_handler(
@@ -124,6 +320,30 @@ pub async fn restart_app_handler(
} }
} }
pub async fn get_app_details_handler(
Path(app_name): Path<String>,
) -> Result<Json<serde_json::Value>, StatusCode> {
use crate::config::get_config_manager;
let config_manager = get_config_manager();
let apps = config_manager.get_apps();
match apps.iter().find(|a| a.name == app_name) {
Some(app) => {
Ok(Json(serde_json::json!({
"success": true,
"data": app
})))
}
None => {
Ok(Json(serde_json::json!({
"success": false,
"error": format!("Aplicación '{}' no encontrada", app_name)
})))
}
}
}
pub async fn get_app_status_handler( pub async fn get_app_status_handler(
State(_state): State<Arc<ApiState>>, State(_state): State<Arc<ApiState>>,
Path(app_name): Path<String>, Path(app_name): Path<String>,

View File

@@ -32,6 +32,10 @@ pub struct MonitoredApp {
#[serde(default = "default_mode")] #[serde(default = "default_mode")]
pub mode: String, pub mode: String,
/// Usuario del sistema que ejecuta la aplicación
#[serde(default = "default_user")]
pub user: String,
/// Ruta completa al archivo .service de systemd /// Ruta completa al archivo .service de systemd
#[serde(default)] #[serde(default)]
pub service_file_path: String, pub service_file_path: String,
@@ -53,6 +57,12 @@ pub struct MonitoredApp {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub deleted_reason: Option<String>, pub deleted_reason: Option<String>,
// --- VARIABLES DE ENTORNO ADICIONALES ---
/// Variables de entorno ADICIONALES (las del .env se cargan con EnvironmentFile)
/// Solo almacenamos aquí las variables que el usuario agrega manualmente desde el panel
#[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
pub environment: std::collections::HashMap<String, String>,
// DEPRECATED: Mantener por compatibilidad con versiones antiguas // DEPRECATED: Mantener por compatibilidad con versiones antiguas
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub systemd_service: Option<String>, pub systemd_service: Option<String>,
@@ -64,6 +74,13 @@ fn default_mode() -> String {
"production".to_string() "production".to_string()
} }
fn default_user() -> String {
// Intentar obtener el usuario actual del sistema
std::env::var("USER")
.or_else(|_| std::env::var("LOGNAME"))
.unwrap_or_else(|_| "root".to_string())
}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppConfig { pub struct AppConfig {
pub apps: Vec<MonitoredApp>, pub apps: Vec<MonitoredApp>,
@@ -200,11 +217,13 @@ impl ConfigManager {
entry_point: String::new(), entry_point: String::new(),
node_bin: String::new(), node_bin: String::new(),
mode: "production".to_string(), mode: "production".to_string(),
user: default_user(),
service_file_path: String::new(), service_file_path: String::new(),
registered_at, registered_at,
deleted: false, deleted: false,
deleted_at: None, deleted_at: None,
deleted_reason: None, deleted_reason: None,
environment: std::collections::HashMap::new(),
systemd_service: None, systemd_service: None,
created_at: None, created_at: None,
}; };

View File

@@ -255,11 +255,13 @@ pub fn sync_discovered_services(services: Vec<DiscoveredService>) {
entry_point: service.entry_point.unwrap_or_default(), entry_point: service.entry_point.unwrap_or_default(),
node_bin: service.node_bin.unwrap_or_default(), node_bin: service.node_bin.unwrap_or_default(),
mode: service.node_env, mode: service.node_env,
user: service.user.clone().unwrap_or_else(|| "root".to_string()),
service_file_path: service.service_file.clone(), service_file_path: service.service_file.clone(),
registered_at, registered_at,
deleted: false, deleted: false,
deleted_at: None, deleted_at: None,
deleted_reason: None, deleted_reason: None,
environment: std::collections::HashMap::new(),
systemd_service: None, systemd_service: None,
created_at: None, created_at: None,
}; };

View File

@@ -54,6 +54,7 @@ pub fn create_web_router() -> Router {
.route("/scan", get(scan_processes_handler)) .route("/scan", get(scan_processes_handler))
.route("/select", get(select_processes_handler)) .route("/select", get(select_processes_handler))
.route("/register", get(register_handler)) .route("/register", get(register_handler))
.route("/edit", get(edit_handler))
.route("/add-process", post(add_process_handler)) .route("/add-process", post(add_process_handler))
.route("/logs", get(logs_handler)) .route("/logs", get(logs_handler))
.route("/clear-logs", post(clear_logs_handler)) .route("/clear-logs", post(clear_logs_handler))
@@ -122,6 +123,11 @@ async fn register_handler() -> Html<String> {
Html(template.to_string()) Html(template.to_string())
} }
async fn edit_handler() -> Html<String> {
let template = include_str!("../web/edit.html");
Html(template.to_string())
}
async fn api_docs_handler() -> Html<String> { async fn api_docs_handler() -> Html<String> {
let template = include_str!("../web/api-docs.html"); let template = include_str!("../web/api-docs.html");
Html(template.to_string()) Html(template.to_string())

View File

@@ -71,7 +71,7 @@ async fn main() {
.route("/api/logs/errors", get(api::get_system_error_logs)) .route("/api/logs/errors", get(api::get_system_error_logs))
.route("/api/apps", get(api::list_apps_handler).post(api::register_app_handler)) .route("/api/apps", get(api::list_apps_handler).post(api::register_app_handler))
.route("/api/apps/deleted", get(api::get_deleted_apps_handler)) .route("/api/apps/deleted", get(api::get_deleted_apps_handler))
.route("/api/apps/:name", delete(api::unregister_app_handler)) .route("/api/apps/:name", get(api::get_app_details_handler).delete(api::unregister_app_handler).put(api::update_app_handler))
.route("/api/apps/:name/status", get(api::get_app_status_handler)) .route("/api/apps/:name/status", get(api::get_app_status_handler))
.route("/api/apps/:name/start", post(api::start_app_handler)) .route("/api/apps/:name/start", post(api::start_app_handler))
.route("/api/apps/:name/stop", post(api::stop_app_handler)) .route("/api/apps/:name/stop", post(api::stop_app_handler))

View File

@@ -86,11 +86,13 @@ impl AppManager {
entry_point, entry_point,
node_bin, node_bin,
mode, mode,
user: config.user.clone(),
service_file_path, service_file_path,
registered_at, registered_at,
deleted: false, deleted: false,
deleted_at: None, deleted_at: None,
deleted_reason: None, deleted_reason: None,
environment: config.environment.clone(),
systemd_service: None, systemd_service: None,
created_at: None, created_at: None,
}; };

View File

@@ -3,6 +3,7 @@ use crate::models::ServiceConfig;
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
use std::process::Command; use std::process::Command;
use std::collections::HashMap;
use crate::logger::get_logger; use crate::logger::get_logger;
pub struct ServiceGenerator; pub struct ServiceGenerator;
@@ -74,7 +75,7 @@ impl ServiceGenerator {
format!("{} {}", executable, config.script_path) format!("{} {}", executable, config.script_path)
}; };
// Generar variables de entorno del usuario // Generar líneas de variables de entorno ADICIONALES (solo las del config, no del .env)
let mut env_lines: Vec<String> = config.environment let mut env_lines: Vec<String> = config.environment
.iter() .iter()
.map(|(key, value)| format!("Environment=\"{}={}\"", key, value)) .map(|(key, value)| format!("Environment=\"{}={}\"", key, value))
@@ -99,11 +100,17 @@ impl ServiceGenerator {
} }
} }
let env_vars = env_lines.join("\n");
// Agregar SyslogIdentifier para logs más claros // Agregar SyslogIdentifier para logs más claros
let syslog_id = format!("SyslogIdentifier=siax-app-{}", config.app_name); let syslog_id = format!("SyslogIdentifier=siax-app-{}", config.app_name);
// Verificar si existe .env en el proyecto
let env_file_path = Path::new(&config.working_directory).join(".env");
let has_env_file = env_file_path.exists();
if has_env_file {
logger.info("ServiceGenerator", &format!("📄 .env encontrado, usando EnvironmentFile: {}", env_file_path.display()));
}
// Construir el servicio con orden lógico // Construir el servicio con orden lógico
let mut service = format!( let mut service = format!(
r#"[Unit] r#"[Unit]
@@ -120,9 +127,26 @@ WorkingDirectory={}
config.working_directory config.working_directory
); );
// Agregar variables de entorno (PATH primero, luego las demás) // Agregar PATH si usa NVM (debe ir primero)
if !env_vars.is_empty() { // Extraer PATH de env_lines si está en la primera posición
service.push_str(&env_vars); let mut path_line: Option<String> = None;
if !env_lines.is_empty() && env_lines[0].starts_with("Environment=PATH=") {
path_line = Some(env_lines.remove(0));
}
if let Some(path) = path_line {
service.push_str(&path);
service.push('\n');
}
// ✅ AGREGAR EnvironmentFile si existe .env en el proyecto
if has_env_file {
service.push_str(&format!("EnvironmentFile={}\n", env_file_path.display()));
}
// Agregar variables de entorno ADICIONALES (las del formulario/JSON)
if !env_lines.is_empty() {
service.push_str(&env_lines.join("\n"));
service.push('\n'); service.push('\n');
} }
@@ -295,4 +319,55 @@ WantedBy=multi-user.target
Err(_) => false, Err(_) => false,
} }
} }
/// Lee el archivo .env del directorio de trabajo y retorna las variables
pub fn read_env_file(working_directory: &str) -> HashMap<String, String> {
let logger = get_logger();
let env_path = Path::new(working_directory).join(".env");
let mut env_vars = HashMap::new();
if !env_path.exists() {
logger.info("ServiceGenerator", &format!("No se encontró archivo .env en: {}", env_path.display()));
return env_vars;
}
logger.info("ServiceGenerator", &format!("Leyendo archivo .env desde: {}", env_path.display()));
match fs::read_to_string(&env_path) {
Ok(content) => {
for line in content.lines() {
let line = line.trim();
// Ignorar líneas vacías y comentarios
if line.is_empty() || line.starts_with('#') {
continue;
}
// Parsear línea KEY=VALUE
if let Some(pos) = line.find('=') {
let key = line[..pos].trim().to_string();
let mut value = line[pos + 1..].trim().to_string();
// Remover comillas simples o dobles
if (value.starts_with('\'') && value.ends_with('\'')) ||
(value.starts_with('"') && value.ends_with('"')) {
value = value[1..value.len()-1].to_string();
}
if !key.is_empty() {
env_vars.insert(key, value);
}
}
}
logger.info("ServiceGenerator", &format!("✅ Cargadas {} variables desde .env", env_vars.len()));
}
Err(e) => {
logger.warning("ServiceGenerator", &format!("Error leyendo .env: {}", e), None);
}
}
env_vars
}
} }

View File

@@ -497,24 +497,92 @@ sudo journalctl -u siax_monitor -f
4. Verificar logs en pestaña "Logs de App" 4. Verificar logs en pestaña "Logs de App"
5. Verificar errores del sistema en pestaña "Errores del Sistema" 5. Verificar errores del sistema en pestaña "Errores del Sistema"
===============================================================================
🔮 FASE 5 - MEJORAS FUTURAS Y TAREAS PENDIENTES
===============================================================================
**Fase 5.1: Script de Inicialización de .env** 🔄 PENDIENTE
[ ] Crear script/comando para sincronizar .env desde servidor central
[ ] Implementar endpoint en API Central para servir .env de producción
[ ] Script de deploy que descargue .env automáticamente:
- Opción 1: GET https://api-central.com/env/{app_name}
- Opción 2: SCP desde servidor de secrets
- Opción 3: Integración con Vault/Secrets Manager
[ ] Validación de variables requeridas antes de iniciar servicio
[ ] Logging de variables faltantes (sin exponer valores sensibles)
[ ] Documentación de variables requeridas por app
**Motivación:**
- Actualmente .env está en .gitignore (correcto para seguridad)
- Al deployar, el .env NO se copia al servidor
- Las apps fallan con "DB param: undefined"
- Proceso manual de copiar .env es propenso a errores
- Necesario automatizar la distribución segura de secrets
**Implementación Sugerida:**
```bash
# Script: sync_env.sh
#!/bin/bash
APP_NAME=$1
API_CENTRAL="https://api-central.telcotronics.com"
# Descargar .env desde servidor central
curl -H "Authorization: Bearer $SECRET_TOKEN" \
"$API_CENTRAL/secrets/$APP_NAME/.env" \
-o /home/user_apps/apps/$APP_NAME/.env
# Verificar descarga
if [ -f "/home/user_apps/apps/$APP_NAME/.env" ]; then
echo "✅ .env descargado correctamente"
# Re-registrar app para cargar variables
curl -X PUT http://localhost:8080/api/apps/$APP_NAME \
-H "Content-Type: application/json" \
-d @/tmp/app_config.json
else
echo "❌ Error descargando .env"
exit 1
fi
```
**Fase 5.2: Template de .env** 🔄 PENDIENTE
[ ] Crear .env.example en cada proyecto
[ ] Documentar variables requeridas vs opcionales
[ ] Script de validación: check_env.sh
[ ] Generar .env desde template interactivo
**Fase 5.3: Gestión Centralizada de Secrets** 🔄 PENDIENTE
[ ] Integración con HashiCorp Vault
[ ] Soporte para AWS Secrets Manager
[ ] Rotación automática de passwords
[ ] Auditoría de acceso a secrets
=============================================================================== ===============================================================================
📊 MÉTRICAS DEL PROYECTO 📊 MÉTRICAS DEL PROYECTO
=============================================================================== ===============================================================================
**Líneas de código:** ~3,500 **Líneas de código:** ~4,200
**Archivos Rust:** 15 **Archivos Rust:** 15
**Archivos HTML:** 8 **Archivos HTML:** 9 (agregado edit.html)
**Endpoints API:** 12 **Endpoints API:** 15 (GET/PUT/DELETE /apps/:name, GET /apps/deleted, POST /apps/:name/restore)
**Commits totales:** 15+ **Commits totales:** 25+
**Tiempo desarrollo:** ~3 días **Tiempo desarrollo:** ~4 días
**Bugs críticos resueltos:** 8 **Bugs críticos resueltos:** 12
**Fase actual:** 4.8 (Completada) **Fase actual:** 4.8 (Completada) + Mejoras (Soft Delete, CRUD Update, Auto .env)
**Nuevas Features:**
✅ Soft Delete con historial
✅ Función EDITAR apps (CRUD completo)
✅ Auto-carga de variables desde .env
✅ Campo 'user' en configuración
✅ Eliminación robusta (3 fuentes)
✅ UI mejorada (overflow logs, modal claro)
=============================================================================== ===============================================================================
🎉 FIN DEL DOCUMENTO 🎉 FIN DEL DOCUMENTO
=============================================================================== ===============================================================================
Última actualización: 2026-01-18 23:45:00 Última actualización: 2026-01-21 22:30:00
Actualizado por: Claude AI Assistant Actualizado por: Claude AI Assistant
Proyecto: SIAX Monitor v0.1.0 Proyecto: SIAX Monitor v0.1.0
Estado: PRODUCTION-READY ✅ Estado: PRODUCTION-READY ✅
Próxima fase: 5.1 (Script inicialización .env)

615
web/edit.html Normal file
View File

@@ -0,0 +1,615 @@
<!doctype html>
<html class="dark" lang="es" dir="ltr">
<head>
<meta charset="utf-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>Editar Aplicación - SIAX Monitor</title>
<link rel="icon" type="image/svg+xml" href="/static/icon/favicon.svg" />
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap"
rel="stylesheet"
/>
<script>
tailwind.config = {
darkMode: "class",
theme: {
extend: {
colors: {
primary: "#137fec",
"background-light": "#f6f7f8",
"background-dark": "#101922",
},
fontFamily: { display: ["Inter", "sans-serif"] },
borderRadius: {
DEFAULT: "0.25rem",
lg: "0.5rem",
xl: "0.75rem",
full: "9999px",
},
},
},
};
</script>
<style>
body {
font-family: "Inter", sans-serif;
}
.material-symbols-outlined {
font-variation-settings:
"FILL" 0,
"wght" 400,
"GRAD" 0,
"opsz" 24;
}
</style>
</head>
<body
class="bg-background-light dark:bg-background-dark font-display text-white min-h-screen flex flex-col"
>
<!-- Sticky Top Navigation -->
<header
class="sticky top-0 z-50 w-full border-b border-solid border-[#283039] bg-background-dark/80 backdrop-blur-md px-4 md:px-10 py-3"
>
<div
class="max-w-[1200px] mx-auto flex items-center justify-between whitespace-nowrap"
>
<div class="flex items-center gap-8">
<div class="flex items-center gap-4 text-white">
<div
class="size-8 bg-primary rounded-lg flex items-center justify-center"
>
<img
src="/static/icon/logo.png"
alt="Logo"
class="w-full h-full object-cover"
/>
</div>
<h2
class="text-white text-lg font-bold leading-tight tracking-[-0.015em]"
>
SIAX Monitor
</h2>
</div>
</div>
<div class="flex flex-1 justify-end gap-6 items-center">
<nav class="hidden md:flex items-center gap-6">
<a
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
href="/"
>Panel</a
>
<a
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
href="/scan"
>Escanear</a
>
<a
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
href="/select"
>Selecionar Detectada</a
>
<a
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
href="/logs"
>Registros</a
>
</nav>
<button
class="hidden lg:flex cursor-pointer items-center justify-center rounded-lg h-9 px-4 bg-primary text-white text-sm font-bold transition-opacity hover:opacity-90"
onclick="window.location.href = '/register'"
>
<span>Nueva App</span>
</button>
</div>
</div>
</header>
<main class="flex-1 max-w-[900px] mx-auto w-full px-4 py-8 space-y-6">
<!-- Page Heading -->
<div class="flex flex-col gap-2">
<h1
class="text-white text-4xl font-black leading-tight tracking-[-0.033em]"
>
Register New Application
</h1>
<p class="text-[#9dabb9] text-base font-normal">
Register a Node.js or Python application to manage with
systemd.
</p>
</div>
<!-- Alert Messages -->
<div
id="alert-success"
class="hidden rounded-xl p-4 bg-green-500/20 border border-green-500/30"
>
<div class="flex items-center gap-3">
<span class="material-symbols-outlined text-green-400"
>check_circle</span
>
<p
class="text-green-400 text-sm font-medium"
id="success-message"
></p>
</div>
</div>
<div
id="alert-error"
class="hidden rounded-xl p-4 bg-red-500/20 border border-red-500/30"
>
<div class="flex items-center gap-3">
<span class="material-symbols-outlined text-red-400"
>error</span
>
<p
class="text-red-400 text-sm font-medium"
id="error-message"
></p>
</div>
</div>
<!-- Registration Form -->
<form id="registerForm" class="space-y-6">
<!-- Basic Information Card -->
<div
class="rounded-xl border border-[#283039] bg-[#161f2a] p-6 space-y-5"
>
<h3 class="text-white text-lg font-bold">
Basic Information
</h3>
<div class="space-y-2">
<label class="block text-[#9dabb9] text-sm font-medium">
Application Name <span class="text-red-400">*</span>
</label>
<input
type="text"
id="app_name"
name="app_name"
required
placeholder="mi-app"
class="w-full bg-[#1c2730] border border-[#283039] rounded-lg px-4 py-2.5 text-white placeholder-[#9dabb9] focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
/>
<p class="text-[#9dabb9] text-xs">
Solo letras, números, guiones y guiones bajos
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<div class="space-y-2">
<label
class="block text-[#9dabb9] text-sm font-medium"
>
Application Type
<span class="text-red-400">*</span>
</label>
<select
id="app_type"
name="app_type"
required
class="w-full bg-[#1c2730] border border-[#283039] rounded-lg px-4 py-2.5 text-white focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
>
<option value="nodejs">Node.js</option>
<option value="python">Python / FastAPI</option>
</select>
</div>
<div class="space-y-2">
<label
class="block text-[#9dabb9] text-sm font-medium"
>
Restart Policy
<span class="text-red-400">*</span>
</label>
<select
id="restart_policy"
name="restart_policy"
required
class="w-full bg-[#1c2730] border border-[#283039] rounded-lg px-4 py-2.5 text-white focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
>
<option value="always">
Always (Always restart)
</option>
<option value="on-failure">
On-Failure (Only if fails)
</option>
<option value="no">No (No reiniciar)</option>
</select>
</div>
</div>
<div class="space-y-2">
<label class="block text-[#9dabb9] text-sm font-medium">
Description
</label>
<textarea
id="description"
name="description"
rows="2"
placeholder="Descripción de la aplicación..."
class="w-full bg-[#1c2730] border border-[#283039] rounded-lg px-4 py-2.5 text-white placeholder-[#9dabb9] focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent resize-none"
></textarea>
</div>
</div>
<!-- Paths & User Card -->
<div
class="rounded-xl border border-[#283039] bg-[#161f2a] p-6 space-y-5"
>
<h3 class="text-white text-lg font-bold">
Rutas y Usuario
</h3>
<div class="space-y-2">
<label class="block text-[#9dabb9] text-sm font-medium">
Script Path <span class="text-red-400">*</span>
</label>
<input
type="text"
id="script_path"
name="script_path"
required
placeholder="/opt/apps/mi-app/index.js"
class="w-full bg-[#1c2730] border border-[#283039] rounded-lg px-4 py-2.5 text-white placeholder-[#9dabb9] focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
/>
<p class="text-[#9dabb9] text-xs">
Ruta completa al archivo principal (.js o .py)
</p>
</div>
<div class="space-y-2">
<label class="block text-[#9dabb9] text-sm font-medium">
Working Directory
<span class="text-red-400">*</span>
</label>
<input
type="text"
id="working_directory"
name="working_directory"
required
placeholder="/opt/apps/mi-app"
class="w-full bg-[#1c2730] border border-[#283039] rounded-lg px-4 py-2.5 text-white placeholder-[#9dabb9] focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
/>
<p class="text-[#9dabb9] text-xs">
Directorio desde el cual se ejecutará la aplicación
</p>
</div>
<div class="space-y-2">
<label class="block text-[#9dabb9] text-sm font-medium">
System User <span class="text-red-400">*</span>
</label>
<input
type="text"
id="user"
name="user"
required
placeholder="nodejs"
value="nodejs"
class="w-full bg-[#1c2730] border border-[#283039] rounded-lg px-4 py-2.5 text-white placeholder-[#9dabb9] focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
/>
<p class="text-[#9dabb9] text-xs">
Usuario bajo el cual se ejecutará el proceso
</p>
</div>
</div>
<!-- Environment Variables Card -->
<div
class="rounded-xl border border-[#283039] bg-[#161f2a] p-6 space-y-5"
>
<div class="flex items-center justify-between">
<h3 class="text-white text-lg font-bold">
Environment Variables
</h3>
<button
type="button"
onclick="addEnvVar()"
class="flex items-center gap-2 px-4 py-2 bg-[#1c2730] hover:bg-[#283039] border border-[#283039] rounded-lg text-white text-sm font-medium transition-colors"
>
<span class="material-symbols-outlined text-[18px]"
>add</span
>
Add Variable
</button>
</div>
<div id="env-container" class="space-y-3">
<div
class="env-item grid grid-cols-1 md:grid-cols-[1fr_1fr_auto] gap-3"
>
<input
type="text"
placeholder="KEY"
class="env-key bg-[#1c2730] border border-[#283039] rounded-lg px-4 py-2.5 text-white placeholder-[#9dabb9] focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
/>
<input
type="text"
placeholder="valor"
class="env-value bg-[#1c2730] border border-[#283039] rounded-lg px-4 py-2.5 text-white placeholder-[#9dabb9] focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
/>
<button
type="button"
onclick="removeEnvVar(this)"
class="flex items-center justify-center w-full md:w-auto px-4 py-2.5 bg-red-500/20 hover:bg-red-500/30 border border-red-500/30 rounded-lg text-red-400 transition-colors"
>
<span
class="material-symbols-outlined text-[18px]"
>delete</span
>
</button>
</div>
</div>
</div>
<!-- Action Buttons -->
<div
class="flex flex-col-reverse sm:flex-row gap-3 justify-between pt-4"
>
<button
type="button"
onclick="window.location.href = '/'"
class="flex items-center justify-center rounded-lg h-12 px-6 bg-[#1c2730] hover:bg-[#283039] border border-[#283039] text-white text-sm font-bold transition-colors"
>
Cancel
</button>
<button
type="submit"
class="flex items-center justify-center rounded-lg h-12 px-6 bg-primary hover:brightness-110 text-white text-sm font-bold transition-all gap-2"
>
<span class="material-symbols-outlined text-[18px]"
>check_circle</span
>
<span>Editar Aplicación</span>
</button>
</div>
</form>
</main>
<script>
function addEnvVar() {
addEnvironmentVariable("", "");
}
function addEnvironmentVariable(key, value) {
const container = document.getElementById("env-container");
const envItem = document.createElement("div");
envItem.className =
"env-item grid grid-cols-1 md:grid-cols-[1fr_1fr_auto] gap-3";
envItem.innerHTML = `
<input
type="text"
placeholder="KEY"
value="${key}"
class="env-key bg-[#1c2730] border border-[#283039] rounded-lg px-4 py-2.5 text-white placeholder-[#9dabb9] focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
/>
<input
type="text"
placeholder="valor"
value="${value}"
class="env-value bg-[#1c2730] border border-[#283039] rounded-lg px-4 py-2.5 text-white placeholder-[#9dabb9] focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
/>
<button
type="button"
onclick="removeEnvVar(this)"
class="flex items-center justify-center w-full md:w-auto px-4 py-2.5 bg-red-500/20 hover:bg-red-500/30 border border-red-500/30 rounded-lg text-red-400 transition-colors"
>
<span class="material-symbols-outlined text-[18px]">delete</span>
</button>
`;
container.appendChild(envItem);
}
function removeEnvVar(btn) {
const container = document.getElementById("env-container");
if (container.children.length > 1) {
btn.closest(".env-item").remove();
}
}
function showAlert(message, type) {
const successAlert = document.getElementById("alert-success");
const errorAlert = document.getElementById("alert-error");
if (type === "success") {
document.getElementById("success-message").textContent =
message;
successAlert.classList.remove("hidden");
errorAlert.classList.add("hidden");
} else {
document.getElementById("error-message").textContent =
message;
errorAlert.classList.remove("hidden");
successAlert.classList.add("hidden");
}
// Scroll to top
window.scrollTo({ top: 0, behavior: "smooth" });
// Hide after 5 seconds
setTimeout(() => {
successAlert.classList.add("hidden");
errorAlert.classList.add("hidden");
}, 5000);
}
// Obtener nombre de app desde URL
const urlParams = new URLSearchParams(window.location.search);
const appName = urlParams.get("app");
if (!appName) {
alert("No se especificó el nombre de la aplicación");
window.location.href = "/";
}
// Cargar datos de la app
async function loadAppData() {
try {
const response = await fetch(`/api/apps/${appName}`);
const result = await response.json();
if (!result.success) {
alert("Error: " + result.error);
window.location.href = "/";
return;
}
const app = result.data;
console.log("App data:", app); // Debug
// Llenar formulario con datos actuales
document.getElementById("app_name").value = app.name || "";
document.getElementById("app_name").readOnly = true; // No cambiar nombre
// Construir script_path completo
const scriptPath =
app.path && app.entry_point
? `${app.path}/${app.entry_point}`.replace(
"//",
"/",
)
: app.entry_point || "";
document.getElementById("script_path").value = scriptPath;
document.getElementById("working_directory").value =
app.path || "";
// Cargar usuario desde JSON (sin fallback)
document.getElementById("user").value = app.user;
document.getElementById("restart_policy").value = "always";
document.getElementById("app_type").value = "nodejs";
document.getElementById("description").value = "";
// ✅ Cargar variables de entorno ADICIONALES desde JSON
// (Las del .env se cargan automáticamente con EnvironmentFile)
if (
app.environment &&
Object.keys(app.environment).length > 0
) {
// Limpiar el campo vacío por defecto
document.getElementById("env-container").innerHTML = "";
// Agregar cada variable del JSON
Object.entries(app.environment).forEach(
([key, value]) => {
addEnvironmentVariable(key, value);
},
);
console.log(
`✅ Cargadas ${Object.keys(app.environment).length} variables adicionales desde JSON`,
);
}
} catch (error) {
console.error("Error cargando app:", error);
alert("Error al cargar los datos de la aplicación");
}
}
loadAppData();
document
.getElementById("registerForm")
.addEventListener("submit", async (e) => {
e.preventDefault();
const formData = {
app_name: document.getElementById("app_name").value,
script_path:
document.getElementById("script_path").value,
working_directory:
document.getElementById("working_directory").value,
user: document.getElementById("user").value,
environment: {},
restart_policy:
document.getElementById("restart_policy").value,
app_type: document.getElementById("app_type").value,
description:
document.getElementById("description").value ||
null,
};
// Collect environment variables
const envItems = document.querySelectorAll(".env-item");
envItems.forEach((item) => {
const key = item.querySelector(".env-key").value;
const value = item.querySelector(".env-value").value;
if (key && value) {
formData.environment[key] = value;
}
});
try {
const response = await fetch(`/api/apps/${appName}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
});
const result = await response.json();
if (result.success) {
showAlert(
`✅ Aplicación actualizada: ${formData.app_name}`,
"success",
);
setTimeout(() => {
window.location.href = "/";
}, 2000);
// Reset form
document.getElementById("registerForm").reset();
document.getElementById("env-container").innerHTML =
`
<div class="env-item grid grid-cols-1 md:grid-cols-[1fr_1fr_auto] gap-3">
<input type="text" placeholder="KEY" class="env-key bg-[#1c2730] border border-[#283039] rounded-lg px-4 py-2.5 text-white placeholder-[#9dabb9] focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" />
<input type="text" placeholder="valor" class="env-value bg-[#1c2730] border border-[#283039] rounded-lg px-4 py-2.5 text-white placeholder-[#9dabb9] focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" />
<button type="button" onclick="removeEnvVar(this)" class="flex items-center justify-center w-full md:w-auto px-4 py-2.5 bg-red-500/20 hover:bg-red-500/30 border border-red-500/30 rounded-lg text-red-400 transition-colors">
<span class="material-symbols-outlined text-[18px]">delete</span>
</button>
</div>
`;
} else {
showAlert(
"Error: " +
(result.error || "Error desconocido"),
"error",
);
}
} catch (error) {
showAlert(
"Connection error: " + error.message,
"error",
);
}
});
// Auto-fill working_directory based on script_path
document
.getElementById("script_path")
.addEventListener("blur", function () {
const scriptPath = this.value;
const workingDirInput =
document.getElementById("working_directory");
if (scriptPath && !workingDirInput.value) {
const dir = scriptPath.substring(
0,
scriptPath.lastIndexOf("/"),
);
workingDirInput.value = dir;
}
});
</script>
</body>
</html>

View File

@@ -626,28 +626,35 @@
class="material-symbols-outlined text-[14px] text-red-500" class="material-symbols-outlined text-[14px] text-red-500"
>close</span >close</span
> >
Servicio systemd (siax-app-*.service) Servicio systemd:
<span
id="delete-service-name"
class="font-mono"
></span>
</li> </li>
<li class="flex items-center gap-2"> <li class="flex items-center gap-2">
<span <span
class="material-symbols-outlined text-[14px] text-red-500" class="material-symbols-outlined text-[14px] text-red-500"
>close</span >close</span
> >
Archivo de configuración en /etc/systemd/system/ Archivo .service en /etc/systemd/system/
</li> </li>
<li class="flex items-center gap-2"> <li class="flex items-center gap-2">
<span <span
class="material-symbols-outlined text-[14px] text-red-500" class="material-symbols-outlined text-[14px] text-amber-500"
>close</span >archive</span
> >
Registro en monitored_apps.json Se marcará como eliminada en monitored_apps.json
(soft delete)
</li> </li>
<li class="flex items-center gap-2"> <li
class="flex items-center gap-2 text-green-600 dark:text-green-400"
>
<span <span
class="material-symbols-outlined text-[14px] text-red-500" class="material-symbols-outlined text-[14px]"
>close</span >info</span
> >
Historial de monitoreo Podrás restaurarla desde el historial
</li> </li>
</ul> </ul>
</div> </div>
@@ -785,6 +792,11 @@
</button> </button>
` `
} }
<button class="text-purple-400 hover:text-purple-300 transition-colors p-1.5 rounded hover:bg-purple-900/20"
onclick="window.location.href='/edit?app=${app.name}'"
title="Editar">
<span class="material-symbols-outlined text-[20px]">edit</span>
</button>
<button class="text-blue-400 hover:text-blue-300 transition-colors p-1.5 rounded hover:bg-blue-900/20" <button class="text-blue-400 hover:text-blue-300 transition-colors p-1.5 rounded hover:bg-blue-900/20"
onclick="window.location.href='/logs'" onclick="window.location.href='/logs'"
title="Ver logs"> title="Ver logs">
@@ -866,6 +878,8 @@
appToDelete = appName; appToDelete = appName;
document.getElementById("delete-app-name").textContent = document.getElementById("delete-app-name").textContent =
appName; appName;
document.getElementById("delete-service-name").textContent =
`siax-app-${appName}.service`;
document document
.getElementById("delete-modal") .getElementById("delete-modal")
.classList.remove("hidden"); .classList.remove("hidden");

View File

@@ -238,9 +238,12 @@
<!-- Tab Content: App Logs --> <!-- Tab Content: App Logs -->
<div <div
id="content-app-logs" id="content-app-logs"
class="flex-1 bg-[#0a0f16] overflow-y-auto p-4 font-mono text-sm tab-content" class="flex-1 bg-[#0a0f16] overflow-y-auto overflow-x-auto p-4 font-mono text-sm tab-content"
> >
<div id="log-container" class="space-y-1"> <div
id="log-container"
class="space-y-1 break-words overflow-wrap-anywhere"
>
<!-- Welcome Message --> <!-- Welcome Message -->
<div class="text-[#9dabb9] opacity-50"> <div class="text-[#9dabb9] opacity-50">
<span class="text-green-400"></span> SIAX Monitor <span class="text-green-400"></span> SIAX Monitor
@@ -256,9 +259,12 @@
<!-- Tab Content: System Errors --> <!-- Tab Content: System Errors -->
<div <div
id="content-system-errors" id="content-system-errors"
class="hidden flex-1 bg-[#0a0f16] overflow-y-auto p-4 font-mono text-sm tab-content" class="hidden flex-1 bg-[#0a0f16] overflow-y-auto overflow-x-auto p-4 font-mono text-sm tab-content"
> >
<div id="system-errors-container" class="space-y-1"> <div
id="system-errors-container"
class="space-y-1 break-words overflow-wrap-anywhere"
>
<div class="text-[#9dabb9] opacity-50"> <div class="text-[#9dabb9] opacity-50">
<span class="text-yellow-400"></span> Cargando logs <span class="text-yellow-400"></span> Cargando logs
de errores del sistema... de errores del sistema...
@@ -405,7 +411,8 @@
function appendLog(type, message, logData = null) { function appendLog(type, message, logData = null) {
const logContainer = document.getElementById("log-container"); const logContainer = document.getElementById("log-container");
const logEntry = document.createElement("div"); const logEntry = document.createElement("div");
logEntry.className = "log-line"; logEntry.className =
"log-line break-words overflow-wrap-anywhere";
const timestamp = new Date() const timestamp = new Date()
.toISOString() .toISOString()
@@ -572,7 +579,7 @@
color = "text-blue-400"; color = "text-blue-400";
} }
return `<div class="log-line ${color}">${icon} ${escapeHtml(line)}</div>`; return `<div class="log-line break-words overflow-wrap-anywhere ${color}">${icon} ${escapeHtml(line)}</div>`;
}) })
.join(""); .join("");