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:
553
web/register.html
Normal file
553
web/register.html
Normal file
@@ -0,0 +1,553 @@
|
||||
<!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>Registrar Aplicación - SIAX Monitor</title>
|
||||
<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"
|
||||
>
|
||||
<span class="material-symbols-outlined text-white"
|
||||
>monitoring</span
|
||||
>
|
||||
</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"
|
||||
>Agregar Detectada</a
|
||||
>
|
||||
<a
|
||||
class="text-primary text-sm font-medium border-b-2 border-primary pb-1"
|
||||
href="/register"
|
||||
>Nueva App</a
|
||||
>
|
||||
<a
|
||||
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
|
||||
href="/logs"
|
||||
>Registros</a
|
||||
>
|
||||
</nav>
|
||||
</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>Registrar Aplicación</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
function addEnvVar() {
|
||||
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"
|
||||
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>
|
||||
`;
|
||||
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);
|
||||
}
|
||||
|
||||
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(
|
||||
"http://localhost:8080/api/apps",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(formData),
|
||||
},
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showAlert(
|
||||
`Application registered successfully: ${formData.app_name}`,
|
||||
"success",
|
||||
);
|
||||
|
||||
// Ask if user wants to start the app
|
||||
if (
|
||||
confirm("¿Deseas iniciar la aplicación ahora?")
|
||||
) {
|
||||
const startResponse = await fetch(
|
||||
`http://localhost:8080/api/apps/${formData.app_name}/start`,
|
||||
{ method: "POST" },
|
||||
);
|
||||
|
||||
const startResult = await startResponse.json();
|
||||
if (startResult.success) {
|
||||
showAlert(
|
||||
"Application started successfully!",
|
||||
"success",
|
||||
);
|
||||
setTimeout(() => {
|
||||
window.location.href = "/";
|
||||
}, 2000);
|
||||
}
|
||||
} else {
|
||||
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>
|
||||
Reference in New Issue
Block a user