feat: Implementación completa Fase 4 - Sistema de monitoreo con API REST y WebSocket

 Nuevas funcionalidades:
- API REST unificada en puerto 8080 (eliminado CORS)
- WebSocket para logs en tiempo real desde journalctl
- Integración completa con systemd para gestión de servicios
- Escaneo automático de procesos Node.js y Python
- Rate limiting (1 operación/segundo por app)
- Interface web moderna con Tailwind CSS (tema oscuro)
- Documentación API estilo Swagger completamente en español

🎨 Interface Web (todas las páginas en español):
- Dashboard con estadísticas en tiempo real
- Visor de escaneo de procesos con filtros
- Formulario de registro de aplicaciones con variables de entorno
- Visor de logs en tiempo real con WebSocket y sidebar
- Página de selección de apps detectadas
- Documentación completa de API REST

🏗️ Arquitectura:
- Módulo models: ServiceConfig, ManagedApp, AppStatus
- Módulo systemd: wrapper de systemctl, generador de .service, parser
- Módulo orchestrator: AppManager, LifecycleManager con validaciones
- Módulo api: handlers REST, WebSocket manager, DTOs
- Servidor unificado en puerto 8080 (Web + API + WS)

🔧 Mejoras técnicas:
- Eliminación de CORS mediante servidor unificado
- Separación clara frontend/backend con carga dinámica
- Thread-safe con Arc<DashMap> para estado compartido
- Reconciliación de estados: sysinfo vs systemd
- Validaciones de paths, usuarios y configuraciones
- Manejo robusto de errores con thiserror

📝 Documentación:
- README.md actualizado con arquitectura completa
- EJEMPLOS.md con casos de uso detallados
- ESTADO_PROYECTO.md con progreso de Fase 4
- API docs interactiva en /api-docs
- Script de despliegue mejorado con health checks

🚀 Producción:
- Deployment script con validaciones
- Health checks y rollback capability
- Configuración de sudoers para systemctl
- Hardening de seguridad en servicios systemd
This commit is contained in:
2026-01-13 08:24:13 -05:00
parent 3595e55a1e
commit b0489739cf
33 changed files with 6893 additions and 1261 deletions

195
src/api/handlers.rs Normal file
View File

