feat: Sistema de monitoreo base con logging y configuración dinámica
- Implementado monitor de procesos Node.js con detección automática - Sistema de logging con niveles (Info, Warning, Error, Critical) - ConfigManager para gestión dinámica de apps monitoreadas - Interfaz web básica con escaneo de procesos - Integración con API central para reportar estados - User-Agent tracking para identificación de agentes - Persistencia de configuración en JSON - Logs almacenados en archivo con rotación - Sistema modular: monitor, interface, logger, config Estructura: - src/main.rs: Orquestador principal - src/monitor.rs: Monitoreo de procesos y envío a API - src/interface.rs: Servidor web Axum con endpoints - src/logger.rs: Sistema de logging a archivo y consola - src/config.rs: Gestión de configuración persistente - web/: Templates HTML para interfaz web - config/: Configuración de apps monitoreadas - logs/: Archivos de log del sistema Features implementadas: ✅ Detección automática de procesos Node.js ✅ Monitoreo de CPU y RAM por proceso ✅ Reportes periódicos a API central (cada 60s) ✅ Interfaz web en puerto 8080 ✅ Logs estructurados con timestamps ✅ Configuración dinámica sin reinicio ✅ Script de despliegue automatizado Próximos pasos: - Integración con systemd para control de procesos - Dashboard mejorado con cards de apps - Logs en tiempo real vía WebSocket - Start/Stop/Restart de aplicaciones
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Ignored default folder with query files
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/siax_monitor.iml" filepath="$PROJECT_DIR$/.idea/siax_monitor.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
11
.idea/siax_monitor.iml
generated
Normal file
11
.idea/siax_monitor.iml
generated
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="EMPTY_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
1897
Cargo.lock
generated
Normal file
1897
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
13
Cargo.toml
Normal file
13
Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "siax_monitor"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
axum = "0.7"
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
sysinfo = "0.30"
|
||||
chrono = "0.4"
|
||||
12
config/monitored_apps.json
Normal file
12
config/monitored_apps.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"apps": [
|
||||
{
|
||||
"name": "app_tareas",
|
||||
"port": 3000
|
||||
},
|
||||
{
|
||||
"name": "fidelizacion",
|
||||
"port": 3001
|
||||
}
|
||||
]
|
||||
}
|
||||
65
desplegar_agent.sh
Executable file
65
desplegar_agent.sh
Executable file
@@ -0,0 +1,65 @@
|
||||
#!/bin/bash
|
||||
# --- CONFIGURACIÓN GLOBAL ---
|
||||
BINARY_NAME="siax_monitor" # ← Cambié esto para que coincida con Cargo.toml
|
||||
TARGET="x86_64-unknown-linux-gnu" # ← Cambié a gnu en lugar de musl
|
||||
|
||||
echo "📦 Compilando binario para Linux ($TARGET)..."
|
||||
cargo build --release --target $TARGET
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "❌ Error en la compilación."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- FUNCIÓN MAESTRA DE DESPLIEGUE ---
|
||||
# Parámetros: IP, USUARIO, RUTA_DESTINO
|
||||
deploy_to_server() {
|
||||
local IP=$1
|
||||
local USER=$2
|
||||
local DEST_PATH=$3
|
||||
|
||||
echo "------------------------------------------------"
|
||||
echo "📡 Desplegando en: $USER@$IP:$DEST_PATH"
|
||||
|
||||
# 1. Crear directorio y asegurar permisos
|
||||
ssh $USER@$IP "mkdir -p $DEST_PATH"
|
||||
|
||||
# 2. Subir binario
|
||||
scp target/$TARGET/release/$BINARY_NAME $USER@$IP:$DEST_PATH/
|
||||
|
||||
# 3. Hacer ejecutable
|
||||
ssh $USER@$IP "chmod +x $DEST_PATH/$BINARY_NAME"
|
||||
|
||||
# 4. Configurar Systemd
|
||||
echo "⚙️ Configurando servicio Systemd para $USER..."
|
||||
ssh $USER@$IP "sudo bash -c 'cat <<EOF > /etc/systemd/system/siax_monitor.service
|
||||
[Unit]
|
||||
Description=SIAX Monitor Agent - $IP
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=$USER
|
||||
WorkingDirectory=$DEST_PATH
|
||||
ExecStart=$DEST_PATH/$BINARY_NAME
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF'"
|
||||
|
||||
# 5. Reiniciar servicio
|
||||
ssh $USER@$IP "sudo systemctl daemon-reload && sudo systemctl enable siax_monitor && sudo systemctl restart siax_monitor"
|
||||
|
||||
echo "✅ Servidor $IP configurado correctamente."
|
||||
}
|
||||
|
||||
# --- LISTA PERSONALIZADA DE SERVIDORES ---
|
||||
#deploy_to_server "192.168.1.140" "root" "/root/app"
|
||||
deploy_to_server "192.168.10.145" "root" "/root/app"
|
||||
deploy_to_server "192.168.10.150" "pablinux" "/home/pablinux/app"
|
||||
deploy_to_server "192.168.10.160" "user_apps" "/home/user_apps/apps"
|
||||
|
||||
echo "------------------------------------------------"
|
||||
echo "🎉 ¡Despliegue masivo completado!"
|
||||
4
logs/errors.log
Normal file
4
logs/errors.log
Normal file
@@ -0,0 +1,4 @@
|
||||
[2026-01-11 22:01:17] [INFO] [Interface] Logs limpiados por el usuario
|
||||
[2026-01-11 22:08:35] [INFO] [Sistema] Iniciando SIAX Agent
|
||||
[2026-01-11 22:08:35] [INFO] [Sistema] Sistema SIAX completamente operativo
|
||||
[2026-01-11 22:08:35] [INFO] [Monitor] Vigilando procesos para siax-intel [SIAX-Agent/0.1.0 (linux/x86_64; Rust-Monitor)]
|
||||
129
src/config.rs
129
src/config.rs
@@ -0,0 +1,129 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use std::fs::{self, create_dir_all};
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, RwLock, OnceLock};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MonitoredApp {
|
||||
pub name: String,
|
||||
pub port: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AppConfig {
|
||||
pub apps: Vec<MonitoredApp>,
|
||||
}
|
||||
|
||||
impl Default for AppConfig {
|
||||
fn default() -> Self {
|
||||
AppConfig {
|
||||
apps: vec![
|
||||
MonitoredApp { name: "app_tareas".to_string(), port: 3000 },
|
||||
MonitoredApp { name: "fidelizacion".to_string(), port: 3001 },
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ConfigManager {
|
||||
config_path: String,
|
||||
config: Arc<RwLock<AppConfig>>,
|
||||
}
|
||||
|
||||
impl ConfigManager {
|
||||
pub fn new(config_path: &str) -> Self {
|
||||
// Crear directorio config si no existe
|
||||
if let Some(parent) = Path::new(config_path).parent() {
|
||||
let _ = create_dir_all(parent);
|
||||
}
|
||||
|
||||
// Cargar o crear configuración
|
||||
let config = Self::load_config(config_path);
|
||||
|
||||
ConfigManager {
|
||||
config_path: config_path.to_string(),
|
||||
config: Arc::new(RwLock::new(config)),
|
||||
}
|
||||
}
|
||||
|
||||
fn load_config(path: &str) -> AppConfig {
|
||||
match fs::read_to_string(path) {
|
||||
Ok(content) => {
|
||||
match serde_json::from_str(&content) {
|
||||
Ok(config) => {
|
||||
println!("✅ Configuración cargada desde: {}", path);
|
||||
config
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("⚠️ Error parseando config: {}. Usando default.", e);
|
||||
AppConfig::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
println!("ℹ️ Archivo de config no encontrado. Creando uno nuevo...");
|
||||
let default_config = AppConfig::default();
|
||||
let _ = Self::save_config_to_file(path, &default_config);
|
||||
default_config
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn save_config_to_file(path: &str, config: &AppConfig) -> std::io::Result<()> {
|
||||
let json = serde_json::to_string_pretty(config)?;
|
||||
fs::write(path, json)?;
|
||||
println!("💾 Configuración guardada en: {}", path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_apps(&self) -> Vec<MonitoredApp> {
|
||||
let config = self.config.read().unwrap();
|
||||
config.apps.clone()
|
||||
}
|
||||
|
||||
pub fn add_app(&self, name: String, port: i32) -> Result<(), String> {
|
||||
let mut config = self.config.write().unwrap();
|
||||
|
||||
// Verificar si ya existe
|
||||
if config.apps.iter().any(|app| app.name == name) {
|
||||
return Err(format!("La app '{}' ya está siendo monitoreada", name));
|
||||
}
|
||||
|
||||
config.apps.push(MonitoredApp { name, port });
|
||||
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_app(&self, name: &str) -> Result<(), String> {
|
||||
let mut config = self.config.write().unwrap();
|
||||
|
||||
let original_len = config.apps.len();
|
||||
config.apps.retain(|app| app.name != name);
|
||||
|
||||
if config.apps.len() == original_len {
|
||||
return Err(format!("La app '{}' no se encontró", name));
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_arc(&self) -> Arc<RwLock<AppConfig>> {
|
||||
Arc::clone(&self.config)
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton global del ConfigManager
|
||||
static CONFIG_MANAGER: OnceLock<ConfigManager> = OnceLock::new();
|
||||
|
||||
// ⚠️ IMPORTANTE: Esta función DEBE ser pública
|
||||
pub fn get_config_manager() -> &'static ConfigManager {
|
||||
CONFIG_MANAGER.get_or_init(|| ConfigManager::new("config/monitored_apps.json"))
|
||||
}
|
||||
260
src/interface.rs
260
src/interface.rs
@@ -0,0 +1,260 @@
|
||||
use axum::{
|
||||
routing::{get, post},
|
||||
response::Html,
|
||||
Router,
|
||||
extract::Form,
|
||||
};
|
||||
use std::net::SocketAddr;
|
||||
use sysinfo::System;
|
||||
use serde::Deserialize;
|
||||
use crate::logger::{get_logger, LogLevel};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ProcessForm {
|
||||
app_name: String,
|
||||
port: String,
|
||||
}
|
||||
|
||||
pub async fn start_web_server(port: u16) {
|
||||
let app = Router::new()
|
||||
.route("/", get(index_handler))
|
||||
.route("/scan", get(scan_processes_handler))
|
||||
.route("/select", get(select_processes_handler))
|
||||
.route("/add-process", post(add_process_handler))
|
||||
.route("/logs", get(logs_handler))
|
||||
.route("/clear-logs", post(clear_logs_handler));
|
||||
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||
println!("🖥️ Interface Web en: http://localhost:{}", port);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}
|
||||
|
||||
async fn index_handler() -> Html<String> {
|
||||
let template = include_str!("../web/index.html");
|
||||
let html = template.replace("{{SERVER_NAME}}", "siax-intel");
|
||||
Html(html)
|
||||
}
|
||||
|
||||
async fn scan_processes_handler() -> Html<String> {
|
||||
let mut sys = System::new_all();
|
||||
sys.refresh_all();
|
||||
|
||||
let template = include_str!("../web/scan.html");
|
||||
let mut content = String::new();
|
||||
let mut node_count = 0;
|
||||
|
||||
for (pid, process) in sys.processes() {
|
||||
let process_name = process.name();
|
||||
|
||||
if process_name.contains("node") {
|
||||
node_count += 1;
|
||||
let cpu = process.cpu_usage();
|
||||
let mem_mb = process.memory() as f64 / 1024.0 / 1024.0;
|
||||
|
||||
let cwd = if let Some(path) = process.cwd() {
|
||||
path.to_string_lossy().to_string()
|
||||
} else {
|
||||
"N/A".to_string()
|
||||
};
|
||||
|
||||
content.push_str(&format!(
|
||||
r#"
|
||||
<div class="process">
|
||||
<div><span class="pid">PID: {}</span> | <span class="name">{}</span></div>
|
||||
<div><span class="cpu">CPU: {:.2}%</span> | <span class="mem">RAM: {:.2} MB</span></div>
|
||||
<div class="path">📁 {}</div>
|
||||
</div>
|
||||
"#,
|
||||
pid.as_u32(),
|
||||
process_name,
|
||||
cpu,
|
||||
mem_mb,
|
||||
cwd
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if node_count == 0 {
|
||||
content = r#"<div class="no-results">⚠️ No se detectaron procesos Node.js en ejecución</div>"#.to_string();
|
||||
} else {
|
||||
let summary = format!(r#"<p class="summary">✅ Total: {} proceso(s) Node.js detectado(s)</p>"#, node_count);
|
||||
content = summary + &content;
|
||||
}
|
||||
|
||||
let html = template.replace("{{CONTENT}}", &content);
|
||||
Html(html)
|
||||
}
|
||||
|
||||
async fn select_processes_handler() -> Html<String> {
|
||||
let mut sys = System::new_all();
|
||||
sys.refresh_all();
|
||||
|
||||
let template = include_str!("../web/select.html");
|
||||
let mut processes_list = String::new();
|
||||
let mut node_processes = Vec::new();
|
||||
|
||||
for (pid, process) in sys.processes() {
|
||||
let process_name = process.name();
|
||||
|
||||
if process_name.contains("node") {
|
||||
let cwd = if let Some(path) = process.cwd() {
|
||||
path.to_string_lossy().to_string()
|
||||
} else {
|
||||
"N/A".to_string()
|
||||
};
|
||||
|
||||
node_processes.push((pid.as_u32(), process_name.to_string(), cwd));
|
||||
}
|
||||
}
|
||||
|
||||
if node_processes.is_empty() {
|
||||
processes_list = r#"<div class="no-results">⚠️ No se detectaron procesos Node.js en ejecución</div>"#.to_string();
|
||||
} else {
|
||||
for (pid, name, cwd) in node_processes {
|
||||
let suggested_name = cwd.split(&['/', '\\'][..])
|
||||
.filter(|s| !s.is_empty())
|
||||
.last()
|
||||
.unwrap_or("app");
|
||||
|
||||
processes_list.push_str(&format!(
|
||||
r#"
|
||||
<div class="process-item">
|
||||
<div class="process-info">
|
||||
<div><span class="pid">PID: {}</span> | {}</div>
|
||||
<div class="path">📁 {}</div>
|
||||
</div>
|
||||
<button class="select-btn" onclick="fillForm('{}', {})">
|
||||
✅ Seleccionar
|
||||
</button>
|
||||
</div>
|
||||
"#,
|
||||
pid, name, cwd, suggested_name, pid
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let html = template.replace("{{PROCESSES_LIST}}", &processes_list);
|
||||
Html(html)
|
||||
}
|
||||
|
||||
async fn add_process_handler(Form(form): Form<ProcessForm>) -> Html<String> {
|
||||
let logger = get_logger();
|
||||
let template = include_str!("../web/success.html");
|
||||
let port: i32 = form.port.parse().unwrap_or(0);
|
||||
|
||||
logger.info("Interface", &format!("Nuevo proceso agregado: {} en puerto {}", form.app_name, port));
|
||||
|
||||
let html = template
|
||||
.replace("{{APP_NAME}}", &form.app_name)
|
||||
.replace("{{PORT}}", &port.to_string());
|
||||
|
||||
Html(html)
|
||||
}
|
||||
|
||||
async fn logs_handler() -> Html<String> {
|
||||
let logger = get_logger();
|
||||
let template = include_str!("../web/logs.html");
|
||||
|
||||
let logs = logger.read_logs(Some(100)); // Últimos 100 logs
|
||||
|
||||
// Calcular estadísticas
|
||||
let mut info_count = 0;
|
||||
let mut warning_count = 0;
|
||||
let mut error_count = 0;
|
||||
let mut critical_count = 0;
|
||||
|
||||
for log in &logs {
|
||||
match log.level {
|
||||
LogLevel::Info => info_count += 1,
|
||||
LogLevel::Warning => warning_count += 1,
|
||||
LogLevel::Error => error_count += 1,
|
||||
LogLevel::Critical => critical_count += 1,
|
||||
}
|
||||
}
|
||||
|
||||
let stats = format!(
|
||||
r#"
|
||||
<div class="stat-item stat-info">
|
||||
<div class="stat-number" style="color: #3b82f6;">{}</div>
|
||||
<div class="stat-label">Info</div>
|
||||
</div>
|
||||
<div class="stat-item stat-warning">
|
||||
<div class="stat-number" style="color: #f59e0b;">{}</div>
|
||||
<div class="stat-label">Warnings</div>
|
||||
</div>
|
||||
<div class="stat-item stat-error">
|
||||
<div class="stat-number" style="color: #ef4444;">{}</div>
|
||||
<div class="stat-label">Errors</div>
|
||||
</div>
|
||||
<div class="stat-item stat-critical">
|
||||
<div class="stat-number" style="color: #dc2626;">{}</div>
|
||||
<div class="stat-label">Critical</div>
|
||||
</div>
|
||||
"#,
|
||||
info_count, warning_count, error_count, critical_count
|
||||
);
|
||||
|
||||
let mut logs_html = String::new();
|
||||
|
||||
if logs.is_empty() {
|
||||
logs_html = r#"<div class="no-logs">📭 No hay logs registrados</div>"#.to_string();
|
||||
} else {
|
||||
for log in logs {
|
||||
let level_class = match log.level {
|
||||
LogLevel::Info => "info",
|
||||
LogLevel::Warning => "warning",
|
||||
LogLevel::Error => "error",
|
||||
LogLevel::Critical => "critical",
|
||||
};
|
||||
|
||||
let details_html = if let Some(details) = &log.details {
|
||||
format!(r#"<div class="log-details">📝 {}</div>"#, details)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
logs_html.push_str(&format!(
|
||||
r#"
|
||||
<div class="log-entry log-{}" data-level="{}">
|
||||
<div class="log-header">
|
||||
<span class="log-module">[{}]</span>
|
||||
<span class="log-timestamp">{}</span>
|
||||
</div>
|
||||
<div class="log-message">{} {}</div>
|
||||
{}
|
||||
</div>
|
||||
"#,
|
||||
level_class,
|
||||
level_class,
|
||||
log.module,
|
||||
log.timestamp,
|
||||
log.level.emoji(),
|
||||
log.message,
|
||||
details_html
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let html = template
|
||||
.replace("{{STATS}}", &stats)
|
||||
.replace("{{LOGS}}", &logs_html);
|
||||
|
||||
Html(html)
|
||||
}
|
||||
|
||||
async fn clear_logs_handler() -> Html<&'static str> {
|
||||
let logger = get_logger();
|
||||
|
||||
match logger.clear_logs() {
|
||||
Ok(_) => {
|
||||
logger.info("Interface", "Logs limpiados por el usuario");
|
||||
Html("OK")
|
||||
}
|
||||
Err(e) => {
|
||||
logger.error("Interface", "Error al limpiar logs", Some(&e.to_string()));
|
||||
Html("ERROR")
|
||||
}
|
||||
}
|
||||
}
|
||||
237
src/logger.rs
237
src/logger.rs
@@ -0,0 +1,237 @@
|
||||
use std::fs::{OpenOptions, create_dir_all};
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use chrono::Local;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LogEntry {
|
||||
pub timestamp: String,
|
||||
pub level: LogLevel,
|
||||
pub module: String,
|
||||
pub message: String,
|
||||
pub details: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum LogLevel {
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
Critical,
|
||||
}
|
||||
|
||||
impl LogLevel {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
LogLevel::Info => "INFO",
|
||||
LogLevel::Warning => "WARNING",
|
||||
LogLevel::Error => "ERROR",
|
||||
LogLevel::Critical => "CRITICAL",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color(&self) -> &str {
|
||||
match self {
|
||||
LogLevel::Info => "#3b82f6",
|
||||
LogLevel::Warning => "#f59e0b",
|
||||
LogLevel::Error => "#ef4444",
|
||||
LogLevel::Critical => "#dc2626",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn emoji(&self) -> &str {
|
||||
match self {
|
||||
LogLevel::Info => "ℹ️",
|
||||
LogLevel::Warning => "⚠️",
|
||||
LogLevel::Error => "❌",
|
||||
LogLevel::Critical => "🔥",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Logger {
|
||||
log_file: String,
|
||||
}
|
||||
|
||||
impl Logger {
|
||||
pub fn new() -> Self {
|
||||
let log_file = "logs/errors.log".to_string();
|
||||
|
||||
// Crear directorio logs si no existe
|
||||
if let Some(parent) = Path::new(&log_file).parent() {
|
||||
let _ = create_dir_all(parent);
|
||||
}
|
||||
|
||||
Logger { log_file }
|
||||
}
|
||||
|
||||
pub fn log(&self, level: LogLevel, module: &str, message: &str, details: Option<&str>) {
|
||||
let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
||||
|
||||
let entry = LogEntry {
|
||||
timestamp: timestamp.clone(),
|
||||
level: level.clone(),
|
||||
module: module.to_string(),
|
||||
message: message.to_string(),
|
||||
details: details.map(|d| d.to_string()),
|
||||
};
|
||||
|
||||
// Escribir en archivo
|
||||
if let Err(e) = self.write_to_file(&entry) {
|
||||
eprintln!("Error escribiendo log: {}", e);
|
||||
}
|
||||
|
||||
// También imprimir en consola
|
||||
self.print_to_console(&entry);
|
||||
}
|
||||
|
||||
fn write_to_file(&self, entry: &LogEntry) -> std::io::Result<()> {
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&self.log_file)?;
|
||||
|
||||
let log_line = if let Some(details) = &entry.details {
|
||||
format!(
|
||||
"[{}] [{}] [{}] {} | {}\n",
|
||||
entry.timestamp,
|
||||
entry.level.as_str(),
|
||||
entry.module,
|
||||
entry.message,
|
||||
details
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"[{}] [{}] [{}] {}\n",
|
||||
entry.timestamp,
|
||||
entry.level.as_str(),
|
||||
entry.module,
|
||||
entry.message
|
||||
)
|
||||
};
|
||||
|
||||
file.write_all(log_line.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_to_console(&self, entry: &LogEntry) {
|
||||
let emoji = entry.level.emoji();
|
||||
let level = entry.level.as_str();
|
||||
|
||||
if let Some(details) = &entry.details {
|
||||
println!(
|
||||
"{} [{}] [{}] {} | {}",
|
||||
emoji, level, entry.module, entry.message, details
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"{} [{}] [{}] {}",
|
||||
emoji, level, entry.module, entry.message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_logs(&self, limit: Option<usize>) -> Vec<LogEntry> {
|
||||
use std::fs::File;
|
||||
use std::io::{BufRead, BufReader};
|
||||
|
||||
let mut entries = Vec::new();
|
||||
|
||||
if let Ok(file) = File::open(&self.log_file) {
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
for line in reader.lines() {
|
||||
if let Ok(line_content) = line {
|
||||
if let Some(entry) = self.parse_log_line(&line_content) {
|
||||
entries.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Invertir para mostrar los más recientes primero
|
||||
entries.reverse();
|
||||
|
||||
// Limitar cantidad si se especifica
|
||||
if let Some(limit) = limit {
|
||||
entries.truncate(limit);
|
||||
}
|
||||
|
||||
entries
|
||||
}
|
||||
|
||||
fn parse_log_line(&self, line: &str) -> Option<LogEntry> {
|
||||
// Formato: [timestamp] [level] [module] message | details
|
||||
let parts: Vec<&str> = line.split("] [").collect();
|
||||
|
||||
if parts.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let timestamp = parts[0].trim_start_matches('[').to_string();
|
||||
let level_str = parts[1];
|
||||
let module = parts[2].to_string();
|
||||
|
||||
let level = match level_str {
|
||||
"INFO" => LogLevel::Info,
|
||||
"WARNING" => LogLevel::Warning,
|
||||
"ERROR" => LogLevel::Error,
|
||||
"CRITICAL" => LogLevel::Critical,
|
||||
_ => LogLevel::Info,
|
||||
};
|
||||
|
||||
// El resto es el mensaje y detalles
|
||||
let rest = parts.get(3..)?.join("] [");
|
||||
let rest = rest.trim_end_matches(']');
|
||||
|
||||
let (message, details) = if let Some(idx) = rest.find(" | ") {
|
||||
let msg = rest[..idx].to_string();
|
||||
let det = rest[idx + 3..].to_string();
|
||||
(msg, Some(det))
|
||||
} else {
|
||||
(rest.to_string(), None)
|
||||
};
|
||||
|
||||
Some(LogEntry {
|
||||
timestamp,
|
||||
level,
|
||||
module,
|
||||
message,
|
||||
details,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn clear_logs(&self) -> std::io::Result<()> {
|
||||
use std::fs;
|
||||
fs::write(&self.log_file, "")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Métodos de conveniencia
|
||||
pub fn info(&self, module: &str, message: &str) {
|
||||
self.log(LogLevel::Info, module, message, None);
|
||||
}
|
||||
|
||||
pub fn warning(&self, module: &str, message: &str, details: Option<&str>) {
|
||||
self.log(LogLevel::Warning, module, message, details);
|
||||
}
|
||||
|
||||
pub fn error(&self, module: &str, message: &str, details: Option<&str>) {
|
||||
self.log(LogLevel::Error, module, message, details);
|
||||
}
|
||||
|
||||
pub fn critical(&self, module: &str, message: &str, details: Option<&str>) {
|
||||
self.log(LogLevel::Critical, module, message, details);
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton global del logger
|
||||
static LOGGER: OnceLock<Logger> = OnceLock::new();
|
||||
|
||||
// ⚠️ IMPORTANTE: Esta función DEBE ser pública
|
||||
pub fn get_logger() -> &'static Logger {
|
||||
LOGGER.get_or_init(|| Logger::new())
|
||||
}
|
||||
39
src/main.rs
Normal file
39
src/main.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
mod monitor;
|
||||
mod interface;
|
||||
mod logger;
|
||||
mod config;
|
||||
|
||||
use logger::get_logger;
|
||||
use config::get_config_manager;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Inicializar logger
|
||||
let logger = get_logger();
|
||||
logger.info("Sistema", "Iniciando SIAX Agent");
|
||||
|
||||
// Inicializar config manager
|
||||
let config_manager = get_config_manager();
|
||||
let apps = config_manager.get_apps();
|
||||
println!("📋 Apps a monitorear: {:?}", apps);
|
||||
|
||||
let server_name = "siax-intel".to_string();
|
||||
let api_key = "ak_VVeNzGxK2mCq8s7YpFtHjL3b9dR4TuZ6".to_string();
|
||||
let cloud_url = "https://api.siax-system.net/api/apps_servcs/apps".to_string();
|
||||
|
||||
// 1. Iniciamos el Monitor
|
||||
let monitor_handle = tokio::spawn(async move {
|
||||
monitor::run_monitoring(server_name, api_key, cloud_url).await;
|
||||
});
|
||||
|
||||
// 2. Iniciamos la Interface Web
|
||||
let web_handle = tokio::spawn(async move {
|
||||
interface::start_web_server(8080).await;
|
||||
});
|
||||
|
||||
println!("✅ Sistema SIAX operativo. Monitor en segundo plano e Interface en puerto 8080.");
|
||||
logger.info("Sistema", "Sistema SIAX completamente operativo");
|
||||
|
||||
// Esperamos a ambos
|
||||
let _ = tokio::join!(monitor_handle, web_handle);
|
||||
}
|
||||
160
src/monitor.rs
160
src/monitor.rs
@@ -0,0 +1,160 @@
|
||||
use sysinfo::System;
|
||||
use serde::Serialize;
|
||||
use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT};
|
||||
use std::time::Duration;
|
||||
use crate::logger::get_logger;
|
||||
use crate::config::get_config_manager;
|
||||
|
||||
// User-Agent dinámico
|
||||
fn generate_user_agent() -> String {
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
let os = std::env::consts::OS;
|
||||
let arch = std::env::consts::ARCH;
|
||||
|
||||
format!("SIAX-Agent/{} ({}/{}; Rust-Monitor)", version, os, arch)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct AppStatusUpdate {
|
||||
app_name: String,
|
||||
server: String,
|
||||
status: String,
|
||||
port: i32,
|
||||
pid: i32,
|
||||
memory_usage: String,
|
||||
cpu_usage: String,
|
||||
last_check: String,
|
||||
}
|
||||
|
||||
pub async fn run_monitoring(server_name: String, api_key: String, cloud_url: String) {
|
||||
let logger = get_logger();
|
||||
let config_manager = get_config_manager();
|
||||
let mut sys = System::new_all();
|
||||
let user_agent = generate_user_agent();
|
||||
|
||||
logger.info("Monitor", &format!("Vigilando procesos para {} [{}]", server_name, user_agent));
|
||||
println!("🚀 Monitor: Vigilando procesos para {}", server_name);
|
||||
println!("📡 User-Agent: {}", user_agent);
|
||||
|
||||
loop {
|
||||
sys.refresh_all();
|
||||
|
||||
// ✨ LEER APPS DESDE CONFIG (dinámico)
|
||||
let apps_to_monitor = config_manager.get_apps();
|
||||
|
||||
if apps_to_monitor.is_empty() {
|
||||
logger.warning("Monitor", "No hay apps configuradas para monitorear", None);
|
||||
}
|
||||
|
||||
for app in apps_to_monitor {
|
||||
let data = collect_metrics(&sys, &app.name, app.port, &server_name);
|
||||
|
||||
match send_to_cloud(data, &api_key, &cloud_url, &user_agent).await {
|
||||
Ok(_) => {},
|
||||
Err(e) => {
|
||||
logger.error(
|
||||
"Monitor",
|
||||
&format!("Error enviando {}", app.name),
|
||||
Some(&e.to_string())
|
||||
);
|
||||
eprintln!("❌ Error enviando {}: {}", app.name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(60)).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_metrics(sys: &System, name: &str, port: i32, server: &str) -> AppStatusUpdate {
|
||||
let mut pid_encontrado = 0;
|
||||
let mut cpu = 0.0;
|
||||
let mut mem = 0.0;
|
||||
let mut status = "stopped".to_string();
|
||||
|
||||
for (pid, process) in sys.processes() {
|
||||
let process_name = process.name();
|
||||
|
||||
if process_name.contains("node") {
|
||||
if let Some(cwd) = process.cwd() {
|
||||
let cwd_str = cwd.to_string_lossy();
|
||||
if cwd_str.contains(name) {
|
||||
pid_encontrado = pid.as_u32() as i32;
|
||||
cpu = process.cpu_usage();
|
||||
mem = process.memory() as f64 / 1024.0 / 1024.0;
|
||||
status = "running".to_string();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let now = chrono::Local::now();
|
||||
let timestamp = now.format("%Y-%m-%d %H:%M:%S").to_string();
|
||||
|
||||
AppStatusUpdate {
|
||||
app_name: name.to_string(),
|
||||
server: server.to_string(),
|
||||
status,
|
||||
port,
|
||||
pid: pid_encontrado,
|
||||
memory_usage: format!("{:.2}MB", mem),
|
||||
cpu_usage: format!("{:.2}%", cpu),
|
||||
last_check: timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_to_cloud(
|
||||
data: AppStatusUpdate,
|
||||
api_key: &str,
|
||||
cloud_url: &str,
|
||||
user_agent: &str
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let logger = get_logger();
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
"x-api-key",
|
||||
HeaderValue::from_str(api_key)?
|
||||
);
|
||||
headers.insert(
|
||||
"Content-Type",
|
||||
HeaderValue::from_static("application/json")
|
||||
);
|
||||
headers.insert(
|
||||
USER_AGENT,
|
||||
HeaderValue::from_str(user_agent)?
|
||||
);
|
||||
|
||||
let response = client
|
||||
.post(cloud_url)
|
||||
.headers(headers)
|
||||
.json(&data)
|
||||
.timeout(Duration::from_secs(10))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
println!("📤 {} -> {} (PID: {}, CPU: {}, RAM: {})",
|
||||
data.app_name,
|
||||
data.status,
|
||||
data.pid,
|
||||
data.cpu_usage,
|
||||
data.memory_usage
|
||||
);
|
||||
Ok(())
|
||||
} else {
|
||||
let status = response.status();
|
||||
let error_text = response.text().await.unwrap_or_else(|_| "Sin respuesta".to_string());
|
||||
|
||||
logger.error(
|
||||
"Monitor",
|
||||
&format!("Error enviando datos de {}", data.app_name),
|
||||
Some(&format!("HTTP {}: {}", status, error_text))
|
||||
);
|
||||
|
||||
eprintln!("⚠️ Error HTTP {}: {}", status, error_text);
|
||||
Err(format!("HTTP {}: {}", status, error_text).into())
|
||||
}
|
||||
}
|
||||
78
web/index.html
Normal file
78
web/index.html
Normal file
@@ -0,0 +1,78 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>SIAX Emergency Panel</title>
|
||||
<style>
|
||||
body {
|
||||
background: #0f172a;
|
||||
color: white;
|
||||
font-family: sans-serif;
|
||||
padding: 40px;
|
||||
}
|
||||
h1 {
|
||||
color: #3b82f6;
|
||||
}
|
||||
.status-online {
|
||||
color: #22c55e;
|
||||
font-weight: bold;
|
||||
}
|
||||
.server-info {
|
||||
color: #94a3b8;
|
||||
}
|
||||
.button-container {
|
||||
margin-top: 30px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #3b82f6;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
.btn-success {
|
||||
background: #22c55e;
|
||||
}
|
||||
.btn-success:hover {
|
||||
background: #16a34a;
|
||||
}
|
||||
.btn-warning {
|
||||
background: #f59e0b;
|
||||
}
|
||||
.btn-warning:hover {
|
||||
background: #d97706;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🚨 SIAX EMERGENCY PANEL</h1>
|
||||
<p>Estado del Agente: <span class="status-online">● ONLINE</span></p>
|
||||
<p class="server-info">Servidor: {{SERVER_NAME}}</p>
|
||||
|
||||
<div class="button-container">
|
||||
<a href="/scan" class="btn btn-primary">
|
||||
🔍 Escanear Sistema
|
||||
</a>
|
||||
|
||||
<a href="/select" class="btn btn-success">
|
||||
⚙️ Gestionar Procesos
|
||||
</a>
|
||||
|
||||
<a href="/logs" class="btn btn-warning">
|
||||
📋 Ver Logs
|
||||
</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
246
web/logs.html
Normal file
246
web/logs.html
Normal file
@@ -0,0 +1,246 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Logs del Sistema - SIAX</title>
|
||||
<style>
|
||||
body {
|
||||
background: #0f172a;
|
||||
color: white;
|
||||
font-family: 'Courier New', monospace;
|
||||
padding: 40px;
|
||||
margin: 0;
|
||||
}
|
||||
h1 {
|
||||
color: #3b82f6;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
.controls {
|
||||
margin: 20px 0;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #3b82f6;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
.btn-danger {
|
||||
background: #ef4444;
|
||||
}
|
||||
.btn-danger:hover {
|
||||
background: #dc2626;
|
||||
}
|
||||
.btn-secondary {
|
||||
background: #64748b;
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background: #475569;
|
||||
}
|
||||
.stats {
|
||||
background: #1e293b;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.stat-info {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border: 1px solid #3b82f6;
|
||||
}
|
||||
.stat-warning {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
border: 1px solid #f59e0b;
|
||||
}
|
||||
.stat-error {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid #ef4444;
|
||||
}
|
||||
.stat-critical {
|
||||
background: rgba(220, 38, 38, 0.1);
|
||||
border: 1px solid #dc2626;
|
||||
}
|
||||
.stat-number {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.log-entry {
|
||||
background: #1e293b;
|
||||
padding: 15px;
|
||||
margin: 10px 0;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.log-info {
|
||||
border-left-color: #3b82f6;
|
||||
}
|
||||
.log-warning {
|
||||
border-left-color: #f59e0b;
|
||||
}
|
||||
.log-error {
|
||||
border-left-color: #ef4444;
|
||||
}
|
||||
.log-critical {
|
||||
border-left-color: #dc2626;
|
||||
background: rgba(220, 38, 38, 0.1);
|
||||
}
|
||||
.log-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.log-timestamp {
|
||||
color: #94a3b8;
|
||||
font-size: 11px;
|
||||
}
|
||||
.log-level {
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.log-module {
|
||||
color: #60a5fa;
|
||||
}
|
||||
.log-message {
|
||||
margin: 8px 0;
|
||||
}
|
||||
.log-details {
|
||||
background: #0f172a;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin-top: 8px;
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
}
|
||||
.no-logs {
|
||||
background: #1e293b;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
border-radius: 8px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.filter-bar {
|
||||
background: #1e293b;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.filter-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
.filter-checkbox input {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.filter-checkbox label {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>📋 Logs del Sistema SIAX</h1>
|
||||
|
||||
<div class="controls">
|
||||
<button onclick="location.reload()" class="btn btn-primary">🔄 Refrescar</button>
|
||||
<button onclick="clearLogs()" class="btn btn-danger">🗑️ Limpiar Logs</button>
|
||||
<a href="/" class="btn btn-secondary">← Volver al Panel</a>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
{{STATS}}
|
||||
</div>
|
||||
|
||||
<div class="filter-bar">
|
||||
<span style="color: #60a5fa; font-weight: bold;">Filtrar por nivel:</span>
|
||||
<div class="filter-checkbox">
|
||||
<input type="checkbox" id="filter-info" checked onchange="filterLogs()">
|
||||
<label for="filter-info">ℹ️ Info</label>
|
||||
</div>
|
||||
<div class="filter-checkbox">
|
||||
<input type="checkbox" id="filter-warning" checked onchange="filterLogs()">
|
||||
<label for="filter-warning">⚠️ Warning</label>
|
||||
</div>
|
||||
<div class="filter-checkbox">
|
||||
<input type="checkbox" id="filter-error" checked onchange="filterLogs()">
|
||||
<label for="filter-error">❌ Error</label>
|
||||
</div>
|
||||
<div class="filter-checkbox">
|
||||
<input type="checkbox" id="filter-critical" checked onchange="filterLogs()">
|
||||
<label for="filter-critical">🔥 Critical</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="logs-container">
|
||||
{{LOGS}}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function clearLogs() {
|
||||
if (confirm('¿Estás seguro de que quieres eliminar todos los logs?')) {
|
||||
fetch('/clear-logs', { method: 'POST' })
|
||||
.then(() => location.reload())
|
||||
.catch(err => alert('Error al limpiar logs: ' + err));
|
||||
}
|
||||
}
|
||||
|
||||
function filterLogs() {
|
||||
const showInfo = document.getElementById('filter-info').checked;
|
||||
const showWarning = document.getElementById('filter-warning').checked;
|
||||
const showError = document.getElementById('filter-error').checked;
|
||||
const showCritical = document.getElementById('filter-critical').checked;
|
||||
|
||||
const logs = document.querySelectorAll('.log-entry');
|
||||
logs.forEach(log => {
|
||||
const level = log.dataset.level;
|
||||
let show = false;
|
||||
|
||||
if (level === 'info' && showInfo) show = true;
|
||||
if (level === 'warning' && showWarning) show = true;
|
||||
if (level === 'error' && showError) show = true;
|
||||
if (level === 'critical' && showCritical) show = true;
|
||||
|
||||
log.style.display = show ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-refresh cada 30 segundos
|
||||
setTimeout(() => location.reload(), 30000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
71
web/scan.html
Normal file
71
web/scan.html
Normal file
@@ -0,0 +1,71 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Scan Results - SIAX</title>
|
||||
<style>
|
||||
body {
|
||||
background: #0f172a;
|
||||
color: white;
|
||||
font-family: monospace;
|
||||
padding: 40px;
|
||||
}
|
||||
h1 {
|
||||
color: #3b82f6;
|
||||
}
|
||||
.process {
|
||||
background: #1e293b;
|
||||
padding: 15px;
|
||||
margin: 10px 0;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #3b82f6;
|
||||
}
|
||||
.pid {
|
||||
color: #22c55e;
|
||||
font-weight: bold;
|
||||
}
|
||||
.name {
|
||||
color: #60a5fa;
|
||||
}
|
||||
.cpu {
|
||||
color: #f59e0b;
|
||||
}
|
||||
.mem {
|
||||
color: #8b5cf6;
|
||||
}
|
||||
.path {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.summary {
|
||||
color: #22c55e;
|
||||
font-size: 18px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.no-results {
|
||||
background: #7f1d1d;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #ef4444;
|
||||
}
|
||||
.back-btn {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
background: #64748b;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.back-btn:hover {
|
||||
background: #475569;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🔍 Escaneo de Procesos Node.js</h1>
|
||||
{{CONTENT}}
|
||||
<a href="/" class="back-btn">← Volver al Panel</a>
|
||||
</body>
|
||||
</html>
|
||||
160
web/select.html
Normal file
160
web/select.html
Normal file
@@ -0,0 +1,160 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Gestionar Procesos - SIAX</title>
|
||||
<style>
|
||||
body {
|
||||
background: #0f172a;
|
||||
color: white;
|
||||
font-family: sans-serif;
|
||||
padding: 40px;
|
||||
}
|
||||
h1 {
|
||||
color: #3b82f6;
|
||||
}
|
||||
h2 {
|
||||
color: #60a5fa;
|
||||
margin-top: 40px;
|
||||
}
|
||||
.process-item {
|
||||
background: #1e293b;
|
||||
padding: 15px;
|
||||
margin: 10px 0;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.process-info {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.pid {
|
||||
color: #22c55e;
|
||||
font-weight: bold;
|
||||
}
|
||||
.path {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.select-btn {
|
||||
padding: 8px 16px;
|
||||
background: #22c55e;
|
||||
border: none;
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
.select-btn:hover {
|
||||
background: #16a34a;
|
||||
}
|
||||
.form-section {
|
||||
background: #1e293b;
|
||||
padding: 25px;
|
||||
border-radius: 12px;
|
||||
margin-top: 20px;
|
||||
border: 2px solid #3b82f6;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
color: #60a5fa;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background: #0f172a;
|
||||
border: 2px solid #475569;
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
small {
|
||||
color: #94a3b8;
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.submit-btn {
|
||||
padding: 12px 24px;
|
||||
background: #3b82f6;
|
||||
border: none;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.submit-btn:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
.back-btn {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
background: #64748b;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.back-btn:hover {
|
||||
background: #475569;
|
||||
}
|
||||
.no-results {
|
||||
background: #7f1d1d;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #ef4444;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>⚙️ Gestionar Procesos a Monitorear</h1>
|
||||
|
||||
<h2>📋 Procesos Node.js Detectados</h2>
|
||||
{{PROCESSES_LIST}}
|
||||
|
||||
<h2>➕ Agregar Proceso Personalizado</h2>
|
||||
<div class="form-section">
|
||||
<form method="POST" action="/add-process">
|
||||
<div class="form-group">
|
||||
<label for="app_name">Nombre de la Aplicación:</label>
|
||||
<input type="text" id="app_name" name="app_name" placeholder="Ej: app_tareas, fidelizacion, mi-api" required>
|
||||
<small>💡 Este nombre se usará para identificar el proceso en el directorio de trabajo</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="port">Puerto:</label>
|
||||
<input type="number" id="port" name="port" placeholder="Ej: 3000, 3001, 8080" required>
|
||||
<small>💡 Puerto donde corre la aplicación</small>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="submit-btn">💾 Guardar y Monitorear</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<a href="/" class="back-btn">← Volver al Panel</a>
|
||||
|
||||
<script>
|
||||
function fillForm(appName, pid) {
|
||||
document.getElementById('app_name').value = appName;
|
||||
document.querySelector('.form-section').scrollIntoView({ behavior: 'smooth' });
|
||||
document.querySelector('.form-section').style.borderColor = '#22c55e';
|
||||
setTimeout(() => {
|
||||
document.querySelector('.form-section').style.borderColor = '#3b82f6';
|
||||
}, 2000);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
58
web/success.html
Normal file
58
web/success.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="refresh" content="3;url=/select">
|
||||
<title>Proceso Agregado - SIAX</title>
|
||||
<style>
|
||||
body {
|
||||
background: #0f172a;
|
||||
color: white;
|
||||
font-family: sans-serif;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
.success {
|
||||
background: #064e3b;
|
||||
border: 2px solid #22c55e;
|
||||
padding: 40px;
|
||||
border-radius: 16px;
|
||||
max-width: 500px;
|
||||
margin: 100px auto;
|
||||
}
|
||||
h1 {
|
||||
color: #22c55e;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.details {
|
||||
background: #0f172a;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-top: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
.label {
|
||||
color: #60a5fa;
|
||||
font-weight: bold;
|
||||
}
|
||||
.redirect-msg {
|
||||
color: #94a3b8;
|
||||
margin-top: 30px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="success">
|
||||
<h1>✅ Proceso Agregado Exitosamente</h1>
|
||||
<p style="color:#94a3b8;">El proceso será monitoreado en el próximo ciclo</p>
|
||||
|
||||
<div class="details">
|
||||
<p><span class="label">Aplicación:</span> {{APP_NAME}}</p>
|
||||
<p><span class="label">Puerto:</span> {{PORT}}</p>
|
||||
</div>
|
||||
|
||||
<p class="redirect-msg">Redirigiendo en 3 segundos...</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user