feat: Sistema de variables de entorno mejorado con EnvironmentFile

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

Beneficios:
 No duplicación de variables (.env es la fuente de verdad)
 JSON solo guarda variables extras (pequeño y limpio)
 .env funciona igual que en desarrollo
 systemd lee .env con EnvironmentFile
 Variables adicionales se persisten en JSON
 Al editar, se pre-cargan variables adicionales guardadas
This commit is contained in:
2026-01-21 21:48:59 -05:00
parent 93d178b216
commit 058e4781e6
6 changed files with 68 additions and 24 deletions

View File

@@ -176,6 +176,7 @@ pub async fn update_app_handler(
deleted: false, deleted: false,
deleted_at: None, deleted_at: None,
deleted_reason: None, deleted_reason: None,
environment: config.environment.clone(),
systemd_service: None, systemd_service: None,
created_at: None, created_at: None,
}; };

View File

@@ -57,6 +57,12 @@ pub struct MonitoredApp {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub deleted_reason: Option<String>, pub deleted_reason: Option<String>,
// --- VARIABLES DE ENTORNO ADICIONALES ---
/// Variables de entorno ADICIONALES (las del .env se cargan con EnvironmentFile)
/// Solo almacenamos aquí las variables que el usuario agrega manualmente desde el panel
#[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
pub environment: std::collections::HashMap<String, String>,
// DEPRECATED: Mantener por compatibilidad con versiones antiguas // DEPRECATED: Mantener por compatibilidad con versiones antiguas
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub systemd_service: Option<String>, pub systemd_service: Option<String>,
@@ -217,6 +223,7 @@ impl ConfigManager {
deleted: false, deleted: false,
deleted_at: None, deleted_at: None,
deleted_reason: None, deleted_reason: None,
environment: std::collections::HashMap::new(),
systemd_service: None, systemd_service: None,
created_at: None, created_at: None,
}; };

View File

@@ -261,6 +261,7 @@ pub fn sync_discovered_services(services: Vec<DiscoveredService>) {
deleted: false, deleted: false,
deleted_at: None, deleted_at: None,
deleted_reason: None, deleted_reason: None,
environment: std::collections::HashMap::new(),
systemd_service: None, systemd_service: None,
created_at: None, created_at: None,
}; };

View File

@@ -92,6 +92,7 @@ impl AppManager {
deleted: false, deleted: false,
deleted_at: None, deleted_at: None,
deleted_reason: None, deleted_reason: None,
environment: config.environment.clone(),
systemd_service: None, systemd_service: None,
created_at: None, created_at: None,
}; };

View File

@@ -75,21 +75,8 @@ impl ServiceGenerator {
format!("{} {}", executable, config.script_path) format!("{} {}", executable, config.script_path)
}; };
// 1. Leer variables del archivo .env si existe // Generar líneas de variables de entorno ADICIONALES (solo las del config, no del .env)
let env_file_vars = Self::read_env_file(&config.working_directory); let mut env_lines: Vec<String> = config.environment
// 2. Merge: .env primero, luego sobrescribir con variables del config
let mut merged_env = env_file_vars.clone();
for (key, value) in &config.environment {
merged_env.insert(key.clone(), value.clone());
}
if !env_file_vars.is_empty() {
logger.info("ServiceGenerator", &format!("📄 Usando {} variables desde .env", env_file_vars.len()));
}
// Generar variables de entorno (desde .env + config manual)
let mut env_lines: Vec<String> = merged_env
.iter() .iter()
.map(|(key, value)| format!("Environment=\"{}={}\"", key, value)) .map(|(key, value)| format!("Environment=\"{}={}\"", key, value))
.collect(); .collect();
@@ -113,11 +100,17 @@ impl ServiceGenerator {
} }
} }
let env_vars = env_lines.join("\n");
// Agregar SyslogIdentifier para logs más claros // Agregar SyslogIdentifier para logs más claros
let syslog_id = format!("SyslogIdentifier=siax-app-{}", config.app_name); let syslog_id = format!("SyslogIdentifier=siax-app-{}", config.app_name);
// Verificar si existe .env en el proyecto
let env_file_path = Path::new(&config.working_directory).join(".env");
let has_env_file = env_file_path.exists();
if has_env_file {
logger.info("ServiceGenerator", &format!("📄 .env encontrado, usando EnvironmentFile: {}", env_file_path.display()));
}
// Construir el servicio con orden lógico // Construir el servicio con orden lógico
let mut service = format!( let mut service = format!(
r#"[Unit] r#"[Unit]
@@ -134,9 +127,26 @@ WorkingDirectory={}
config.working_directory config.working_directory
); );
// Agregar variables de entorno (PATH primero, luego las demás) // Agregar PATH si usa NVM (debe ir primero)
if !env_vars.is_empty() { // Extraer PATH de env_lines si está en la primera posición
service.push_str(&env_vars); let mut path_line: Option<String> = None;
if !env_lines.is_empty() && env_lines[0].starts_with("Environment=PATH=") {
path_line = Some(env_lines.remove(0));
}
if let Some(path) = path_line {
service.push_str(&path);
service.push('\n');
}
// ✅ AGREGAR EnvironmentFile si existe .env en el proyecto
if has_env_file {
service.push_str(&format!("EnvironmentFile={}\n", env_file_path.display()));
}
// Agregar variables de entorno ADICIONALES (las del formulario/JSON)
if !env_lines.is_empty() {
service.push_str(&env_lines.join("\n"));
service.push('\n'); service.push('\n');
} }

View File

@@ -374,6 +374,10 @@
<script> <script>
function addEnvVar() { function addEnvVar() {
addEnvironmentVariable("", "");
}
function addEnvironmentVariable(key, value) {
const container = document.getElementById("env-container"); const container = document.getElementById("env-container");
const envItem = document.createElement("div"); const envItem = document.createElement("div");
envItem.className = envItem.className =
@@ -382,11 +386,13 @@
<input <input
type="text" type="text"
placeholder="KEY" 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" 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 <input
type="text" type="text"
placeholder="valor" 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" 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 <button
@@ -475,14 +481,32 @@
document.getElementById("working_directory").value = document.getElementById("working_directory").value =
app.path || ""; app.path || "";
// Cargar usuario desde JSON o usar valor por defecto // Cargar usuario desde JSON (sin fallback)
document.getElementById("user").value = app.user || "root"; document.getElementById("user").value = app.user;
document.getElementById("restart_policy").value = "always"; document.getElementById("restart_policy").value = "always";
document.getElementById("app_type").value = "nodejs"; document.getElementById("app_type").value = "nodejs";
document.getElementById("description").value = ""; document.getElementById("description").value = "";
// Cargar variables de entorno (si las hay guardadas) // Cargar variables de entorno ADICIONALES desde JSON
// Por ahora mostramos un campo vacío, el .env se carga automáticamente // (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) { } catch (error) {
console.error("Error cargando app:", error); console.error("Error cargando app:", error);
alert("Error al cargar los datos de la aplicación"); alert("Error al cargar los datos de la aplicación");