@@ -0,0 +1,195 @@
use axum::{
extract::{Path, State},
http::StatusCode,
response::Json,
};
use std::sync::Arc;
use sysinfo::System;
use crate::orchestrator::{AppManager, LifecycleManager};
use crate::models::{ServiceConfig, RestartPolicy, AppType};
use super::dto::*;
pub struct ApiState {
pub app_manager: Arc<AppManager>,
pub lifecycle_manager: Arc<LifecycleManager>,
}
pub async fn register_app_handler(
State(state): State<Arc<ApiState>>,
Json(payload): Json<RegisterAppRequest>,
) -> Result<Json<ApiResponse<OperationResponse>>, StatusCode> {
// 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,
};
match state.app_manager.register_app(config) {
Ok(_) => Ok(Json(ApiResponse::success(OperationResponse {
app_name: payload.app_name,
operation: "register".to_string(),
success: true,
message: "Aplicación registrada exitosamente".to_string(),
}))),
Err(e) => Ok(Json(ApiResponse::error(e.to_string()))),
}
}
pub async fn unregister_app_handler(
State(state): State<Arc<ApiState>>,
Path(app_name): Path<String>,
) -> Result<Json<ApiResponse<OperationResponse>>, StatusCode> {
match state.app_manager.unregister_app(&app_name) {
Ok(_) => Ok(Json(ApiResponse::success(OperationResponse {
app_name: app_name.clone(),
operation: "unregister".to_string(),
success: true,
message: "Aplicación eliminada exitosamente".to_string(),
}))),
Err(e) => Ok(Json(ApiResponse::error(e.to_string()))),
}
}
pub async fn start_app_handler(
State(state): State<Arc<ApiState>>,
Path(app_name): Path<String>,
) -> Result<Json<ApiResponse<OperationResponse>>, StatusCode> {
match state.lifecycle_manager.start_app(&app_name) {
Ok(_) => Ok(Json(ApiResponse::success(OperationResponse {
app_name: app_name.clone(),
operation: "start".to_string(),
success: true,
message: "Aplicación iniciada exitosamente".to_string(),
}))),
Err(e) => Ok(Json(ApiResponse::error(e.to_string()))),
}
}
pub async fn stop_app_handler(
State(state): State<Arc<ApiState>>,
Path(app_name): Path<String>,
) -> Result<Json<ApiResponse<OperationResponse>>, StatusCode> {
match state.lifecycle_manager.stop_app(&app_name) {
Ok(_) => Ok(Json(ApiResponse::success(OperationResponse {
app_name: app_name.clone(),
operation: "stop".to_string(),
success: true,
message: "Aplicación detenida exitosamente".to_string(),
}))),
Err(e) => Ok(Json(ApiResponse::error(e.to_string()))),
}
}
pub async fn restart_app_handler(
State(state): State<Arc<ApiState>>,
Path(app_name): Path<String>,
) -> Result<Json<ApiResponse<OperationResponse>>, StatusCode> {
match state.lifecycle_manager.restart_app(&app_name) {
Ok(_) => Ok(Json(ApiResponse::success(OperationResponse {
app_name: app_name.clone(),
operation: "restart".to_string(),
success: true,
message: "Aplicación reiniciada exitosamente".to_string(),
}))),
Err(e) => Ok(Json(ApiResponse::error(e.to_string()))),
}
}
pub async fn get_app_status_handler(
State(state): State<Arc<ApiState>>,
Path(app_name): Path<String>,
) -> Result<Json<ApiResponse<AppStatusResponse>>, StatusCode> {
match state.app_manager.get_app_status(&app_name) {
Some(managed_app) => {
let response = AppStatusResponse {
name: managed_app.name,
status: managed_app.status.as_str().to_string(),
pid: managed_app.pid,
cpu_usage: managed_app.cpu_usage,
memory_usage: format!("{:.2} MB", managed_app.memory_usage as f64 / 1024.0 / 1024.0),
systemd_status: managed_app.systemd_status.as_str().to_string(),
last_updated: managed_app.last_updated,
};
Ok(Json(ApiResponse::success(response)))
}
None => Ok(Json(ApiResponse::error(
format!("Aplicación '{}' no encontrada", app_name)
))),
}
}
pub async fn list_apps_handler(
State(state): State<Arc<ApiState>>,
) -> Result<Json<ApiResponse<AppListResponse>>, StatusCode> {
let apps = state.app_manager.list_apps();
let total = apps.len();
Ok(Json(ApiResponse::success(AppListResponse { apps, total })))
}
pub async fn scan_processes_handler() -> Result<Json<ApiResponse<ProcessScanResponse>>, StatusCode> {
let mut sys = System::new_all();
sys.refresh_all();
let mut detected_processes = Vec::new();
for (pid, process) in sys.processes() {
let process_name = process.name().to_lowercase();
let cmd = process.cmd().join(" ").to_lowercase();
let process_type = if process_name.contains("node") || cmd.contains("node") {
Some("nodejs")
} else if process_name.contains("python") || cmd.contains("python") {
Some("python")
} else {
None
};
if let Some(ptype) = process_type {
detected_processes.push(DetectedProcess {
pid: pid.as_u32() as i32,
name: process.name().to_string(),
user: process.user_id().map(|u| u.to_string()),
cpu_usage: process.cpu_usage() as f64,
memory_mb: process.memory() as f64 / 1024.0 / 1024.0,
process_type: ptype.to_string(),
});
}
}
let total = detected_processes.len();
Ok(Json(ApiResponse::success(ProcessScanResponse {
processes: detected_processes,
total,
})))
}