Compare commits
24 Commits
f67704f289
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 058e4781e6 | |||
| 93d178b216 | |||
| cd14cc5c06 | |||
| bb25004e67 | |||
| 9e56490b05 | |||
| d2b8d0222c | |||
| d8b3214ede | |||
| 2f867cb7ed | |||
| 6fa7b5c86c | |||
| fb3db3c713 | |||
| 7a66f25150 | |||
| 13b36dda5f | |||
| 60f38be957 | |||
| 6ab43980aa | |||
| e850a081f4 | |||
| 3798f911f1 | |||
| fbc89e9bf0 | |||
| 868f3a2d30 | |||
| 87ce154789 | |||
| f9e6439b24 | |||
| 246b5c8342 | |||
| 8822e9e6b5 | |||
| ad9b46bdc5 | |||
| b6fa1fa472 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1,3 @@
|
|||||||
/target
|
/target
|
||||||
|
logs/*.log
|
||||||
|
config/monitored_apps.json
|
||||||
|
|||||||
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,347 +1,51 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
#######################################
|
# --- CONFIGURACIÓN ---
|
||||||
# SIAX Agent - Script de Despliegue
|
BINARY_NAME="siax_monitor"
|
||||||
# Instalación automática production-ready
|
TARGET="x86_64-unknown-linux-gnu"
|
||||||
#######################################
|
LOCAL_PATH="target/$TARGET/release/$BINARY_NAME"
|
||||||
|
|
||||||
set -e # Salir si hay errores
|
# 1. Preguntar método de transferencia
|
||||||
|
echo "Selecciona el método de transferencia:"
|
||||||
# Colores para output
|
select METODO in "scp" "rsync"; do
|
||||||
RED='\033[0;31m'
|
case $METODO in
|
||||||
GREEN='\033[0;32m'
|
scp|rsync) break ;;
|
||||||
YELLOW='\033[1;33m'
|
*) echo "Opción inválida, elige 1 o 2." ;;
|
||||||
BLUE='\033[0;34m'
|
esac
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Variables
|
|
||||||
INSTALL_DIR="/opt/siax-agent"
|
|
||||||
SERVICE_USER="siax-agent"
|
|
||||||
BACKUP_DIR="/tmp/siax-agent-backup-$(date +%s)"
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Funciones
|
|
||||||
#######################################
|
|
||||||
|
|
||||||
print_header() {
|
|
||||||
echo -e "${BLUE}"
|
|
||||||
echo "============================================"
|
|
||||||
echo " SIAX Agent - Deployment Script"
|
|
||||||
echo "============================================"
|
|
||||||
echo -e "${NC}"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_success() {
|
|
||||||
echo -e "${GREEN}✅ $1${NC}"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_error() {
|
|
||||||
echo -e "${RED}❌ $1${NC}"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_warning() {
|
|
||||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_info() {
|
|
||||||
echo -e "${BLUE}ℹ️ $1${NC}"
|
|
||||||
}
|
|
||||||
|
|
||||||
check_root() {
|
|
||||||
if [ "$EUID" -ne 0 ]; then
|
|
||||||
print_error "Este script debe ejecutarse como root"
|
|
||||||
echo "Usa: sudo ./desplegar_agent.sh"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
check_dependencies() {
|
|
||||||
print_info "Verificando dependencias..."
|
|
||||||
|
|
||||||
local deps=("systemctl" "cargo" "rustc")
|
|
||||||
local missing=()
|
|
||||||
|
|
||||||
for dep in "${deps[@]}"; do
|
|
||||||
if ! command -v $dep &> /dev/null; then
|
|
||||||
missing+=($dep)
|
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ ${#missing[@]} -ne 0 ]; then
|
# 2. Compilar
|
||||||
print_error "Faltan dependencias: ${missing[*]}"
|
echo "📦 Compilando..."
|
||||||
echo ""
|
cargo build --release --target $TARGET
|
||||||
echo "Instalación de Rust:"
|
if [ $? -ne 0 ]; then echo "❌ Error en compilación"; exit 1; fi
|
||||||
echo " curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
|
|
||||||
echo ""
|
|
||||||
echo "Instalación de systemd (debería estar instalado por defecto):"
|
|
||||||
echo " sudo apt-get install systemd # Debian/Ubuntu"
|
|
||||||
echo " sudo yum install systemd # RedHat/CentOS"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
print_success "Todas las dependencias están instaladas"
|
# --- FUNCIÓN DE SUBIDA ---
|
||||||
}
|
upload_file() {
|
||||||
|
local IP=$1
|
||||||
|
local USER=$2
|
||||||
|
local DEST=$3
|
||||||
|
|
||||||
backup_existing() {
|
echo "🚀 Subiendo a $USER@$IP vía $METODO..."
|
||||||
if [ -d "$INSTALL_DIR" ]; then
|
|
||||||
print_warning "Instalación existente detectada"
|
|
||||||
print_info "Creando backup en: $BACKUP_DIR"
|
|
||||||
mkdir -p "$BACKUP_DIR"
|
|
||||||
cp -r "$INSTALL_DIR" "$BACKUP_DIR/"
|
|
||||||
print_success "Backup creado"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
compile_release() {
|
if [ "$METODO" = "scp" ]; then
|
||||||
print_info "Compilando SIAX Agent en modo release..."
|
scp "$LOCAL_PATH" "$USER@$IP:$DEST/"
|
||||||
|
|
||||||
if cargo build --release; then
|
|
||||||
print_success "Compilación exitosa"
|
|
||||||
else
|
else
|
||||||
print_error "Error en la compilación"
|
# rsync -avz: a (archivo/permisos), v (visual), z (comprimido)
|
||||||
rollback
|
rsync -avz "$LOCAL_PATH" "$USER@$IP:$DEST/"
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
}
|
|
||||||
|
|
||||||
create_user() {
|
if [ $? -eq 0 ]; then
|
||||||
if id "$SERVICE_USER" &>/dev/null; then
|
echo "✅ $IP: Completado."
|
||||||
print_info "Usuario $SERVICE_USER ya existe"
|
|
||||||
else
|
else
|
||||||
print_info "Creando usuario del sistema: $SERVICE_USER"
|
echo "❌ $IP: Falló la subida."
|
||||||
useradd --system --no-create-home --shell /bin/false "$SERVICE_USER"
|
|
||||||
print_success "Usuario creado"
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
install_binary() {
|
# --- LISTA DE SERVIDORES ---
|
||||||
print_info "Instalando binario en $INSTALL_DIR..."
|
# Formato: upload_file "IP" "USUARIO" "RUTA_DESTINO"
|
||||||
|
upload_file "192.168.10.145" "root" "/root/app"
|
||||||
|
upload_file "192.168.10.150" "pablinux" "/home/pablinux/app"
|
||||||
|
upload_file "192.168.10.160" "user_apps" "/home/user_apps/apps"
|
||||||
|
|
||||||
mkdir -p "$INSTALL_DIR"
|
echo "------------------------------------------------"
|
||||||
mkdir -p "$INSTALL_DIR/config"
|
echo "Done!"
|
||||||
mkdir -p "$INSTALL_DIR/logs"
|
|
||||||
|
|
||||||
cp target/release/siax_monitor "$INSTALL_DIR/siax-agent"
|
|
||||||
chmod +x "$INSTALL_DIR/siax-agent"
|
|
||||||
|
|
||||||
# Copiar archivos de configuración si existen
|
|
||||||
if [ -f "config/monitored_apps.json" ]; then
|
|
||||||
cp config/monitored_apps.json "$INSTALL_DIR/config/"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Copiar archivos web
|
|
||||||
if [ -d "web" ]; then
|
|
||||||
cp -r web "$INSTALL_DIR/"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Permisos
|
|
||||||
chown -R $SERVICE_USER:$SERVICE_USER "$INSTALL_DIR"
|
|
||||||
|
|
||||||
print_success "Binario instalado"
|
|
||||||
}
|
|
||||||
|
|
||||||
configure_sudoers() {
|
|
||||||
print_info "Configurando permisos sudo para systemctl..."
|
|
||||||
|
|
||||||
local sudoers_file="/etc/sudoers.d/siax-agent"
|
|
||||||
|
|
||||||
cat > "$sudoers_file" << EOF
|
|
||||||
# SIAX Agent - Permisos para gestionar servicios systemd
|
|
||||||
$SERVICE_USER ALL=(ALL) NOPASSWD: /bin/systemctl start *
|
|
||||||
$SERVICE_USER ALL=(ALL) NOPASSWD: /bin/systemctl stop *
|
|
||||||
$SERVICE_USER ALL=(ALL) NOPASSWD: /bin/systemctl restart *
|
|
||||||
$SERVICE_USER ALL=(ALL) NOPASSWD: /bin/systemctl status *
|
|
||||||
$SERVICE_USER ALL=(ALL) NOPASSWD: /bin/systemctl enable *
|
|
||||||
$SERVICE_USER ALL=(ALL) NOPASSWD: /bin/systemctl disable *
|
|
||||||
$SERVICE_USER ALL=(ALL) NOPASSWD: /bin/systemctl daemon-reload
|
|
||||||
$SERVICE_USER ALL=(ALL) NOPASSWD: /bin/systemctl is-active *
|
|
||||||
$SERVICE_USER ALL=(ALL) NOPASSWD: /bin/systemctl list-unit-files *
|
|
||||||
$SERVICE_USER ALL=(ALL) NOPASSWD: /bin/journalctl *
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chmod 0440 "$sudoers_file"
|
|
||||||
|
|
||||||
# Validar sintaxis
|
|
||||||
if visudo -c -f "$sudoers_file" &>/dev/null; then
|
|
||||||
print_success "Configuración de sudoers creada"
|
|
||||||
else
|
|
||||||
print_error "Error en configuración de sudoers"
|
|
||||||
rm -f "$sudoers_file"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
create_systemd_service() {
|
|
||||||
print_info "Creando servicio systemd para SIAX Agent..."
|
|
||||||
|
|
||||||
cat > /etc/systemd/system/siax-agent.service << EOF
|
|
||||||
[Unit]
|
|
||||||
Description=SIAX Agent - Process Monitor and Manager
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=$SERVICE_USER
|
|
||||||
WorkingDirectory=$INSTALL_DIR
|
|
||||||
ExecStart=$INSTALL_DIR/siax-agent
|
|
||||||
Restart=always
|
|
||||||
RestartSec=10
|
|
||||||
StandardOutput=journal
|
|
||||||
StandardError=journal
|
|
||||||
|
|
||||||
# Security hardening
|
|
||||||
NoNewPrivileges=true
|
|
||||||
PrivateTmp=true
|
|
||||||
ProtectSystem=strict
|
|
||||||
ReadWritePaths=$INSTALL_DIR/config $INSTALL_DIR/logs /etc/systemd/system
|
|
||||||
ProtectHome=true
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl enable siax-agent.service
|
|
||||||
|
|
||||||
print_success "Servicio systemd creado y habilitado"
|
|
||||||
}
|
|
||||||
|
|
||||||
verify_installation() {
|
|
||||||
print_info "Verificando instalación..."
|
|
||||||
|
|
||||||
local errors=0
|
|
||||||
|
|
||||||
# Verificar binario
|
|
||||||
if [ ! -f "$INSTALL_DIR/siax-agent" ]; then
|
|
||||||
print_error "Binario no encontrado"
|
|
||||||
((errors++))
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Verificar permisos
|
|
||||||
if [ ! -r "$INSTALL_DIR/siax-agent" ]; then
|
|
||||||
print_error "Permisos incorrectos en binario"
|
|
||||||
((errors++))
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Verificar servicio
|
|
||||||
if ! systemctl is-enabled siax-agent.service &>/dev/null; then
|
|
||||||
print_error "Servicio no habilitado"
|
|
||||||
((errors++))
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Verificar sudoers
|
|
||||||
if [ ! -f "/etc/sudoers.d/siax-agent" ]; then
|
|
||||||
print_warning "Configuración de sudoers no encontrada"
|
|
||||||
echo " El agente podría tener problemas para gestionar servicios"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ $errors -eq 0 ]; then
|
|
||||||
print_success "Verificación exitosa"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
print_error "Verificación falló con $errors errores"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
start_service() {
|
|
||||||
print_info "Iniciando SIAX Agent..."
|
|
||||||
|
|
||||||
if systemctl start siax-agent.service; then
|
|
||||||
sleep 2
|
|
||||||
if systemctl is-active siax-agent.service &>/dev/null; then
|
|
||||||
print_success "SIAX Agent iniciado correctamente"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
print_error "SIAX Agent no pudo iniciarse"
|
|
||||||
echo ""
|
|
||||||
echo "Ver logs con: journalctl -u siax-agent.service -n 50"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
print_error "Error al iniciar el servicio"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
rollback() {
|
|
||||||
print_warning "Ejecutando rollback..."
|
|
||||||
|
|
||||||
systemctl stop siax-agent.service 2>/dev/null || true
|
|
||||||
systemctl disable siax-agent.service 2>/dev/null || true
|
|
||||||
|
|
||||||
if [ -d "$BACKUP_DIR" ]; then
|
|
||||||
rm -rf "$INSTALL_DIR"
|
|
||||||
cp -r "$BACKUP_DIR/siax-agent" "$INSTALL_DIR"
|
|
||||||
systemctl start siax-agent.service 2>/dev/null || true
|
|
||||||
print_success "Rollback completado"
|
|
||||||
else
|
|
||||||
print_warning "No hay backup disponible para rollback"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
print_summary() {
|
|
||||||
echo ""
|
|
||||||
echo -e "${GREEN}============================================${NC}"
|
|
||||||
echo -e "${GREEN} ✅ SIAX Agent instalado exitosamente${NC}"
|
|
||||||
echo -e "${GREEN}============================================${NC}"
|
|
||||||
echo ""
|
|
||||||
echo "📊 Interface Web: http://localhost:8080"
|
|
||||||
echo "🔌 API REST: http://localhost:8081/api"
|
|
||||||
echo "📡 WebSocket: ws://localhost:8081/ws/logs/:app_name"
|
|
||||||
echo ""
|
|
||||||
echo "Comandos útiles:"
|
|
||||||
echo " Estado: sudo systemctl status siax-agent"
|
|
||||||
echo " Logs: sudo journalctl -u siax-agent -f"
|
|
||||||
echo " Reiniciar: sudo systemctl restart siax-agent"
|
|
||||||
echo " Detener: sudo systemctl stop siax-agent"
|
|
||||||
echo ""
|
|
||||||
echo "Directorio de instalación: $INSTALL_DIR"
|
|
||||||
echo "Configuración: $INSTALL_DIR/config/monitored_apps.json"
|
|
||||||
echo ""
|
|
||||||
}
|
|
||||||
|
|
||||||
#######################################
|
|
||||||
# Main
|
|
||||||
#######################################
|
|
||||||
|
|
||||||
main() {
|
|
||||||
print_header
|
|
||||||
|
|
||||||
check_root
|
|
||||||
check_dependencies
|
|
||||||
backup_existing
|
|
||||||
compile_release
|
|
||||||
create_user
|
|
||||||
install_binary
|
|
||||||
configure_sudoers
|
|
||||||
create_systemd_service
|
|
||||||
|
|
||||||
if verify_installation; then
|
|
||||||
if start_service; then
|
|
||||||
print_summary
|
|
||||||
exit 0
|
|
||||||
else
|
|
||||||
print_error "El servicio no pudo iniciarse correctamente"
|
|
||||||
print_info "Revisa los logs: journalctl -u siax-agent -n 50"
|
|
||||||
echo ""
|
|
||||||
echo "¿Deseas hacer rollback? (y/n)"
|
|
||||||
read -r response
|
|
||||||
if [[ "$response" =~ ^[Yy]$ ]]; then
|
|
||||||
rollback
|
|
||||||
fi
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
print_error "La verificación falló"
|
|
||||||
echo ""
|
|
||||||
echo "¿Deseas hacer rollback? (y/n)"
|
|
||||||
read -r response
|
|
||||||
if [[ "$response" =~ ^[Yy]$ ]]; then
|
|
||||||
rollback
|
|
||||||
fi
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
main
|
|
||||||
|
|||||||
347
instalador.sh
Executable file
347
instalador.sh
Executable file
@@ -0,0 +1,347 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# SIAX Agent - Script de Despliegue
|
||||||
|
# Instalación automática production-ready
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
set -e # Salir si hay errores
|
||||||
|
|
||||||
|
# Colores para output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Variables
|
||||||
|
INSTALL_DIR="/opt/siax-agent"
|
||||||
|
SERVICE_USER="siax-agent"
|
||||||
|
BACKUP_DIR="/tmp/siax-agent-backup-$(date +%s)"
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Funciones
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
print_header() {
|
||||||
|
echo -e "${BLUE}"
|
||||||
|
echo "============================================"
|
||||||
|
echo " SIAX Agent - Deployment Script"
|
||||||
|
echo "============================================"
|
||||||
|
echo -e "${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_success() {
|
||||||
|
echo -e "${GREEN}✅ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_error() {
|
||||||
|
echo -e "${RED}❌ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_warning() {
|
||||||
|
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_info() {
|
||||||
|
echo -e "${BLUE}ℹ️ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_root() {
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
print_error "Este script debe ejecutarse como root"
|
||||||
|
echo "Usa: sudo ./desplegar_agent.sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_dependencies() {
|
||||||
|
print_info "Verificando dependencias..."
|
||||||
|
|
||||||
|
local deps=("systemctl" "cargo" "rustc")
|
||||||
|
local missing=()
|
||||||
|
|
||||||
|
for dep in "${deps[@]}"; do
|
||||||
|
if ! command -v $dep &> /dev/null; then
|
||||||
|
missing+=($dep)
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#missing[@]} -ne 0 ]; then
|
||||||
|
print_error "Faltan dependencias: ${missing[*]}"
|
||||||
|
echo ""
|
||||||
|
echo "Instalación de Rust:"
|
||||||
|
echo " curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
|
||||||
|
echo ""
|
||||||
|
echo "Instalación de systemd (debería estar instalado por defecto):"
|
||||||
|
echo " sudo apt-get install systemd # Debian/Ubuntu"
|
||||||
|
echo " sudo yum install systemd # RedHat/CentOS"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_success "Todas las dependencias están instaladas"
|
||||||
|
}
|
||||||
|
|
||||||
|
backup_existing() {
|
||||||
|
if [ -d "$INSTALL_DIR" ]; then
|
||||||
|
print_warning "Instalación existente detectada"
|
||||||
|
print_info "Creando backup en: $BACKUP_DIR"
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
cp -r "$INSTALL_DIR" "$BACKUP_DIR/"
|
||||||
|
print_success "Backup creado"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
compile_release() {
|
||||||
|
print_info "Compilando SIAX Agent en modo release..."
|
||||||
|
|
||||||
|
if cargo build --release; then
|
||||||
|
print_success "Compilación exitosa"
|
||||||
|
else
|
||||||
|
print_error "Error en la compilación"
|
||||||
|
rollback
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
create_user() {
|
||||||
|
if id "$SERVICE_USER" &>/dev/null; then
|
||||||
|
print_info "Usuario $SERVICE_USER ya existe"
|
||||||
|
else
|
||||||
|
print_info "Creando usuario del sistema: $SERVICE_USER"
|
||||||
|
useradd --system --no-create-home --shell /bin/false "$SERVICE_USER"
|
||||||
|
print_success "Usuario creado"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
install_binary() {
|
||||||
|
print_info "Instalando binario en $INSTALL_DIR..."
|
||||||
|
|
||||||
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
mkdir -p "$INSTALL_DIR/config"
|
||||||
|
mkdir -p "$INSTALL_DIR/logs"
|
||||||
|
|
||||||
|
cp target/release/siax_monitor "$INSTALL_DIR/siax-agent"
|
||||||
|
chmod +x "$INSTALL_DIR/siax-agent"
|
||||||
|
|
||||||
|
# Copiar archivos de configuración si existen
|
||||||
|
if [ -f "config/monitored_apps.json" ]; then
|
||||||
|
cp config/monitored_apps.json "$INSTALL_DIR/config/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Copiar archivos web
|
||||||
|
if [ -d "web" ]; then
|
||||||
|
cp -r web "$INSTALL_DIR/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Permisos
|
||||||
|
chown -R $SERVICE_USER:$SERVICE_USER "$INSTALL_DIR"
|
||||||
|
|
||||||
|
print_success "Binario instalado"
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_sudoers() {
|
||||||
|
print_info "Configurando permisos sudo para systemctl..."
|
||||||
|
|
||||||
|
local sudoers_file="/etc/sudoers.d/siax-agent"
|
||||||
|
|
||||||
|
cat > "$sudoers_file" << EOF
|
||||||
|
# SIAX Agent - Permisos para gestionar servicios systemd
|
||||||
|
$SERVICE_USER ALL=(ALL) NOPASSWD: /bin/systemctl start *
|
||||||
|
$SERVICE_USER ALL=(ALL) NOPASSWD: /bin/systemctl stop *
|
||||||
|
$SERVICE_USER ALL=(ALL) NOPASSWD: /bin/systemctl restart *
|
||||||
|
$SERVICE_USER ALL=(ALL) NOPASSWD: /bin/systemctl status *
|
||||||
|
$SERVICE_USER ALL=(ALL) NOPASSWD: /bin/systemctl enable *
|
||||||
|
$SERVICE_USER ALL=(ALL) NOPASSWD: /bin/systemctl disable *
|
||||||
|
$SERVICE_USER ALL=(ALL) NOPASSWD: /bin/systemctl daemon-reload
|
||||||
|
$SERVICE_USER ALL=(ALL) NOPASSWD: /bin/systemctl is-active *
|
||||||
|
$SERVICE_USER ALL=(ALL) NOPASSWD: /bin/systemctl list-unit-files *
|
||||||
|
$SERVICE_USER ALL=(ALL) NOPASSWD: /bin/journalctl *
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod 0440 "$sudoers_file"
|
||||||
|
|
||||||
|
# Validar sintaxis
|
||||||
|
if visudo -c -f "$sudoers_file" &>/dev/null; then
|
||||||
|
print_success "Configuración de sudoers creada"
|
||||||
|
else
|
||||||
|
print_error "Error en configuración de sudoers"
|
||||||
|
rm -f "$sudoers_file"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
create_systemd_service() {
|
||||||
|
print_info "Creando servicio systemd para SIAX Agent..."
|
||||||
|
|
||||||
|
cat > /etc/systemd/system/siax-agent.service << EOF
|
||||||
|
[Unit]
|
||||||
|
Description=SIAX Agent - Process Monitor and Manager
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=$SERVICE_USER
|
||||||
|
WorkingDirectory=$INSTALL_DIR
|
||||||
|
ExecStart=$INSTALL_DIR/siax-agent
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
|
# Security hardening
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ReadWritePaths=$INSTALL_DIR/config $INSTALL_DIR/logs /etc/systemd/system
|
||||||
|
ProtectHome=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable siax-agent.service
|
||||||
|
|
||||||
|
print_success "Servicio systemd creado y habilitado"
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_installation() {
|
||||||
|
print_info "Verificando instalación..."
|
||||||
|
|
||||||
|
local errors=0
|
||||||
|
|
||||||
|
# Verificar binario
|
||||||
|
if [ ! -f "$INSTALL_DIR/siax-agent" ]; then
|
||||||
|
print_error "Binario no encontrado"
|
||||||
|
((errors++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verificar permisos
|
||||||
|
if [ ! -r "$INSTALL_DIR/siax-agent" ]; then
|
||||||
|
print_error "Permisos incorrectos en binario"
|
||||||
|
((errors++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verificar servicio
|
||||||
|
if ! systemctl is-enabled siax-agent.service &>/dev/null; then
|
||||||
|
print_error "Servicio no habilitado"
|
||||||
|
((errors++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verificar sudoers
|
||||||
|
if [ ! -f "/etc/sudoers.d/siax-agent" ]; then
|
||||||
|
print_warning "Configuración de sudoers no encontrada"
|
||||||
|
echo " El agente podría tener problemas para gestionar servicios"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $errors -eq 0 ]; then
|
||||||
|
print_success "Verificación exitosa"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_error "Verificación falló con $errors errores"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
start_service() {
|
||||||
|
print_info "Iniciando SIAX Agent..."
|
||||||
|
|
||||||
|
if systemctl start siax-agent.service; then
|
||||||
|
sleep 2
|
||||||
|
if systemctl is-active siax-agent.service &>/dev/null; then
|
||||||
|
print_success "SIAX Agent iniciado correctamente"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_error "SIAX Agent no pudo iniciarse"
|
||||||
|
echo ""
|
||||||
|
echo "Ver logs con: journalctl -u siax-agent.service -n 50"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_error "Error al iniciar el servicio"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
rollback() {
|
||||||
|
print_warning "Ejecutando rollback..."
|
||||||
|
|
||||||
|
systemctl stop siax-agent.service 2>/dev/null || true
|
||||||
|
systemctl disable siax-agent.service 2>/dev/null || true
|
||||||
|
|
||||||
|
if [ -d "$BACKUP_DIR" ]; then
|
||||||
|
rm -rf "$INSTALL_DIR"
|
||||||
|
cp -r "$BACKUP_DIR/siax-agent" "$INSTALL_DIR"
|
||||||
|
systemctl start siax-agent.service 2>/dev/null || true
|
||||||
|
print_success "Rollback completado"
|
||||||
|
else
|
||||||
|
print_warning "No hay backup disponible para rollback"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
print_summary() {
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}============================================${NC}"
|
||||||
|
echo -e "${GREEN} ✅ SIAX Agent instalado exitosamente${NC}"
|
||||||
|
echo -e "${GREEN}============================================${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "📊 Interface Web: http://localhost:8080"
|
||||||
|
echo "🔌 API REST: http://localhost:8081/api"
|
||||||
|
echo "📡 WebSocket: ws://localhost:8081/ws/logs/:app_name"
|
||||||
|
echo ""
|
||||||
|
echo "Comandos útiles:"
|
||||||
|
echo " Estado: sudo systemctl status siax-agent"
|
||||||
|
echo " Logs: sudo journalctl -u siax-agent -f"
|
||||||
|
echo " Reiniciar: sudo systemctl restart siax-agent"
|
||||||
|
echo " Detener: sudo systemctl stop siax-agent"
|
||||||
|
echo ""
|
||||||
|
echo "Directorio de instalación: $INSTALL_DIR"
|
||||||
|
echo "Configuración: $INSTALL_DIR/config/monitored_apps.json"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Main
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
main() {
|
||||||
|
print_header
|
||||||
|
|
||||||
|
check_root
|
||||||
|
check_dependencies
|
||||||
|
backup_existing
|
||||||
|
compile_release
|
||||||
|
create_user
|
||||||
|
install_binary
|
||||||
|
configure_sudoers
|
||||||
|
create_systemd_service
|
||||||
|
|
||||||
|
if verify_installation; then
|
||||||
|
if start_service; then
|
||||||
|
print_summary
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
print_error "El servicio no pudo iniciarse correctamente"
|
||||||
|
print_info "Revisa los logs: journalctl -u siax-agent -n 50"
|
||||||
|
echo ""
|
||||||
|
echo "¿Deseas hacer rollback? (y/n)"
|
||||||
|
read -r response
|
||||||
|
if [[ "$response" =~ ^[Yy]$ ]]; then
|
||||||
|
rollback
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_error "La verificación falló"
|
||||||
|
echo ""
|
||||||
|
echo "¿Deseas hacer rollback? (y/n)"
|
||||||
|
read -r response
|
||||||
|
if [[ "$response" =~ ^[Yy]$ ]]; then
|
||||||
|
rollback
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main
|
||||||
315
install-remote.sh
Normal file
315
install-remote.sh
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# SIAX Agent - Script de Instalación Remota
|
||||||
|
# Descarga e instala SIAX Agent desde servidor central
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
set -e # Salir si hay errores
|
||||||
|
|
||||||
|
# Variables (CONFIGURAR AQUÍ)
|
||||||
|
CENTRAL_SERVER="${SIAX_SERVER:-localhost:8080}" # Servidor central
|
||||||
|
INSTALL_DIR="/opt/siax-agent"
|
||||||
|
SERVICE_USER="siax-agent"
|
||||||
|
BACKUP_DIR="/tmp/siax-agent-backup-$(date +%s)"
|
||||||
|
DOWNLOAD_DIR="/tmp/siax-agent-download-$(date +%s)"
|
||||||
|
|
||||||
|
# Colores para output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Funciones
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
print_header() {
|
||||||
|
echo -e "${BLUE}"
|
||||||
|
echo "============================================"
|
||||||
|
echo " SIAX Agent - Remote Installation"
|
||||||
|
echo " Server: $CENTRAL_SERVER"
|
||||||
|
echo "============================================"
|
||||||
|
echo -e "${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_success() {
|
||||||
|
echo -e "${GREEN}✅ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_error() {
|
||||||
|
echo -e "${RED}❌ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_warning() {
|
||||||
|
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_info() {
|
||||||
|
echo -e "${BLUE}ℹ️ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_root() {
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
print_error "Este script debe ejecutarse como root"
|
||||||
|
echo "Usa: curl -sSL http://$CENTRAL_SERVER/install.sh | sudo bash"
|
||||||
|
echo "O con variable: curl -sSL http://$CENTRAL_SERVER/install.sh | sudo SIAX_SERVER=tu-servidor:8080 bash"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_dependencies() {
|
||||||
|
print_info "Verificando dependencias..."
|
||||||
|
|
||||||
|
local deps=("systemctl" "curl")
|
||||||
|
local missing=()
|
||||||
|
|
||||||
|
for dep in "${deps[@]}"; do
|
||||||
|
if ! command -v $dep &> /dev/null; then
|
||||||
|
missing+=($dep)
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#missing[@]} -ne 0 ]; then
|
||||||
|
print_error "Faltan dependencias: ${missing[*]}"
|
||||||
|
echo ""
|
||||||
|
echo "Instalación en Debian/Ubuntu:"
|
||||||
|
echo " sudo apt-get update && sudo apt-get install -y curl systemd"
|
||||||
|
echo ""
|
||||||
|
echo "Instalación en RedHat/CentOS:"
|
||||||
|
echo " sudo yum install -y curl systemd"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_success "Todas las dependencias están instaladas"
|
||||||
|
}
|
||||||
|
|
||||||
|
download_binary() {
|
||||||
|
print_info "Descargando binario desde $CENTRAL_SERVER..."
|
||||||
|
|
||||||
|
mkdir -p "$DOWNLOAD_DIR"
|
||||||
|
|
||||||
|
# Intentar descargar el binario pre-compilado
|
||||||
|
if curl -f -L -o "$DOWNLOAD_DIR/siax-agent" "http://$CENTRAL_SERVER/static/binary/siax-agent"; then
|
||||||
|
chmod +x "$DOWNLOAD_DIR/siax-agent"
|
||||||
|
print_success "Binario descargado"
|
||||||
|
else
|
||||||
|
print_error "No se pudo descargar el binario desde http://$CENTRAL_SERVER/static/binary/siax-agent"
|
||||||
|
echo ""
|
||||||
|
echo "Asegúrate de que:"
|
||||||
|
echo " 1. El servidor $CENTRAL_SERVER está accesible"
|
||||||
|
echo " 2. El binario está en web/static/binary/siax-agent"
|
||||||
|
echo " 3. Compilaste con: cargo build --release && cp target/release/siax_monitor web/static/binary/siax-agent"
|
||||||
|
rm -rf "$DOWNLOAD_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
download_web_files() {
|
||||||
|
print_info "Descargando archivos web..."
|
||||||
|
|
||||||
|
mkdir -p "$DOWNLOAD_DIR/web"
|
||||||
|
|
||||||
|
# Descargar archivos HTML principales (opcional, solo si quieres que cada agente tenga su propia interfaz)
|
||||||
|
# Para agentes worker, probablemente no necesites esto
|
||||||
|
print_info "Archivos web no necesarios para worker nodes (omitiendo)"
|
||||||
|
}
|
||||||
|
|
||||||
|
backup_existing() {
|
||||||
|
if [ -d "$INSTALL_DIR" ]; then
|
||||||
|
print_warning "Instalación existente detectada"
|
||||||
|
print_info "Creando backup en: $BACKUP_DIR"
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
cp -r "$INSTALL_DIR" "$BACKUP_DIR/"
|
||||||
|
print_success "Backup creado"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
create_user() {
|
||||||
|
if id "$SERVICE_USER" &>/dev/null; then
|
||||||
|
print_info "Usuario $SERVICE_USER ya existe"
|
||||||
|
else
|
||||||
|
print_info "Creando usuario del sistema: $SERVICE_USER"
|
||||||
|
useradd --system --no-create-home --shell /bin/false "$SERVICE_USER"
|
||||||
|
print_success "Usuario creado"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
install_binary() {
|
||||||
|
print_info "Instalando binario en $INSTALL_DIR..."
|
||||||
|
|
||||||
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
mkdir -p "$INSTALL_DIR/config"
|
||||||
|
mkdir -p "$INSTALL_DIR/logs"
|
||||||
|
mkdir -p "$INSTALL_DIR/web/static"
|
||||||
|
|
||||||
|
# Copiar binario
|
||||||
|
cp "$DOWNLOAD_DIR/siax-agent" "$INSTALL_DIR/siax-agent"
|
||||||
|
chmod +x "$INSTALL_DIR/siax-agent"
|
||||||
|
|
||||||
|
# Crear configuración inicial vacía si no existe
|
||||||
|
if [ ! -f "$INSTALL_DIR/config/monitored_apps.json" ]; then
|
||||||
|
echo '{"apps":[]}' > "$INSTALL_DIR/config/monitored_apps.json"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Permisos
|
||||||
|
chown -R $SERVICE_USER:$SERVICE_USER "$INSTALL_DIR"
|
||||||
|
|
||||||
|
print_success "Binario instalado"
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_sudoers() {
|
||||||
|
print_info "Configurando permisos sudo para systemctl..."
|
||||||
|
|
||||||
|
local sudoers_file="/etc/sudoers.d/siax-agent"
|
||||||
|
|
||||||
|
cat > "$sudoers_file" << 'EOF'
|
||||||
|
# SIAX Agent - Permisos para gestionar servicios systemd
|
||||||
|
siax-agent ALL=(ALL) NOPASSWD: /bin/systemctl start *
|
||||||
|
siax-agent ALL=(ALL) NOPASSWD: /bin/systemctl stop *
|
||||||
|
siax-agent ALL=(ALL) NOPASSWD: /bin/systemctl restart *
|
||||||
|
siax-agent ALL=(ALL) NOPASSWD: /bin/systemctl status *
|
||||||
|
siax-agent ALL=(ALL) NOPASSWD: /bin/systemctl enable *
|
||||||
|
siax-agent ALL=(ALL) NOPASSWD: /bin/systemctl disable *
|
||||||
|
siax-agent ALL=(ALL) NOPASSWD: /bin/systemctl daemon-reload
|
||||||
|
siax-agent ALL=(ALL) NOPASSWD: /bin/systemctl is-active *
|
||||||
|
siax-agent ALL=(ALL) NOPASSWD: /bin/systemctl list-unit-files *
|
||||||
|
siax-agent ALL=(ALL) NOPASSWD: /bin/journalctl *
|
||||||
|
siax-agent ALL=(ALL) NOPASSWD: /usr/bin/systemctl start *
|
||||||
|
siax-agent ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop *
|
||||||
|
siax-agent ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart *
|
||||||
|
siax-agent ALL=(ALL) NOPASSWD: /usr/bin/systemctl status *
|
||||||
|
siax-agent ALL=(ALL) NOPASSWD: /usr/bin/systemctl enable *
|
||||||
|
siax-agent ALL=(ALL) NOPASSWD: /usr/bin/systemctl disable *
|
||||||
|
siax-agent ALL=(ALL) NOPASSWD: /usr/bin/systemctl daemon-reload
|
||||||
|
siax-agent ALL=(ALL) NOPASSWD: /usr/bin/systemctl is-active *
|
||||||
|
siax-agent ALL=(ALL) NOPASSWD: /usr/bin/systemctl list-unit-files *
|
||||||
|
siax-agent ALL=(ALL) NOPASSWD: /usr/bin/journalctl *
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod 0440 "$sudoers_file"
|
||||||
|
|
||||||
|
# Validar sintaxis
|
||||||
|
if visudo -c -f "$sudoers_file" &>/dev/null; then
|
||||||
|
print_success "Configuración de sudoers creada"
|
||||||
|
else
|
||||||
|
print_error "Error en configuración de sudoers"
|
||||||
|
rm -f "$sudoers_file"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
create_systemd_service() {
|
||||||
|
print_info "Creando servicio systemd para SIAX Agent..."
|
||||||
|
|
||||||
|
cat > /etc/systemd/system/siax-agent.service << EOF
|
||||||
|
[Unit]
|
||||||
|
Description=SIAX Agent - Process Monitor and Manager
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=$SERVICE_USER
|
||||||
|
WorkingDirectory=$INSTALL_DIR
|
||||||
|
ExecStart=$INSTALL_DIR/siax-agent
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
|
# Security hardening
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ReadWritePaths=$INSTALL_DIR/config $INSTALL_DIR/logs /etc/systemd/system
|
||||||
|
ProtectHome=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable siax-agent.service
|
||||||
|
|
||||||
|
print_success "Servicio systemd creado y habilitado"
|
||||||
|
}
|
||||||
|
|
||||||
|
start_service() {
|
||||||
|
print_info "Iniciando SIAX Agent..."
|
||||||
|
|
||||||
|
if systemctl start siax-agent.service; then
|
||||||
|
sleep 2
|
||||||
|
if systemctl is-active siax-agent.service &>/dev/null; then
|
||||||
|
print_success "SIAX Agent iniciado correctamente"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_error "SIAX Agent no pudo iniciarse"
|
||||||
|
echo ""
|
||||||
|
echo "Ver logs con: journalctl -u siax-agent.service -n 50"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_error "Error al iniciar el servicio"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
print_info "Limpiando archivos temporales..."
|
||||||
|
rm -rf "$DOWNLOAD_DIR"
|
||||||
|
print_success "Limpieza completada"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_summary() {
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}============================================${NC}"
|
||||||
|
echo -e "${GREEN} ✅ SIAX Agent instalado exitosamente${NC}"
|
||||||
|
echo -e "${GREEN}============================================${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "📊 Interface Web: http://localhost:8080"
|
||||||
|
echo "🔌 API REST: http://localhost:8080/api"
|
||||||
|
echo "📡 WebSocket: ws://localhost:8080/api/apps/:name/logs"
|
||||||
|
echo ""
|
||||||
|
echo "Comandos útiles:"
|
||||||
|
echo " Estado: sudo systemctl status siax-agent"
|
||||||
|
echo " Logs: sudo journalctl -u siax-agent -f"
|
||||||
|
echo " Reiniciar: sudo systemctl restart siax-agent"
|
||||||
|
echo " Detener: sudo systemctl stop siax-agent"
|
||||||
|
echo ""
|
||||||
|
echo "Directorio de instalación: $INSTALL_DIR"
|
||||||
|
echo "Configuración: $INSTALL_DIR/config/monitored_apps.json"
|
||||||
|
echo ""
|
||||||
|
echo "🌐 Servidor Central: $CENTRAL_SERVER"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Main
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
main() {
|
||||||
|
print_header
|
||||||
|
|
||||||
|
check_root
|
||||||
|
check_dependencies
|
||||||
|
backup_existing
|
||||||
|
download_binary
|
||||||
|
create_user
|
||||||
|
install_binary
|
||||||
|
configure_sudoers
|
||||||
|
create_systemd_service
|
||||||
|
|
||||||
|
if start_service; then
|
||||||
|
cleanup
|
||||||
|
print_summary
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
print_error "El servicio no pudo iniciarse correctamente"
|
||||||
|
print_info "Revisa los logs: journalctl -u siax-agent -n 50"
|
||||||
|
cleanup
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main
|
||||||
2381
logs/errors.log
2381
logs/errors.log
File diff suppressed because it is too large
Load Diff
66
preparar_binario.sh
Executable file
66
preparar_binario.sh
Executable file
@@ -0,0 +1,66 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# SIAX Agent - Preparar Binario para Distribución
|
||||||
|
# Compila y copia el binario a web/static/binary/
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
echo -e "${BLUE}============================================${NC}"
|
||||||
|
echo -e "${BLUE} Preparando SIAX Agent para Distribución${NC}"
|
||||||
|
echo -e "${BLUE}============================================${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Compilar en release
|
||||||
|
echo -e "${BLUE}📦 Compilando en modo release...${NC}"
|
||||||
|
cargo build --release
|
||||||
|
|
||||||
|
if [ ! -f "target/release/siax_monitor" ]; then
|
||||||
|
echo -e "${RED}❌ Error: No se pudo compilar el binario${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}✅ Compilación exitosa${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Crear directorio para binarios
|
||||||
|
echo -e "${BLUE}📁 Creando directorio web/static/binary/${NC}"
|
||||||
|
mkdir -p web/static/binary
|
||||||
|
|
||||||
|
# Copiar binario
|
||||||
|
echo -e "${BLUE}📋 Copiando binario...${NC}"
|
||||||
|
cp target/release/siax_monitor web/static/binary/siax-agent
|
||||||
|
chmod +x web/static/binary/siax-agent
|
||||||
|
|
||||||
|
echo -e "${GREEN}✅ Binario copiado a web/static/binary/siax-agent${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Mostrar información
|
||||||
|
BINARY_SIZE=$(du -h web/static/binary/siax-agent | cut -f1)
|
||||||
|
echo -e "${GREEN}============================================${NC}"
|
||||||
|
echo -e "${GREEN} ✅ Preparación completada${NC}"
|
||||||
|
echo -e "${GREEN}============================================${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "📊 Tamaño del binario: $BINARY_SIZE"
|
||||||
|
echo "📂 Ubicación: web/static/binary/siax-agent"
|
||||||
|
echo ""
|
||||||
|
echo "🚀 Ahora puedes:"
|
||||||
|
echo ""
|
||||||
|
echo " 1. Iniciar el servidor:"
|
||||||
|
echo " cargo run --release"
|
||||||
|
echo ""
|
||||||
|
echo " 2. Desde otro servidor, instalar con:"
|
||||||
|
echo " curl -sSL http://TU-SERVIDOR:8080/install.sh | sudo bash"
|
||||||
|
echo ""
|
||||||
|
echo " O especificar el servidor:"
|
||||||
|
echo " curl -sSL http://TU-SERVIDOR:8080/install.sh | sudo SIAX_SERVER=TU-SERVIDOR:8080 bash"
|
||||||
|
echo ""
|
||||||
|
echo "Ejemplo VPN:"
|
||||||
|
echo " curl -sSL http://10.8.0.1:8080/install.sh | sudo SIAX_SERVER=10.8.0.1:8080 bash"
|
||||||
|
echo ""
|
||||||
@@ -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(_) => {
|
||||||
|
logger.info("API", &format!("✅ Eliminado desde AppManager: {}", app_name));
|
||||||
|
deleted_from_memory = true;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
logger.warning("API", &format!("App no encontrada en AppManager: {}", e), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(),
|
app_name: app_name.clone(),
|
||||||
operation: "unregister".to_string(),
|
operation: "unregister".to_string(),
|
||||||
success: true,
|
success: true,
|
||||||
message: "Aplicación eliminada exitosamente".to_string(),
|
message: format!("Aplicación '{}' eliminada exitosamente (servicio systemd + soft delete en JSON)", app_name),
|
||||||
}))),
|
})))
|
||||||
Err(e) => Ok(Json(ApiResponse::error(e.to_string()))),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start_app_handler(
|
pub async fn start_app_handler(
|
||||||
@@ -124,22 +320,87 @@ 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>,
|
||||||
) -> Result<Json<ApiResponse<AppStatusResponse>>, StatusCode> {
|
) -> Result<Json<ApiResponse<AppStatusResponse>>, StatusCode> {
|
||||||
|
use crate::config::get_config_manager;
|
||||||
|
use crate::systemd::SystemCtl;
|
||||||
|
use crate::models::{AppStatus, ServiceStatus};
|
||||||
|
|
||||||
match state.app_manager.get_app_status(&app_name) {
|
let config_manager = get_config_manager();
|
||||||
Some(managed_app) => {
|
let apps = config_manager.get_apps();
|
||||||
let response = AppStatusResponse {
|
|
||||||
name: managed_app.name,
|
// Buscar la app en monitored_apps.json
|
||||||
status: managed_app.status.as_str().to_string(),
|
let app = apps.iter().find(|a| a.name == app_name);
|
||||||
pid: managed_app.pid,
|
|
||||||
cpu_usage: managed_app.cpu_usage,
|
match app {
|
||||||
memory_usage: format!("{:.2} MB", managed_app.memory_usage as f64 / 1024.0 / 1024.0),
|
Some(app) => {
|
||||||
systemd_status: managed_app.systemd_status.as_str().to_string(),
|
let service_name = format!("siax-app-{}.service", app.name);
|
||||||
last_updated: managed_app.last_updated,
|
let systemd_status = SystemCtl::status(&service_name);
|
||||||
|
|
||||||
|
// Obtener métricas del proceso
|
||||||
|
let mut sys = System::new_all();
|
||||||
|
sys.refresh_all();
|
||||||
|
|
||||||
|
let mut pid = None;
|
||||||
|
let mut cpu_usage = 0.0;
|
||||||
|
let mut memory_mb = 0.0;
|
||||||
|
|
||||||
|
// Buscar proceso por nombre de app
|
||||||
|
for (process_pid, process) in sys.processes() {
|
||||||
|
let cmd = process.cmd().join(" ");
|
||||||
|
if cmd.contains(&app.name) || cmd.contains(&app.entry_point) {
|
||||||
|
pid = Some(process_pid.as_u32() as i32);
|
||||||
|
cpu_usage = process.cpu_usage();
|
||||||
|
memory_mb = process.memory() as f64 / 1024.0 / 1024.0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = match systemd_status {
|
||||||
|
ServiceStatus::Active => "Running",
|
||||||
|
ServiceStatus::Inactive => "Stopped",
|
||||||
|
ServiceStatus::Failed => "Failed",
|
||||||
|
ServiceStatus::Activating => "Starting",
|
||||||
|
ServiceStatus::Deactivating => "Stopping",
|
||||||
|
ServiceStatus::Unknown => "Unknown",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let response = AppStatusResponse {
|
||||||
|
name: app.name.clone(),
|
||||||
|
status: status.to_string(),
|
||||||
|
pid,
|
||||||
|
cpu_usage,
|
||||||
|
memory_usage: format!("{:.2} MB", memory_mb),
|
||||||
|
systemd_status: systemd_status.as_str().to_string(),
|
||||||
|
last_updated: chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
Ok(Json(ApiResponse::success(response)))
|
Ok(Json(ApiResponse::success(response)))
|
||||||
}
|
}
|
||||||
None => Ok(Json(ApiResponse::error(
|
None => Ok(Json(ApiResponse::error(
|
||||||
@@ -149,13 +410,49 @@ pub async fn get_app_status_handler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_apps_handler(
|
pub async fn list_apps_handler(
|
||||||
State(state): State<Arc<ApiState>>,
|
State(_state): State<Arc<ApiState>>,
|
||||||
) -> Result<Json<ApiResponse<AppListResponse>>, StatusCode> {
|
) -> Result<Json<serde_json::Value>, StatusCode> {
|
||||||
|
use crate::config::get_config_manager;
|
||||||
|
use crate::systemd::SystemCtl;
|
||||||
|
|
||||||
let apps = state.app_manager.list_apps();
|
// Leer apps desde monitored_apps.json (apps descubiertas + registradas)
|
||||||
let total = apps.len();
|
let config_manager = get_config_manager();
|
||||||
|
let monitored_apps = config_manager.get_apps();
|
||||||
|
|
||||||
Ok(Json(ApiResponse::success(AppListResponse { apps, total })))
|
// Crear respuesta con información de cada app
|
||||||
|
let mut apps_with_status = Vec::new();
|
||||||
|
|
||||||
|
for app in monitored_apps {
|
||||||
|
// Verificar estado en systemd
|
||||||
|
let service_name = format!("siax-app-{}.service", app.name);
|
||||||
|
let systemd_status = SystemCtl::status(&service_name);
|
||||||
|
|
||||||
|
let status = match systemd_status {
|
||||||
|
crate::models::ServiceStatus::Active => "Running",
|
||||||
|
crate::models::ServiceStatus::Inactive => "Stopped",
|
||||||
|
crate::models::ServiceStatus::Failed => "Failed",
|
||||||
|
crate::models::ServiceStatus::Activating => "Starting",
|
||||||
|
crate::models::ServiceStatus::Deactivating => "Stopping",
|
||||||
|
crate::models::ServiceStatus::Unknown => "Unknown",
|
||||||
|
};
|
||||||
|
|
||||||
|
apps_with_status.push(serde_json::json!({
|
||||||
|
"name": app.name,
|
||||||
|
"status": status,
|
||||||
|
"port": app.port,
|
||||||
|
"service_name": app.service_name,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let total = apps_with_status.len();
|
||||||
|
|
||||||
|
Ok(Json(serde_json::json!({
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"apps": apps_with_status,
|
||||||
|
"total": total
|
||||||
|
}
|
||||||
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn scan_processes_handler() -> Result<Json<ApiResponse<ProcessScanResponse>>, StatusCode> {
|
pub async fn scan_processes_handler() -> Result<Json<ApiResponse<ProcessScanResponse>>, StatusCode> {
|
||||||
@@ -222,3 +519,111 @@ pub async fn health_handler(
|
|||||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Endpoint para ver las apps monitoreadas desde el JSON
|
||||||
|
pub async fn get_monitored_apps_handler() -> Result<Json<serde_json::Value>, StatusCode> {
|
||||||
|
use crate::config::get_config_manager;
|
||||||
|
|
||||||
|
let config_manager = get_config_manager();
|
||||||
|
let apps = config_manager.get_apps();
|
||||||
|
|
||||||
|
let response = serde_json::json!({
|
||||||
|
"success": true,
|
||||||
|
"count": apps.len(),
|
||||||
|
"apps": apps
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Json(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Endpoint para obtener los logs de errores del sistema
|
||||||
|
pub async fn get_system_error_logs() -> Result<Json<serde_json::Value>, StatusCode> {
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
let log_path = "logs/errors.log";
|
||||||
|
|
||||||
|
// Verificar si el archivo existe
|
||||||
|
if !Path::new(log_path).exists() {
|
||||||
|
return Ok(Json(serde_json::json!({
|
||||||
|
"success": true,
|
||||||
|
"logs": [],
|
||||||
|
"message": "Archivo de logs no encontrado"
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leer el archivo
|
||||||
|
match fs::read_to_string(log_path) {
|
||||||
|
Ok(content) => {
|
||||||
|
// Dividir en líneas y tomar las últimas 500
|
||||||
|
let lines: Vec<&str> = content.lines().collect();
|
||||||
|
let total = lines.len();
|
||||||
|
let recent_lines: Vec<&str> = if lines.len() > 500 {
|
||||||
|
lines[lines.len() - 500..].to_vec()
|
||||||
|
} else {
|
||||||
|
lines
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Json(serde_json::json!({
|
||||||
|
"success": true,
|
||||||
|
"logs": recent_lines,
|
||||||
|
"total_lines": total
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
Ok(Json(serde_json::json!({
|
||||||
|
"success": false,
|
||||||
|
"error": format!("Error leyendo archivo: {}", e)
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Endpoint para obtener apps eliminadas (soft delete history)
|
||||||
|
pub async fn get_deleted_apps_handler() -> Result<Json<serde_json::Value>, StatusCode> {
|
||||||
|
use crate::config::get_config_manager;
|
||||||
|
|
||||||
|
let config_manager = get_config_manager();
|
||||||
|
let deleted_apps = config_manager.get_deleted_apps();
|
||||||
|
|
||||||
|
// Formatear respuesta con información de cada app eliminada
|
||||||
|
let apps_info: Vec<serde_json::Value> = deleted_apps.iter().map(|app| {
|
||||||
|
serde_json::json!({
|
||||||
|
"name": app.name,
|
||||||
|
"port": app.port,
|
||||||
|
"path": app.path,
|
||||||
|
"entry_point": app.entry_point,
|
||||||
|
"mode": app.mode,
|
||||||
|
"registered_at": app.registered_at,
|
||||||
|
"deleted_at": app.deleted_at,
|
||||||
|
"deleted_reason": app.deleted_reason,
|
||||||
|
})
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
Ok(Json(serde_json::json!({
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"apps": apps_info,
|
||||||
|
"total": apps_info.len()
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Endpoint para restaurar una app eliminada (soft delete)
|
||||||
|
pub async fn restore_app_handler(
|
||||||
|
Path(app_name): Path<String>,
|
||||||
|
) -> Result<Json<ApiResponse<OperationResponse>>, StatusCode> {
|
||||||
|
use crate::config::get_config_manager;
|
||||||
|
|
||||||
|
let config_manager = get_config_manager();
|
||||||
|
|
||||||
|
match config_manager.restore_app(&app_name) {
|
||||||
|
Ok(_) => Ok(Json(ApiResponse::success(OperationResponse {
|
||||||
|
app_name: app_name.clone(),
|
||||||
|
operation: "restore".to_string(),
|
||||||
|
success: true,
|
||||||
|
message: format!("Aplicación '{}' restaurada exitosamente. Nota: el servicio systemd debe ser recreado manualmente.", app_name),
|
||||||
|
}))),
|
||||||
|
Err(e) => Ok(Json(ApiResponse::error(e))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ async fn handle_logs_socket(
|
|||||||
ws_manager: Arc<WebSocketManager>,
|
ws_manager: Arc<WebSocketManager>,
|
||||||
) {
|
) {
|
||||||
let logger = get_logger();
|
let logger = get_logger();
|
||||||
let service_name = format!("{}.service", app_name);
|
let service_name = format!("siax-app-{}.service", app_name);
|
||||||
|
|
||||||
// Iniciar journalctl
|
// Iniciar journalctl
|
||||||
let mut child = match TokioCommand::new("journalctl")
|
let mut child = match TokioCommand::new("journalctl")
|
||||||
|
|||||||
180
src/config.rs
180
src/config.rs
@@ -6,14 +6,81 @@ use crate::logger::get_logger;
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct MonitoredApp {
|
pub struct MonitoredApp {
|
||||||
|
/// Nombre de la aplicación
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
||||||
|
/// Nombre del servicio systemd (ej: siax-app-TAREAS.service)
|
||||||
|
#[serde(default)]
|
||||||
|
pub service_name: String,
|
||||||
|
|
||||||
|
/// Ruta completa al directorio de la aplicación (WorkingDirectory)
|
||||||
|
#[serde(default)]
|
||||||
|
pub path: String,
|
||||||
|
|
||||||
|
/// Puerto donde escucha la aplicación
|
||||||
pub port: i32,
|
pub port: i32,
|
||||||
|
|
||||||
|
/// Archivo de entrada (ej: server.js, app.js)
|
||||||
|
#[serde(default)]
|
||||||
|
pub entry_point: String,
|
||||||
|
|
||||||
|
/// Ruta completa al binario de node/python
|
||||||
|
#[serde(default)]
|
||||||
|
pub node_bin: String,
|
||||||
|
|
||||||
|
/// Modo de ejecución (production, development, test)
|
||||||
|
#[serde(default = "default_mode")]
|
||||||
|
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
|
||||||
|
#[serde(default)]
|
||||||
|
pub service_file_path: String,
|
||||||
|
|
||||||
|
/// Fecha de registro (ISO 8601)
|
||||||
|
#[serde(default, skip_serializing_if = "String::is_empty", rename = "reg")]
|
||||||
|
pub registered_at: String,
|
||||||
|
|
||||||
|
// --- SOFT DELETE FIELDS ---
|
||||||
|
/// Indica si la app fue eliminada (soft delete)
|
||||||
|
#[serde(default)]
|
||||||
|
pub deleted: bool,
|
||||||
|
|
||||||
|
/// Fecha de eliminación (ISO 8601)
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub deleted_at: Option<String>,
|
||||||
|
|
||||||
|
/// Razón de eliminación (opcional)
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
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
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub systemd_service: Option<String>,
|
pub systemd_service: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub created_at: Option<String>,
|
pub created_at: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_mode() -> 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>,
|
||||||
@@ -94,28 +161,40 @@ impl ConfigManager {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Obtiene las apps activas (no eliminadas)
|
||||||
pub fn get_apps(&self) -> Vec<MonitoredApp> {
|
pub fn get_apps(&self) -> Vec<MonitoredApp> {
|
||||||
|
let config = self.config.read().unwrap();
|
||||||
|
config.apps.iter()
|
||||||
|
.filter(|app| !app.deleted)
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtiene TODAS las apps, incluyendo las eliminadas
|
||||||
|
pub fn get_all_apps(&self) -> Vec<MonitoredApp> {
|
||||||
let config = self.config.read().unwrap();
|
let config = self.config.read().unwrap();
|
||||||
config.apps.clone()
|
config.apps.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_app(&self, name: String, port: i32) -> Result<(), String> {
|
/// Obtiene solo las apps eliminadas
|
||||||
|
pub fn get_deleted_apps(&self) -> Vec<MonitoredApp> {
|
||||||
|
let config = self.config.read().unwrap();
|
||||||
|
config.apps.iter()
|
||||||
|
.filter(|app| app.deleted)
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Agrega una app con información completa
|
||||||
|
pub fn add_app_full(&self, app: MonitoredApp) -> Result<(), String> {
|
||||||
let mut config = self.config.write().unwrap();
|
let mut config = self.config.write().unwrap();
|
||||||
|
|
||||||
// Verificar si ya existe
|
// Verificar si ya existe
|
||||||
if config.apps.iter().any(|app| app.name == name) {
|
if config.apps.iter().any(|a| a.name == app.name) {
|
||||||
return Err(format!("La app '{}' ya está siendo monitoreada", name));
|
return Err(format!("La app '{}' ya está siendo monitoreada", app.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
let systemd_service = format!("siax-app-{}.service", name);
|
config.apps.push(app);
|
||||||
let created_at = chrono::Local::now().to_rfc3339();
|
|
||||||
|
|
||||||
config.apps.push(MonitoredApp {
|
|
||||||
name,
|
|
||||||
port,
|
|
||||||
systemd_service: Some(systemd_service),
|
|
||||||
created_at: Some(created_at),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Guardar en disco
|
// Guardar en disco
|
||||||
match Self::save_config_to_file(&self.config_path, &config) {
|
match Self::save_config_to_file(&self.config_path, &config) {
|
||||||
@@ -124,6 +203,83 @@ impl ConfigManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Método simplificado para compatibilidad (DEPRECATED)
|
||||||
|
#[deprecated(note = "Usar add_app_full() con MonitoredApp completo")]
|
||||||
|
pub fn add_app(&self, name: String, port: i32) -> Result<(), String> {
|
||||||
|
let service_name = format!("siax-app-{}.service", name);
|
||||||
|
let registered_at = chrono::Local::now().to_rfc3339();
|
||||||
|
|
||||||
|
let app = MonitoredApp {
|
||||||
|
name,
|
||||||
|
service_name,
|
||||||
|
path: String::new(),
|
||||||
|
port,
|
||||||
|
entry_point: String::new(),
|
||||||
|
node_bin: String::new(),
|
||||||
|
mode: "production".to_string(),
|
||||||
|
user: default_user(),
|
||||||
|
service_file_path: String::new(),
|
||||||
|
registered_at,
|
||||||
|
deleted: false,
|
||||||
|
deleted_at: None,
|
||||||
|
deleted_reason: None,
|
||||||
|
environment: std::collections::HashMap::new(),
|
||||||
|
systemd_service: None,
|
||||||
|
created_at: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.add_app_full(app)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Realiza un soft delete: marca la app como eliminada pero mantiene el registro
|
||||||
|
pub fn soft_delete_app(&self, name: &str, reason: Option<String>) -> Result<(), String> {
|
||||||
|
let mut config = self.config.write().unwrap();
|
||||||
|
|
||||||
|
// Buscar la app
|
||||||
|
let app = config.apps.iter_mut().find(|a| a.name == name && !a.deleted);
|
||||||
|
|
||||||
|
match app {
|
||||||
|
Some(app) => {
|
||||||
|
// Marcar como eliminada
|
||||||
|
app.deleted = true;
|
||||||
|
app.deleted_at = Some(chrono::Local::now().to_rfc3339());
|
||||||
|
app.deleted_reason = reason;
|
||||||
|
|
||||||
|
// Guardar en disco
|
||||||
|
match Self::save_config_to_file(&self.config_path, &config) {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) => Err(format!("Error al guardar configuración: {}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Err(format!("La app '{}' no se encontró o ya está eliminada", name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restaura una app previamente eliminada (soft delete)
|
||||||
|
pub fn restore_app(&self, name: &str) -> Result<(), String> {
|
||||||
|
let mut config = self.config.write().unwrap();
|
||||||
|
|
||||||
|
// Buscar la app eliminada
|
||||||
|
let app = config.apps.iter_mut().find(|a| a.name == name && a.deleted);
|
||||||
|
|
||||||
|
match app {
|
||||||
|
Some(app) => {
|
||||||
|
// Restaurar
|
||||||
|
app.deleted = false;
|
||||||
|
app.deleted_at = None;
|
||||||
|
app.deleted_reason = None;
|
||||||
|
|
||||||
|
// Guardar en disco
|
||||||
|
match Self::save_config_to_file(&self.config_path, &config) {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) => Err(format!("Error al guardar configuración: {}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Err(format!("La app '{}' no se encontró en apps eliminadas", name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// HARD DELETE: Elimina permanentemente una app del JSON (usar con precaución)
|
||||||
pub fn remove_app(&self, name: &str) -> Result<(), String> {
|
pub fn remove_app(&self, name: &str) -> Result<(), String> {
|
||||||
let mut config = self.config.write().unwrap();
|
let mut config = self.config.write().unwrap();
|
||||||
|
|
||||||
|
|||||||
326
src/discovery.rs
Normal file
326
src/discovery.rs
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
/// Módulo para descubrir servicios systemd existentes
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
use crate::logger::get_logger;
|
||||||
|
use crate::config::{get_config_manager, MonitoredApp};
|
||||||
|
|
||||||
|
const SYSTEMD_DIR: &str = "/etc/systemd/system";
|
||||||
|
const SERVICE_PREFIX: &str = "siax-app-";
|
||||||
|
|
||||||
|
/// Descubre servicios systemd existentes con prefijo siax-app-*
|
||||||
|
pub fn discover_services() -> Vec<DiscoveredService> {
|
||||||
|
let logger = get_logger();
|
||||||
|
logger.info("Discovery", &format!("🔍 Escaneando servicios systemd en: {}", SYSTEMD_DIR));
|
||||||
|
println!("🔍 Discovery: Buscando servicios en {}", SYSTEMD_DIR);
|
||||||
|
|
||||||
|
let mut services = Vec::new();
|
||||||
|
|
||||||
|
// Leer directorio de systemd
|
||||||
|
let entries = match fs::read_dir(SYSTEMD_DIR) {
|
||||||
|
Ok(entries) => {
|
||||||
|
logger.info("Discovery", &format!("✅ Directorio {} accesible", SYSTEMD_DIR));
|
||||||
|
println!("✅ Discovery: Directorio {} accesible", SYSTEMD_DIR);
|
||||||
|
entries
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
logger.error("Discovery", &format!("❌ No se pudo leer directorio {}", SYSTEMD_DIR), Some(&e.to_string()));
|
||||||
|
println!("❌ Discovery: ERROR - No se pudo leer {}: {}", SYSTEMD_DIR, e);
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Buscar archivos siax-app-*.service
|
||||||
|
let mut total_files = 0;
|
||||||
|
let mut siax_files = 0;
|
||||||
|
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
total_files += 1;
|
||||||
|
let path = entry.path();
|
||||||
|
if let Some(filename) = path.file_name() {
|
||||||
|
let filename_str = filename.to_string_lossy();
|
||||||
|
|
||||||
|
// Verificar que sea un archivo .service con nuestro prefijo
|
||||||
|
if filename_str.starts_with(SERVICE_PREFIX) && filename_str.ends_with(".service") {
|
||||||
|
siax_files += 1;
|
||||||
|
logger.info("Discovery", &format!("✅ Encontrado: {}", filename_str));
|
||||||
|
println!("✅ Discovery: Servicio detectado: {}", filename_str);
|
||||||
|
|
||||||
|
// Extraer nombre de la app
|
||||||
|
let app_name = extract_app_name(&filename_str);
|
||||||
|
|
||||||
|
// Leer configuración del servicio
|
||||||
|
if let Some(service) = parse_service_file(&path, &app_name) {
|
||||||
|
services.push(service);
|
||||||
|
} else {
|
||||||
|
logger.warning("Discovery", &format!("⚠️ No se pudo parsear: {}", filename_str), None);
|
||||||
|
println!("⚠️ Discovery: No se pudo parsear {}", filename_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Discovery", &format!("📊 Escaneados {} archivos, {} con prefijo '{}', {} parseados exitosamente",
|
||||||
|
total_files, siax_files, SERVICE_PREFIX, services.len()));
|
||||||
|
println!("📊 Discovery: Archivos totales: {}, siax-app-*: {}, parseados: {}",
|
||||||
|
total_files, siax_files, services.len());
|
||||||
|
|
||||||
|
services
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extrae el nombre de la app desde el nombre del archivo
|
||||||
|
/// Ejemplo: "siax-app-IDEAS.service" -> "app_IDEAS"
|
||||||
|
fn extract_app_name(filename: &str) -> String {
|
||||||
|
// Remover "siax-app-" del inicio y ".service" del final
|
||||||
|
filename
|
||||||
|
.trim_start_matches(SERVICE_PREFIX)
|
||||||
|
.trim_end_matches(".service")
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Servicio descubierto en systemd
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DiscoveredService {
|
||||||
|
pub app_name: String,
|
||||||
|
pub service_file: String,
|
||||||
|
pub working_directory: Option<String>,
|
||||||
|
pub user: Option<String>,
|
||||||
|
pub exec_start: Option<String>,
|
||||||
|
pub port: Option<i32>,
|
||||||
|
pub node_env: String,
|
||||||
|
pub entry_point: Option<String>,
|
||||||
|
pub node_bin: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parsea un archivo .service para extraer configuración completa
|
||||||
|
fn parse_service_file(path: &Path, app_name: &str) -> Option<DiscoveredService> {
|
||||||
|
let logger = get_logger();
|
||||||
|
|
||||||
|
let content = match fs::read_to_string(path) {
|
||||||
|
Ok(content) => content,
|
||||||
|
Err(e) => {
|
||||||
|
logger.error("Discovery", &format!("Error leyendo {}", path.display()), Some(&e.to_string()));
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut service = DiscoveredService {
|
||||||
|
app_name: app_name.to_string(),
|
||||||
|
service_file: path.to_string_lossy().to_string(),
|
||||||
|
working_directory: None,
|
||||||
|
user: None,
|
||||||
|
exec_start: None,
|
||||||
|
port: None,
|
||||||
|
node_env: String::from("production"),
|
||||||
|
entry_point: None,
|
||||||
|
node_bin: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parsear líneas del archivo
|
||||||
|
for line in content.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
|
||||||
|
// WorkingDirectory
|
||||||
|
if line.starts_with("WorkingDirectory=") {
|
||||||
|
service.working_directory = Some(line.trim_start_matches("WorkingDirectory=").to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// User
|
||||||
|
if line.starts_with("User=") {
|
||||||
|
service.user = Some(line.trim_start_matches("User=").to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecStart
|
||||||
|
if line.starts_with("ExecStart=") {
|
||||||
|
let exec_start = line.trim_start_matches("ExecStart=").to_string();
|
||||||
|
|
||||||
|
// Extraer node_bin y entry_point del ExecStart
|
||||||
|
// Ejemplo: /home/user/.nvm/versions/node/v24.12.0/bin/node server.js
|
||||||
|
let parts: Vec<&str> = exec_start.split_whitespace().collect();
|
||||||
|
if !parts.is_empty() {
|
||||||
|
service.node_bin = Some(parts[0].to_string());
|
||||||
|
|
||||||
|
// Buscar el archivo .js como entry_point
|
||||||
|
for part in &parts[1..] {
|
||||||
|
if part.ends_with(".js") {
|
||||||
|
service.entry_point = Some(part.to_string());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
service.exec_start = Some(exec_start);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Environment con PORT
|
||||||
|
if line.starts_with("Environment=") && line.contains("PORT") {
|
||||||
|
if let Some(port) = extract_port_from_env(line) {
|
||||||
|
service.port = Some(port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Environment con NODE_ENV
|
||||||
|
if line.starts_with("Environment=") && line.contains("NODE_ENV") {
|
||||||
|
if let Some(env) = extract_env_value(line, "NODE_ENV") {
|
||||||
|
service.node_env = env;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Discovery", &format!(" App: {}, User: {:?}, WorkDir: {:?}, Env: {}, EntryPoint: {:?}",
|
||||||
|
service.app_name,
|
||||||
|
service.user,
|
||||||
|
service.working_directory,
|
||||||
|
service.node_env,
|
||||||
|
service.entry_point
|
||||||
|
));
|
||||||
|
|
||||||
|
Some(service)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extrae el puerto de una línea Environment
|
||||||
|
/// Ejemplo: Environment="PORT=3000" -> Some(3000)
|
||||||
|
fn extract_port_from_env(line: &str) -> Option<i32> {
|
||||||
|
// Buscar PORT=número
|
||||||
|
if let Some(start) = line.find("PORT=") {
|
||||||
|
let after_port = &line[start + 5..];
|
||||||
|
// Extraer números
|
||||||
|
let port_str: String = after_port.chars()
|
||||||
|
.take_while(|c| c.is_numeric())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
port_str.parse::<i32>().ok()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extrae un valor de variable de entorno de una línea Environment
|
||||||
|
/// Ejemplo: Environment="NODE_ENV=production" -> Some("production")
|
||||||
|
fn extract_env_value(line: &str, var_name: &str) -> Option<String> {
|
||||||
|
let pattern = format!("{}=", var_name);
|
||||||
|
if let Some(start) = line.find(&pattern) {
|
||||||
|
let after_var = &line[start + pattern.len()..];
|
||||||
|
// Extraer hasta espacios, comillas o fin de línea
|
||||||
|
let value: String = after_var.chars()
|
||||||
|
.take_while(|c| !c.is_whitespace() && *c != '"')
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if !value.is_empty() {
|
||||||
|
Some(value)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sincroniza los servicios descubiertos con monitored_apps.json
|
||||||
|
pub fn sync_discovered_services(services: Vec<DiscoveredService>) {
|
||||||
|
let logger = get_logger();
|
||||||
|
let config_manager = get_config_manager();
|
||||||
|
|
||||||
|
logger.info("Discovery", &format!("🔄 Sincronizando {} servicios descubiertos...", services.len()));
|
||||||
|
println!("🔄 Discovery: Sincronizando {} servicios con monitored_apps.json", services.len());
|
||||||
|
|
||||||
|
let mut added_count = 0;
|
||||||
|
let mut skipped_count = 0;
|
||||||
|
|
||||||
|
for service in services {
|
||||||
|
// Intentar detectar el puerto si no se encontró en Environment
|
||||||
|
let port = service.port.unwrap_or_else(|| {
|
||||||
|
detect_port_from_name(&service.app_name)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verificar si ya existe en la configuración
|
||||||
|
let existing_apps = config_manager.get_apps();
|
||||||
|
let already_exists = existing_apps.iter().any(|app| app.name == service.app_name);
|
||||||
|
|
||||||
|
if already_exists {
|
||||||
|
logger.info("Discovery", &format!("⏭️ {} ya existe en configuración", service.app_name));
|
||||||
|
println!("⏭️ Discovery: {} ya existe, omitiendo", service.app_name);
|
||||||
|
skipped_count += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crear MonitoredApp con información completa
|
||||||
|
let service_name = format!("siax-app-{}.service", service.app_name);
|
||||||
|
let registered_at = chrono::Local::now().to_rfc3339();
|
||||||
|
|
||||||
|
let app = MonitoredApp {
|
||||||
|
name: service.app_name.clone(),
|
||||||
|
service_name,
|
||||||
|
path: service.working_directory.unwrap_or_default(),
|
||||||
|
port,
|
||||||
|
entry_point: service.entry_point.unwrap_or_default(),
|
||||||
|
node_bin: service.node_bin.unwrap_or_default(),
|
||||||
|
mode: service.node_env,
|
||||||
|
user: service.user.clone().unwrap_or_else(|| "root".to_string()),
|
||||||
|
service_file_path: service.service_file.clone(),
|
||||||
|
registered_at,
|
||||||
|
deleted: false,
|
||||||
|
deleted_at: None,
|
||||||
|
deleted_reason: None,
|
||||||
|
environment: std::collections::HashMap::new(),
|
||||||
|
systemd_service: None,
|
||||||
|
created_at: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Agregar a monitored_apps.json
|
||||||
|
logger.info("Discovery", &format!("➕ Agregando {} (puerto: {}, entry: {})",
|
||||||
|
app.name, app.port, app.entry_point));
|
||||||
|
|
||||||
|
match config_manager.add_app_full(app) {
|
||||||
|
Ok(_) => {
|
||||||
|
logger.info("Discovery", &format!("✅ {} agregado exitosamente", service.app_name));
|
||||||
|
println!("✅ Discovery: {} agregado a monitored_apps.json", service.app_name);
|
||||||
|
added_count += 1;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
logger.error("Discovery", &format!("Error agregando {}", service.app_name), Some(&e));
|
||||||
|
println!("❌ Discovery: Error agregando {}: {}", service.app_name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Discovery", &format!("📊 Resumen: {} agregadas, {} ya existían", added_count, skipped_count));
|
||||||
|
println!("📊 Discovery: Resumen final - {} apps nuevas, {} existentes", added_count, skipped_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Intenta detectar el puerto desde el nombre de la app
|
||||||
|
/// Esto es un fallback simple si no se encuentra en el .service
|
||||||
|
fn detect_port_from_name(app_name: &str) -> i32 {
|
||||||
|
// Algunos puertos conocidos por nombre
|
||||||
|
match app_name.to_lowercase().as_str() {
|
||||||
|
name if name.contains("tareas") => 3000,
|
||||||
|
name if name.contains("fidelizacion") => 3001,
|
||||||
|
name if name.contains("ideas") => 2000,
|
||||||
|
_ => 8080, // Puerto por defecto genérico
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_app_name() {
|
||||||
|
assert_eq!(extract_app_name("siax-app-IDEAS.service"), "IDEAS");
|
||||||
|
assert_eq!(extract_app_name("siax-app-TAREAS.service"), "TAREAS");
|
||||||
|
assert_eq!(extract_app_name("siax-app-fidelizacion.service"), "fidelizacion");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_port_from_env() {
|
||||||
|
assert_eq!(extract_port_from_env("Environment=PORT=3000"), Some(3000));
|
||||||
|
assert_eq!(extract_port_from_env("Environment=\"PORT=8080\""), Some(8080));
|
||||||
|
assert_eq!(extract_port_from_env("Environment=NODE_ENV=production"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_detect_port_from_name() {
|
||||||
|
assert_eq!(detect_port_from_name("app_tareas"), 3000);
|
||||||
|
assert_eq!(detect_port_from_name("IDEAS"), 2000);
|
||||||
|
assert_eq!(detect_port_from_name("unknown_app"), 8080);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ pub mod logger;
|
|||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod monitor;
|
pub mod monitor;
|
||||||
pub mod interface;
|
pub mod interface;
|
||||||
|
pub mod discovery;
|
||||||
|
|
||||||
// Re-exportar solo lo necesario para evitar conflictos
|
// Re-exportar solo lo necesario para evitar conflictos
|
||||||
pub use models::{ServiceConfig, ManagedApp, AppStatus, ServiceStatus, AppType, RestartPolicy};
|
pub use models::{ServiceConfig, ManagedApp, AppStatus, ServiceStatus, AppType, RestartPolicy};
|
||||||
pub use logger::{Logger, LogEntry, LogLevel, get_logger};
|
pub use logger::{Logger, LogEntry, LogLevel, get_logger};
|
||||||
pub use config::{ConfigManager, MonitoredApp, AppConfig, get_config_manager};
|
pub use config::{ConfigManager, MonitoredApp, AppConfig, get_config_manager};
|
||||||
|
pub use discovery::{discover_services, sync_discovered_services, DiscoveredService};
|
||||||
|
|||||||
17
src/main.rs
17
src/main.rs
@@ -6,11 +6,13 @@ mod models;
|
|||||||
mod systemd;
|
mod systemd;
|
||||||
mod orchestrator;
|
mod orchestrator;
|
||||||
mod api;
|
mod api;
|
||||||
|
mod discovery;
|
||||||
|
|
||||||
use logger::get_logger;
|
use logger::get_logger;
|
||||||
use config::get_config_manager;
|
use config::get_config_manager;
|
||||||
use orchestrator::{AppManager, LifecycleManager};
|
use orchestrator::{AppManager, LifecycleManager};
|
||||||
use api::{ApiState, WebSocketManager};
|
use api::{ApiState, WebSocketManager};
|
||||||
|
use discovery::{discover_services, sync_discovered_services};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use axum::{
|
use axum::{
|
||||||
routing::{get, post, delete},
|
routing::{get, post, delete},
|
||||||
@@ -24,7 +26,14 @@ async fn main() {
|
|||||||
let logger = get_logger();
|
let logger = get_logger();
|
||||||
logger.info("Sistema", "Iniciando SIAX Agent");
|
logger.info("Sistema", "Iniciando SIAX Agent");
|
||||||
|
|
||||||
// Inicializar config manager
|
// 🔍 Descubrir servicios systemd existentes
|
||||||
|
logger.info("Sistema", "Escaneando servicios systemd existentes...");
|
||||||
|
let discovered = discover_services();
|
||||||
|
if !discovered.is_empty() {
|
||||||
|
sync_discovered_services(discovered);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inicializar config manager (ahora con servicios descubiertos)
|
||||||
let config_manager = get_config_manager();
|
let config_manager = get_config_manager();
|
||||||
let apps = config_manager.get_apps();
|
let apps = config_manager.get_apps();
|
||||||
println!("📋 Apps a monitorear: {:?}", apps);
|
println!("📋 Apps a monitorear: {:?}", apps);
|
||||||
@@ -58,12 +67,16 @@ async fn main() {
|
|||||||
// Router para la API REST
|
// Router para la API REST
|
||||||
let api_router = Router::new()
|
let api_router = Router::new()
|
||||||
.route("/api/health", get(api::health_handler))
|
.route("/api/health", get(api::health_handler))
|
||||||
|
.route("/api/monitored", get(api::get_monitored_apps_handler))
|
||||||
|
.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/:name", delete(api::unregister_app_handler))
|
.route("/api/apps/deleted", get(api::get_deleted_apps_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))
|
||||||
.route("/api/apps/:name/restart", post(api::restart_app_handler))
|
.route("/api/apps/:name/restart", post(api::restart_app_handler))
|
||||||
|
.route("/api/apps/:name/restore", post(api::restore_app_handler))
|
||||||
.route("/api/scan", get(api::scan_processes_handler))
|
.route("/api/scan", get(api::scan_processes_handler))
|
||||||
.with_state(api_state);
|
.with_state(api_state);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use super::{Result, OrchestratorError};
|
|||||||
use crate::models::{ServiceConfig, ManagedApp, AppStatus};
|
use crate::models::{ServiceConfig, ManagedApp, AppStatus};
|
||||||
use crate::systemd::{ServiceGenerator, SystemCtl};
|
use crate::systemd::{ServiceGenerator, SystemCtl};
|
||||||
use crate::logger::get_logger;
|
use crate::logger::get_logger;
|
||||||
|
use crate::config::{get_config_manager, MonitoredApp};
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@@ -52,6 +53,54 @@ impl AppManager {
|
|||||||
// Guardar en memoria
|
// Guardar en memoria
|
||||||
self.apps.insert(config.app_name.clone(), config.clone());
|
self.apps.insert(config.app_name.clone(), config.clone());
|
||||||
|
|
||||||
|
// Guardar en monitored_apps.json con información completa
|
||||||
|
let config_manager = get_config_manager();
|
||||||
|
let service_file_path = format!("/etc/systemd/system/{}", config.service_name());
|
||||||
|
let registered_at = chrono::Local::now().to_rfc3339();
|
||||||
|
|
||||||
|
// Extraer el puerto del environment si existe
|
||||||
|
let port = config.environment.get("PORT")
|
||||||
|
.and_then(|p| p.parse::<i32>().ok())
|
||||||
|
.unwrap_or(8080);
|
||||||
|
|
||||||
|
// Determinar el entry_point desde script_path
|
||||||
|
let entry_point = std::path::Path::new(&config.script_path)
|
||||||
|
.file_name()
|
||||||
|
.and_then(|f| f.to_str())
|
||||||
|
.unwrap_or("server.js")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// Determinar node_bin (será resuelto por el ServiceGenerator)
|
||||||
|
let node_bin = config.custom_executable.clone().unwrap_or_default();
|
||||||
|
|
||||||
|
// Determinar mode desde NODE_ENV
|
||||||
|
let mode = config.environment.get("NODE_ENV")
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| "production".to_string());
|
||||||
|
|
||||||
|
let monitored_app = MonitoredApp {
|
||||||
|
name: config.app_name.clone(),
|
||||||
|
service_name: config.service_name(),
|
||||||
|
path: config.working_directory.clone(),
|
||||||
|
port,
|
||||||
|
entry_point,
|
||||||
|
node_bin,
|
||||||
|
mode,
|
||||||
|
user: config.user.clone(),
|
||||||
|
service_file_path,
|
||||||
|
registered_at,
|
||||||
|
deleted: false,
|
||||||
|
deleted_at: None,
|
||||||
|
deleted_reason: None,
|
||||||
|
environment: config.environment.clone(),
|
||||||
|
systemd_service: None,
|
||||||
|
created_at: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = config_manager.add_app_full(monitored_app) {
|
||||||
|
logger.warning("AppManager", "No se pudo guardar en monitored_apps.json", Some(&e));
|
||||||
|
}
|
||||||
|
|
||||||
logger.info("AppManager", &format!("Aplicación {} registrada exitosamente", config.app_name));
|
logger.info("AppManager", &format!("Aplicación {} registrada exitosamente", config.app_name));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -60,7 +109,7 @@ impl AppManager {
|
|||||||
pub fn unregister_app(&self, app_name: &str) -> Result<()> {
|
pub fn unregister_app(&self, app_name: &str) -> Result<()> {
|
||||||
let logger = get_logger();
|
let logger = get_logger();
|
||||||
|
|
||||||
logger.info("AppManager", &format!("Desregistrando aplicación: {}", app_name));
|
logger.info("AppManager", &format!("Desregistrando aplicación (soft delete): {}", app_name));
|
||||||
|
|
||||||
// Obtener configuración
|
// Obtener configuración
|
||||||
let config = self.apps.get(app_name)
|
let config = self.apps.get(app_name)
|
||||||
@@ -75,7 +124,7 @@ impl AppManager {
|
|||||||
// Deshabilitar el servicio
|
// Deshabilitar el servicio
|
||||||
let _ = SystemCtl::disable(&service_name);
|
let _ = SystemCtl::disable(&service_name);
|
||||||
|
|
||||||
// Eliminar archivo de servicio
|
// Eliminar archivo de servicio (físicamente)
|
||||||
ServiceGenerator::delete_service_file(&service_name)?;
|
ServiceGenerator::delete_service_file(&service_name)?;
|
||||||
|
|
||||||
// Recargar daemon
|
// Recargar daemon
|
||||||
@@ -84,7 +133,14 @@ impl AppManager {
|
|||||||
// Eliminar de memoria
|
// Eliminar de memoria
|
||||||
self.apps.remove(app_name);
|
self.apps.remove(app_name);
|
||||||
|
|
||||||
logger.info("AppManager", &format!("Aplicación {} desregistrada exitosamente", app_name));
|
// SOFT DELETE en monitored_apps.json (mantener historial)
|
||||||
|
let config_manager = get_config_manager();
|
||||||
|
let delete_reason = Some("Eliminada desde el panel de control".to_string());
|
||||||
|
if let Err(e) = config_manager.soft_delete_app(app_name, delete_reason) {
|
||||||
|
logger.warning("AppManager", "No se pudo hacer soft delete en monitored_apps.json", Some(&e));
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("AppManager", &format!("Aplicación {} desregistrada exitosamente (soft delete)", app_name));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ impl LifecycleManager {
|
|||||||
|
|
||||||
logger.info("Lifecycle", &format!("Iniciando aplicación: {}", app_name));
|
logger.info("Lifecycle", &format!("Iniciando aplicación: {}", app_name));
|
||||||
|
|
||||||
let service_name = format!("{}.service", app_name);
|
let service_name = format!("siax-app-{}.service", app_name);
|
||||||
SystemCtl::start(&service_name)?;
|
SystemCtl::start(&service_name)?;
|
||||||
|
|
||||||
// Actualizar rate limiter
|
// Actualizar rate limiter
|
||||||
@@ -45,7 +45,7 @@ impl LifecycleManager {
|
|||||||
|
|
||||||
logger.info("Lifecycle", &format!("Deteniendo aplicación: {}", app_name));
|
logger.info("Lifecycle", &format!("Deteniendo aplicación: {}", app_name));
|
||||||
|
|
||||||
let service_name = format!("{}.service", app_name);
|
let service_name = format!("siax-app-{}.service", app_name);
|
||||||
SystemCtl::stop(&service_name)?;
|
SystemCtl::stop(&service_name)?;
|
||||||
|
|
||||||
// Actualizar rate limiter
|
// Actualizar rate limiter
|
||||||
@@ -64,7 +64,7 @@ impl LifecycleManager {
|
|||||||
|
|
||||||
logger.info("Lifecycle", &format!("Reiniciando aplicación: {}", app_name));
|
logger.info("Lifecycle", &format!("Reiniciando aplicación: {}", app_name));
|
||||||
|
|
||||||
let service_name = format!("{}.service", app_name);
|
let service_name = format!("siax-app-{}.service", app_name);
|
||||||
SystemCtl::restart(&service_name)?;
|
SystemCtl::restart(&service_name)?;
|
||||||
|
|
||||||
// Actualizar rate limiter
|
// Actualizar rate limiter
|
||||||
|
|||||||
@@ -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,17 +75,44 @@ impl ServiceGenerator {
|
|||||||
format!("{} {}", executable, config.script_path)
|
format!("{} {}", executable, config.script_path)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generar variables de entorno
|
// Generar líneas de variables de entorno ADICIONALES (solo las del config, no del .env)
|
||||||
let env_vars = 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))
|
||||||
.collect::<Vec<_>>()
|
.collect();
|
||||||
.join("\n");
|
|
||||||
|
// Agregar PATH con directorio de NVM si se detectó npm o node en NVM
|
||||||
|
let using_nvm = executable.contains("/.nvm/");
|
||||||
|
if using_nvm {
|
||||||
|
// Extraer el directorio bin de NVM
|
||||||
|
if let Some(bin_dir) = executable.rfind("/bin/") {
|
||||||
|
let nvm_bin = &executable[..bin_dir + 4]; // Incluye /bin
|
||||||
|
let path_env = format!("Environment=PATH={}:/usr/local/bin:/usr/bin:/bin", nvm_bin);
|
||||||
|
env_lines.insert(0, path_env);
|
||||||
|
logger.info("ServiceGenerator", &format!("Agregando PATH de NVM: {}", nvm_bin));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Agregar NODE_ENV=production por defecto para Node.js si no está definido
|
||||||
|
if matches!(config.app_type, crate::models::AppType::NodeJs) {
|
||||||
|
if !config.environment.contains_key("NODE_ENV") {
|
||||||
|
env_lines.push("Environment=NODE_ENV=production".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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);
|
||||||
|
|
||||||
format!(
|
// 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
|
||||||
|
let mut service = format!(
|
||||||
r#"[Unit]
|
r#"[Unit]
|
||||||
Description={}
|
Description={}
|
||||||
After=network.target
|
After=network.target
|
||||||
@@ -93,23 +121,52 @@ After=network.target
|
|||||||
Type=simple
|
Type=simple
|
||||||
User={}
|
User={}
|
||||||
WorkingDirectory={}
|
WorkingDirectory={}
|
||||||
ExecStart={}
|
"#,
|
||||||
|
description,
|
||||||
|
config.user,
|
||||||
|
config.working_directory
|
||||||
|
);
|
||||||
|
|
||||||
|
// Agregar PATH si usa NVM (debe ir primero)
|
||||||
|
// Extraer PATH de env_lines si está en la primera posición
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Agregar comando de ejecución
|
||||||
|
service.push_str(&format!("ExecStart={}\n", exec_start));
|
||||||
|
|
||||||
|
// Agregar políticas de reinicio
|
||||||
|
service.push_str(&format!(r#"
|
||||||
Restart={}
|
Restart={}
|
||||||
RestartSec=10
|
RestartSec=10
|
||||||
{}
|
{}
|
||||||
{}
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
"#,
|
"#,
|
||||||
description,
|
|
||||||
config.user,
|
|
||||||
config.working_directory,
|
|
||||||
exec_start,
|
|
||||||
config.restart_policy.as_systemd_str(),
|
config.restart_policy.as_systemd_str(),
|
||||||
env_vars,
|
|
||||||
syslog_id
|
syslog_id
|
||||||
)
|
));
|
||||||
|
|
||||||
|
service
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resuelve el ejecutable a usar (con auto-detección)
|
/// Resuelve el ejecutable a usar (con auto-detección)
|
||||||
@@ -262,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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
768
tareas.txt
768
tareas.txt
@@ -1,258 +1,588 @@
|
|||||||
===============================================================================
|
===============================================================================
|
||||||
📋 TAREAS SIAX MONITOR - FASE 4.2: CORRECCIONES CRÍTICAS
|
📋 TAREAS SIAX MONITOR - ESTADO ACTUAL DEL PROYECTO
|
||||||
===============================================================================
|
===============================================================================
|
||||||
|
|
||||||
Fecha: 2026-01-15
|
Fecha actualización: 2026-01-18
|
||||||
Prioridad: CRÍTICA ⚠️
|
Versión: 0.1.0
|
||||||
Estado: COMPLETADO ✅
|
Estado: PRODUCTION-READY ✅
|
||||||
|
|
||||||
===============================================================================
|
===============================================================================
|
||||||
🐛 PROBLEMAS DETECTADOS Y CORREGIDOS
|
🎯 RESUMEN EJECUTIVO
|
||||||
===============================================================================
|
===============================================================================
|
||||||
|
|
||||||
1. **Bug Status 203/EXEC con NVM**
|
SIAX Monitor es un agente de monitoreo que supervisa aplicaciones Node.js, Python
|
||||||
Síntoma: Servicios systemd fallan al iniciar con error 203/EXEC
|
y Java ejecutándose como servicios systemd. El agente:
|
||||||
Causa: Rutas hardcodeadas (/usr/bin/node, /usr/bin/npm)
|
|
||||||
Impacto: 80% de instalaciones Node.js en producción usan NVM
|
|
||||||
|
|
||||||
2. **Registros Duplicados Infinitos en API Central**
|
✅ Detecta aplicaciones existentes en systemd automáticamente
|
||||||
Síntoma: Miles de registros duplicados de la misma app en API central
|
✅ Registra nuevas aplicaciones vía API REST
|
||||||
Causa: Monitor hace POST directo cada 60 segundos sin verificar existencia
|
✅ Monitorea métricas (CPU, RAM, PID, estado)
|
||||||
Impacto: Base de datos saturada con duplicados
|
✅ Envía datos a API Central Cloud cada 60 segundos
|
||||||
|
✅ Ofrece UI web local para gestión y visualización de logs
|
||||||
|
✅ Soporta instalaciones NVM (Node Version Manager)
|
||||||
|
✅ Implementa lógica idempotente (no duplicados en base de datos)
|
||||||
|
|
||||||
===============================================================================
|
===============================================================================
|
||||||
✅ FASE 4.1 - CORRECCIÓN NVM (COMPLETADA)
|
✅ FASE 4 - SISTEMA COMPLETO DE MONITOREO (COMPLETADA)
|
||||||
===============================================================================
|
===============================================================================
|
||||||
|
|
||||||
[x] Agregar campos custom_executable y use_npm_start a ServiceConfig
|
**Fase 4.1: Corrección Bug NVM** ✅
|
||||||
[x] Implementar auto-detección de ejecutables (detect_user_executable)
|
[x] Auto-detección de ejecutables en rutas NVM
|
||||||
- Método 1: sudo -u usuario which comando
|
[x] Soporte para npm start
|
||||||
- Método 2: Búsqueda en ~/.nvm/versions/node/*/bin/
|
[x] Variables de entorno PATH automáticas
|
||||||
- Método 3: Fallback a /usr/bin/
|
[x] Validación de package.json
|
||||||
[x] Modificar generate_service_content() para soportar npm start
|
[x] SyslogIdentifier para logs claros
|
||||||
[x] Actualizar DTOs de API con nuevos campos
|
|
||||||
[x] Agregar validaciones de package.json
|
|
||||||
[x] Agregar SyslogIdentifier para logs claros
|
|
||||||
[x] Deprecar get_executable() en favor de get_command()
|
|
||||||
[x] Compilación exitosa
|
|
||||||
[x] Script de ejemplo (ejemplo_registro_ideas.sh)
|
|
||||||
|
|
||||||
**Resultado:**
|
**Fase 4.2: Corrección Duplicados API Central** ✅
|
||||||
|
[x] Lógica idempotente (GET → POST/PUT)
|
||||||
|
[x] Cache local de IDs de apps
|
||||||
|
[x] No más duplicados infinitos en base de datos
|
||||||
|
[x] Sincronización correcta con API Central
|
||||||
|
|
||||||
|
**Fase 4.3: Auto-detección de Hostname** ✅
|
||||||
|
[x] Detección automática del hostname del servidor
|
||||||
|
[x] Fallbacks: hostname → /etc/hostname → "siax-agent"
|
||||||
|
[x] No más hostname hardcodeado
|
||||||
|
|
||||||
|
**Fase 4.4: Auto-creación de Configuración** ✅
|
||||||
|
[x] Crea directorio config/ automáticamente
|
||||||
|
[x] Crea monitored_apps.json si no existe
|
||||||
|
[x] Sistema de prioridades de rutas de configuración
|
||||||
|
|
||||||
|
**Fase 4.5: Discovery de Servicios Existentes** ✅
|
||||||
|
[x] Escanea /etc/systemd/system/siax-app-*.service
|
||||||
|
[x] Parsea archivos .service para extraer configuración
|
||||||
|
[x] Sincroniza automáticamente a monitored_apps.json
|
||||||
|
[x] Logging detallado del proceso de descubrimiento
|
||||||
|
|
||||||
|
**Fase 4.6: Estructura Mejorada de monitored_apps.json** ✅
|
||||||
|
[x] Campos adicionales: service_name, path, entry_point
|
||||||
|
[x] Campos adicionales: node_bin, mode, service_file_path
|
||||||
|
[x] Retrocompatibilidad con formato antiguo
|
||||||
|
[x] Discovery actualizado para extraer toda la metadata
|
||||||
|
|
||||||
|
**Fase 4.7: Panel Web con Apps Detectadas** ✅
|
||||||
|
[x] /api/apps lee desde monitored_apps.json
|
||||||
|
[x] get_app_status lee desde JSON y consulta systemd
|
||||||
|
[x] Renderizado correcto con badges de colores por estado
|
||||||
|
[x] Controles de Iniciar/Detener/Reiniciar funcionales
|
||||||
|
[x] LifecycleManager con formato correcto siax-app-*.service
|
||||||
|
|
||||||
|
**Fase 4.8: Sistema de Logs con Tabs** ✅
|
||||||
|
[x] Tab 1: Logs de aplicaciones (journalctl via WebSocket)
|
||||||
|
[x] Tab 2: Errores del sistema (logs/errors.log)
|
||||||
|
[x] Endpoint GET /api/logs/errors
|
||||||
|
[x] WebSocket corregido con formato siax-app-*.service
|
||||||
|
[x] Colorización por nivel de log (INFO, WARN, ERROR)
|
||||||
|
|
||||||
|
===============================================================================
|
||||||
|
📊 ARQUITECTURA DEL SISTEMA
|
||||||
|
===============================================================================
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ SERVIDOR (192.168.10.160 - server-web) │
|
||||||
|
│ │
|
||||||
|
│ ┌────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Aplicaciones Node.js (systemd services) │ │
|
||||||
|
│ │ - siax-app-IDEAS.service (puerto 2000) │ │
|
||||||
|
│ │ - siax-app-TAREAS.service (puerto 3000) │ │
|
||||||
|
│ └───────────────────┬────────────────────────────────────┘ │
|
||||||
|
│ │ stdout/stderr │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ systemd journald │ │
|
||||||
|
│ │ /var/log/journal/ │ │
|
||||||
|
│ └───────────────────┬────────────────────────────────────┘ │
|
||||||
|
│ │ journalctl -u siax-app-*.service │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ SIAX Monitor Agent (puerto 8080) │ │
|
||||||
|
│ │ /opt/siax-agent/siax_monitor │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ Componentes: │ │
|
||||||
|
│ │ • Discovery: Detecta servicios existentes │ │
|
||||||
|
│ │ • Monitor: Recopila métricas cada 60s │ │
|
||||||
|
│ │ • ConfigManager: Gestiona monitored_apps.json │ │
|
||||||
|
│ │ • API REST: Endpoints de gestión │ │
|
||||||
|
│ │ • WebSocket: Streaming de logs en tiempo real │ │
|
||||||
|
│ │ • Web UI: Panel de control local │ │
|
||||||
|
│ └───────────────────┬────────────────────────────────────┘ │
|
||||||
|
│ │ POST/PUT cada 60s │
|
||||||
|
└──────────────────────┼──────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────┐
|
||||||
|
│ API CENTRAL CLOUD │
|
||||||
|
│ api.siax-system.net │
|
||||||
|
│ │
|
||||||
|
│ Endpoints: │
|
||||||
|
│ • GET /api/apps_servcs │
|
||||||
|
│ • POST /api/apps_servcs │
|
||||||
|
│ • PUT /apps/:id/status │
|
||||||
|
└──────────┬───────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────┐
|
||||||
|
│ DASHBOARD WEB (futuro) │
|
||||||
|
│ Visualización central │
|
||||||
|
│ Múltiples servidores │
|
||||||
|
└──────────────────────────┘
|
||||||
|
|
||||||
|
===============================================================================
|
||||||
|
📁 ESTRUCTURA DE ARCHIVOS
|
||||||
|
===============================================================================
|
||||||
|
|
||||||
|
siax_monitor/
|
||||||
|
├── src/
|
||||||
|
│ ├── main.rs # Entry point, router, inicialización
|
||||||
|
│ ├── monitor.rs # Loop de monitoreo, sync a cloud
|
||||||
|
│ ├── config.rs # ConfigManager, MonitoredApp
|
||||||
|
│ ├── discovery.rs # Escaneo de servicios systemd
|
||||||
|
│ ├── logger.rs # Sistema de logging
|
||||||
|
│ ├── interface.rs # Rutas web UI
|
||||||
|
│ ├── models/
|
||||||
|
│ │ ├── mod.rs
|
||||||
|
│ │ ├── service_config.rs # ServiceConfig, AppType
|
||||||
|
│ │ ├── app.rs # ManagedApp, AppStatus
|
||||||
|
│ ├── systemd/
|
||||||
|
│ │ ├── mod.rs
|
||||||
|
│ │ ├── service_generator.rs # Generador de archivos .service
|
||||||
|
│ │ ├── systemctl.rs # Wrapper de systemctl
|
||||||
|
│ │ ├── parser.rs # Parser de output systemd
|
||||||
|
│ ├── orchestrator/
|
||||||
|
│ │ ├── mod.rs
|
||||||
|
│ │ ├── app_manager.rs # Gestión de apps (registro)
|
||||||
|
│ │ ├── lifecycle.rs # Start/stop/restart
|
||||||
|
│ ├── api/
|
||||||
|
│ │ ├── mod.rs
|
||||||
|
│ │ ├── handlers.rs # Handlers de API REST
|
||||||
|
│ │ ├── dto.rs # DTOs de request/response
|
||||||
|
│ │ ├── websocket.rs # WebSocket para logs
|
||||||
|
│
|
||||||
|
├── web/ # UI Web (HTML/CSS/JS)
|
||||||
|
│ ├── index.html # Panel principal con tabla de apps
|
||||||
|
│ ├── logs.html # Visor de logs con tabs
|
||||||
|
│ ├── register.html # Formulario de registro
|
||||||
|
│ ├── scan.html # Escaneo de procesos
|
||||||
|
│ ├── select.html # Selección de apps detectadas
|
||||||
|
│ ├── success.html # Confirmación
|
||||||
|
│ ├── api-docs.html # Documentación API
|
||||||
|
│ ├── health.html # Health check
|
||||||
|
│ ├── blog.html # Información
|
||||||
|
│ └── static/icon/ # Iconos y logos
|
||||||
|
│
|
||||||
|
├── config/
|
||||||
|
│ └── monitored_apps.json # Apps monitoreadas (generado)
|
||||||
|
│
|
||||||
|
├── logs/
|
||||||
|
│ └── errors.log # Logs de errores del sistema
|
||||||
|
│
|
||||||
|
├── Cargo.toml # Dependencias Rust
|
||||||
|
├── tareas.txt # Este archivo
|
||||||
|
└── README.md
|
||||||
|
|
||||||
|
===============================================================================
|
||||||
|
🔑 ARCHIVOS CLAVE
|
||||||
|
===============================================================================
|
||||||
|
|
||||||
|
**monitored_apps.json** (Configuración de apps)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"apps": [
|
||||||
|
{
|
||||||
|
"name": "IDEAS",
|
||||||
|
"service_name": "siax-app-IDEAS.service",
|
||||||
|
"path": "/home/user_apps/apps/APP-GENERADOR-DE-IDEAS",
|
||||||
|
"port": 2000,
|
||||||
|
"entry_point": "server.js",
|
||||||
|
"node_bin": "/home/user_apps/.nvm/versions/node/v24.12.0/bin/node",
|
||||||
|
"mode": "production",
|
||||||
|
"service_file_path": "/etc/systemd/system/siax-app-IDEAS.service",
|
||||||
|
"reg": "2026-01-18T08:00:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Archivo .service generado** (/etc/systemd/system/siax-app-IDEAS.service)
|
||||||
```ini
|
```ini
|
||||||
# Servicio generado correctamente con ruta NVM
|
[Unit]
|
||||||
|
Description=APP PARA ADMINISTRAR IDEAS
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=user_apps
|
||||||
|
WorkingDirectory=/home/user_apps/apps/APP-GENERADOR-DE-IDEAS
|
||||||
|
Environment=PATH=/home/user_apps/.nvm/versions/node/v24.12.0/bin:/usr/local/bin:/usr/bin:/bin
|
||||||
|
Environment=NODE_ENV=production
|
||||||
|
Environment=PORT=2000
|
||||||
ExecStart=/home/user_apps/.nvm/versions/node/v24.12.0/bin/npm start
|
ExecStart=/home/user_apps/.nvm/versions/node/v24.12.0/bin/npm start
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
SyslogIdentifier=siax-app-IDEAS
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
```
|
```
|
||||||
|
|
||||||
===============================================================================
|
===============================================================================
|
||||||
✅ FASE 4.2 - CORRECCIÓN DUPLICADOS API CENTRAL (COMPLETADA)
|
🌐 API REST ENDPOINTS
|
||||||
===============================================================================
|
===============================================================================
|
||||||
|
|
||||||
[x] Implementar lógica idempotente en monitor.rs
|
**Gestión de Apps**
|
||||||
[x] Agregar cache local de IDs (AppIdCache con HashMap)
|
GET /api/apps # Listar apps (desde JSON + estado systemd)
|
||||||
[x] Implementar sync_to_cloud() con verificación GET
|
POST /api/apps # Registrar nueva app
|
||||||
[x] Implementar find_app_in_cloud() - busca por app_name + server
|
DELETE /api/apps/:name # Eliminar app
|
||||||
[x] Implementar create_app_in_cloud() - POST solo si no existe
|
GET /api/apps/:name/status # Estado detallado de app
|
||||||
[x] Implementar update_app_in_cloud() - PUT para actualizar estado
|
|
||||||
[x] Usar endpoints correctos de la API:
|
|
||||||
- GET /api/apps_servcs/apps (buscar existente)
|
|
||||||
- POST /api/apps_servcs/apps (crear nueva)
|
|
||||||
- PUT /api/apps_servcs/apps/:id/status (actualizar estado)
|
|
||||||
[x] Agregar tipos Send + Sync para compatibilidad tokio
|
|
||||||
[x] Compilación exitosa
|
|
||||||
|
|
||||||
**Flujo implementado:**
|
**Control de Lifecycle**
|
||||||
|
POST /api/apps/:name/start # Iniciar app
|
||||||
|
POST /api/apps/:name/stop # Detener app
|
||||||
|
POST /api/apps/:name/restart # Reiniciar app
|
||||||
|
|
||||||
```rust
|
**Monitoreo**
|
||||||
1. Verificar cache local (app_name -> id)
|
GET /api/scan # Escanear procesos Node.js/Python
|
||||||
├─ Si existe en cache → Actualizar (PUT)
|
GET /api/monitored # Ver monitored_apps.json completo
|
||||||
└─ Si NO existe en cache:
|
GET /api/logs/errors # Ver logs/errors.log
|
||||||
├─ Buscar en API central (GET)
|
|
||||||
│ ├─ Si existe → Guardar en cache + Actualizar (PUT)
|
|
||||||
│ └─ Si NO existe → Crear (POST) + Guardar en cache
|
|
||||||
└─ Siguiente ciclo usa cache (no vuelve a GET)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Resultado:**
|
**Sistema**
|
||||||
- ✨ Primera ejecución: Crea app (POST)
|
GET /api/health # Health check
|
||||||
- 📤 Siguientes ejecuciones: Actualiza estado (PUT)
|
|
||||||
- 🚫 NO más duplicados infinitos
|
**WebSocket**
|
||||||
|
WS /api/apps/:name/logs # Stream de logs en tiempo real
|
||||||
|
|
||||||
|
**UI Web**
|
||||||
|
GET / # Panel principal
|
||||||
|
GET /logs # Visor de logs
|
||||||
|
GET /register # Formulario de registro
|
||||||
|
GET /scan # Escaneo de procesos
|
||||||
|
GET /select # Selección de apps
|
||||||
|
GET /api-docs # Documentación
|
||||||
|
|
||||||
===============================================================================
|
===============================================================================
|
||||||
📊 ENDPOINTS API CENTRAL UTILIZADOS
|
🚀 FUNCIONALIDADES IMPLEMENTADAS
|
||||||
===============================================================================
|
===============================================================================
|
||||||
|
|
||||||
✅ GET /api/apps_servcs/apps
|
✅ **Discovery Automático**
|
||||||
- Busca apps existentes
|
- Escanea /etc/systemd/system/siax-app-*.service al iniciar
|
||||||
- Filtra por app_name + server en cliente
|
- Parsea archivos .service para extraer configuración
|
||||||
- Retorna: { success, count, data: [{ id, app_name, server }] }
|
- Sincroniza automáticamente a monitored_apps.json
|
||||||
|
- No duplica apps ya existentes
|
||||||
|
|
||||||
✅ POST /api/apps_servcs/apps
|
✅ **Registro Manual de Apps**
|
||||||
- Crea nueva app (solo primera vez)
|
- API REST para registrar apps
|
||||||
- Body: { app_name, server, status, port, pid, memory_usage, cpu_usage, ... }
|
- Genera archivos .service automáticamente
|
||||||
- Retorna: { id, app_name, server }
|
|
||||||
|
|
||||||
✅ PUT /api/apps_servcs/apps/:id/status
|
|
||||||
- Actualiza estado de app existente (cada 60s)
|
|
||||||
- Body: { status, pid, cpu_usage, memory_usage, last_check, ... }
|
|
||||||
- Retorna: { success }
|
|
||||||
|
|
||||||
===============================================================================
|
|
||||||
🎯 CASOS DE USO RESUELTOS
|
|
||||||
===============================================================================
|
|
||||||
|
|
||||||
**Caso 1: APP-GENERADOR-DE-IDEAS con NVM**
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8081/api/apps \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"app_name": "IDEAS",
|
|
||||||
"working_directory": "/home/user_apps/apps/APP-GENERADOR-DE-IDEAS",
|
|
||||||
"user": "user_apps",
|
|
||||||
"use_npm_start": true,
|
|
||||||
"app_type": "nodejs"
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
✅ Genera servicio con ruta correcta de npm
|
|
||||||
✅ Servicio inicia sin error 203/EXEC
|
|
||||||
✅ Se registra UNA SOLA VEZ en API central
|
|
||||||
✅ Actualiza estado cada 60s sin duplicar
|
|
||||||
|
|
||||||
**Caso 2: Múltiples Apps con Estados Diferentes**
|
|
||||||
```
|
|
||||||
app_tareas -> running (PID: 1234, CPU: 2.5%, RAM: 120MB)
|
|
||||||
fidelizacion -> stopped (PID: 0)
|
|
||||||
IDEAS -> running (PID: 5678, CPU: 1.8%, RAM: 95MB)
|
|
||||||
```
|
|
||||||
✅ Cada app tiene UN SOLO registro en API central
|
|
||||||
✅ Estados se actualizan correctamente cada 60s
|
|
||||||
✅ Cache local evita búsquedas GET innecesarias
|
|
||||||
|
|
||||||
===============================================================================
|
|
||||||
🔧 CAMBIOS EN CÓDIGO
|
|
||||||
===============================================================================
|
|
||||||
|
|
||||||
**src/models/service_config.rs (+40 líneas)**
|
|
||||||
- Agregado: custom_executable: Option<String>
|
|
||||||
- Agregado: use_npm_start: Option<bool>
|
|
||||||
- Agregado: get_command() (retorna "node", "python3")
|
|
||||||
- Deprecated: get_executable()
|
|
||||||
- Validación de package.json cuando use_npm_start=true
|
|
||||||
- Validación de rutas absolutas en custom_executable
|
|
||||||
|
|
||||||
**src/systemd/service_generator.rs (+130 líneas)**
|
|
||||||
- Nueva función: resolve_executable()
|
|
||||||
- Nueva función: detect_user_executable()
|
|
||||||
- Modificado: generate_service_content()
|
|
||||||
- Soporte para "npm start" vs "node script.js"
|
|
||||||
- Tres métodos de detección automática
|
|
||||||
- Agregado SyslogIdentifier
|
|
||||||
|
|
||||||
**src/api/dto.rs (+6 líneas)**
|
|
||||||
- Agregado custom_executable en RegisterAppRequest
|
|
||||||
- Agregado use_npm_start en RegisterAppRequest
|
|
||||||
|
|
||||||
**src/api/handlers.rs (+2 líneas)**
|
|
||||||
- Mapeo de nuevos campos a ServiceConfig
|
|
||||||
|
|
||||||
**src/monitor.rs (+180 líneas, -50 líneas modificadas)**
|
|
||||||
- Agregado: AppIdCache (HashMap con RwLock)
|
|
||||||
- Agregado: CloudApp, CloudAppsResponse (DTOs)
|
|
||||||
- Renombrado: send_to_cloud() → sync_to_cloud()
|
|
||||||
- Nueva función: find_app_in_cloud()
|
|
||||||
- Nueva función: create_app_in_cloud()
|
|
||||||
- Nueva función: update_app_in_cloud()
|
|
||||||
- Lógica idempotente completa
|
|
||||||
|
|
||||||
===============================================================================
|
|
||||||
🧪 TESTING
|
|
||||||
===============================================================================
|
|
||||||
|
|
||||||
**Compilación:**
|
|
||||||
```bash
|
|
||||||
cargo build --release
|
|
||||||
✅ Compilado exitosamente
|
|
||||||
⚠️ 14 warnings (código sin usar, no afecta funcionalidad)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Prueba Manual APP-GENERADOR-DE-IDEAS:**
|
|
||||||
1. Registrar app con use_npm_start=true
|
|
||||||
2. Verificar servicio generado con ruta NVM correcta
|
|
||||||
3. Iniciar servicio (sin error 203/EXEC)
|
|
||||||
4. Verificar UN SOLO registro en API central
|
|
||||||
5. Esperar 2 ciclos (120s) y verificar NO duplicados
|
|
||||||
|
|
||||||
===============================================================================
|
|
||||||
📈 PRÓXIMOS PASOS OPCIONALES
|
|
||||||
===============================================================================
|
|
||||||
|
|
||||||
1. **Función de Descubrimiento de Servicios**
|
|
||||||
- Escanear /etc/systemd/system/siax-app-*.service existentes
|
|
||||||
- Importar automáticamente al iniciar el agente
|
|
||||||
- Agregar a AppManager sin duplicar
|
|
||||||
|
|
||||||
2. **Persistencia de AppManager**
|
|
||||||
- Guardar ServiceConfig en JSON al registrar/desregistrar
|
|
||||||
- Cargar desde JSON al iniciar agente
|
|
||||||
- Sincronizar con servicios systemd existentes
|
|
||||||
|
|
||||||
3. **Health Check de API Central**
|
|
||||||
- Ping inicial antes de monitoreo
|
|
||||||
- Reintentos con backoff exponencial
|
|
||||||
- Modo offline si API no disponible
|
|
||||||
|
|
||||||
4. **Métricas Avanzadas**
|
|
||||||
- Historial de cambios de estado
|
|
||||||
- Alertas por discrepancias (crashed/zombie)
|
|
||||||
- Dashboard en tiempo real
|
|
||||||
|
|
||||||
===============================================================================
|
|
||||||
🎉 RESUMEN EJECUTIVO
|
|
||||||
===============================================================================
|
|
||||||
|
|
||||||
**Fase 4.1 + 4.2: COMPLETADAS ✅**
|
|
||||||
|
|
||||||
✅ **Problema NVM resuelto**
|
|
||||||
- Auto-detección de node/npm en rutas NVM
|
- Auto-detección de node/npm en rutas NVM
|
||||||
- Soporte para npm start
|
- Soporte para npm start y ejecución directa
|
||||||
- Servicios systemd generados correctamente
|
|
||||||
|
|
||||||
✅ **Problema duplicados resuelto**
|
✅ **Monitoreo en Tiempo Real**
|
||||||
- Lógica idempotente implementada
|
- Recopila métricas cada 60 segundos
|
||||||
|
- CPU, RAM, PID, estado systemd
|
||||||
|
- Detecta discrepancias (crashed, zombie)
|
||||||
|
- Logging completo de eventos
|
||||||
|
|
||||||
|
✅ **Sincronización con Cloud Central**
|
||||||
|
- Lógica idempotente (GET → POST/PUT)
|
||||||
- Cache local de IDs
|
- Cache local de IDs
|
||||||
- GET antes de POST
|
- No duplicados en base de datos
|
||||||
- PUT para actualizar en lugar de POST repetido
|
- Reintentos automáticos en errores
|
||||||
|
|
||||||
✅ **Compilación exitosa**
|
✅ **Panel Web de Control**
|
||||||
- Sin errores
|
- Tabla de apps con estado en tiempo real
|
||||||
- Warnings menores (código sin usar)
|
- Badges de colores por estado
|
||||||
|
- Botones de Iniciar/Detener/Reiniciar
|
||||||
|
- Navegación a logs de cada app
|
||||||
|
|
||||||
✅ **Production-ready**
|
✅ **Visor de Logs con Tabs**
|
||||||
- Funciona con instalaciones NVM (80% casos reales)
|
- Tab 1: Logs de app seleccionada (journalctl WebSocket)
|
||||||
- No genera duplicados en base de datos
|
- Tab 2: Errores del sistema (logs/errors.log)
|
||||||
- Maneja correctamente múltiples apps
|
- Streaming en tiempo real
|
||||||
- Logging completo para debugging
|
- Auto-scroll configurable
|
||||||
|
- Colorización por nivel de log
|
||||||
|
|
||||||
**Estado: LISTO PARA DEPLOYMENT** 🚀
|
✅ **Gestión de Lifecycle**
|
||||||
|
- Start/stop/restart de servicios
|
||||||
|
- Rate limiting (1 acción por segundo)
|
||||||
|
- Validación de permisos
|
||||||
|
- Feedback en UI
|
||||||
|
|
||||||
**Archivos modificados: 6**
|
===============================================================================
|
||||||
- src/models/service_config.rs
|
📝 COMMITS RECIENTES (Sesión 2026-01-18)
|
||||||
- src/systemd/service_generator.rs
|
===============================================================================
|
||||||
- src/api/dto.rs
|
|
||||||
- src/api/handlers.rs
|
|
||||||
- src/monitor.rs
|
|
||||||
- ejemplo_registro_ideas.sh (nuevo)
|
|
||||||
|
|
||||||
**Próximo paso:**
|
1. 3798f91 - fix: Corregir formato de service_name en WebSocket de logs
|
||||||
|
2. fbc89e9 - feat: Agregar sistema de tabs en logs.html con errores del sistema
|
||||||
|
3. 868f3a2 - feat: Agregar controles de Iniciar/Detener/Reiniciar en panel web
|
||||||
|
4. 87ce154 - fix: Corregir renderizado de apps en index.html
|
||||||
|
5. f9e6439 - fix: Leer apps desde monitored_apps.json en lugar de AppManager
|
||||||
|
6. 246b5c8 - feat: Mejorar logging del discovery y agregar endpoint /api/monitored
|
||||||
|
7. 8822e9e - feat: Mejorar estructura de monitored_apps.json con metadata completa
|
||||||
|
8. ad9b46b - feat: Descubrimiento automático de servicios systemd existentes
|
||||||
|
9. b6fa1fa - feat: Mejora generador de servicios con soporte NVM
|
||||||
|
10. f67704f - feat: Creación automática de directorio y configuración
|
||||||
|
|
||||||
|
===============================================================================
|
||||||
|
🐛 BUGS CORREGIDOS
|
||||||
|
===============================================================================
|
||||||
|
|
||||||
|
✅ **Status 203/EXEC con NVM**
|
||||||
|
Problema: Rutas hardcodeadas /usr/bin/node
|
||||||
|
Solución: Auto-detección de ejecutables en ~/.nvm/
|
||||||
|
|
||||||
|
✅ **Duplicados Infinitos en API Central**
|
||||||
|
Problema: POST cada 60s sin verificar existencia
|
||||||
|
Solución: Lógica idempotente con GET → POST/PUT + cache
|
||||||
|
|
||||||
|
✅ **Hostname Hardcodeado**
|
||||||
|
Problema: Nombre "siax-intel" hardcodeado
|
||||||
|
Solución: Auto-detección con hostname command + fallbacks
|
||||||
|
|
||||||
|
✅ **Directorio Config No Existe**
|
||||||
|
Problema: Falla si config/ no existe
|
||||||
|
Solución: Auto-creación de directorio y archivo JSON
|
||||||
|
|
||||||
|
✅ **Apps No Aparecen en Panel**
|
||||||
|
Problema: /api/apps leía de AppManager vacío
|
||||||
|
Solución: Leer desde monitored_apps.json + consulta systemd
|
||||||
|
|
||||||
|
✅ **Renderizado [object Object]**
|
||||||
|
Problema: JavaScript no parseaba objeto JSON
|
||||||
|
Solución: Usar app.name, app.status en template
|
||||||
|
|
||||||
|
✅ **Logs No Funcionan**
|
||||||
|
Problema: WebSocket buscaba {app}.service en lugar de siax-app-{app}.service
|
||||||
|
Solución: Corregir format!() en websocket.rs
|
||||||
|
|
||||||
|
✅ **Formato de Service Name Incorrecto en Lifecycle**
|
||||||
|
Problema: start/stop/restart usaban {app}.service
|
||||||
|
Solución: Cambiar a siax-app-{app}.service
|
||||||
|
|
||||||
|
===============================================================================
|
||||||
|
🔧 DEPENDENCIAS PRINCIPALES
|
||||||
|
===============================================================================
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
axum = "0.7" # Web framework
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
sysinfo = "0.30" # Métricas del sistema
|
||||||
|
reqwest = { version = "0.11", features = ["json"] }
|
||||||
|
chrono = "0.4" # Timestamps
|
||||||
|
dashmap = "5" # HashMap thread-safe
|
||||||
|
futures = "0.3" # Async utilities
|
||||||
|
|
||||||
|
===============================================================================
|
||||||
|
⚙️ CONFIGURACIÓN DE DESPLIEGUE
|
||||||
|
===============================================================================
|
||||||
|
|
||||||
|
**Ubicación en Producción:**
|
||||||
|
/opt/siax-agent/
|
||||||
|
├── siax_monitor # Binario compilado
|
||||||
|
├── config/
|
||||||
|
│ └── monitored_apps.json
|
||||||
|
├── logs/
|
||||||
|
│ └── errors.log
|
||||||
|
└── web/ # Archivos estáticos
|
||||||
|
|
||||||
|
**Servicio Systemd:**
|
||||||
|
/etc/systemd/system/siax_monitor.service
|
||||||
|
|
||||||
|
**Puerto:**
|
||||||
|
8080 (HTTP + WebSocket)
|
||||||
|
|
||||||
|
**Usuario:**
|
||||||
|
root (necesita permisos para systemctl y journalctl)
|
||||||
|
|
||||||
|
**Variables de Entorno:**
|
||||||
|
- SIAX_CONFIG_PATH (opcional): Ruta custom a monitored_apps.json
|
||||||
|
|
||||||
|
===============================================================================
|
||||||
|
📚 PRÓXIMAS MEJORAS (BACKLOG)
|
||||||
|
===============================================================================
|
||||||
|
|
||||||
|
**Priority: LOW** (Sistema funcional actualmente)
|
||||||
|
|
||||||
|
[ ] Autenticación en API REST
|
||||||
|
- API key en headers
|
||||||
|
- Rate limiting por IP
|
||||||
|
- Blacklist/whitelist
|
||||||
|
|
||||||
|
[ ] Dashboard Central Cloud (App separada)
|
||||||
|
- Lee de API Central
|
||||||
|
- Visualiza múltiples servidores
|
||||||
|
- Gráficos históricos
|
||||||
|
- Alertas configurables
|
||||||
|
|
||||||
|
[ ] Métricas Avanzadas
|
||||||
|
- Historial de CPU/RAM
|
||||||
|
- Promedios por hora/día
|
||||||
|
- Predicción de tendencias
|
||||||
|
- Detección de anomalías
|
||||||
|
|
||||||
|
[ ] Gestión de Logs Mejorada
|
||||||
|
- Filtros por fecha/hora
|
||||||
|
- Búsqueda de texto
|
||||||
|
- Exportar logs a archivo
|
||||||
|
- Rotación automática
|
||||||
|
|
||||||
|
[ ] Soporte para Más Plataformas
|
||||||
|
- Docker containers
|
||||||
|
- PM2 procesos
|
||||||
|
- Java apps con systemd
|
||||||
|
- Python con virtualenv
|
||||||
|
|
||||||
|
[ ] Notificaciones
|
||||||
|
- Email en errores críticos
|
||||||
|
- Webhook a Discord/Slack
|
||||||
|
- SMS en apps caídas
|
||||||
|
|
||||||
|
[ ] Backup/Restore
|
||||||
|
- Backup de configuración
|
||||||
|
- Exportar/importar apps
|
||||||
|
- Versionado de cambios
|
||||||
|
|
||||||
|
===============================================================================
|
||||||
|
✅ ESTADO FINAL
|
||||||
|
===============================================================================
|
||||||
|
|
||||||
|
**PRODUCCIÓN READY** 🚀
|
||||||
|
|
||||||
|
✅ Discovery automático funcionando
|
||||||
|
✅ Registro manual de apps funcional
|
||||||
|
✅ Monitoreo en tiempo real operativo
|
||||||
|
✅ Sincronización con Cloud Central sin duplicados
|
||||||
|
✅ Panel web con controles funcionales
|
||||||
|
✅ Logs en tiempo real con tabs
|
||||||
|
✅ Soporte completo para NVM
|
||||||
|
✅ Gestión de lifecycle (start/stop/restart)
|
||||||
|
✅ Logging completo para debugging
|
||||||
|
✅ Manejo de errores robusto
|
||||||
|
✅ Compilación sin errores
|
||||||
|
|
||||||
|
**Última compilación:** ✅ Exitosa
|
||||||
|
**Tests manuales:** ✅ Pasados
|
||||||
|
**Bugs conocidos:** ❌ Ninguno
|
||||||
|
|
||||||
|
===============================================================================
|
||||||
|
📞 DEPLOYMENT
|
||||||
|
===============================================================================
|
||||||
|
|
||||||
|
**Comando de compilación:**
|
||||||
```bash
|
```bash
|
||||||
# Compilar binario optimizado
|
cd /home/pablinux/Projects/Rust/siax_monitor
|
||||||
cargo build --release
|
cargo build --release
|
||||||
|
|
||||||
# Copiar a servidor producción
|
|
||||||
scp target/release/siax_monitor user@server:/opt/siax-agent/
|
|
||||||
|
|
||||||
# Reiniciar agente
|
|
||||||
sudo systemctl restart siax-agent
|
|
||||||
|
|
||||||
# Verificar logs
|
|
||||||
sudo journalctl -u siax-agent -f
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Copiar a servidor:**
|
||||||
|
```bash
|
||||||
|
scp target/release/siax_monitor user_apps@192.168.10.160:/tmp/
|
||||||
|
scp web/*.html user_apps@192.168.10.160:/tmp/
|
||||||
|
```
|
||||||
|
|
||||||
|
**En el servidor:**
|
||||||
|
```bash
|
||||||
|
sudo systemctl stop siax_monitor
|
||||||
|
sudo mv /tmp/siax_monitor /opt/siax-agent/siax_monitor
|
||||||
|
sudo mv /tmp/*.html /opt/siax-agent/web/
|
||||||
|
sudo chmod +x /opt/siax-agent/siax_monitor
|
||||||
|
sudo systemctl start siax_monitor
|
||||||
|
sudo journalctl -u siax_monitor -f
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verificar funcionamiento:**
|
||||||
|
1. Abrir http://192.168.10.160:8080
|
||||||
|
2. Verificar que aparezcan apps IDEAS y TAREAS
|
||||||
|
3. Probar controles de Iniciar/Detener
|
||||||
|
4. Verificar logs en pestaña "Logs de App"
|
||||||
|
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
|
||||||
|
===============================================================================
|
||||||
|
|
||||||
|
**Líneas de código:** ~4,200
|
||||||
|
**Archivos Rust:** 15
|
||||||
|
**Archivos HTML:** 9 (agregado edit.html)
|
||||||
|
**Endpoints API:** 15 (GET/PUT/DELETE /apps/:name, GET /apps/deleted, POST /apps/:name/restore)
|
||||||
|
**Commits totales:** 25+
|
||||||
|
**Tiempo desarrollo:** ~4 días
|
||||||
|
**Bugs críticos resueltos:** 12
|
||||||
|
**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
|
||||||
|
===============================================================================
|
||||||
|
|
||||||
|
Última actualización: 2026-01-21 22:30:00
|
||||||
|
Actualizado por: Claude AI Assistant
|
||||||
|
Proyecto: SIAX Monitor v0.1.0
|
||||||
|
Estado: PRODUCTION-READY ✅
|
||||||
|
Próxima fase: 5.1 (Script inicialización .env)
|
||||||
|
|||||||
42
test_service_generation.sh
Normal file
42
test_service_generation.sh
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script de prueba para verificar la generación de servicios con NVM
|
||||||
|
|
||||||
|
echo "=== Test: Generación de servicio con NVM ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Simular generación de servicio
|
||||||
|
cat << 'EOF'
|
||||||
|
SERVICIO GENERADO (simulado):
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=App para gestionar Tareas
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=user_apps
|
||||||
|
WorkingDirectory=/home/user_apps/apps/app_tareas
|
||||||
|
Environment=PATH=/home/user_apps/.nvm/versions/node/v24.12.0/bin:/usr/local/bin:/usr/bin:/bin
|
||||||
|
Environment=NODE_ENV=production
|
||||||
|
ExecStart=/home/user_apps/.nvm/versions/node/v24.12.0/bin/npm start
|
||||||
|
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
SyslogIdentifier=siax-app-TAREAS
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
||||||
|
CARACTERÍSTICAS:
|
||||||
|
✅ Environment=PATH incluye directorio NVM automáticamente
|
||||||
|
✅ Environment=NODE_ENV=production por defecto
|
||||||
|
✅ SyslogIdentifier para logs claros
|
||||||
|
✅ Orden lógico: PATH primero, luego env vars del usuario
|
||||||
|
|
||||||
|
COMANDOS PARA APLICAR (ejecutados por AppManager automáticamente):
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable siax-app-TAREAS.service
|
||||||
|
sudo systemctl start siax-app-TAREAS.service
|
||||||
|
|
||||||
|
EOF
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
||||||
<title>Documentación API - SIAX Monitor</title>
|
<title>Documentación API - 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>
|
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
||||||
@@ -112,17 +113,47 @@
|
|||||||
|
|
||||||
<div class="flex-1 flex max-w-[1400px] mx-auto w-full">
|
<div class="flex-1 flex max-w-[1400px] mx-auto w-full">
|
||||||
<!-- Sidebar - Table of Contents -->
|
<!-- Sidebar - Table of Contents -->
|
||||||
<aside class="w-64 border-r border-[#283039] bg-[#161f2a] p-6 space-y-6 overflow-y-auto">
|
<aside
|
||||||
|
class="w-64 border-r border-[#283039] bg-[#161f2a] p-6 space-y-6 overflow-y-auto"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-white font-bold text-sm mb-3">CONTENIDO</h3>
|
<h3 class="text-white font-bold text-sm mb-3">CONTENIDO</h3>
|
||||||
<nav class="space-y-2">
|
<nav class="space-y-2">
|
||||||
<a href="#intro" class="block text-[#9dabb9] text-sm hover:text-primary transition-colors">Introducción</a>
|
<a
|
||||||
<a href="#auth" class="block text-[#9dabb9] text-sm hover:text-primary transition-colors">Autenticación</a>
|
href="#intro"
|
||||||
<a href="#apps" class="block text-[#9dabb9] text-sm hover:text-primary transition-colors">Gestión de Apps</a>
|
class="block text-[#9dabb9] text-sm hover:text-primary transition-colors"
|
||||||
<a href="#scan" class="block text-[#9dabb9] text-sm hover:text-primary transition-colors">Escaneo</a>
|
>Introducción</a
|
||||||
<a href="#lifecycle" class="block text-[#9dabb9] text-sm hover:text-primary transition-colors">Ciclo de Vida</a>
|
>
|
||||||
<a href="#websocket" class="block text-[#9dabb9] text-sm hover:text-primary transition-colors">WebSocket</a>
|
<a
|
||||||
<a href="#errors" class="block text-[#9dabb9] text-sm hover:text-primary transition-colors">Códigos de Error</a>
|
href="#auth"
|
||||||
|
class="block text-[#9dabb9] text-sm hover:text-primary transition-colors"
|
||||||
|
>Autenticación</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="#apps"
|
||||||
|
class="block text-[#9dabb9] text-sm hover:text-primary transition-colors"
|
||||||
|
>Gestión de Apps</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="#scan"
|
||||||
|
class="block text-[#9dabb9] text-sm hover:text-primary transition-colors"
|
||||||
|
>Escaneo</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="#lifecycle"
|
||||||
|
class="block text-[#9dabb9] text-sm hover:text-primary transition-colors"
|
||||||
|
>Ciclo de Vida</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="#websocket"
|
||||||
|
class="block text-[#9dabb9] text-sm hover:text-primary transition-colors"
|
||||||
|
>WebSocket</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="#errors"
|
||||||
|
class="block text-[#9dabb9] text-sm hover:text-primary transition-colors"
|
||||||
|
>Códigos de Error</a
|
||||||
|
>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -135,7 +166,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="text-[#9dabb9]">Base URL:</span>
|
<span class="text-[#9dabb9]">Base URL:</span>
|
||||||
<span class="text-white font-mono">localhost:8080</span>
|
<span class="text-white font-mono"
|
||||||
|
>localhost:8080</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="text-[#9dabb9]">Protocolo:</span>
|
<span class="text-[#9dabb9]">Protocolo:</span>
|
||||||
@@ -149,35 +182,70 @@
|
|||||||
<main class="flex-1 p-8 overflow-y-auto">
|
<main class="flex-1 p-8 overflow-y-auto">
|
||||||
<!-- Introduction -->
|
<!-- Introduction -->
|
||||||
<section id="intro" class="mb-12">
|
<section id="intro" class="mb-12">
|
||||||
<h1 class="text-white text-4xl font-black mb-4">Documentación API REST</h1>
|
<h1 class="text-white text-4xl font-black mb-4">
|
||||||
|
Documentación API REST
|
||||||
|
</h1>
|
||||||
<p class="text-[#9dabb9] text-lg mb-6">
|
<p class="text-[#9dabb9] text-lg mb-6">
|
||||||
API para gestión y monitoreo de aplicaciones Node.js y Python con systemd.
|
API para gestión y monitoreo de aplicaciones Node.js y
|
||||||
|
Python con systemd.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="rounded-xl border border-primary/30 bg-primary/10 p-4 mb-6">
|
<div
|
||||||
|
class="rounded-xl border border-primary/30 bg-primary/10 p-4 mb-6"
|
||||||
|
>
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<span class="material-symbols-outlined text-primary mt-0.5">info</span>
|
<span
|
||||||
|
class="material-symbols-outlined text-primary mt-0.5"
|
||||||
|
>info</span
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-white font-semibold mb-1">Endpoint Base</p>
|
<p class="text-white font-semibold mb-1">
|
||||||
<code class="text-primary font-mono text-sm">http://localhost:8080/api</code>
|
Endpoint Base
|
||||||
|
</p>
|
||||||
|
<code class="text-primary font-mono text-sm"
|
||||||
|
>/api</code
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<div class="rounded-xl border border-[#283039] bg-[#161f2a] p-4">
|
<div
|
||||||
<span class="material-symbols-outlined text-green-400 mb-2">check_circle</span>
|
class="rounded-xl border border-[#283039] bg-[#161f2a] p-4"
|
||||||
<p class="text-white font-semibold text-sm">REST API</p>
|
>
|
||||||
|
<span
|
||||||
|
class="material-symbols-outlined text-green-400 mb-2"
|
||||||
|
>check_circle</span
|
||||||
|
>
|
||||||
|
<p class="text-white font-semibold text-sm">
|
||||||
|
REST API
|
||||||
|
</p>
|
||||||
<p class="text-[#9dabb9] text-xs">JSON responses</p>
|
<p class="text-[#9dabb9] text-xs">JSON responses</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-xl border border-[#283039] bg-[#161f2a] p-4">
|
<div
|
||||||
<span class="material-symbols-outlined text-blue-400 mb-2">bolt</span>
|
class="rounded-xl border border-[#283039] bg-[#161f2a] p-4"
|
||||||
<p class="text-white font-semibold text-sm">WebSocket</p>
|
>
|
||||||
<p class="text-[#9dabb9] text-xs">Logs en tiempo real</p>
|
<span
|
||||||
|
class="material-symbols-outlined text-blue-400 mb-2"
|
||||||
|
>bolt</span
|
||||||
|
>
|
||||||
|
<p class="text-white font-semibold text-sm">
|
||||||
|
WebSocket
|
||||||
|
</p>
|
||||||
|
<p class="text-[#9dabb9] text-xs">
|
||||||
|
Logs en tiempo real
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-xl border border-[#283039] bg-[#161f2a] p-4">
|
<div
|
||||||
<span class="material-symbols-outlined text-purple-400 mb-2">schedule</span>
|
class="rounded-xl border border-[#283039] bg-[#161f2a] p-4"
|
||||||
<p class="text-white font-semibold text-sm">Rate Limiting</p>
|
>
|
||||||
|
<span
|
||||||
|
class="material-symbols-outlined text-purple-400 mb-2"
|
||||||
|
>schedule</span
|
||||||
|
>
|
||||||
|
<p class="text-white font-semibold text-sm">
|
||||||
|
Rate Limiting
|
||||||
|
</p>
|
||||||
<p class="text-[#9dabb9] text-xs">1 op/segundo</p>
|
<p class="text-[#9dabb9] text-xs">1 op/segundo</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -185,19 +253,34 @@
|
|||||||
|
|
||||||
<!-- Authentication -->
|
<!-- Authentication -->
|
||||||
<section id="auth" class="mb-12">
|
<section id="auth" class="mb-12">
|
||||||
<h2 class="text-white text-2xl font-bold mb-4 flex items-center gap-2">
|
<h2
|
||||||
<span class="material-symbols-outlined text-primary">lock</span>
|
class="text-white text-2xl font-bold mb-4 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined text-primary"
|
||||||
|
>lock</span
|
||||||
|
>
|
||||||
Autenticación
|
Autenticación
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-[#9dabb9] mb-4">
|
<p class="text-[#9dabb9] mb-4">
|
||||||
Actualmente la API no requiere autenticación ya que está diseñada para acceso local vía VPN.
|
Actualmente la API no requiere autenticación ya que está
|
||||||
|
diseñada para acceso local vía VPN.
|
||||||
</p>
|
</p>
|
||||||
<div class="rounded-xl border border-yellow-500/30 bg-yellow-500/10 p-4">
|
<div
|
||||||
|
class="rounded-xl border border-yellow-500/30 bg-yellow-500/10 p-4"
|
||||||
|
>
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<span class="material-symbols-outlined text-yellow-400">warning</span>
|
<span
|
||||||
|
class="material-symbols-outlined text-yellow-400"
|
||||||
|
>warning</span
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-yellow-400 font-semibold">Nota de Seguridad</p>
|
<p class="text-yellow-400 font-semibold">
|
||||||
<p class="text-[#9dabb9] text-sm">Esta API debe ser accesible solo desde redes privadas o VPN.</p>
|
Nota de Seguridad
|
||||||
|
</p>
|
||||||
|
<p class="text-[#9dabb9] text-sm">
|
||||||
|
Esta API debe ser accesible solo desde redes
|
||||||
|
privadas o VPN.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -205,52 +288,98 @@
|
|||||||
|
|
||||||
<!-- Apps Management -->
|
<!-- Apps Management -->
|
||||||
<section id="apps" class="mb-12">
|
<section id="apps" class="mb-12">
|
||||||
<h2 class="text-white text-2xl font-bold mb-6 flex items-center gap-2">
|
<h2
|
||||||
<span class="material-symbols-outlined text-primary">apps</span>
|
class="text-white text-2xl font-bold mb-6 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined text-primary"
|
||||||
|
>apps</span
|
||||||
|
>
|
||||||
Gestión de Aplicaciones
|
Gestión de Aplicaciones
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<!-- List Apps -->
|
<!-- List Apps -->
|
||||||
<div class="mb-8 rounded-xl border border-[#283039] bg-[#161f2a] overflow-hidden">
|
<div
|
||||||
<div class="bg-[#1c2730] px-6 py-4 border-b border-[#283039]">
|
class="mb-8 rounded-xl border border-[#283039] bg-[#161f2a] overflow-hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-[#1c2730] px-6 py-4 border-b border-[#283039]"
|
||||||
|
>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="px-2 py-1 rounded bg-green-500/20 text-green-400 text-xs font-bold font-mono">GET</span>
|
<span
|
||||||
<code class="text-white font-mono text-sm">/api/apps</code>
|
class="px-2 py-1 rounded bg-green-500/20 text-green-400 text-xs font-bold font-mono"
|
||||||
|
>GET</span
|
||||||
|
>
|
||||||
|
<code class="text-white font-mono text-sm"
|
||||||
|
>/api/apps</code
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-[#9dabb9] text-sm mt-2">Listar todas las aplicaciones registradas</p>
|
<p class="text-[#9dabb9] text-sm mt-2">
|
||||||
|
Listar todas las aplicaciones registradas
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-6 space-y-4">
|
<div class="p-6 space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-white font-semibold text-sm mb-2">Respuesta exitosa (200)</p>
|
<p
|
||||||
<pre class="code-block bg-[#0a0f16] p-4 rounded-lg text-sm text-green-400 overflow-x-auto">{
|
class="text-white font-semibold text-sm mb-2"
|
||||||
|
>
|
||||||
|
Respuesta exitosa (200)
|
||||||
|
</p>
|
||||||
|
<pre
|
||||||
|
class="code-block bg-[#0a0f16] p-4 rounded-lg text-sm text-green-400 overflow-x-auto"
|
||||||
|
>
|
||||||
|
{
|
||||||
"success": true,
|
"success": true,
|
||||||
"data": {
|
"data": {
|
||||||
"apps": ["app_tareas", "fidelizacion"],
|
"apps": ["app_tareas", "fidelizacion"],
|
||||||
"total": 2
|
"total": 2
|
||||||
},
|
},
|
||||||
"error": null
|
"error": null
|
||||||
}</pre>
|
}</pre
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<button onclick="tryEndpoint('GET', '/api/apps')" class="flex items-center gap-2 px-4 py-2 bg-primary hover:brightness-110 rounded-lg text-white text-sm font-medium transition-all">
|
<button
|
||||||
<span class="material-symbols-outlined text-sm">play_arrow</span>
|
onclick="tryEndpoint('GET', '/api/apps')"
|
||||||
|
class="flex items-center gap-2 px-4 py-2 bg-primary hover:brightness-110 rounded-lg text-white text-sm font-medium transition-all"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined text-sm"
|
||||||
|
>play_arrow</span
|
||||||
|
>
|
||||||
Probar endpoint
|
Probar endpoint
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Register App -->
|
<!-- Register App -->
|
||||||
<div class="mb-8 rounded-xl border border-[#283039] bg-[#161f2a] overflow-hidden">
|
<div
|
||||||
<div class="bg-[#1c2730] px-6 py-4 border-b border-[#283039]">
|
class="mb-8 rounded-xl border border-[#283039] bg-[#161f2a] overflow-hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-[#1c2730] px-6 py-4 border-b border-[#283039]"
|
||||||
|
>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="px-2 py-1 rounded bg-blue-500/20 text-blue-400 text-xs font-bold font-mono">POST</span>
|
<span
|
||||||
<code class="text-white font-mono text-sm">/api/apps</code>
|
class="px-2 py-1 rounded bg-blue-500/20 text-blue-400 text-xs font-bold font-mono"
|
||||||
|
>POST</span
|
||||||
|
>
|
||||||
|
<code class="text-white font-mono text-sm"
|
||||||
|
>/api/apps</code
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-[#9dabb9] text-sm mt-2">Registrar una nueva aplicación</p>
|
<p class="text-[#9dabb9] text-sm mt-2">
|
||||||
|
Registrar una nueva aplicación
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-6 space-y-4">
|
<div class="p-6 space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-white font-semibold text-sm mb-2">Body (JSON)</p>
|
<p
|
||||||
<pre class="code-block bg-[#0a0f16] p-4 rounded-lg text-sm text-blue-400 overflow-x-auto">{
|
class="text-white font-semibold text-sm mb-2"
|
||||||
|
>
|
||||||
|
Body (JSON)
|
||||||
|
</p>
|
||||||
|
<pre
|
||||||
|
class="code-block bg-[#0a0f16] p-4 rounded-lg text-sm text-blue-400 overflow-x-auto"
|
||||||
|
>
|
||||||
|
{
|
||||||
"app_name": "mi-app",
|
"app_name": "mi-app",
|
||||||
"script_path": "/opt/apps/mi-app/index.js",
|
"script_path": "/opt/apps/mi-app/index.js",
|
||||||
"working_directory": "/opt/apps/mi-app",
|
"working_directory": "/opt/apps/mi-app",
|
||||||
@@ -262,11 +391,19 @@
|
|||||||
"restart_policy": "always",
|
"restart_policy": "always",
|
||||||
"app_type": "nodejs",
|
"app_type": "nodejs",
|
||||||
"description": "Mi aplicación Node.js"
|
"description": "Mi aplicación Node.js"
|
||||||
}</pre>
|
}</pre
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-white font-semibold text-sm mb-2">Respuesta exitosa (200)</p>
|
<p
|
||||||
<pre class="code-block bg-[#0a0f16] p-4 rounded-lg text-sm text-green-400 overflow-x-auto">{
|
class="text-white font-semibold text-sm mb-2"
|
||||||
|
>
|
||||||
|
Respuesta exitosa (200)
|
||||||
|
</p>
|
||||||
|
<pre
|
||||||
|
class="code-block bg-[#0a0f16] p-4 rounded-lg text-sm text-green-400 overflow-x-auto"
|
||||||
|
>
|
||||||
|
{
|
||||||
"success": true,
|
"success": true,
|
||||||
"data": {
|
"data": {
|
||||||
"app_name": "mi-app",
|
"app_name": "mi-app",
|
||||||
@@ -275,27 +412,48 @@
|
|||||||
"message": "Aplicación registrada exitosamente"
|
"message": "Aplicación registrada exitosamente"
|
||||||
},
|
},
|
||||||
"error": null
|
"error": null
|
||||||
}</pre>
|
}</pre
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Delete App -->
|
<!-- Delete App -->
|
||||||
<div class="mb-8 rounded-xl border border-[#283039] bg-[#161f2a] overflow-hidden">
|
<div
|
||||||
<div class="bg-[#1c2730] px-6 py-4 border-b border-[#283039]">
|
class="mb-8 rounded-xl border border-[#283039] bg-[#161f2a] overflow-hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-[#1c2730] px-6 py-4 border-b border-[#283039]"
|
||||||
|
>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="px-2 py-1 rounded bg-red-500/20 text-red-400 text-xs font-bold font-mono">DELETE</span>
|
<span
|
||||||
<code class="text-white font-mono text-sm">/api/apps/:name</code>
|
class="px-2 py-1 rounded bg-red-500/20 text-red-400 text-xs font-bold font-mono"
|
||||||
|
>DELETE</span
|
||||||
|
>
|
||||||
|
<code class="text-white font-mono text-sm"
|
||||||
|
>/api/apps/:name</code
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-[#9dabb9] text-sm mt-2">Eliminar una aplicación registrada</p>
|
<p class="text-[#9dabb9] text-sm mt-2">
|
||||||
|
Eliminar una aplicación registrada
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-6 space-y-4">
|
<div class="p-6 space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-white font-semibold text-sm mb-2">Parámetros</p>
|
<p
|
||||||
|
class="text-white font-semibold text-sm mb-2"
|
||||||
|
>
|
||||||
|
Parámetros
|
||||||
|
</p>
|
||||||
<ul class="space-y-2">
|
<ul class="space-y-2">
|
||||||
<li class="flex items-start gap-2">
|
<li class="flex items-start gap-2">
|
||||||
<code class="text-primary font-mono text-sm">name</code>
|
<code
|
||||||
<span class="text-[#9dabb9] text-sm">- Nombre de la aplicación</span>
|
class="text-primary font-mono text-sm"
|
||||||
|
>name</code
|
||||||
|
>
|
||||||
|
<span class="text-[#9dabb9] text-sm"
|
||||||
|
>- Nombre de la aplicación</span
|
||||||
|
>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -303,18 +461,36 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Get Status -->
|
<!-- Get Status -->
|
||||||
<div class="mb-8 rounded-xl border border-[#283039] bg-[#161f2a] overflow-hidden">
|
<div
|
||||||
<div class="bg-[#1c2730] px-6 py-4 border-b border-[#283039]">
|
class="mb-8 rounded-xl border border-[#283039] bg-[#161f2a] overflow-hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-[#1c2730] px-6 py-4 border-b border-[#283039]"
|
||||||
|
>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="px-2 py-1 rounded bg-green-500/20 text-green-400 text-xs font-bold font-mono">GET</span>
|
<span
|
||||||
<code class="text-white font-mono text-sm">/api/apps/:name/status</code>
|
class="px-2 py-1 rounded bg-green-500/20 text-green-400 text-xs font-bold font-mono"
|
||||||
|
>GET</span
|
||||||
|
>
|
||||||
|
<code class="text-white font-mono text-sm"
|
||||||
|
>/api/apps/:name/status</code
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-[#9dabb9] text-sm mt-2">Obtener estado de una aplicación</p>
|
<p class="text-[#9dabb9] text-sm mt-2">
|
||||||
|
Obtener estado de una aplicación
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-6 space-y-4">
|
<div class="p-6 space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-white font-semibold text-sm mb-2">Respuesta exitosa (200)</p>
|
<p
|
||||||
<pre class="code-block bg-[#0a0f16] p-4 rounded-lg text-sm text-green-400 overflow-x-auto">{
|
class="text-white font-semibold text-sm mb-2"
|
||||||
|
>
|
||||||
|
Respuesta exitosa (200)
|
||||||
|
</p>
|
||||||
|
<pre
|
||||||
|
class="code-block bg-[#0a0f16] p-4 rounded-lg text-sm text-green-400 overflow-x-auto"
|
||||||
|
>
|
||||||
|
{
|
||||||
"success": true,
|
"success": true,
|
||||||
"data": {
|
"data": {
|
||||||
"name": "mi-app",
|
"name": "mi-app",
|
||||||
@@ -325,7 +501,8 @@
|
|||||||
"systemd_status": "active",
|
"systemd_status": "active",
|
||||||
"last_updated": "2026-01-13T12:34:56"
|
"last_updated": "2026-01-13T12:34:56"
|
||||||
}
|
}
|
||||||
}</pre>
|
}</pre
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -333,23 +510,45 @@
|
|||||||
|
|
||||||
<!-- Scan -->
|
<!-- Scan -->
|
||||||
<section id="scan" class="mb-12">
|
<section id="scan" class="mb-12">
|
||||||
<h2 class="text-white text-2xl font-bold mb-6 flex items-center gap-2">
|
<h2
|
||||||
<span class="material-symbols-outlined text-primary">search</span>
|
class="text-white text-2xl font-bold mb-6 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined text-primary"
|
||||||
|
>search</span
|
||||||
|
>
|
||||||
Escaneo de Procesos
|
Escaneo de Procesos
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="mb-8 rounded-xl border border-[#283039] bg-[#161f2a] overflow-hidden">
|
<div
|
||||||
<div class="bg-[#1c2730] px-6 py-4 border-b border-[#283039]">
|
class="mb-8 rounded-xl border border-[#283039] bg-[#161f2a] overflow-hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-[#1c2730] px-6 py-4 border-b border-[#283039]"
|
||||||
|
>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="px-2 py-1 rounded bg-green-500/20 text-green-400 text-xs font-bold font-mono">GET</span>
|
<span
|
||||||
<code class="text-white font-mono text-sm">/api/scan</code>
|
class="px-2 py-1 rounded bg-green-500/20 text-green-400 text-xs font-bold font-mono"
|
||||||
|
>GET</span
|
||||||
|
>
|
||||||
|
<code class="text-white font-mono text-sm"
|
||||||
|
>/api/scan</code
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-[#9dabb9] text-sm mt-2">Escanear procesos Node.js y Python en ejecución</p>
|
<p class="text-[#9dabb9] text-sm mt-2">
|
||||||
|
Escanear procesos Node.js y Python en ejecución
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-6 space-y-4">
|
<div class="p-6 space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-white font-semibold text-sm mb-2">Respuesta exitosa (200)</p>
|
<p
|
||||||
<pre class="code-block bg-[#0a0f16] p-4 rounded-lg text-sm text-green-400 overflow-x-auto">{
|
class="text-white font-semibold text-sm mb-2"
|
||||||
|
>
|
||||||
|
Respuesta exitosa (200)
|
||||||
|
</p>
|
||||||
|
<pre
|
||||||
|
class="code-block bg-[#0a0f16] p-4 rounded-lg text-sm text-green-400 overflow-x-auto"
|
||||||
|
>
|
||||||
|
{
|
||||||
"success": true,
|
"success": true,
|
||||||
"data": {
|
"data": {
|
||||||
"processes": [
|
"processes": [
|
||||||
@@ -364,10 +563,16 @@
|
|||||||
],
|
],
|
||||||
"total": 1
|
"total": 1
|
||||||
}
|
}
|
||||||
}</pre>
|
}</pre
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<button onclick="tryEndpoint('GET', '/api/scan')" class="flex items-center gap-2 px-4 py-2 bg-primary hover:brightness-110 rounded-lg text-white text-sm font-medium transition-all">
|
<button
|
||||||
<span class="material-symbols-outlined text-sm">play_arrow</span>
|
onclick="tryEndpoint('GET', '/api/scan')"
|
||||||
|
class="flex items-center gap-2 px-4 py-2 bg-primary hover:brightness-110 rounded-lg text-white text-sm font-medium transition-all"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined text-sm"
|
||||||
|
>play_arrow</span
|
||||||
|
>
|
||||||
Probar endpoint
|
Probar endpoint
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -376,50 +581,97 @@
|
|||||||
|
|
||||||
<!-- Lifecycle -->
|
<!-- Lifecycle -->
|
||||||
<section id="lifecycle" class="mb-12">
|
<section id="lifecycle" class="mb-12">
|
||||||
<h2 class="text-white text-2xl font-bold mb-6 flex items-center gap-2">
|
<h2
|
||||||
<span class="material-symbols-outlined text-primary">settings_power</span>
|
class="text-white text-2xl font-bold mb-6 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined text-primary"
|
||||||
|
>settings_power</span
|
||||||
|
>
|
||||||
Ciclo de Vida
|
Ciclo de Vida
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<!-- Start -->
|
<!-- Start -->
|
||||||
<div class="mb-8 rounded-xl border border-[#283039] bg-[#161f2a] overflow-hidden">
|
<div
|
||||||
<div class="bg-[#1c2730] px-6 py-4 border-b border-[#283039]">
|
class="mb-8 rounded-xl border border-[#283039] bg-[#161f2a] overflow-hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-[#1c2730] px-6 py-4 border-b border-[#283039]"
|
||||||
|
>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="px-2 py-1 rounded bg-blue-500/20 text-blue-400 text-xs font-bold font-mono">POST</span>
|
<span
|
||||||
<code class="text-white font-mono text-sm">/api/apps/:name/start</code>
|
class="px-2 py-1 rounded bg-blue-500/20 text-blue-400 text-xs font-bold font-mono"
|
||||||
|
>POST</span
|
||||||
|
>
|
||||||
|
<code class="text-white font-mono text-sm"
|
||||||
|
>/api/apps/:name/start</code
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-[#9dabb9] text-sm mt-2">Iniciar una aplicación</p>
|
<p class="text-[#9dabb9] text-sm mt-2">
|
||||||
|
Iniciar una aplicación
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stop -->
|
<!-- Stop -->
|
||||||
<div class="mb-8 rounded-xl border border-[#283039] bg-[#161f2a] overflow-hidden">
|
<div
|
||||||
<div class="bg-[#1c2730] px-6 py-4 border-b border-[#283039]">
|
class="mb-8 rounded-xl border border-[#283039] bg-[#161f2a] overflow-hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-[#1c2730] px-6 py-4 border-b border-[#283039]"
|
||||||
|
>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="px-2 py-1 rounded bg-blue-500/20 text-blue-400 text-xs font-bold font-mono">POST</span>
|
<span
|
||||||
<code class="text-white font-mono text-sm">/api/apps/:name/stop</code>
|
class="px-2 py-1 rounded bg-blue-500/20 text-blue-400 text-xs font-bold font-mono"
|
||||||
|
>POST</span
|
||||||
|
>
|
||||||
|
<code class="text-white font-mono text-sm"
|
||||||
|
>/api/apps/:name/stop</code
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-[#9dabb9] text-sm mt-2">Detener una aplicación</p>
|
<p class="text-[#9dabb9] text-sm mt-2">
|
||||||
|
Detener una aplicación
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Restart -->
|
<!-- Restart -->
|
||||||
<div class="mb-8 rounded-xl border border-[#283039] bg-[#161f2a] overflow-hidden">
|
<div
|
||||||
<div class="bg-[#1c2730] px-6 py-4 border-b border-[#283039]">
|
class="mb-8 rounded-xl border border-[#283039] bg-[#161f2a] overflow-hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-[#1c2730] px-6 py-4 border-b border-[#283039]"
|
||||||
|
>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="px-2 py-1 rounded bg-blue-500/20 text-blue-400 text-xs font-bold font-mono">POST</span>
|
<span
|
||||||
<code class="text-white font-mono text-sm">/api/apps/:name/restart</code>
|
class="px-2 py-1 rounded bg-blue-500/20 text-blue-400 text-xs font-bold font-mono"
|
||||||
|
>POST</span
|
||||||
|
>
|
||||||
|
<code class="text-white font-mono text-sm"
|
||||||
|
>/api/apps/:name/restart</code
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-[#9dabb9] text-sm mt-2">Reiniciar una aplicación</p>
|
<p class="text-[#9dabb9] text-sm mt-2">
|
||||||
|
Reiniciar una aplicación
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-xl border border-yellow-500/30 bg-yellow-500/10 p-4">
|
<div
|
||||||
|
class="rounded-xl border border-yellow-500/30 bg-yellow-500/10 p-4"
|
||||||
|
>
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<span class="material-symbols-outlined text-yellow-400">schedule</span>
|
<span
|
||||||
|
class="material-symbols-outlined text-yellow-400"
|
||||||
|
>schedule</span
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-yellow-400 font-semibold">Rate Limiting</p>
|
<p class="text-yellow-400 font-semibold">
|
||||||
<p class="text-[#9dabb9] text-sm">Las operaciones están limitadas a 1 por segundo por aplicación.</p>
|
Rate Limiting
|
||||||
|
</p>
|
||||||
|
<p class="text-[#9dabb9] text-sm">
|
||||||
|
Las operaciones están limitadas a 1 por
|
||||||
|
segundo por aplicación.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -427,23 +679,45 @@
|
|||||||
|
|
||||||
<!-- WebSocket -->
|
<!-- WebSocket -->
|
||||||
<section id="websocket" class="mb-12">
|
<section id="websocket" class="mb-12">
|
||||||
<h2 class="text-white text-2xl font-bold mb-6 flex items-center gap-2">
|
<h2
|
||||||
<span class="material-symbols-outlined text-primary">cable</span>
|
class="text-white text-2xl font-bold mb-6 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined text-primary"
|
||||||
|
>cable</span
|
||||||
|
>
|
||||||
WebSocket (Logs en tiempo real)
|
WebSocket (Logs en tiempo real)
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="mb-8 rounded-xl border border-[#283039] bg-[#161f2a] overflow-hidden">
|
<div
|
||||||
<div class="bg-[#1c2730] px-6 py-4 border-b border-[#283039]">
|
class="mb-8 rounded-xl border border-[#283039] bg-[#161f2a] overflow-hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-[#1c2730] px-6 py-4 border-b border-[#283039]"
|
||||||
|
>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="px-2 py-1 rounded bg-purple-500/20 text-purple-400 text-xs font-bold font-mono">WS</span>
|
<span
|
||||||
<code class="text-white font-mono text-sm">ws://localhost:8080/api/apps/:name/logs</code>
|
class="px-2 py-1 rounded bg-purple-500/20 text-purple-400 text-xs font-bold font-mono"
|
||||||
|
>WS</span
|
||||||
|
>
|
||||||
|
<code class="text-white font-mono text-sm"
|
||||||
|
>ws://localhost:8080/api/apps/:name/logs</code
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-[#9dabb9] text-sm mt-2">Stream de logs en tiempo real desde journalctl</p>
|
<p class="text-[#9dabb9] text-sm mt-2">
|
||||||
|
Stream de logs en tiempo real desde journalctl
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-6 space-y-4">
|
<div class="p-6 space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-white font-semibold text-sm mb-2">Ejemplo JavaScript</p>
|
<p
|
||||||
<pre class="code-block bg-[#0a0f16] p-4 rounded-lg text-sm text-purple-400 overflow-x-auto">const ws = new WebSocket('ws://localhost:8080/api/apps/mi-app/logs');
|
class="text-white font-semibold text-sm mb-2"
|
||||||
|
>
|
||||||
|
Ejemplo JavaScript
|
||||||
|
</p>
|
||||||
|
<pre
|
||||||
|
class="code-block bg-[#0a0f16] p-4 rounded-lg text-sm text-purple-400 overflow-x-auto"
|
||||||
|
>
|
||||||
|
const ws = new WebSocket('ws://localhost:8080/api/apps/mi-app/logs');
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
console.log('Conectado a logs');
|
console.log('Conectado a logs');
|
||||||
@@ -460,18 +734,35 @@ ws.onerror = (error) => {
|
|||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
console.log('Desconectado');
|
console.log('Desconectado');
|
||||||
};</pre>
|
};</pre
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-white font-semibold text-sm mb-2">Límites</p>
|
<p
|
||||||
|
class="text-white font-semibold text-sm mb-2"
|
||||||
|
>
|
||||||
|
Límites
|
||||||
|
</p>
|
||||||
<ul class="space-y-2">
|
<ul class="space-y-2">
|
||||||
<li class="flex items-start gap-2">
|
<li class="flex items-start gap-2">
|
||||||
<span class="material-symbols-outlined text-primary text-sm">check</span>
|
<span
|
||||||
<span class="text-[#9dabb9] text-sm">Máximo 5 conexiones concurrentes por aplicación</span>
|
class="material-symbols-outlined text-primary text-sm"
|
||||||
|
>check</span
|
||||||
|
>
|
||||||
|
<span class="text-[#9dabb9] text-sm"
|
||||||
|
>Máximo 5 conexiones concurrentes
|
||||||
|
por aplicación</span
|
||||||
|
>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex items-start gap-2">
|
<li class="flex items-start gap-2">
|
||||||
<span class="material-symbols-outlined text-primary text-sm">check</span>
|
<span
|
||||||
<span class="text-[#9dabb9] text-sm">Formato JSON desde systemd journalctl</span>
|
class="material-symbols-outlined text-primary text-sm"
|
||||||
|
>check</span
|
||||||
|
>
|
||||||
|
<span class="text-[#9dabb9] text-sm"
|
||||||
|
>Formato JSON desde systemd
|
||||||
|
journalctl</span
|
||||||
|
>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -481,52 +772,98 @@ ws.onclose = () => {
|
|||||||
|
|
||||||
<!-- Error Codes -->
|
<!-- Error Codes -->
|
||||||
<section id="errors" class="mb-12">
|
<section id="errors" class="mb-12">
|
||||||
<h2 class="text-white text-2xl font-bold mb-6 flex items-center gap-2">
|
<h2
|
||||||
<span class="material-symbols-outlined text-primary">error</span>
|
class="text-white text-2xl font-bold mb-6 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined text-primary"
|
||||||
|
>error</span
|
||||||
|
>
|
||||||
Códigos de Error
|
Códigos de Error
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="rounded-xl border border-[#283039] bg-[#161f2a] p-4">
|
<div
|
||||||
|
class="rounded-xl border border-[#283039] bg-[#161f2a] p-4"
|
||||||
|
>
|
||||||
<div class="flex items-center gap-3 mb-2">
|
<div class="flex items-center gap-3 mb-2">
|
||||||
<span class="px-2 py-1 rounded bg-red-500/20 text-red-400 text-xs font-bold font-mono">400</span>
|
<span
|
||||||
<p class="text-white font-semibold">Bad Request</p>
|
class="px-2 py-1 rounded bg-red-500/20 text-red-400 text-xs font-bold font-mono"
|
||||||
|
>400</span
|
||||||
|
>
|
||||||
|
<p class="text-white font-semibold">
|
||||||
|
Bad Request
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-[#9dabb9] text-sm">Datos de entrada inválidos o faltantes</p>
|
<p class="text-[#9dabb9] text-sm">
|
||||||
|
Datos de entrada inválidos o faltantes
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-xl border border-[#283039] bg-[#161f2a] p-4">
|
<div
|
||||||
|
class="rounded-xl border border-[#283039] bg-[#161f2a] p-4"
|
||||||
|
>
|
||||||
<div class="flex items-center gap-3 mb-2">
|
<div class="flex items-center gap-3 mb-2">
|
||||||
<span class="px-2 py-1 rounded bg-red-500/20 text-red-400 text-xs font-bold font-mono">404</span>
|
<span
|
||||||
<p class="text-white font-semibold">Not Found</p>
|
class="px-2 py-1 rounded bg-red-500/20 text-red-400 text-xs font-bold font-mono"
|
||||||
|
>404</span
|
||||||
|
>
|
||||||
|
<p class="text-white font-semibold">
|
||||||
|
Not Found
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-[#9dabb9] text-sm">Aplicación no encontrada</p>
|
<p class="text-[#9dabb9] text-sm">
|
||||||
|
Aplicación no encontrada
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-xl border border-[#283039] bg-[#161f2a] p-4">
|
<div
|
||||||
|
class="rounded-xl border border-[#283039] bg-[#161f2a] p-4"
|
||||||
|
>
|
||||||
<div class="flex items-center gap-3 mb-2">
|
<div class="flex items-center gap-3 mb-2">
|
||||||
<span class="px-2 py-1 rounded bg-red-500/20 text-red-400 text-xs font-bold font-mono">429</span>
|
<span
|
||||||
<p class="text-white font-semibold">Too Many Requests</p>
|
class="px-2 py-1 rounded bg-red-500/20 text-red-400 text-xs font-bold font-mono"
|
||||||
|
>429</span
|
||||||
|
>
|
||||||
|
<p class="text-white font-semibold">
|
||||||
|
Too Many Requests
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-[#9dabb9] text-sm">Rate limit excedido (1 operación/segundo)</p>
|
<p class="text-[#9dabb9] text-sm">
|
||||||
|
Rate limit excedido (1 operación/segundo)
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-xl border border-[#283039] bg-[#161f2a] p-4">
|
<div
|
||||||
|
class="rounded-xl border border-[#283039] bg-[#161f2a] p-4"
|
||||||
|
>
|
||||||
<div class="flex items-center gap-3 mb-2">
|
<div class="flex items-center gap-3 mb-2">
|
||||||
<span class="px-2 py-1 rounded bg-red-500/20 text-red-400 text-xs font-bold font-mono">500</span>
|
<span
|
||||||
<p class="text-white font-semibold">Internal Server Error</p>
|
class="px-2 py-1 rounded bg-red-500/20 text-red-400 text-xs font-bold font-mono"
|
||||||
|
>500</span
|
||||||
|
>
|
||||||
|
<p class="text-white font-semibold">
|
||||||
|
Internal Server Error
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-[#9dabb9] text-sm">Error interno del servidor</p>
|
<p class="text-[#9dabb9] text-sm">
|
||||||
|
Error interno del servidor
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
<p class="text-white font-semibold text-sm mb-2">Estructura de error</p>
|
<p class="text-white font-semibold text-sm mb-2">
|
||||||
<pre class="code-block bg-[#0a0f16] p-4 rounded-lg text-sm text-red-400 overflow-x-auto">{
|
Estructura de error
|
||||||
|
</p>
|
||||||
|
<pre
|
||||||
|
class="code-block bg-[#0a0f16] p-4 rounded-lg text-sm text-red-400 overflow-x-auto"
|
||||||
|
>
|
||||||
|
{
|
||||||
"success": false,
|
"success": false,
|
||||||
"data": null,
|
"data": null,
|
||||||
"error": "Descripción del error"
|
"error": "Descripción del error"
|
||||||
}</pre>
|
}</pre
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
@@ -534,14 +871,18 @@ ws.onclose = () => {
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
async function tryEndpoint(method, path) {
|
async function tryEndpoint(method, path) {
|
||||||
const resultDiv = event.target.parentElement.querySelector('.result') ||
|
const resultDiv =
|
||||||
event.target.parentElement.appendChild(document.createElement('div'));
|
event.target.parentElement.querySelector(".result") ||
|
||||||
resultDiv.className = 'result mt-4 code-block bg-[#0a0f16] p-4 rounded-lg text-sm overflow-x-auto';
|
event.target.parentElement.appendChild(
|
||||||
resultDiv.textContent = 'Ejecutando...';
|
document.createElement("div"),
|
||||||
|
);
|
||||||
|
resultDiv.className =
|
||||||
|
"result mt-4 code-block bg-[#0a0f16] p-4 rounded-lg text-sm overflow-x-auto";
|
||||||
|
resultDiv.textContent = "Ejecutando...";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`http://localhost:8080${path}`, {
|
const response = await fetch(path, {
|
||||||
method: method
|
method: method,
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
resultDiv.innerHTML = `<pre class="text-green-400">${JSON.stringify(data, null, 2)}</pre>`;
|
resultDiv.innerHTML = `<pre class="text-green-400">${JSON.stringify(data, null, 2)}</pre>`;
|
||||||
@@ -551,12 +892,17 @@ ws.onclose = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Smooth scroll for anchor links
|
// Smooth scroll for anchor links
|
||||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
|
||||||
anchor.addEventListener('click', function (e) {
|
anchor.addEventListener("click", function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const target = document.querySelector(this.getAttribute('href'));
|
const target = document.querySelector(
|
||||||
|
this.getAttribute("href"),
|
||||||
|
);
|
||||||
if (target) {
|
if (target) {
|
||||||
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
target.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "start",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
764
web/blog.html
Normal file
764
web/blog.html
Normal file
@@ -0,0 +1,764 @@
|
|||||||
|
<!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>
|
||||||
|
SIAX Monitor: Sistema de Monitoreo de Aplicaciones en Rust - Blog
|
||||||
|
Telcotronics
|
||||||
|
</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;800;900&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&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"],
|
||||||
|
mono: ["JetBrains Mono", "monospace"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: "Inter", sans-serif;
|
||||||
|
}
|
||||||
|
.material-symbols-outlined {
|
||||||
|
font-variation-settings:
|
||||||
|
"FILL" 0,
|
||||||
|
"wght" 400,
|
||||||
|
"GRAD" 0,
|
||||||
|
"opsz" 24;
|
||||||
|
}
|
||||||
|
.blog-content h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 800;
|
||||||
|
margin-top: 3rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.blog-content h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
.blog-content p {
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
line-height: 1.75;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
.blog-content ul,
|
||||||
|
.blog-content ol {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
.blog-content li {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
line-height: 1.75;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
.blog-content ul li {
|
||||||
|
list-style-type: disc;
|
||||||
|
}
|
||||||
|
.blog-content ol li {
|
||||||
|
list-style-type: decimal;
|
||||||
|
}
|
||||||
|
.blog-content strong {
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.blog-content code {
|
||||||
|
background: #1e293b;
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #60a5fa;
|
||||||
|
}
|
||||||
|
.blog-content pre {
|
||||||
|
background: #0f172a;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
}
|
||||||
|
.blog-content pre code {
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
.blog-content blockquote {
|
||||||
|
border-left: 4px solid #137fec;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
font-style: italic;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-background-dark text-slate-300">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="border-b border-slate-800 bg-[#0a0f16]">
|
||||||
|
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
class="text-2xl font-black text-white hover:text-primary transition-colors"
|
||||||
|
>
|
||||||
|
SIAX Monitor
|
||||||
|
</a>
|
||||||
|
<nav class="flex items-center gap-6">
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
class="text-slate-400 hover:text-white transition-colors"
|
||||||
|
>Dashboard</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="/api-docs"
|
||||||
|
class="text-slate-400 hover:text-white transition-colors"
|
||||||
|
>API</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="https://git.telcotronics.net/pablinux/SIAX-MONITOR"
|
||||||
|
target="_blank"
|
||||||
|
class="text-slate-400 hover:text-primary transition-colors flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined text-sm"
|
||||||
|
>code</span
|
||||||
|
>
|
||||||
|
Git
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Article Container -->
|
||||||
|
<article class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<!-- Article Header -->
|
||||||
|
<header class="mb-12">
|
||||||
|
<!-- Categories -->
|
||||||
|
<div class="flex flex-wrap gap-2 mb-6">
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center gap-1 px-3 py-1 rounded-full bg-primary/20 text-primary text-sm font-semibold"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined text-xs"
|
||||||
|
>folder</span
|
||||||
|
>
|
||||||
|
DevOps
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center gap-1 px-3 py-1 rounded-full bg-green-500/20 text-green-400 text-sm font-semibold"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined text-xs"
|
||||||
|
>code</span
|
||||||
|
>
|
||||||
|
Rust
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center gap-1 px-3 py-1 rounded-full bg-blue-500/20 text-blue-400 text-sm font-semibold"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined text-xs"
|
||||||
|
>monitoring</span
|
||||||
|
>
|
||||||
|
Monitoring
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<h1
|
||||||
|
class="text-4xl md:text-5xl font-black text-white mb-6 leading-tight"
|
||||||
|
>
|
||||||
|
SIAX Monitor: Sistema de Monitoreo y Gestión de Aplicaciones
|
||||||
|
Node.js y Python
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<!-- Meta info -->
|
||||||
|
<div
|
||||||
|
class="flex flex-wrap items-center gap-4 text-slate-400 text-sm"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
class="w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="material-symbols-outlined text-primary text-lg"
|
||||||
|
>person</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
>Por
|
||||||
|
<strong class="text-white">pablinux</strong></span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<span class="material-symbols-outlined text-sm"
|
||||||
|
>calendar_today</span
|
||||||
|
>
|
||||||
|
<time datetime="2026-01-13">13 de enero, 2026</time>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<span class="material-symbols-outlined text-sm"
|
||||||
|
>schedule</span
|
||||||
|
>
|
||||||
|
<span>10 min de lectura</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Featured Image -->
|
||||||
|
<div
|
||||||
|
class="mb-12 rounded-2xl overflow-hidden border border-slate-800 bg-gradient-to-br from-primary/20 via-background-dark to-background-dark p-12"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<div class="text-center">
|
||||||
|
<div
|
||||||
|
class="inline-flex items-center justify-center w-24 h-24 rounded-2xl bg-primary/20 border-2 border-primary/30 mb-4"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="material-symbols-outlined text-primary"
|
||||||
|
style="font-size: 3rem"
|
||||||
|
>monitoring</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<p class="text-slate-400 text-lg">
|
||||||
|
Monitoreo inteligente con Rust + Systemd
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Article Content -->
|
||||||
|
<div class="blog-content">
|
||||||
|
<p class="text-xl text-slate-300 mb-8 leading-relaxed">
|
||||||
|
En el mundo del desarrollo moderno, gestionar múltiples
|
||||||
|
aplicaciones Node.js y Python en servidores de producción
|
||||||
|
puede convertirse rápidamente en un dolor de cabeza. SIAX
|
||||||
|
Monitor nace como una solución elegante, ligera y poderosa
|
||||||
|
para este problema, aprovechando la velocidad y seguridad de
|
||||||
|
Rust.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>¿Qué es SIAX Monitor?</h2>
|
||||||
|
<p>
|
||||||
|
SIAX Monitor es un
|
||||||
|
<strong>agente de monitoreo inteligente</strong> diseñado
|
||||||
|
específicamente para entornos Linux con systemd. A
|
||||||
|
diferencia de soluciones enterprise como Prometheus o
|
||||||
|
Grafana que pueden resultar excesivas para equipos pequeños,
|
||||||
|
SIAX Monitor ofrece exactamente lo que necesitas sin
|
||||||
|
complicaciones innecesarias.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Desarrollado completamente en Rust, combina alto rendimiento
|
||||||
|
con un consumo mínimo de recursos. El proyecto utiliza
|
||||||
|
tecnologías modernas como Tokio para async runtime, Axum
|
||||||
|
para el servidor web, y se integra nativamente con systemd y
|
||||||
|
journalctl.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Características Principales</h2>
|
||||||
|
|
||||||
|
<h3>🔍 Escaneo Automático de Procesos</h3>
|
||||||
|
<p>
|
||||||
|
El sistema detecta automáticamente procesos Node.js y Python
|
||||||
|
en ejecución, recopilando información detallada como:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>PID y nombre del proceso</li>
|
||||||
|
<li>Usuario propietario</li>
|
||||||
|
<li>Uso de CPU en tiempo real</li>
|
||||||
|
<li>Consumo de memoria RAM</li>
|
||||||
|
<li>Comando completo de ejecución</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>⚙️ Gestión de Ciclo de Vida</h3>
|
||||||
|
<p>
|
||||||
|
Control total sobre tus aplicaciones mediante la API REST:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<code>POST /api/apps</code> - Registrar nueva aplicación
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>POST /api/apps/:name/start</code> - Iniciar
|
||||||
|
servicio
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>POST /api/apps/:name/stop</code> - Detener
|
||||||
|
servicio
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>POST /api/apps/:name/restart</code> - Reiniciar
|
||||||
|
servicio
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>GET /api/apps/:name/status</code> - Consultar
|
||||||
|
estado
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
El sistema incluye <strong>rate limiting</strong> (1
|
||||||
|
operación/segundo por app) para evitar abusos y validaciones
|
||||||
|
de seguridad en todos los endpoints.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>📝 Logs en Tiempo Real</h3>
|
||||||
|
<p>
|
||||||
|
Uno de los puntos más fuertes es el streaming de logs vía
|
||||||
|
WebSocket. Conectándote al endpoint
|
||||||
|
<code>ws://localhost:8080/api/apps/:name/logs</code>,
|
||||||
|
recibes logs en tiempo real desde journalctl sin necesidad
|
||||||
|
de SSH al servidor.
|
||||||
|
</p>
|
||||||
|
<p>La interfaz web incluye un visor tipo terminal con:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Auto-scroll inteligente</li>
|
||||||
|
<li>Colores para niveles de log (ERROR, WARN, INFO)</li>
|
||||||
|
<li>Timestamps formateados</li>
|
||||||
|
<li>Botón para pausar/reanudar</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>🛡️ Seguridad y Validaciones</h3>
|
||||||
|
<p>SIAX Monitor toma la seguridad en serio:</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Validación estricta de paths de trabajo (previene
|
||||||
|
directory traversal)
|
||||||
|
</li>
|
||||||
|
<li>Lista blanca de usuarios permitidos</li>
|
||||||
|
<li>
|
||||||
|
Configuración automatizada de sudoers para systemctl
|
||||||
|
</li>
|
||||||
|
<li>Hardening de servicios systemd generados</li>
|
||||||
|
<li>Rate limiting en operaciones críticas</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>🎨 Dashboard Moderno</h3>
|
||||||
|
<p>
|
||||||
|
La interfaz web está construida con
|
||||||
|
<strong>Tailwind CSS</strong> en tema oscuro (#101922 de
|
||||||
|
fondo, #137fec como color primario). Incluye:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>/</strong> - Dashboard con estadísticas y lista
|
||||||
|
de apps
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>/scan</strong> - Escaneo de procesos activos
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>/select</strong> - Selección de procesos para
|
||||||
|
registrar
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>/register</strong> - Formulario de registro
|
||||||
|
manual
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>/logs</strong> - Visor de logs en tiempo real
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>/api-docs</strong> - Documentación completa de
|
||||||
|
la API
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>¿Cómo Funciona?</h2>
|
||||||
|
|
||||||
|
<h3>Arquitectura Multi-Threaded</h3>
|
||||||
|
<p>
|
||||||
|
SIAX Monitor utiliza una arquitectura basada en tres
|
||||||
|
componentes principales:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p><strong>1. Monitor en Background</strong></p>
|
||||||
|
<p>Un thread dedicado ejecuta cada 60 segundos para:</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Recopilar métricas de CPU y RAM usando
|
||||||
|
<code>sysinfo</code>
|
||||||
|
</li>
|
||||||
|
<li>Reconciliar estados entre sysinfo y systemd</li>
|
||||||
|
<li>Reportar al cloud API de SIAX (opcional)</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p><strong>2. Servidor Web Unificado</strong></p>
|
||||||
|
<p>Un servidor HTTP en puerto 8080 que fusiona:</p>
|
||||||
|
<ul>
|
||||||
|
<li>API REST (JSON responses)</li>
|
||||||
|
<li>WebSocket para logs</li>
|
||||||
|
<li>Interfaz web HTML estática</li>
|
||||||
|
<li>Archivos estáticos (favicon, logos)</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
Esto elimina problemas de CORS al servir todo desde el mismo
|
||||||
|
origen.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p><strong>3. Integración Systemd</strong></p>
|
||||||
|
<p>
|
||||||
|
El módulo <code>systemd_manager</code> genera archivos
|
||||||
|
<code>.service</code> automáticamente con:
|
||||||
|
</p>
|
||||||
|
<pre><code>[Unit]
|
||||||
|
Description=App gestionada por SIAX Monitor
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=app-user
|
||||||
|
WorkingDirectory=/opt/app
|
||||||
|
ExecStart=/usr/bin/node server.js
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target</code></pre>
|
||||||
|
|
||||||
|
<h2>Stack Tecnológico</h2>
|
||||||
|
<p>
|
||||||
|
El proyecto está construido sobre tecnologías modernas y
|
||||||
|
probadas:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>Rust</strong> - Lenguaje core (seguridad de
|
||||||
|
memoria, velocidad)
|
||||||
|
</li>
|
||||||
|
<li><strong>Tokio</strong> - Runtime asíncrono</li>
|
||||||
|
<li><strong>Axum 0.7</strong> - Framework web moderno</li>
|
||||||
|
<li><strong>Serde</strong> - Serialización JSON</li>
|
||||||
|
<li><strong>Sysinfo</strong> - Información del sistema</li>
|
||||||
|
<li>
|
||||||
|
<strong>Tower-HTTP</strong> - Middleware (CORS, static
|
||||||
|
files)
|
||||||
|
</li>
|
||||||
|
<li><strong>DashMap</strong> - HashMap thread-safe</li>
|
||||||
|
<li>
|
||||||
|
<strong>Tailwind CSS</strong> - Estilos del frontend
|
||||||
|
</li>
|
||||||
|
<li><strong>Material Symbols</strong> - Iconos</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Ventajas y Consideraciones</h2>
|
||||||
|
|
||||||
|
<h3>✅ Ventajas</h3>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>Alto Rendimiento</strong>: Rust ofrece velocidad
|
||||||
|
cercana a C con seguridad de memoria garantizada
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Ligero</strong>: Binario compilado de ~15MB,
|
||||||
|
consumo mínimo de RAM
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Sin Dependencias</strong>: No requiere Node.js,
|
||||||
|
Python o base de datos
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Integración Nativa</strong>: Aprovecha systemd y
|
||||||
|
journalctl del sistema
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Fácil Despliegue</strong>: Single binary +
|
||||||
|
script de instalación
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Open Source</strong>: Código auditable y
|
||||||
|
personalizable
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>⚠️ Consideraciones</h3>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>Solo Linux + Systemd</strong>: Requiere
|
||||||
|
distribuciones con systemd (no macOS/Windows)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Permisos Sudo</strong>: Necesita configurar
|
||||||
|
sudoers para systemctl
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Sin Métricas Históricas</strong>: No almacena
|
||||||
|
histórico, solo tiempo real
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Solo Node.js y Python</strong>: Otros lenguajes
|
||||||
|
requieren extensión del código
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Sin Autenticación</strong>: Diseñado para acceso
|
||||||
|
local/VPN, no exponer públicamente
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Casos de Uso</h2>
|
||||||
|
|
||||||
|
<h3>👔 Equipos DevOps</h3>
|
||||||
|
<p>
|
||||||
|
Gestión centralizada de microservicios en múltiples
|
||||||
|
servidores. El monitor actúa como worker node que reporta al
|
||||||
|
cloud API central, permitiendo visibilidad de toda la
|
||||||
|
infraestructura desde un solo panel.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>💻 Desarrolladores</h3>
|
||||||
|
<p>
|
||||||
|
Monitoreo de aplicaciones en entornos de desarrollo y
|
||||||
|
staging sin la complejidad de herramientas enterprise.
|
||||||
|
Perfecto para proyectos pequeños a medianos que necesitan
|
||||||
|
control básico de servicios.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>🖥️ Administradores de Sistemas</h3>
|
||||||
|
<p>
|
||||||
|
Control de servicios systemd con una interfaz web moderna.
|
||||||
|
Alternativa visual a comandos
|
||||||
|
<code>systemctl</code> repetitivos, con la ventaja de logs
|
||||||
|
centralizados y accesibles desde el navegador.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Instalación Rápida</h2>
|
||||||
|
<p>El proceso de instalación es extremadamente simple:</p>
|
||||||
|
<pre><code># Clonar el repositorio
|
||||||
|
git clone https://git.telcotronics.net/pablinux/SIAX-MONITOR.git
|
||||||
|
cd SIAX-MONITOR
|
||||||
|
|
||||||
|
# Compilar en modo release
|
||||||
|
cargo build --release
|
||||||
|
|
||||||
|
# Ejecutar instalador (crea usuario, configura sudoers, instala servicio)
|
||||||
|
sudo ./instalador.sh
|
||||||
|
|
||||||
|
# El servicio estará disponible en http://localhost:8080
|
||||||
|
</code></pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
El script <code>instalador.sh</code> realiza
|
||||||
|
automáticamente:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Crear usuario del sistema <code>siax-agent</code></li>
|
||||||
|
<li>Configurar permisos sudoers para systemctl</li>
|
||||||
|
<li>Copiar binario a <code>/opt/siax-agent/</code></li>
|
||||||
|
<li>Instalar y habilitar servicio systemd</li>
|
||||||
|
<li>Verificar salud del servicio</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Arquitectura de Despliegue</h2>
|
||||||
|
<p>
|
||||||
|
SIAX Monitor fue diseñado pensando en una arquitectura
|
||||||
|
distribuida:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>Cloud API</strong>:
|
||||||
|
<code>https://api.siax-system.net</code> - Panel central
|
||||||
|
de control
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Worker Nodes</strong>: Agentes SIAX Monitor en
|
||||||
|
cada servidor
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Comunicación</strong>: VPN segura entre workers
|
||||||
|
y cloud API
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
Cada worker reporta cada 60 segundos su estado, permitiendo
|
||||||
|
monitoreo centralizado de toda la infraestructura sin
|
||||||
|
exponer puertos públicamente.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Conclusión</h2>
|
||||||
|
<p>
|
||||||
|
SIAX Monitor demuestra que no siempre necesitas soluciones
|
||||||
|
enterprise complejas para problemas simples. Con menos de
|
||||||
|
2,000 líneas de código Rust bien estructurado, ofrece
|
||||||
|
exactamente lo necesario para gestionar aplicaciones Node.js
|
||||||
|
y Python en producción.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
La combinación de Rust + Systemd + WebSocket resulta en una
|
||||||
|
herramienta rápida, confiable y fácil de mantener. Es
|
||||||
|
perfecta para equipos pequeños o medianos que buscan
|
||||||
|
simplicidad sin sacrificar funcionalidad.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Si administras servidores Linux con aplicaciones Node.js o
|
||||||
|
Python, definitivamente vale la pena darle una oportunidad.
|
||||||
|
El código está disponible en
|
||||||
|
<a
|
||||||
|
href="https://git.telcotronics.net/pablinux/SIAX-MONITOR"
|
||||||
|
target="_blank"
|
||||||
|
class="text-primary hover:underline"
|
||||||
|
>Git Telcotronics</a
|
||||||
|
>
|
||||||
|
bajo licencia open source.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<blockquote>
|
||||||
|
"A veces la mejor solución no es la más compleja, sino la
|
||||||
|
que resuelve tu problema específico de la manera más
|
||||||
|
elegante posible." - Filosofía detrás de SIAX Monitor
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Article Footer -->
|
||||||
|
<footer class="mt-16 pt-8 border-t border-slate-800">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
class="w-12 h-12 rounded-full bg-primary/20 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined text-primary"
|
||||||
|
>person</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-white font-semibold">pablinux</p>
|
||||||
|
<p class="text-slate-400 text-sm">
|
||||||
|
DevOps Engineer · Rust Enthusiast
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<a
|
||||||
|
href="https://git.telcotronics.net/pablinux/SIAX-MONITOR"
|
||||||
|
target="_blank"
|
||||||
|
class="inline-flex items-center gap-2 px-6 py-3 bg-primary hover:brightness-110 rounded-lg text-white font-semibold transition-all"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined text-sm"
|
||||||
|
>code</span
|
||||||
|
>
|
||||||
|
Ver en Git
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/api-docs"
|
||||||
|
class="inline-flex items-center gap-2 px-6 py-3 bg-slate-800 hover:bg-slate-700 rounded-lg text-white font-semibold transition-all"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined text-sm"
|
||||||
|
>description</span
|
||||||
|
>
|
||||||
|
Documentación
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- Related Articles / Tags -->
|
||||||
|
<div
|
||||||
|
class="mt-12 p-6 rounded-2xl border border-slate-800 bg-[#0a0f16]"
|
||||||
|
>
|
||||||
|
<h3 class="text-lg font-bold text-white mb-4">Etiquetas</h3>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<span
|
||||||
|
class="px-3 py-1 rounded-full bg-slate-800 text-slate-300 text-sm hover:bg-slate-700 transition-colors cursor-pointer"
|
||||||
|
>rust</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="px-3 py-1 rounded-full bg-slate-800 text-slate-300 text-sm hover:bg-slate-700 transition-colors cursor-pointer"
|
||||||
|
>systemd</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="px-3 py-1 rounded-full bg-slate-800 text-slate-300 text-sm hover:bg-slate-700 transition-colors cursor-pointer"
|
||||||
|
>monitoring</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="px-3 py-1 rounded-full bg-slate-800 text-slate-300 text-sm hover:bg-slate-700 transition-colors cursor-pointer"
|
||||||
|
>devops</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="px-3 py-1 rounded-full bg-slate-800 text-slate-300 text-sm hover:bg-slate-700 transition-colors cursor-pointer"
|
||||||
|
>nodejs</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="px-3 py-1 rounded-full bg-slate-800 text-slate-300 text-sm hover:bg-slate-700 transition-colors cursor-pointer"
|
||||||
|
>python</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="px-3 py-1 rounded-full bg-slate-800 text-slate-300 text-sm hover:bg-slate-700 transition-colors cursor-pointer"
|
||||||
|
>websocket</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="px-3 py-1 rounded-full bg-slate-800 text-slate-300 text-sm hover:bg-slate-700 transition-colors cursor-pointer"
|
||||||
|
>axum</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="px-3 py-1 rounded-full bg-slate-800 text-slate-300 text-sm hover:bg-slate-700 transition-colors cursor-pointer"
|
||||||
|
>tokio</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="px-3 py-1 rounded-full bg-slate-800 text-slate-300 text-sm hover:bg-slate-700 transition-colors cursor-pointer"
|
||||||
|
>linux</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="bg-[#0a0f16] border-t border-slate-800 mt-20 py-12">
|
||||||
|
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="text-center mb-6">
|
||||||
|
<h3 class="text-2xl font-bold text-white mb-2">
|
||||||
|
SIAX Monitor
|
||||||
|
</h3>
|
||||||
|
<p class="text-slate-400">
|
||||||
|
Sistema de Monitoreo y Gestión de Aplicaciones
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center gap-8 mb-6">
|
||||||
|
<a
|
||||||
|
href="https://git.telcotronics.net/pablinux/SIAX-MONITOR"
|
||||||
|
target="_blank"
|
||||||
|
class="text-slate-400 hover:text-primary transition-colors"
|
||||||
|
>Git</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="/api-docs"
|
||||||
|
class="text-slate-400 hover:text-primary transition-colors"
|
||||||
|
>API Docs</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
class="text-slate-400 hover:text-primary transition-colors"
|
||||||
|
>Dashboard</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<p class="text-slate-500 text-sm text-center">
|
||||||
|
© 2026 SIAX Monitor. Desarrollado con 🦀 Rust y ❤️ por la
|
||||||
|
comunidad
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
615
web/edit.html
Normal file
615
web/edit.html
Normal 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>
|
||||||
563
web/health.html
Normal file
563
web/health.html
Normal file
@@ -0,0 +1,563 @@
|
|||||||
|
<!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>System Health - 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;800;900&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&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"],
|
||||||
|
mono: ["JetBrains Mono", "monospace"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</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-dark text-white min-h-screen">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="border-b border-[#283039] bg-[#0a0f16]">
|
||||||
|
<div class="container mx-auto px-4 py-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div
|
||||||
|
class="rounded-full size-9 border-2 border-slate-700 overflow-hidden"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/static/icon/logo.png"
|
||||||
|
alt="Logo"
|
||||||
|
class="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-xl font-bold">SIAX Monitor</h1>
|
||||||
|
<p class="text-xs text-slate-400">System Health</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Desktop Navigation -->
|
||||||
|
<nav class="hidden md:flex items-center gap-2">
|
||||||
|
<a
|
||||||
|
href="/scan"
|
||||||
|
class="px-4 py-2 rounded-lg text-slate-300 hover:bg-[#161f2a] transition-colors flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined text-lg"
|
||||||
|
>search</span
|
||||||
|
>
|
||||||
|
<span>Escanear</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/logs"
|
||||||
|
class="px-4 py-2 rounded-lg text-slate-300 hover:bg-[#161f2a] transition-colors flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined text-lg"
|
||||||
|
>article</span
|
||||||
|
>
|
||||||
|
<span>Logs</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/register"
|
||||||
|
class="px-4 py-2 rounded-lg text-slate-300 hover:bg-[#161f2a] transition-colors flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined text-lg"
|
||||||
|
>app_registration</span
|
||||||
|
>
|
||||||
|
<span>Registrar</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
class="px-4 py-2 rounded-lg text-slate-300 hover:bg-[#161f2a] transition-colors flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined text-lg"
|
||||||
|
>dashboard</span
|
||||||
|
>
|
||||||
|
<span>Dashboard</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Mobile Menu Button -->
|
||||||
|
<button
|
||||||
|
onclick="toggleMenu()"
|
||||||
|
class="md:hidden px-3 py-2 rounded-lg text-slate-300 hover:bg-[#161f2a] transition-colors"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined text-2xl"
|
||||||
|
>menu</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Menu Dropdown -->
|
||||||
|
<div
|
||||||
|
id="mobile-menu"
|
||||||
|
class="hidden md:hidden mt-4 pb-4 space-y-2"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="/health"
|
||||||
|
class="block px-4 py-3 rounded-lg bg-[#161f2a] text-primary transition-colors flex items-center gap-3"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined"
|
||||||
|
>monitor_heart</span
|
||||||
|
>
|
||||||
|
<span>Health</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/scan"
|
||||||
|
class="block px-4 py-3 rounded-lg text-slate-300 hover:bg-[#161f2a] transition-colors flex items-center gap-3"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined">search</span>
|
||||||
|
<span>Escanear</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/logs"
|
||||||
|
class="block px-4 py-3 rounded-lg text-slate-300 hover:bg-[#161f2a] transition-colors flex items-center gap-3"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined">article</span>
|
||||||
|
<span>Logs</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
class="block px-4 py-3 rounded-lg text-slate-300 hover:bg-[#161f2a] transition-colors flex items-center gap-3"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined">dashboard</span>
|
||||||
|
<span>Dashboard</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="container mx-auto px-4 py-8">
|
||||||
|
<!-- Page Header -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-3xl font-black mb-2">System Health</h2>
|
||||||
|
<p class="text-slate-400">
|
||||||
|
Diagnóstico y estado del sistema de monitoreo
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onclick="refreshHealth()"
|
||||||
|
class="px-4 py-2 rounded-lg bg-primary hover:brightness-110 transition-all flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined text-sm"
|
||||||
|
>refresh</span
|
||||||
|
>
|
||||||
|
<span>Actualizar</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div id="loading-state" class="text-center py-12">
|
||||||
|
<div
|
||||||
|
class="inline-block animate-spin rounded-full h-12 w-12 border-4 border-primary border-t-transparent"
|
||||||
|
></div>
|
||||||
|
<p class="mt-4 text-slate-400">
|
||||||
|
Cargando estado del sistema...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Health Cards Container -->
|
||||||
|
<div id="health-content" class="hidden">
|
||||||
|
<!-- Status Overview -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
||||||
|
<!-- Overall Status -->
|
||||||
|
<div
|
||||||
|
class="rounded-2xl border border-[#283039] bg-[#161f2a] p-6"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<span class="text-slate-400">Estado General</span>
|
||||||
|
<span
|
||||||
|
class="material-symbols-outlined text-green-400"
|
||||||
|
id="status-icon"
|
||||||
|
>check_circle</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="text-2xl font-bold" id="overall-status">
|
||||||
|
OK
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Config Status -->
|
||||||
|
<div
|
||||||
|
class="rounded-2xl border border-[#283039] bg-[#161f2a] p-6"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<span class="text-slate-400">Configuración</span>
|
||||||
|
<span
|
||||||
|
class="material-symbols-outlined text-blue-400"
|
||||||
|
>settings</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="text-2xl font-bold" id="config-status">
|
||||||
|
Cargada
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Apps Count -->
|
||||||
|
<div
|
||||||
|
class="rounded-2xl border border-[#283039] bg-[#161f2a] p-6"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<span class="text-slate-400">Apps Registradas</span>
|
||||||
|
<span
|
||||||
|
class="material-symbols-outlined text-purple-400"
|
||||||
|
>apps</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="text-2xl font-bold" id="apps-count">0</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Version -->
|
||||||
|
<div
|
||||||
|
class="rounded-2xl border border-[#283039] bg-[#161f2a] p-6"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<span class="text-slate-400">Versión</span>
|
||||||
|
<span
|
||||||
|
class="material-symbols-outlined text-yellow-400"
|
||||||
|
>info</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="text-2xl font-bold font-mono" id="version">
|
||||||
|
-
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Detailed Information -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<!-- Configuration Details -->
|
||||||
|
<div
|
||||||
|
class="rounded-2xl border border-[#283039] bg-[#161f2a] p-6"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3 mb-6">
|
||||||
|
<div
|
||||||
|
class="w-10 h-10 rounded-xl bg-blue-500/20 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="material-symbols-outlined text-blue-400"
|
||||||
|
>folder</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-bold">Configuración</h3>
|
||||||
|
<p class="text-sm text-slate-400">
|
||||||
|
Detalles del archivo de configuración
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div
|
||||||
|
class="flex items-start justify-between py-3 border-b border-[#283039]"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-slate-400">
|
||||||
|
Ruta del archivo
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="font-mono text-sm text-primary"
|
||||||
|
id="config-path"
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="material-symbols-outlined text-green-400"
|
||||||
|
id="config-loaded-icon"
|
||||||
|
>check_circle</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex items-start justify-between py-3 border-b border-[#283039]"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-slate-400">Estado</p>
|
||||||
|
<p
|
||||||
|
class="font-semibold"
|
||||||
|
id="config-loaded-text"
|
||||||
|
>
|
||||||
|
Archivo cargado correctamente
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-start justify-between py-3">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-slate-400">
|
||||||
|
Aplicaciones en config
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="text-2xl font-bold"
|
||||||
|
id="config-apps-count"
|
||||||
|
>
|
||||||
|
0
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Systemd Services -->
|
||||||
|
<div
|
||||||
|
class="rounded-2xl border border-[#283039] bg-[#161f2a] p-6"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3 mb-6">
|
||||||
|
<div
|
||||||
|
class="w-10 h-10 rounded-xl bg-green-500/20 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="material-symbols-outlined text-green-400"
|
||||||
|
>settings_system_daydream</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-bold">
|
||||||
|
Servicios Systemd
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-slate-400">
|
||||||
|
Servicios creados por SIAX Monitor
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="systemd-services-list"
|
||||||
|
class="space-y-2 max-h-80 overflow-y-auto"
|
||||||
|
>
|
||||||
|
<!-- Services will be injected here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="no-services"
|
||||||
|
class="hidden text-center py-8 text-slate-400"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="material-symbols-outlined text-4xl mb-2 opacity-50"
|
||||||
|
>info</span
|
||||||
|
>
|
||||||
|
<p>No hay servicios systemd registrados</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- System Commands -->
|
||||||
|
<div
|
||||||
|
class="mt-6 rounded-2xl border border-[#283039] bg-[#161f2a] p-6"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3 mb-6">
|
||||||
|
<div
|
||||||
|
class="w-10 h-10 rounded-xl bg-purple-500/20 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="material-symbols-outlined text-purple-400"
|
||||||
|
>terminal</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-bold">Comandos Útiles</h3>
|
||||||
|
<p class="text-sm text-slate-400">
|
||||||
|
Comandos para gestionar servicios systemd
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div class="rounded-lg bg-[#0a0f16] p-4">
|
||||||
|
<p class="text-xs text-slate-400 mb-2">
|
||||||
|
Listar servicios SIAX
|
||||||
|
</p>
|
||||||
|
<code class="text-sm text-primary font-mono"
|
||||||
|
>systemctl list-units 'siax-app-*'</code
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg bg-[#0a0f16] p-4">
|
||||||
|
<p class="text-xs text-slate-400 mb-2">
|
||||||
|
Ver estado de un servicio
|
||||||
|
</p>
|
||||||
|
<code class="text-sm text-primary font-mono"
|
||||||
|
>systemctl status siax-app-nombre.service</code
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg bg-[#0a0f16] p-4">
|
||||||
|
<p class="text-xs text-slate-400 mb-2">
|
||||||
|
Ver logs de un servicio
|
||||||
|
</p>
|
||||||
|
<code class="text-sm text-primary font-mono"
|
||||||
|
>journalctl -u siax-app-nombre -f</code
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg bg-[#0a0f16] p-4">
|
||||||
|
<p class="text-xs text-slate-400 mb-2">
|
||||||
|
Recargar daemon de systemd
|
||||||
|
</p>
|
||||||
|
<code class="text-sm text-primary font-mono"
|
||||||
|
>systemctl daemon-reload</code
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function loadHealth() {
|
||||||
|
const loading = document.getElementById("loading-state");
|
||||||
|
const content = document.getElementById("health-content");
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.classList.remove("hidden");
|
||||||
|
content.classList.add("hidden");
|
||||||
|
|
||||||
|
const response = await fetch("/api/health");
|
||||||
|
if (!response.ok) throw new Error("Failed to fetch health");
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
const data = result.data;
|
||||||
|
|
||||||
|
loading.classList.add("hidden");
|
||||||
|
content.classList.remove("hidden");
|
||||||
|
|
||||||
|
// Update status cards
|
||||||
|
document.getElementById("overall-status").textContent =
|
||||||
|
data.status.toUpperCase();
|
||||||
|
document.getElementById("config-status").textContent =
|
||||||
|
data.config_loaded ? "Cargada" : "No encontrada";
|
||||||
|
document.getElementById("apps-count").textContent =
|
||||||
|
data.apps_count;
|
||||||
|
document.getElementById("version").textContent =
|
||||||
|
"v" + data.version;
|
||||||
|
|
||||||
|
// Update config details
|
||||||
|
document.getElementById("config-path").textContent =
|
||||||
|
data.config_path;
|
||||||
|
document.getElementById("config-apps-count").textContent =
|
||||||
|
data.apps_count;
|
||||||
|
|
||||||
|
const configIcon =
|
||||||
|
document.getElementById("config-loaded-icon");
|
||||||
|
const configText =
|
||||||
|
document.getElementById("config-loaded-text");
|
||||||
|
|
||||||
|
if (data.config_loaded) {
|
||||||
|
configIcon.textContent = "check_circle";
|
||||||
|
configIcon.className =
|
||||||
|
"material-symbols-outlined text-green-400";
|
||||||
|
configText.textContent =
|
||||||
|
"Archivo cargado correctamente";
|
||||||
|
} else {
|
||||||
|
configIcon.textContent = "error";
|
||||||
|
configIcon.className =
|
||||||
|
"material-symbols-outlined text-yellow-400";
|
||||||
|
configText.textContent =
|
||||||
|
"Archivo no encontrado (se creará automáticamente)";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update systemd services list
|
||||||
|
const servicesList = document.getElementById(
|
||||||
|
"systemd-services-list",
|
||||||
|
);
|
||||||
|
const noServices = document.getElementById("no-services");
|
||||||
|
|
||||||
|
if (
|
||||||
|
data.systemd_services &&
|
||||||
|
data.systemd_services.length > 0
|
||||||
|
) {
|
||||||
|
servicesList.innerHTML = data.systemd_services
|
||||||
|
.map(
|
||||||
|
(service) => `
|
||||||
|
<div class="flex items-center justify-between p-3 rounded-lg bg-[#0a0f16] hover:bg-[#0d1218] transition-colors">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="material-symbols-outlined text-green-400 text-sm">check_circle</span>
|
||||||
|
<span class="font-mono text-sm">${service}</span>
|
||||||
|
</div>
|
||||||
|
<button onclick="copyToClipboard('${service}')" class="text-slate-400 hover:text-primary transition-colors">
|
||||||
|
<span class="material-symbols-outlined text-sm">content_copy</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
noServices.classList.add("hidden");
|
||||||
|
} else {
|
||||||
|
servicesList.innerHTML = "";
|
||||||
|
noServices.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading health:", error);
|
||||||
|
loading.classList.add("hidden");
|
||||||
|
content.innerHTML = `
|
||||||
|
<div class="text-center py-12 text-red-400">
|
||||||
|
<span class="material-symbols-outlined text-6xl mb-4">error</span>
|
||||||
|
<p class="text-xl font-bold mb-2">Error al cargar el estado del sistema</p>
|
||||||
|
<p class="text-slate-400">${error.message}</p>
|
||||||
|
<button onclick="loadHealth()" class="mt-4 px-6 py-2 bg-primary rounded-lg hover:brightness-110">
|
||||||
|
Reintentar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
content.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshHealth() {
|
||||||
|
loadHealth();
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyToClipboard(text) {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
// Simple feedback - you could add a toast notification here
|
||||||
|
console.log("Copied:", text);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMenu() {
|
||||||
|
const menu = document.getElementById("mobile-menu");
|
||||||
|
menu.classList.toggle("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load on page load
|
||||||
|
document.addEventListener("DOMContentLoaded", loadHealth);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
694
web/index.html
694
web/index.html
@@ -4,6 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
||||||
<title>Panel de Monitoreo</title>
|
<title>Panel de Monitoreo</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/static/icon/favicon.svg" />
|
||||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap"
|
||||||
@@ -54,90 +55,117 @@
|
|||||||
>
|
>
|
||||||
<div class="flex h-full grow flex-col">
|
<div class="flex h-full grow flex-col">
|
||||||
<!-- Sticky Top Navigation -->
|
<!-- Sticky Top Navigation -->
|
||||||
<header
|
<header class="border-b border-[#283039] bg-[#0a0f16]">
|
||||||
class="sticky top-0 z-50 w-full border-b border-slate-200 dark:border-slate-800 bg-background-light/80 dark:bg-background-dark/80 backdrop-blur-md"
|
<div class="container mx-auto px-4 py-4">
|
||||||
>
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
<div
|
<div
|
||||||
class="max-w-[1200px] mx-auto px-4 lg:px-10 py-3 flex items-center justify-between"
|
class="rounded-full size-9 border-2 border-slate-700 overflow-hidden"
|
||||||
>
|
|
||||||
<div class="flex items-center gap-8">
|
|
||||||
<div class="flex items-center gap-3 text-primary">
|
|
||||||
<div
|
|
||||||
class="size-8 bg-primary rounded-lg flex items-center justify-center text-white"
|
|
||||||
>
|
|
||||||
<span class="material-symbols-outlined"
|
|
||||||
>monitoring</span
|
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
src="/static/icon/logo.png"
|
||||||
|
alt="Logo"
|
||||||
|
class="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h2
|
<div>
|
||||||
class="text-white text-lg font-bold leading-tight tracking-[-0.015em]"
|
<h1 class="text-xl font-bold">SIAX Monitor</h1>
|
||||||
>
|
<p class="text-xs text-slate-400">Dashboard</p>
|
||||||
SIAX Monitor
|
|
||||||
</h2>
|
|
||||||
</div>
|
</div>
|
||||||
<nav class="hidden md:flex items-center gap-6">
|
</div>
|
||||||
|
|
||||||
|
<!-- Desktop Navigation -->
|
||||||
|
<nav class="hidden md:flex items-center gap-2">
|
||||||
<a
|
<a
|
||||||
class="text-primary text-sm font-semibold leading-normal"
|
href="/health"
|
||||||
href="/"
|
class="px-4 py-2 rounded-lg text-slate-300 hover:bg-[#161f2a] transition-colors flex items-center gap-2"
|
||||||
>Inicio</a
|
|
||||||
>
|
>
|
||||||
|
<span class="material-symbols-outlined text-lg"
|
||||||
|
>monitor_heart</span
|
||||||
|
>
|
||||||
|
<span>Health</span>
|
||||||
|
</a>
|
||||||
<a
|
<a
|
||||||
class="text-slate-600 dark:text-slate-400 hover:text-white text-sm font-medium transition-colors"
|
|
||||||
href="/scan"
|
href="/scan"
|
||||||
|
class="px-4 py-2 rounded-lg text-slate-300 hover:bg-[#161f2a] transition-colors flex items-center gap-2"
|
||||||
>
|
>
|
||||||
Escanear
|
<span class="material-symbols-outlined text-lg"
|
||||||
|
>search</span
|
||||||
|
>
|
||||||
|
<span>Escanear</span>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
class="text-slate-600 dark:text-slate-400 hover:text-white text-sm font-medium transition-colors"
|
|
||||||
href="/select"
|
|
||||||
>
|
|
||||||
Agregar
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="text-slate-600 dark:text-slate-400 hover:text-white text-sm font-medium transition-colors"
|
|
||||||
href="/register"
|
|
||||||
>
|
|
||||||
Nueva App
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="text-slate-600 dark:text-slate-400 hover:text-white text-sm font-medium transition-colors"
|
|
||||||
href="/logs"
|
href="/logs"
|
||||||
|
class="px-4 py-2 rounded-lg text-slate-300 hover:bg-[#161f2a] transition-colors flex items-center gap-2"
|
||||||
>
|
>
|
||||||
Registros
|
<span class="material-symbols-outlined text-lg"
|
||||||
|
>article</span
|
||||||
|
>
|
||||||
|
<span>Logs</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/register"
|
||||||
|
class="px-4 py-2 rounded-lg text-slate-300 hover:bg-[#161f2a] transition-colors flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined text-lg"
|
||||||
|
>app_registration</span
|
||||||
|
>
|
||||||
|
<span>Registrar</span>
|
||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-4">
|
<!-- Mobile Menu Button -->
|
||||||
<div class="hidden sm:block">
|
|
||||||
<label class="relative block">
|
|
||||||
<span
|
|
||||||
class="absolute inset-y-0 left-0 flex items-center pl-3 text-slate-500"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="material-symbols-outlined text-sm"
|
|
||||||
>
|
|
||||||
search
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
class="form-input w-64 rounded-lg border-none bg-slate-200 dark:bg-slate-800 text-sm py-2 pl-10 pr-4 placeholder:text-slate-500 focus:ring-1 focus:ring-primary"
|
|
||||||
placeholder="Buscar..."
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<button
|
<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="toggleMenu()"
|
||||||
onclick="window.location.href = '/register'"
|
class="md:hidden px-3 py-2 rounded-lg text-slate-300 hover:bg-[#161f2a] transition-colors"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined text-2xl"
|
||||||
|
>menu</span
|
||||||
>
|
>
|
||||||
<span>Registrar App</span>
|
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Menu Dropdown -->
|
||||||
<div
|
<div
|
||||||
class="bg-center bg-no-repeat aspect-square bg-cover rounded-full size-9 border-2 border-slate-700"
|
id="mobile-menu"
|
||||||
style="
|
class="hidden md:hidden mt-4 pb-4 space-y-2"
|
||||||
background-image: url("https://lh3.googleusercontent.com/aida-public/AB6AXuCT0iINTncUFHp353HCJXRR5C0OKbSp_7IBOVNoDU07yuF2aToQQdnXNOeGI9RLUjVBsVNcU--ZoTMY90FFJvrQvYvRzKvq-CFCzBlVkCeoi5AgG84cB71wW0NIMg626M_sCjmDjxqmAJwIbkAcSmSlAg3TUThW1U2A3StNVgqFXEpgFbpJcU5nxLs6vuRkfYR1kIXcV44TQpgOosbsjSB1Pk1UTOQJ_OEcQtY-5c3FJw7gXBDxlp6y3jsY3rBm0xWGJi8NWnrUrhpl");
|
>
|
||||||
"
|
<a
|
||||||
></div>
|
href="/health"
|
||||||
|
class="block px-4 py-3 rounded-lg text-slate-300 hover:bg-[#161f2a] transition-colors flex items-center gap-3"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined"
|
||||||
|
>monitor_heart</span
|
||||||
|
>
|
||||||
|
<span>Health</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/scan"
|
||||||
|
class="block px-4 py-3 rounded-lg text-slate-300 hover:bg-[#161f2a] transition-colors flex items-center gap-3"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined"
|
||||||
|
>search</span
|
||||||
|
>
|
||||||
|
<span>Escanear</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/logs"
|
||||||
|
class="block px-4 py-3 rounded-lg text-slate-300 hover:bg-[#161f2a] transition-colors flex items-center gap-3"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined"
|
||||||
|
>article</span
|
||||||
|
>
|
||||||
|
<span>Logs</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/register"
|
||||||
|
class="block px-4 py-3 rounded-lg text-slate-300 hover:bg-[#161f2a] transition-colors flex items-center gap-3"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined"
|
||||||
|
>app_registration</span
|
||||||
|
>
|
||||||
|
<span>Registrar</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -150,7 +178,7 @@
|
|||||||
<h1
|
<h1
|
||||||
class="text-slate-900 dark:text-white text-3xl font-black tracking-tight"
|
class="text-slate-900 dark:text-white text-3xl font-black tracking-tight"
|
||||||
>
|
>
|
||||||
Dashboard Index
|
Panel de Control
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-slate-500 text-sm mt-1">
|
<p class="text-slate-500 text-sm mt-1">
|
||||||
Monitoreo de salud del sistema y procesos en tiempo
|
Monitoreo de salud del sistema y procesos en tiempo
|
||||||
@@ -336,7 +364,7 @@
|
|||||||
<th class="px-6 py-4">Mem %</th>
|
<th class="px-6 py-4">Mem %</th>
|
||||||
<th class="px-6 py-4">Tiempo Activo</th>
|
<th class="px-6 py-4">Tiempo Activo</th>
|
||||||
<th class="px-6 py-4 text-right">
|
<th class="px-6 py-4 text-right">
|
||||||
Actions
|
Acciones
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -369,6 +397,100 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Historial de Apps Eliminadas Section -->
|
||||||
|
<div
|
||||||
|
id="deleted-apps-section"
|
||||||
|
class="mt-10 bg-white dark:bg-slate-800/30 rounded-xl border border-slate-200 dark:border-slate-800 overflow-hidden hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="p-6 border-b border-slate-200 dark:border-slate-800 flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
class="size-10 rounded-full bg-red-500/10 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="material-symbols-outlined text-red-500"
|
||||||
|
>history</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2
|
||||||
|
class="text-slate-900 dark:text-white text-xl font-bold"
|
||||||
|
>
|
||||||
|
Historial de Apps Eliminadas
|
||||||
|
</h2>
|
||||||
|
<p class="text-slate-500 text-sm">
|
||||||
|
Aplicaciones eliminadas que pueden ser
|
||||||
|
restauradas
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onclick="toggleDeletedApps()"
|
||||||
|
class="text-slate-500 hover:text-slate-700 dark:hover:text-slate-300"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined"
|
||||||
|
>expand_more</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="deleted-apps-content" class="hidden">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full">
|
||||||
|
<thead class="bg-slate-50 dark:bg-slate-800/50">
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
class="px-6 py-3 text-left text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
Aplicación
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="px-6 py-3 text-left text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
Puerto
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="px-6 py-3 text-left text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
Eliminada
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="px-6 py-3 text-left text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
Razón
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="px-6 py-3 text-right text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
Acciones
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody
|
||||||
|
id="deleted-apps-list"
|
||||||
|
class="divide-y divide-slate-200 dark:divide-slate-800"
|
||||||
|
>
|
||||||
|
<!-- Deleted apps will be loaded here -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id="deleted-apps-empty"
|
||||||
|
class="hidden p-8 text-center"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="material-symbols-outlined text-6xl text-slate-300 dark:text-slate-700 mb-3"
|
||||||
|
>check_circle</span
|
||||||
|
>
|
||||||
|
<p class="text-slate-500 dark:text-slate-400">
|
||||||
|
No hay apps eliminadas en el historial
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Quick Action Links -->
|
<!-- Quick Action Links -->
|
||||||
<div class="mt-10 grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div class="mt-10 grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div
|
<div
|
||||||
@@ -436,19 +558,135 @@
|
|||||||
<a class="hover:text-primary" href="#"
|
<a class="hover:text-primary" href="#"
|
||||||
>Política de Privacidad</a
|
>Política de Privacidad</a
|
||||||
>
|
>
|
||||||
<a class="hover:text-primary" href="#"
|
<a class="hover:text-primary" href="/api-docs"
|
||||||
>Documentación de API</a
|
>Documentación de API</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal de Confirmación para Eliminar -->
|
||||||
|
<div
|
||||||
|
id="delete-modal"
|
||||||
|
class="hidden fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-white dark:bg-slate-800 rounded-2xl shadow-2xl max-w-md w-full border border-slate-200 dark:border-slate-700 animate-in fade-in zoom-in duration-200"
|
||||||
|
>
|
||||||
|
<!-- Header -->
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-3 p-6 border-b border-slate-200 dark:border-slate-700"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="size-12 rounded-full bg-red-500/10 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="material-symbols-outlined text-red-500 text-2xl"
|
||||||
|
>delete_forever</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3
|
||||||
|
class="text-lg font-bold text-slate-900 dark:text-white"
|
||||||
|
>
|
||||||
|
Eliminar Aplicación
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-slate-500">
|
||||||
|
Esta acción no se puede deshacer
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="p-6 space-y-4">
|
||||||
|
<div
|
||||||
|
class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="text-sm text-slate-700 dark:text-slate-300 mb-3"
|
||||||
|
>
|
||||||
|
¿Estás seguro de eliminar
|
||||||
|
<strong
|
||||||
|
id="delete-app-name"
|
||||||
|
class="text-red-600 dark:text-red-400"
|
||||||
|
></strong
|
||||||
|
>?
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="text-xs text-slate-600 dark:text-slate-400 font-medium mb-2"
|
||||||
|
>
|
||||||
|
Esta acción eliminará:
|
||||||
|
</p>
|
||||||
|
<ul
|
||||||
|
class="text-xs text-slate-600 dark:text-slate-400 space-y-1.5"
|
||||||
|
>
|
||||||
|
<li class="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
class="material-symbols-outlined text-[14px] text-red-500"
|
||||||
|
>close</span
|
||||||
|
>
|
||||||
|
Servicio systemd:
|
||||||
|
<span
|
||||||
|
id="delete-service-name"
|
||||||
|
class="font-mono"
|
||||||
|
></span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
class="material-symbols-outlined text-[14px] text-red-500"
|
||||||
|
>close</span
|
||||||
|
>
|
||||||
|
Archivo .service en /etc/systemd/system/
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
class="material-symbols-outlined text-[14px] text-amber-500"
|
||||||
|
>archive</span
|
||||||
|
>
|
||||||
|
Se marcará como eliminada en monitored_apps.json
|
||||||
|
(soft delete)
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="flex items-center gap-2 text-green-600 dark:text-green-400"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="material-symbols-outlined text-[14px]"
|
||||||
|
>info</span
|
||||||
|
>
|
||||||
|
Podrás restaurarla desde el historial
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div
|
||||||
|
class="flex gap-3 p-6 border-t border-slate-200 dark:border-slate-700"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onclick="closeDeleteModal()"
|
||||||
|
class="flex-1 px-4 py-2.5 rounded-lg border border-slate-300 dark:border-slate-600 text-slate-700 dark:text-slate-300 font-medium hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors"
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick="confirmDelete()"
|
||||||
|
class="flex-1 px-4 py-2.5 rounded-lg bg-red-500 hover:bg-red-600 text-white font-medium transition-colors flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined text-[18px]"
|
||||||
|
>delete</span
|
||||||
|
>
|
||||||
|
Eliminar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
async function loadApps() {
|
async function loadApps() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch("/api/apps");
|
||||||
"http://localhost:8080/api/apps",
|
|
||||||
);
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success && result.data && result.data.apps) {
|
if (result.success && result.data && result.data.apps) {
|
||||||
@@ -470,8 +708,45 @@
|
|||||||
function displayApps(apps) {
|
function displayApps(apps) {
|
||||||
const tbody = document.getElementById("apps-tbody");
|
const tbody = document.getElementById("apps-tbody");
|
||||||
tbody.innerHTML = apps
|
tbody.innerHTML = apps
|
||||||
.map(
|
.map((app) => {
|
||||||
(app) => `
|
// Determinar color del badge según estado
|
||||||
|
const statusColors = {
|
||||||
|
Running: {
|
||||||
|
bg: "bg-green-100 dark:bg-green-900/30",
|
||||||
|
text: "text-green-700 dark:text-green-400",
|
||||||
|
dot: "bg-green-500",
|
||||||
|
},
|
||||||
|
Stopped: {
|
||||||
|
bg: "bg-gray-100 dark:bg-gray-800",
|
||||||
|
text: "text-gray-700 dark:text-gray-400",
|
||||||
|
dot: "bg-gray-400",
|
||||||
|
},
|
||||||
|
Failed: {
|
||||||
|
bg: "bg-red-100 dark:bg-red-900/30",
|
||||||
|
text: "text-red-700 dark:text-red-400",
|
||||||
|
dot: "bg-red-500",
|
||||||
|
},
|
||||||
|
Starting: {
|
||||||
|
bg: "bg-blue-100 dark:bg-blue-900/30",
|
||||||
|
text: "text-blue-700 dark:text-blue-400",
|
||||||
|
dot: "bg-blue-500",
|
||||||
|
},
|
||||||
|
Stopping: {
|
||||||
|
bg: "bg-yellow-100 dark:bg-yellow-900/30",
|
||||||
|
text: "text-yellow-700 dark:text-yellow-400",
|
||||||
|
dot: "bg-yellow-500",
|
||||||
|
},
|
||||||
|
Unknown: {
|
||||||
|
bg: "bg-slate-100 dark:bg-slate-800",
|
||||||
|
text: "text-slate-700 dark:text-slate-400",
|
||||||
|
dot: "bg-slate-400",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusStyle =
|
||||||
|
statusColors[app.status] || statusColors["Unknown"];
|
||||||
|
|
||||||
|
return `
|
||||||
<tr class="hover:bg-slate-50 dark:hover:bg-slate-800/40 transition-colors">
|
<tr class="hover:bg-slate-50 dark:hover:bg-slate-800/40 transition-colors">
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
@@ -479,28 +754,70 @@
|
|||||||
<span class="material-symbols-outlined text-primary text-lg">terminal</span>
|
<span class="material-symbols-outlined text-primary text-lg">terminal</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-slate-900 dark:text-white font-semibold text-sm">${app}</p>
|
<p class="text-slate-900 dark:text-white font-semibold text-sm">${app.name}</p>
|
||||||
<p class="text-slate-500 text-xs">Servicio</p>
|
<p class="text-slate-500 text-xs">${app.service_name || "Servicio"}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium bg-slate-100 text-slate-800 dark:bg-slate-800 dark:text-slate-400">
|
<span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium ${statusStyle.bg} ${statusStyle.text}">
|
||||||
<span class="size-1.5 rounded-full bg-slate-400"></span>
|
<span class="size-1.5 rounded-full ${statusStyle.dot}"></span>
|
||||||
Unknown
|
${app.status}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 text-sm">-</td>
|
<td class="px-6 py-4 text-sm">-</td>
|
||||||
<td class="px-6 py-4 text-sm">-</td>
|
<td class="px-6 py-4 text-sm">-</td>
|
||||||
<td class="px-6 py-4 text-sm text-slate-500">-</td>
|
<td class="px-6 py-4 text-sm text-slate-500">-</td>
|
||||||
<td class="px-6 py-4 text-right">
|
<td class="px-6 py-4 text-right">
|
||||||
<button class="text-slate-400 hover:text-white transition-colors">
|
<div class="flex items-center justify-end gap-2">
|
||||||
<span class="material-symbols-outlined">more_vert</span>
|
${
|
||||||
|
app.status === "Running"
|
||||||
|
? `
|
||||||
|
<button class="text-red-400 hover:text-red-300 transition-colors p-1.5 rounded hover:bg-red-900/20"
|
||||||
|
onclick="controlApp('${app.name}', 'stop')"
|
||||||
|
title="Detener">
|
||||||
|
<span class="material-symbols-outlined text-[20px]">stop</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="text-yellow-400 hover:text-yellow-300 transition-colors p-1.5 rounded hover:bg-yellow-900/20"
|
||||||
|
onclick="controlApp('${app.name}', 'restart')"
|
||||||
|
title="Reiniciar">
|
||||||
|
<span class="material-symbols-outlined text-[20px]">refresh</span>
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
: `
|
||||||
|
<button class="text-green-400 hover:text-green-300 transition-colors p-1.5 rounded hover:bg-green-900/20"
|
||||||
|
onclick="controlApp('${app.name}', 'start')"
|
||||||
|
title="Iniciar">
|
||||||
|
<span class="material-symbols-outlined text-[20px]">play_arrow</span>
|
||||||
|
</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"
|
||||||
|
onclick="window.location.href='/logs'"
|
||||||
|
title="Ver logs">
|
||||||
|
<span class="material-symbols-outlined text-[20px]">visibility</span>
|
||||||
|
</button>
|
||||||
|
${
|
||||||
|
app.status !== "Running"
|
||||||
|
? `
|
||||||
|
<button class="text-red-500 hover:text-red-400 transition-colors p-1.5 rounded hover:bg-red-900/20"
|
||||||
|
onclick="openDeleteModal('${app.name}')"
|
||||||
|
title="Eliminar">
|
||||||
|
<span class="material-symbols-outlined text-[20px]">delete</span>
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`,
|
`;
|
||||||
)
|
})
|
||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,7 +833,218 @@
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("DOMContentLoaded", loadApps);
|
async function controlApp(appName, action) {
|
||||||
|
const actionNames = {
|
||||||
|
start: "Iniciar",
|
||||||
|
stop: "Detener",
|
||||||
|
restart: "Reiniciar",
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmed = confirm(
|
||||||
|
`¿Estás seguro de ${actionNames[action]} la aplicación "${appName}"?`,
|
||||||
|
);
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/apps/${appName}/${action}`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
alert(`✅ ${result.data.message}`);
|
||||||
|
// Recargar la lista de apps
|
||||||
|
loadApps();
|
||||||
|
} else {
|
||||||
|
alert(`❌ Error: ${result.error}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error:", error);
|
||||||
|
alert("❌ Error al ejecutar la acción");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal de confirmación para eliminar
|
||||||
|
let appToDelete = null;
|
||||||
|
|
||||||
|
function openDeleteModal(appName) {
|
||||||
|
appToDelete = appName;
|
||||||
|
document.getElementById("delete-app-name").textContent =
|
||||||
|
appName;
|
||||||
|
document.getElementById("delete-service-name").textContent =
|
||||||
|
`siax-app-${appName}.service`;
|
||||||
|
document
|
||||||
|
.getElementById("delete-modal")
|
||||||
|
.classList.remove("hidden");
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDeleteModal() {
|
||||||
|
appToDelete = null;
|
||||||
|
document.getElementById("delete-modal").classList.add("hidden");
|
||||||
|
document.body.style.overflow = "auto";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmDelete() {
|
||||||
|
if (!appToDelete) return;
|
||||||
|
|
||||||
|
const appName = appToDelete;
|
||||||
|
closeDeleteModal();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/apps/${appName}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
alert(`✅ ${result.data.message}`);
|
||||||
|
loadApps();
|
||||||
|
loadDeletedApps(); // Recargar historial de eliminadas
|
||||||
|
} else {
|
||||||
|
alert(`❌ Error: ${result.error}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error:", error);
|
||||||
|
alert("❌ Error al eliminar la aplicación");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMenu() {
|
||||||
|
const menu = document.getElementById("mobile-menu");
|
||||||
|
menu.classList.toggle("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funciones para apps eliminadas (soft delete)
|
||||||
|
async function loadDeletedApps() {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/apps/deleted");
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (
|
||||||
|
!result.success ||
|
||||||
|
!result.data ||
|
||||||
|
!result.data.apps ||
|
||||||
|
result.data.apps.length === 0
|
||||||
|
) {
|
||||||
|
// No hay apps eliminadas, ocultar sección
|
||||||
|
document
|
||||||
|
.getElementById("deleted-apps-section")
|
||||||
|
.classList.add("hidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mostrar sección si hay apps eliminadas
|
||||||
|
document
|
||||||
|
.getElementById("deleted-apps-section")
|
||||||
|
.classList.remove("hidden");
|
||||||
|
|
||||||
|
const deletedAppsList =
|
||||||
|
document.getElementById("deleted-apps-list");
|
||||||
|
const emptyMessage =
|
||||||
|
document.getElementById("deleted-apps-empty");
|
||||||
|
|
||||||
|
deletedAppsList.innerHTML = result.data.apps
|
||||||
|
.map((app) => {
|
||||||
|
const deletedDate = app.deleted_at
|
||||||
|
? new Date(app.deleted_at).toLocaleString(
|
||||||
|
"es-ES",
|
||||||
|
)
|
||||||
|
: "Desconocida";
|
||||||
|
const reason =
|
||||||
|
app.deleted_reason || "Sin razón especificada";
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr class="hover:bg-slate-50 dark:hover:bg-slate-800/30 transition-colors">
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="size-10 rounded-full bg-slate-100 dark:bg-slate-800 flex items-center justify-center">
|
||||||
|
<span class="material-symbols-outlined text-slate-500 text-xl">deployed_code_history</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-slate-900 dark:text-white font-semibold text-sm">${app.name}</p>
|
||||||
|
<p class="text-slate-500 text-xs">${app.path || "Sin ruta"}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
<span class="text-slate-600 dark:text-slate-400 text-sm">${app.port}</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
<span class="text-slate-600 dark:text-slate-400 text-sm">${deletedDate}</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
<span class="text-slate-600 dark:text-slate-400 text-sm">${reason}</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-right">
|
||||||
|
<button
|
||||||
|
onclick="restoreApp('${app.name}')"
|
||||||
|
class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-green-500 hover:bg-green-600 text-white text-xs font-medium rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined text-sm">restore</span>
|
||||||
|
Restaurar
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
emptyMessage.classList.add("hidden");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading deleted apps:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleDeletedApps() {
|
||||||
|
const content = document.getElementById("deleted-apps-content");
|
||||||
|
content.classList.toggle("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restoreApp(appName) {
|
||||||
|
const confirmed = confirm(
|
||||||
|
`¿Estás seguro de restaurar la aplicación "${appName}"?\n\nNota: Solo se restaurará el registro en el JSON. El servicio systemd debe ser recreado manualmente.`,
|
||||||
|
);
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/apps/${appName}/restore`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
alert(`✅ ${result.data.message}`);
|
||||||
|
loadApps();
|
||||||
|
loadDeletedApps();
|
||||||
|
} else {
|
||||||
|
alert(`❌ Error: ${result.error}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error:", error);
|
||||||
|
alert("❌ Error al restaurar la aplicación");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("DOMContentLoaded", () => {
|
||||||
|
loadApps();
|
||||||
|
loadDeletedApps();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
203
web/logs.html
203
web/logs.html
@@ -4,6 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
||||||
<title>Visor de Registros - SIAX Monitor</title>
|
<title>Visor de Registros - 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>
|
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
||||||
@@ -69,9 +70,11 @@
|
|||||||
<div
|
<div
|
||||||
class="size-8 bg-primary rounded-lg flex items-center justify-center"
|
class="size-8 bg-primary rounded-lg flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<span class="material-symbols-outlined text-white"
|
<img
|
||||||
>monitoring</span
|
src="/static/icon/logo.png"
|
||||||
>
|
alt="Logo"
|
||||||
|
class="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h2
|
<h2
|
||||||
class="text-white text-lg font-bold leading-tight tracking-[-0.015em]"
|
class="text-white text-lg font-bold leading-tight tracking-[-0.015em]"
|
||||||
@@ -95,12 +98,7 @@
|
|||||||
<a
|
<a
|
||||||
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
|
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
|
||||||
href="/select"
|
href="/select"
|
||||||
>Agregar Detectada</a
|
>Selecionar Detectada</a
|
||||||
>
|
|
||||||
<a
|
|
||||||
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
|
|
||||||
href="/register"
|
|
||||||
>Nueva App</a
|
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class="text-primary text-sm font-medium border-b-2 border-primary pb-1"
|
class="text-primary text-sm font-medium border-b-2 border-primary pb-1"
|
||||||
@@ -108,6 +106,12 @@
|
|||||||
>Registros</a
|
>Registros</a
|
||||||
>
|
>
|
||||||
</nav>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -203,12 +207,43 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Terminal Log Output -->
|
<!-- Tabs -->
|
||||||
<div
|
<div class="border-b border-[#283039] bg-[#161f2a] px-4">
|
||||||
class="flex-1 bg-[#0a0f16] overflow-y-auto p-4 font-mono text-sm"
|
<div class="flex gap-1">
|
||||||
id="log-terminal"
|
<button
|
||||||
|
id="tab-app-logs"
|
||||||
|
onclick="switchTab('app-logs')"
|
||||||
|
class="tab-button px-4 py-2 text-sm font-medium transition-colors rounded-t-lg border-b-2 border-primary text-primary"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="material-symbols-outlined text-[16px] align-middle"
|
||||||
|
>terminal</span
|
||||||
|
>
|
||||||
|
Logs de App
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="tab-system-errors"
|
||||||
|
onclick="switchTab('system-errors')"
|
||||||
|
class="tab-button px-4 py-2 text-sm font-medium transition-colors rounded-t-lg border-b-2 border-transparent text-[#9dabb9] hover:text-white"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="material-symbols-outlined text-[16px] align-middle"
|
||||||
|
>error</span
|
||||||
|
>
|
||||||
|
Errores del Sistema
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Content: App Logs -->
|
||||||
|
<div
|
||||||
|
id="content-app-logs"
|
||||||
|
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 break-words overflow-wrap-anywhere"
|
||||||
>
|
>
|
||||||
<div id="log-container" class="space-y-1">
|
|
||||||
<!-- 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
|
||||||
@@ -220,6 +255,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Content: System Errors -->
|
||||||
|
<div
|
||||||
|
id="content-system-errors"
|
||||||
|
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 break-words overflow-wrap-anywhere"
|
||||||
|
>
|
||||||
|
<div class="text-[#9dabb9] opacity-50">
|
||||||
|
<span class="text-yellow-400">⚠</span> Cargando logs
|
||||||
|
de errores del sistema...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -238,20 +289,23 @@
|
|||||||
empty.classList.add("hidden");
|
empty.classList.add("hidden");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch("/api/apps");
|
||||||
"http://localhost:8080/api/apps",
|
const result = await response.json();
|
||||||
);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
loading.classList.add("hidden");
|
loading.classList.add("hidden");
|
||||||
|
|
||||||
if (!data.apps || data.apps.length === 0) {
|
if (
|
||||||
|
!result.success ||
|
||||||
|
!result.data ||
|
||||||
|
!result.data.apps ||
|
||||||
|
result.data.apps.length === 0
|
||||||
|
) {
|
||||||
empty.classList.remove("hidden");
|
empty.classList.remove("hidden");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
appList.classList.remove("hidden");
|
appList.classList.remove("hidden");
|
||||||
appList.innerHTML = data.apps
|
appList.innerHTML = result.data.apps
|
||||||
.map((app) => {
|
.map((app) => {
|
||||||
const statusColor =
|
const statusColor =
|
||||||
app.status === "Running"
|
app.status === "Running"
|
||||||
@@ -321,9 +375,10 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Connect WebSocket
|
// Connect WebSocket
|
||||||
ws = new WebSocket(
|
const protocol =
|
||||||
`ws://localhost:8080/api/apps/${appName}/logs`,
|
window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
);
|
const wsUrl = `${protocol}//${window.location.host}/api/apps/${appName}/logs`;
|
||||||
|
ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
document.getElementById("connection-status").textContent =
|
document.getElementById("connection-status").textContent =
|
||||||
@@ -356,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()
|
||||||
@@ -399,8 +455,9 @@
|
|||||||
logContainer.appendChild(logEntry);
|
logContainer.appendChild(logEntry);
|
||||||
|
|
||||||
// Auto-scroll
|
// Auto-scroll
|
||||||
if (autoScroll) {
|
if (autoScroll && currentTab === "app-logs") {
|
||||||
const terminal = document.getElementById("log-terminal");
|
const terminal =
|
||||||
|
document.getElementById("content-app-logs");
|
||||||
terminal.scrollTop = terminal.scrollHeight;
|
terminal.scrollTop = terminal.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -457,6 +514,100 @@
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tab switching
|
||||||
|
let currentTab = "app-logs";
|
||||||
|
|
||||||
|
function switchTab(tabName) {
|
||||||
|
currentTab = tabName;
|
||||||
|
|
||||||
|
// Update tab buttons
|
||||||
|
document.querySelectorAll(".tab-button").forEach((btn) => {
|
||||||
|
btn.classList.remove("border-primary", "text-primary");
|
||||||
|
btn.classList.add("border-transparent", "text-[#9dabb9]");
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeTab = document.getElementById(`tab-${tabName}`);
|
||||||
|
activeTab.classList.remove(
|
||||||
|
"border-transparent",
|
||||||
|
"text-[#9dabb9]",
|
||||||
|
);
|
||||||
|
activeTab.classList.add("border-primary", "text-primary");
|
||||||
|
|
||||||
|
// Update tab content
|
||||||
|
document.querySelectorAll(".tab-content").forEach((content) => {
|
||||||
|
content.classList.add("hidden");
|
||||||
|
});
|
||||||
|
|
||||||
|
document
|
||||||
|
.getElementById(`content-${tabName}`)
|
||||||
|
.classList.remove("hidden");
|
||||||
|
|
||||||
|
// Load system errors if switching to that tab
|
||||||
|
if (tabName === "system-errors") {
|
||||||
|
loadSystemErrors();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSystemErrors() {
|
||||||
|
const container = document.getElementById(
|
||||||
|
"system-errors-container",
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/logs/errors");
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (
|
||||||
|
result.success &&
|
||||||
|
result.logs &&
|
||||||
|
result.logs.length > 0
|
||||||
|
) {
|
||||||
|
container.innerHTML = result.logs
|
||||||
|
.map((line) => {
|
||||||
|
// Parse log line
|
||||||
|
let icon = "●";
|
||||||
|
let color = "text-white";
|
||||||
|
|
||||||
|
if (line.includes("[ERROR]")) {
|
||||||
|
icon = "✖";
|
||||||
|
color = "text-red-400";
|
||||||
|
} else if (line.includes("[WARN]")) {
|
||||||
|
icon = "⚠";
|
||||||
|
color = "text-yellow-400";
|
||||||
|
} else if (line.includes("[INFO]")) {
|
||||||
|
icon = "ℹ";
|
||||||
|
color = "text-blue-400";
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<div class="log-line break-words overflow-wrap-anywhere ${color}">${icon} ${escapeHtml(line)}</div>`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
// Auto scroll to bottom
|
||||||
|
container.scrollTop = container.scrollHeight;
|
||||||
|
} else if (result.message) {
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="text-[#9dabb9]">
|
||||||
|
<span class="text-yellow-400">⚠</span> ${result.message}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="text-[#9dabb9]">
|
||||||
|
<span class="text-blue-400">ℹ</span> No hay logs de errores disponibles
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading system errors:", error);
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="text-red-400">
|
||||||
|
<span class="text-red-400">✖</span> Error cargando logs del sistema
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Load apps on page load
|
// Load apps on page load
|
||||||
document.addEventListener("DOMContentLoaded", loadApps);
|
document.addEventListener("DOMContentLoaded", loadApps);
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
||||||
<title>Registrar Aplicación - SIAX Monitor</title>
|
<title>Registrar 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>
|
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
||||||
@@ -62,9 +63,11 @@
|
|||||||
<div
|
<div
|
||||||
class="size-8 bg-primary rounded-lg flex items-center justify-center"
|
class="size-8 bg-primary rounded-lg flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<span class="material-symbols-outlined text-white"
|
<img
|
||||||
>monitoring</span
|
src="/static/icon/logo.png"
|
||||||
>
|
alt="Logo"
|
||||||
|
class="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h2
|
<h2
|
||||||
class="text-white text-lg font-bold leading-tight tracking-[-0.015em]"
|
class="text-white text-lg font-bold leading-tight tracking-[-0.015em]"
|
||||||
@@ -88,12 +91,7 @@
|
|||||||
<a
|
<a
|
||||||
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
|
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
|
||||||
href="/select"
|
href="/select"
|
||||||
>Agregar Detectada</a
|
>Selecionar Detectada</a
|
||||||
>
|
|
||||||
<a
|
|
||||||
class="text-primary text-sm font-medium border-b-2 border-primary pb-1"
|
|
||||||
href="/register"
|
|
||||||
>Nueva App</a
|
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
|
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
|
||||||
@@ -101,6 +99,12 @@
|
|||||||
>Registros</a
|
>Registros</a
|
||||||
>
|
>
|
||||||
</nav>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -461,16 +465,13 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch("/api/apps", {
|
||||||
"http://localhost:8080/api/apps",
|
|
||||||
{
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify(formData),
|
body: JSON.stringify(formData),
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
@@ -485,7 +486,7 @@
|
|||||||
confirm("¿Deseas iniciar la aplicación ahora?")
|
confirm("¿Deseas iniciar la aplicación ahora?")
|
||||||
) {
|
) {
|
||||||
const startResponse = await fetch(
|
const startResponse = await fetch(
|
||||||
`http://localhost:8080/api/apps/${formData.app_name}/start`,
|
`/api/apps/${formData.app_name}/start`,
|
||||||
{ method: "POST" },
|
{ method: "POST" },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
||||||
<title>Escaneo de Procesos - SIAX Monitor</title>
|
<title>Escaneo de Procesos - 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>
|
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
||||||
@@ -62,9 +63,7 @@
|
|||||||
<div
|
<div
|
||||||
class="size-8 bg-primary rounded-lg flex items-center justify-center"
|
class="size-8 bg-primary rounded-lg flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<span class="material-symbols-outlined text-white"
|
<img src="/static/icon/logo.png" alt="Logo" class="w-full h-full object-cover">
|
||||||
>monitoring</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<h2
|
<h2
|
||||||
class="text-white text-lg font-bold leading-tight tracking-[-0.015em]"
|
class="text-white text-lg font-bold leading-tight tracking-[-0.015em]"
|
||||||
@@ -88,19 +87,21 @@
|
|||||||
<a
|
<a
|
||||||
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
|
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
|
||||||
href="/select"
|
href="/select"
|
||||||
>Agregar Detectada</a
|
>Selecionar Detectada</a
|
||||||
>
|
|
||||||
<a
|
|
||||||
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
|
|
||||||
href="/register"
|
|
||||||
>Registrar Nueva</a
|
|
||||||
>
|
>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
|
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
|
||||||
href="/logs"
|
href="/logs"
|
||||||
>Registros</a
|
>Registros</a
|
||||||
>
|
>
|
||||||
</nav>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -112,7 +113,7 @@
|
|||||||
<h1
|
<h1
|
||||||
class="text-white text-4xl font-black leading-tight tracking-[-0.033em]"
|
class="text-white text-4xl font-black leading-tight tracking-[-0.033em]"
|
||||||
>
|
>
|
||||||
Process Scan View
|
Visualización de escaneo de procesos
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-[#9dabb9] text-base font-normal">
|
<p class="text-[#9dabb9] text-base font-normal">
|
||||||
Monitoreo activo de procesos Node.js y Python.
|
Monitoreo activo de procesos Node.js y Python.
|
||||||
@@ -253,7 +254,7 @@
|
|||||||
loadingState.classList.remove('hidden');
|
loadingState.classList.remove('hidden');
|
||||||
emptyState.classList.add('hidden');
|
emptyState.classList.add('hidden');
|
||||||
|
|
||||||
const response = await fetch('http://localhost:8080/api/scan');
|
const response = await fetch('/api/scan');
|
||||||
if (!response.ok) throw new Error('Failed to fetch processes');
|
if (!response.ok) throw new Error('Failed to fetch processes');
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
||||||
<title>Agregar App Detectada - SIAX Monitor</title>
|
<title>Agregar App Detectada - 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>
|
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
||||||
@@ -62,9 +63,11 @@
|
|||||||
<div
|
<div
|
||||||
class="size-8 bg-primary rounded-lg flex items-center justify-center"
|
class="size-8 bg-primary rounded-lg flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<span class="material-symbols-outlined text-white"
|
<img
|
||||||
>monitoring</span
|
src="/static/icon/logo.png"
|
||||||
>
|
alt="Logo"
|
||||||
|
class="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h2
|
<h2
|
||||||
class="text-white text-lg font-bold leading-tight tracking-[-0.015em]"
|
class="text-white text-lg font-bold leading-tight tracking-[-0.015em]"
|
||||||
@@ -88,12 +91,7 @@
|
|||||||
<a
|
<a
|
||||||
class="text-primary text-sm font-medium border-b-2 border-primary pb-1"
|
class="text-primary text-sm font-medium border-b-2 border-primary pb-1"
|
||||||
href="/select"
|
href="/select"
|
||||||
>Agregar Detectada</a
|
>Selecionar Detectada</a
|
||||||
>
|
|
||||||
<a
|
|
||||||
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
|
|
||||||
href="/register"
|
|
||||||
>Nueva App</a
|
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
|
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
|
||||||
@@ -101,6 +99,12 @@
|
|||||||
>Registros</a
|
>Registros</a
|
||||||
>
|
>
|
||||||
</nav>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -111,7 +115,7 @@
|
|||||||
<h1
|
<h1
|
||||||
class="text-white text-4xl font-black leading-tight tracking-[-0.033em]"
|
class="text-white text-4xl font-black leading-tight tracking-[-0.033em]"
|
||||||
>
|
>
|
||||||
Add Detected Application
|
Agregar la aplicación detectada
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-[#9dabb9] text-base font-normal">
|
<p class="text-[#9dabb9] text-base font-normal">
|
||||||
Selecciona un proceso detectado y configúralo para
|
Selecciona un proceso detectado y configúralo para
|
||||||
@@ -280,9 +284,7 @@
|
|||||||
const empty = document.getElementById("empty-state");
|
const empty = document.getElementById("empty-state");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch("/api/scan");
|
||||||
"http://localhost:8080/api/scan",
|
|
||||||
);
|
|
||||||
if (!response.ok)
|
if (!response.ok)
|
||||||
throw new Error("Failed to fetch processes");
|
throw new Error("Failed to fetch processes");
|
||||||
|
|
||||||
|
|||||||
BIN
web/static/icon/favicon.ico
Normal file
BIN
web/static/icon/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
44
web/static/icon/favicon.svg
Normal file
44
web/static/icon/favicon.svg
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="128.000000pt" height="128.000000pt" viewBox="0 0 128.000000 128.000000"
|
||||||
|
preserveAspectRatio="xMidYMid meet">
|
||||||
|
|
||||||
|
<g transform="translate(0.000000,128.000000) scale(0.100000,-0.100000)"
|
||||||
|
fill="#000000" stroke="none">
|
||||||
|
<path d="M518 1244 c-75 -18 -188 -75 -239 -121 l-40 -36 -39 38 c-22 21 -42
|
||||||
|
36 -46 32 -3 -3 3 -33 14 -66 20 -58 20 -61 3 -83 -66 -84 -105 -212 -105
|
||||||
|
-343 0 -166 49 -288 164 -408 55 -58 71 -70 81 -60 18 18 64 16 79 -2 9 -11
|
||||||
|
35 -15 106 -15 l94 0 0 165 0 166 -24 -18 c-23 -17 -25 -25 -28 -128 l-3 -110
|
||||||
|
-45 -3 c-24 -2 -50 -8 -57 -14 -9 -7 -17 -7 -28 2 -17 14 -18 20 -5 40 7 12
|
||||||
|
13 12 33 1 13 -7 37 -11 53 -9 l29 3 3 87 c2 74 0 88 -13 88 -12 0 -15 -13
|
||||||
|
-15 -65 l0 -65 -45 0 c-25 0 -54 -6 -65 -12 -23 -15 -46 1 -37 26 5 12 18 13
|
||||||
|
61 9 l56 -6 0 47 c0 25 -4 46 -10 46 -5 0 -10 -11 -10 -25 0 -24 -3 -25 -60
|
||||||
|
-25 -32 0 -71 -5 -85 -12 -21 -9 -29 -9 -42 4 -14 15 -13 17 6 27 15 8 27 9
|
||||||
|
40 1 31 -16 111 -13 111 5 0 11 -11 15 -38 15 -103 0 -207 61 -257 151 -26 47
|
||||||
|
-30 64 -30 129 1 112 50 201 137 246 29 15 30 15 86 -37 66 -60 72 -69 42 -69
|
||||||
|
-13 0 -37 -9 -55 -20 -93 -58 -84 -193 17 -246 59 -31 144 -6 184 53 l22 33
|
||||||
|
22 -52 c12 -28 33 -68 46 -88 24 -36 24 -42 22 -200 l-3 -163 -96 2 c-72 2
|
||||||
|
-99 -1 -107 -12 -10 -12 -4 -18 35 -35 48 -20 138 -42 173 -42 13 0 17 5 13
|
||||||
|
20 -3 11 0 20 6 20 8 0 11 51 11 170 0 107 4 170 10 170 6 0 10 -61 10 -164 0
|
||||||
|
-108 4 -167 11 -172 6 -3 8 -15 5 -26 -4 -19 -1 -20 54 -14 71 8 148 30 174
|
||||||
|
49 37 27 9 37 -105 37 l-109 0 0 159 c0 96 4 162 10 166 7 4 10 -48 10 -149
|
||||||
|
l0 -156 93 0 c65 1 98 5 111 15 23 18 67 20 83 4 16 -16 43 2 106 71 97 108
|
||||||
|
147 240 148 390 0 125 -27 227 -87 322 l-27 42 18 55 c10 31 20 62 22 69 9 23
|
||||||
|
-16 12 -51 -23 -20 -19 -38 -35 -42 -35 -3 0 -25 16 -47 36 -52 44 -163 99
|
||||||
|
-242 119 -72 18 -200 18 -277 -1z m310 -90 c72 -15 187 -56 205 -74 5 -4 -40
|
||||||
|
-31 -100 -60 -89 -44 -121 -65 -182 -126 -52 -53 -80 -74 -99 -74 -17 0 -48
|
||||||
|
23 -106 78 -66 63 -99 85 -178 122 -54 25 -98 48 -98 52 0 11 160 69 230 83
|
||||||
|
91 18 237 18 328 -1z m325 -253 c52 -48 81 -117 81 -196 -1 -82 -17 -126 -70
|
||||||
|
-185 -51 -56 -120 -89 -200 -97 -49 -4 -64 -9 -60 -19 3 -8 -1 -14 -9 -14 -8
|
||||||
|
0 -15 9 -15 19 0 10 -7 21 -15 25 -12 4 -15 -4 -15 -44 0 -50 0 -50 33 -50 18
|
||||||
|
0 38 5 44 11 13 13 33 4 33 -16 0 -20 -20 -29 -33 -16 -6 6 -30 11 -54 11
|
||||||
|
l-43 0 0 56 c0 42 -4 60 -17 70 -10 7 -21 14 -25 14 -5 0 -8 -45 -8 -100 l0
|
||||||
|
-100 34 0 c19 0 38 5 41 10 12 20 35 10 35 -14 0 -32 -19 -44 -35 -23 -8 11
|
||||||
|
-27 17 -54 17 l-41 0 0 120 c0 66 -4 120 -9 120 -22 0 -18 35 13 101 l33 71
|
||||||
|
21 -36 c78 -132 275 -81 275 71 0 56 -25 91 -88 122 l-49 25 59 53 c58 52 59
|
||||||
|
53 84 37 14 -9 39 -28 54 -43z m-103 -510 c0 -13 -27 -21 -45 -15 -25 10 -17
|
||||||
|
24 15 24 17 0 30 -4 30 -9z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.8 KiB |
BIN
web/static/icon/logo.png
Normal file
BIN
web/static/icon/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
web/static/icon/logo_telco128.png
Normal file
BIN
web/static/icon/logo_telco128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
@@ -4,6 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
||||||
<title>Éxito - SIAX Monitor</title>
|
<title>Éxito - 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>
|
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
||||||
@@ -62,9 +63,11 @@
|
|||||||
<div
|
<div
|
||||||
class="size-8 bg-primary rounded-lg flex items-center justify-center"
|
class="size-8 bg-primary rounded-lg flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<span class="material-symbols-outlined text-white"
|
<img
|
||||||
>monitoring</span
|
src="/static/icon/logo.png"
|
||||||
>
|
alt="Logo"
|
||||||
|
class="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h2
|
<h2
|
||||||
class="text-white text-lg font-bold leading-tight tracking-[-0.015em]"
|
class="text-white text-lg font-bold leading-tight tracking-[-0.015em]"
|
||||||
@@ -91,13 +94,7 @@
|
|||||||
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
|
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
|
||||||
href="/select"
|
href="/select"
|
||||||
>
|
>
|
||||||
Agregar Detectada
|
Selecionar Detectada
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
|
|
||||||
href="/register"
|
|
||||||
>
|
|
||||||
Nueva App
|
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
|
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
|
||||||
@@ -106,6 +103,12 @@
|
|||||||
Registros
|
Registros
|
||||||
</a>
|
</a>
|
||||||
</nav>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
Reference in New Issue
Block a user