✨ 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
417 lines
19 KiB
HTML
417 lines
19 KiB
HTML
<!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>Agregar App Detectada - 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-primary text-sm font-medium border-b-2 border-primary pb-1"
|
|
href="/select"
|
|
>Agregar Detectada</a
|
|
>
|
|
<a
|
|
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
|
|
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-[1200px] 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]"
|
|
>
|
|
Add Detected Application
|
|
</h1>
|
|
<p class="text-[#9dabb9] text-base font-normal">
|
|
Selecciona un proceso detectado y configúralo para
|
|
monitoreo.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Detected Processes Section -->
|
|
<div
|
|
class="rounded-xl border border-[#283039] bg-[#161f2a] overflow-hidden"
|
|
>
|
|
<div
|
|
class="flex items-center justify-between p-4 border-b border-[#283039]"
|
|
>
|
|
<h3 class="text-white font-bold px-2">
|
|
Detected Node.js & Python Processes
|
|
</h3>
|
|
<button
|
|
onclick="window.location.href = '/scan'"
|
|
class="flex items-center gap-2 px-4 py-2 bg-primary hover:brightness-110 rounded-lg text-white text-sm font-medium transition-all"
|
|
>
|
|
<span class="material-symbols-outlined text-[18px]"
|
|
>refresh</span
|
|
>
|
|
Scan Again
|
|
</button>
|
|
</div>
|
|
|
|
<div id="processes-container" class="p-4">
|
|
<!-- Processes will be loaded here -->
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div id="loading-state" class="p-8 text-center">
|
|
<span
|
|
class="material-symbols-outlined text-primary text-5xl mb-3 animate-spin"
|
|
>progress_activity</span
|
|
>
|
|
<p class="text-[#9dabb9] text-sm">
|
|
Cargando procesos detectados...
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div id="empty-state" class="hidden p-8 text-center">
|
|
<span
|
|
class="material-symbols-outlined text-[#9dabb9] text-5xl mb-3"
|
|
>search_off</span
|
|
>
|
|
<p class="text-[#9dabb9] text-sm mb-4">
|
|
No se detectaron procesos
|
|
</p>
|
|
<button
|
|
onclick="window.location.href = '/scan'"
|
|
class="px-4 py-2 bg-primary hover:brightness-110 rounded-lg text-white text-sm font-medium transition-all"
|
|
>
|
|
Run Scan
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Add Form -->
|
|
<div
|
|
class="rounded-xl border border-[#283039] bg-[#161f2a] p-6 space-y-5"
|
|
>
|
|
<h3 class="text-white text-lg font-bold">
|
|
Quick Configuration
|
|
</h3>
|
|
<p class="text-[#9dabb9] text-sm">
|
|
Click on a detected process above, or fill in the details
|
|
manually to add it to monitoring.
|
|
</p>
|
|
|
|
<form id="quickAddForm" class="space-y-4">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<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"
|
|
/>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<label
|
|
class="block text-[#9dabb9] text-sm font-medium"
|
|
>
|
|
Process ID (PID)
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="pid"
|
|
name="pid"
|
|
readonly
|
|
placeholder="Auto-detectado"
|
|
class="w-full bg-[#1c2730] border border-[#283039] rounded-lg px-4 py-2.5 text-[#9dabb9] placeholder-[#9dabb9] cursor-not-allowed"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<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"
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
class="flex flex-col-reverse sm:flex-row gap-3 justify-end pt-4"
|
|
>
|
|
<button
|
|
type="button"
|
|
onclick="resetForm()"
|
|
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"
|
|
>
|
|
Clear
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onclick="window.location.href = '/register'"
|
|
class="flex items-center justify-center rounded-lg h-12 px-6 bg-[#283039] hover:bg-[#3a4654] border border-[#283039] text-white text-sm font-bold transition-colors gap-2"
|
|
>
|
|
<span class="material-symbols-outlined text-[18px]"
|
|
>settings</span
|
|
>
|
|
<span>Configuración Avanzada</span>
|
|
</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]"
|
|
>add_circle</span
|
|
>
|
|
<span>Agregar al Monitor</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
let detectedProcesses = [];
|
|
|
|
async function loadProcesses() {
|
|
const container = document.getElementById(
|
|
"processes-container",
|
|
);
|
|
const loading = document.getElementById("loading-state");
|
|
const empty = document.getElementById("empty-state");
|
|
|
|
try {
|
|
const response = await fetch(
|
|
"http://localhost:8080/api/scan",
|
|
);
|
|
if (!response.ok)
|
|
throw new Error("Failed to fetch processes");
|
|
|
|
const data = await response.json();
|
|
detectedProcesses = data.data.processes || [];
|
|
|
|
loading.classList.add("hidden");
|
|
|
|
if (detectedProcesses.length === 0) {
|
|
empty.classList.remove("hidden");
|
|
container.classList.add("hidden");
|
|
return;
|
|
}
|
|
|
|
container.classList.remove("hidden");
|
|
container.innerHTML = `
|
|
<div class="space-y-3">
|
|
${detectedProcesses
|
|
.map((process) => {
|
|
const typeIcon =
|
|
process.process_type === "nodejs"
|
|
? "terminal"
|
|
: "code";
|
|
const typeColor =
|
|
process.process_type === "nodejs"
|
|
? "text-green-400"
|
|
: "text-blue-400";
|
|
const typeLabel =
|
|
process.process_type === "nodejs"
|
|
? "Node.js"
|
|
: "Python";
|
|
|
|
return `
|
|
<div class="flex items-center justify-between p-4 rounded-lg border border-[#283039] hover:border-primary hover:bg-[#1c2730] transition-all cursor-pointer" onclick="selectProcess(${process.pid})">
|
|
<div class="flex items-center gap-4 flex-1">
|
|
<div class="flex items-center justify-center w-12 h-12 rounded-lg bg-[#1c2730]">
|
|
<span class="material-symbols-outlined ${typeColor}">${typeIcon}</span>
|
|
</div>
|
|
<div class="flex-1">
|
|
<div class="flex items-center gap-3 mb-1">
|
|
<span class="text-white font-medium">${process.name}</span>
|
|
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${typeColor} bg-current bg-opacity-20">
|
|
${typeLabel}
|
|
</span>
|
|
</div>
|
|
<div class="text-[#9dabb9] text-sm">
|
|
PID: <span class="font-mono">${process.pid}</span> ·
|
|
CPU: ${process.cpu_usage?.toFixed(2) || "0.00"}% ·
|
|
Memory: ${formatMemory(process.memory_mb)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onclick="event.stopPropagation(); selectProcess(${process.pid})"
|
|
class="flex items-center gap-2 px-4 py-2 bg-primary hover:brightness-110 rounded-lg text-white text-sm font-medium transition-all"
|
|
>
|
|
<span class="material-symbols-outlined text-[18px]">arrow_forward</span>
|
|
Select
|
|
</button>
|
|
</div>
|
|
`;
|
|
})
|
|
.join("")}
|
|
</div>
|
|
`;
|
|
} catch (error) {
|
|
console.error("Error loading processes:", error);
|
|
loading.classList.add("hidden");
|
|
empty.classList.remove("hidden");
|
|
}
|
|
}
|
|
|
|
function selectProcess(pid) {
|
|
const process = detectedProcesses.find((p) => p.pid === pid);
|
|
if (!process) return;
|
|
|
|
document.getElementById("app_name").value = process.name || "";
|
|
document.getElementById("pid").value = pid;
|
|
document.getElementById("script_path").value = ""; // User needs to provide this
|
|
|
|
// Scroll to form
|
|
document
|
|
.getElementById("quickAddForm")
|
|
.scrollIntoView({ behavior: "smooth" });
|
|
|
|
// Focus on script path
|
|
setTimeout(() => {
|
|
document.getElementById("script_path").focus();
|
|
}, 500);
|
|
}
|
|
|
|
function formatMemory(mb) {
|
|
if (!mb) return "0 MB";
|
|
if (mb < 1024) return `${mb.toFixed(0)} MB`;
|
|
return `${(mb / 1024).toFixed(2)} GB`;
|
|
}
|
|
|
|
function resetForm() {
|
|
document.getElementById("quickAddForm").reset();
|
|
}
|
|
|
|
document
|
|
.getElementById("quickAddForm")
|
|
.addEventListener("submit", async (e) => {
|
|
e.preventDefault();
|
|
|
|
alert(
|
|
'¡Función de agregar rápido próximamente! Por favor usa el botón "Configuración Avanzada" para registrar la aplicación con configuración completa.',
|
|
);
|
|
|
|
// Redirect to register page with pre-filled data
|
|
const appName = document.getElementById("app_name").value;
|
|
const scriptPath =
|
|
document.getElementById("script_path").value;
|
|
|
|
if (appName && scriptPath) {
|
|
// Store in sessionStorage for pre-filling
|
|
sessionStorage.setItem("prefill_app_name", appName);
|
|
sessionStorage.setItem(
|
|
"prefill_script_path",
|
|
scriptPath,
|
|
);
|
|
window.location.href = "/register";
|
|
}
|
|
});
|
|
|
|
// Load processes on page load
|
|
document.addEventListener("DOMContentLoaded", loadProcesses);
|
|
</script>
|
|
</body>
|
|
</html>
|