feat: Implementación completa de funcionalidad EDITAR apps (CRUD Update)
Backend: - Endpoint PUT /api/apps/:name para actualizar configuración completa - Endpoint GET /api/apps/:name para obtener datos de una app específica - update_app_handler(): detiene servicio, regenera .service, daemon-reload, actualiza JSON, reinicia - Soft delete de versión anterior al actualizar (mantiene historial) - Logs detallados en cada paso del proceso de actualización - Recarga automática de variables desde .env al actualizar Frontend: - Nueva página /edit?app=NOMBRE para editar apps - Formulario pre-poblado con datos actuales de la app - Nombre de app readonly (no se puede cambiar para evitar inconsistencias) - Botón Editar (morado) en panel principal junto a logs/eliminar - PUT en lugar de POST, mensaje de éxito actualizado - Redirección automática al panel después de 2 segundos Casos de uso resueltos: ✅ Cambiar usuario (ej: GatewaySIGMA con usuario incorrecto) ✅ Actualizar puerto ✅ Modificar variables de entorno ✅ Cambiar política de reinicio ✅ Actualizar ruta del script ✅ Recargar .env sin eliminar la app Completa el patrón CRUD: Create, Read, Update, Delete ✨
This commit is contained in:
@@ -60,6 +60,153 @@ 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,
|
||||
service_file_path,
|
||||
registered_at: chrono::Local::now().to_rfc3339(),
|
||||
deleted: false,
|
||||
deleted_at: None,
|
||||
deleted_reason: None,
|
||||
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(
|
||||
State(state): State<Arc<ApiState>>,
|
||||
Path(app_name): Path<String>,
|
||||
@@ -171,6 +318,30 @@ pub async fn restart_app_handler(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_app_details_handler(
|
||||
Path(app_name): Path<String>,
|
||||
) -> Result<Json<serde_json::Value>, StatusCode> {
|
||||
use crate::config::get_config_manager;
|
||||
|
||||
let config_manager = get_config_manager();
|
||||
let apps = config_manager.get_apps();
|
||||
|
||||
match apps.iter().find(|a| a.name == app_name) {
|
||||
Some(app) => {
|
||||
Ok(Json(serde_json::json!({
|
||||
"success": true,
|
||||
"data": app
|
||||
})))
|
||||
}
|
||||
None => {
|
||||
Ok(Json(serde_json::json!({
|
||||
"success": false,
|
||||
"error": format!("Aplicación '{}' no encontrada", app_name)
|
||||
})))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_app_status_handler(
|
||||
State(_state): State<Arc<ApiState>>,
|
||||
Path(app_name): Path<String>,
|
||||
|
||||
@@ -71,7 +71,7 @@ async fn main() {
|
||||
.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/deleted", get(api::get_deleted_apps_handler))
|
||||
.route("/api/apps/:name", delete(api::unregister_app_handler))
|
||||
.route("/api/apps/:name", get(api::get_app_details_handler).delete(api::unregister_app_handler).put(api::update_app_handler))
|
||||
.route("/api/apps/:name/status", get(api::get_app_status_handler))
|
||||
.route("/api/apps/:name/start", post(api::start_app_handler))
|
||||
.route("/api/apps/:name/stop", post(api::stop_app_handler))
|
||||
|
||||
Reference in New Issue
Block a user