feat: sistema de agente ligero para monitoreo de servidores
Backend: - Servidor model: AgentToken, AgentLastSeen, MetricasJson nuevos campos - AgentHeartbeat: endpoint público POST /agent/heartbeat (auth por token) - GenerateAgentToken: endpoint protegido POST /app/servidor/:id/agent-token - Ruta pública /agent/install.sh sirve el script de instalación Agente Go (agent/): - agent/main.go: binario independiente con gopsutil - Recolecta RAM, CPU, Disco, Uptime, OS, Load average - Lee agent.yml o flags --api-url / --token - Envía POST a /agent/heartbeat cada N segundos - Instala como servicio systemd via install.sh - agent/go.mod: módulo independiente (usite-agent) - agent/agent.yml.sample: configuración de ejemplo - agent/install.sh: instalador en 1 curl para Linux (systemd) Frontend servidor_dashboard.html: - Cards: badge ● En línea / ● Fuera / ○ Sin agente - Barras de progreso live: RAM%, CPU%, Disco% - Valores: GB usado/total, núcleos, uptime, OS real - Tiempo desde último reporte - Botón 'Agente' en cada card → modal de configuración - Modal agente: estado, token con copy, comando curl 1 línea, instalación manual/avanzada con collapse, métricas actuales Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot
parent
f52e266527
commit
d9d3f8a5eb
@@ -36,61 +36,124 @@
|
||||
<h3 class="text-white font-bold text-base" x-text="servidor.nombre"></h3>
|
||||
<p class="text-slate-300 text-xs mt-1 font-mono" x-text="servidor.ip_servidor"></p>
|
||||
</div>
|
||||
<span class="text-xs px-2 py-1 rounded-full font-semibold"
|
||||
:class="servidor.tipo_servidor?.nombre ? 'bg-blue-500 text-white' : 'bg-slate-600 text-slate-300'"
|
||||
x-text="servidor.tipo_servidor?.nombre || 'Servidor'"></span>
|
||||
<div class="flex flex-col items-end gap-1.5">
|
||||
<span class="text-xs px-2 py-1 rounded-full font-semibold"
|
||||
:class="servidor.tipo_servidor?.nombre ? 'bg-blue-500 text-white' : 'bg-slate-600 text-slate-300'"
|
||||
x-text="servidor.tipo_servidor?.nombre || 'Servidor'"></span>
|
||||
<!-- Badge estado agente -->
|
||||
<span class="text-xs px-2 py-0.5 rounded-full font-bold"
|
||||
:class="agentOnline(servidor) ? 'bg-green-400 text-green-900' : (servidor.agent_token ? 'bg-red-400 text-red-900' : 'bg-slate-500 text-slate-200')">
|
||||
<span x-show="agentOnline(servidor)">● En línea</span>
|
||||
<span x-show="!agentOnline(servidor) && servidor.agent_token">● Fuera</span>
|
||||
<span x-show="!servidor.agent_token">○ Sin agente</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card Body -->
|
||||
<div class="p-4 space-y-3">
|
||||
<!-- Info grid -->
|
||||
<div class="grid grid-cols-3 gap-2 text-xs">
|
||||
<div class="bg-slate-50 rounded-lg p-2 text-center">
|
||||
<p class="text-slate-400 font-medium">RAM</p>
|
||||
<p class="text-slate-800 font-bold mt-0.5" x-text="servidor.ram || '—'"></p>
|
||||
</div>
|
||||
<div class="bg-slate-50 rounded-lg p-2 text-center">
|
||||
<p class="text-slate-400 font-medium">CPU</p>
|
||||
<p class="text-slate-800 font-bold mt-0.5" x-text="servidor.nucleos || '—'"></p>
|
||||
</div>
|
||||
<div class="bg-slate-50 rounded-lg p-2 text-center">
|
||||
<p class="text-slate-400 font-medium">Disco</p>
|
||||
<p class="text-slate-800 font-bold mt-0.5" x-text="servidor.disco || '—'"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Provider & OS -->
|
||||
<div class="flex items-center justify-between text-xs text-slate-500">
|
||||
<!-- Métricas live (si hay agente) -->
|
||||
<template x-if="servidor.metricas_json">
|
||||
<div x-data="{ m: parseMetricas(servidor.metricas_json) }" class="space-y-2">
|
||||
<!-- RAM -->
|
||||
<div>
|
||||
<div class="flex justify-between text-xs mb-1">
|
||||
<span class="text-slate-500 font-medium">🧠 RAM</span>
|
||||
<span class="font-mono font-bold text-slate-700"
|
||||
x-text="m.ram?.usado_gb + ' / ' + m.ram?.total_gb + ' GB (' + m.ram?.porcentaje + '%)'"></span>
|
||||
</div>
|
||||
<div class="w-full bg-slate-100 rounded-full h-1.5">
|
||||
<div class="h-1.5 rounded-full transition-all"
|
||||
:class="m.ram?.porcentaje > 85 ? 'bg-red-500' : m.ram?.porcentaje > 65 ? 'bg-amber-400' : 'bg-green-500'"
|
||||
:style="'width:' + Math.min(m.ram?.porcentaje, 100) + '%'"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- CPU -->
|
||||
<div>
|
||||
<div class="flex justify-between text-xs mb-1">
|
||||
<span class="text-slate-500 font-medium">⚙️ CPU</span>
|
||||
<span class="font-mono font-bold text-slate-700"
|
||||
x-text="m.cpu?.porcentaje + '% · ' + m.cpu?.nucleos + ' núcleos'"></span>
|
||||
</div>
|
||||
<div class="w-full bg-slate-100 rounded-full h-1.5">
|
||||
<div class="h-1.5 rounded-full transition-all"
|
||||
:class="m.cpu?.porcentaje > 85 ? 'bg-red-500' : m.cpu?.porcentaje > 65 ? 'bg-amber-400' : 'bg-blue-500'"
|
||||
:style="'width:' + Math.min(m.cpu?.porcentaje, 100) + '%'"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Disco -->
|
||||
<div>
|
||||
<div class="flex justify-between text-xs mb-1">
|
||||
<span class="text-slate-500 font-medium">💾 Disco</span>
|
||||
<span class="font-mono font-bold text-slate-700"
|
||||
x-text="m.disco?.usado_gb + ' / ' + m.disco?.total_gb + ' GB (' + m.disco?.porcentaje + '%)'"></span>
|
||||
</div>
|
||||
<div class="w-full bg-slate-100 rounded-full h-1.5">
|
||||
<div class="h-1.5 rounded-full transition-all"
|
||||
:class="m.disco?.porcentaje > 85 ? 'bg-red-500' : m.disco?.porcentaje > 65 ? 'bg-amber-400' : 'bg-indigo-500'"
|
||||
:style="'width:' + Math.min(m.disco?.porcentaje, 100) + '%'"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Uptime + OS -->
|
||||
<div class="flex justify-between text-xs text-slate-400 pt-1 border-t border-slate-100">
|
||||
<span>⏱ <span x-text="m.uptime || '—'"></span></span>
|
||||
<span x-text="m.os || servidor.so || '—'"></span>
|
||||
</div>
|
||||
<!-- Último reporte -->
|
||||
<div class="text-xs text-slate-400 text-right">
|
||||
Actualizado <span x-text="tiempoDesde(servidor.agent_last_seen)"></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Sin métricas (agente no instalado) -->
|
||||
<template x-if="!servidor.metricas_json">
|
||||
<div class="grid grid-cols-3 gap-2 text-xs">
|
||||
<div class="bg-slate-50 rounded-lg p-2 text-center">
|
||||
<p class="text-slate-400 font-medium">RAM</p>
|
||||
<p class="text-slate-800 font-bold mt-0.5" x-text="servidor.ram || '—'"></p>
|
||||
</div>
|
||||
<div class="bg-slate-50 rounded-lg p-2 text-center">
|
||||
<p class="text-slate-400 font-medium">CPU</p>
|
||||
<p class="text-slate-800 font-bold mt-0.5" x-text="servidor.nucleos || '—'"></p>
|
||||
</div>
|
||||
<div class="bg-slate-50 rounded-lg p-2 text-center">
|
||||
<p class="text-slate-400 font-medium">Disco</p>
|
||||
<p class="text-slate-800 font-bold mt-0.5" x-text="servidor.disco || '—'"></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Provider & Conexiones -->
|
||||
<div class="flex items-center justify-between text-xs text-slate-500 pt-1">
|
||||
<span x-text="servidor.prov_servidor?.nombre || 'Sin proveedor'"></span>
|
||||
<span x-text="servidor.so || 'Sin OS'"></span>
|
||||
</div>
|
||||
|
||||
<!-- Ultimo ping -->
|
||||
<div x-show="servidor.ultimo_ping" class="text-xs text-slate-400">
|
||||
Último ping: <span class="font-mono" x-text="servidor.ultimo_ping"></span>
|
||||
</div>
|
||||
|
||||
<!-- Conexiones count -->
|
||||
<div class="flex items-center gap-2 pt-1">
|
||||
<svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582 4 8 4"/>
|
||||
</svg>
|
||||
<span class="text-sm text-slate-600">
|
||||
<span>
|
||||
<span class="font-bold text-blue-600" x-text="servidor.conx_count ?? servidor._conexiones?.length ?? 0"></span>
|
||||
conexiones BD
|
||||
BD
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Action Button -->
|
||||
<button @click="abrirModal(servidor)"
|
||||
class="w-full px-4 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 active:bg-blue-800 transition-colors flex items-center justify-center gap-2 text-sm mt-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||
</svg>
|
||||
Ver Detalles y Conexiones
|
||||
</button>
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex gap-2 mt-2">
|
||||
<button @click="abrirModal(servidor)"
|
||||
class="flex-1 px-3 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors flex items-center justify-center gap-1.5 text-sm">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||
</svg>
|
||||
Ver
|
||||
</button>
|
||||
<button @click="abrirAgentModal(servidor)" title="Configurar agente de monitoreo"
|
||||
class="px-3 py-2 rounded-lg font-medium transition-colors text-sm border flex items-center gap-1"
|
||||
:class="agentOnline(servidor) ? 'border-green-300 text-green-700 hover:bg-green-50' : 'border-slate-300 text-slate-600 hover:bg-slate-50'">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3H5a2 2 0 00-2 2v4m6-6h10a2 2 0 012 2v4M9 3v18m0 0h10a2 2 0 002-2V9M9 21H5a2 2 0 01-2-2V9m0 0h18"/>
|
||||
</svg>
|
||||
Agente
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -274,11 +337,145 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== Modal Agente ===== -->
|
||||
<div x-show="agentModalServidor" x-cloak
|
||||
class="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4"
|
||||
@click.self="agentModalServidor = null">
|
||||
<div class="bg-white rounded-xl w-full max-w-2xl shadow-2xl overflow-hidden" x-data>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="bg-gradient-to-r from-emerald-600 to-teal-700 p-5 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-white font-bold text-lg">⚡ Agente de Monitoreo</h2>
|
||||
<p class="text-emerald-100 text-sm mt-0.5" x-text="agentModalServidor?.nombre"></p>
|
||||
</div>
|
||||
<button @click="agentModalServidor = null" class="text-emerald-200 hover:text-white p-1.5 rounded-lg hover:bg-white/10">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-6 space-y-5 max-h-[75vh] overflow-y-auto">
|
||||
|
||||
<!-- Estado actual -->
|
||||
<div class="rounded-xl border p-4"
|
||||
:class="agentOnline(agentModalServidor) ? 'bg-green-50 border-green-200' : (agentModalServidor?.agent_token ? 'bg-amber-50 border-amber-200' : 'bg-slate-50 border-slate-200')">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-3 h-3 rounded-full animate-pulse"
|
||||
:class="agentOnline(agentModalServidor) ? 'bg-green-500' : (agentModalServidor?.agent_token ? 'bg-amber-400' : 'bg-slate-400')"></div>
|
||||
<div>
|
||||
<p class="font-bold text-sm"
|
||||
:class="agentOnline(agentModalServidor) ? 'text-green-800' : (agentModalServidor?.agent_token ? 'text-amber-800' : 'text-slate-700')">
|
||||
<span x-show="agentOnline(agentModalServidor)">✅ Agente en línea</span>
|
||||
<span x-show="!agentOnline(agentModalServidor) && agentModalServidor?.agent_token">⚠️ Agente instalado pero sin reportes recientes</span>
|
||||
<span x-show="!agentModalServidor?.agent_token">🔧 Agente no configurado</span>
|
||||
</p>
|
||||
<p class="text-xs mt-0.5"
|
||||
:class="agentOnline(agentModalServidor) ? 'text-green-600' : 'text-slate-500'"
|
||||
x-text="agentModalServidor?.agent_last_seen ? 'Último reporte: ' + tiempoDesde(agentModalServidor.agent_last_seen) : 'Nunca ha reportado'">
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Métricas actuales si está online -->
|
||||
<template x-if="agentModalServidor?.metricas_json">
|
||||
<div x-data="{ m: parseMetricas(agentModalServidor?.metricas_json) }" class="mt-3 grid grid-cols-3 gap-3 text-xs">
|
||||
<div class="bg-white rounded-lg p-2 text-center border">
|
||||
<p class="text-slate-400">RAM</p>
|
||||
<p class="font-bold text-slate-800" x-text="m.ram?.porcentaje + '%'"></p>
|
||||
<p class="text-slate-500" x-text="m.ram?.usado_gb + '/' + m.ram?.total_gb + 'GB'"></p>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg p-2 text-center border">
|
||||
<p class="text-slate-400">CPU</p>
|
||||
<p class="font-bold text-slate-800" x-text="m.cpu?.porcentaje + '%'"></p>
|
||||
<p class="text-slate-500" x-text="m.cpu?.nucleos + ' núcleos'"></p>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg p-2 text-center border">
|
||||
<p class="text-slate-400">Disco</p>
|
||||
<p class="font-bold text-slate-800" x-text="m.disco?.porcentaje + '%'"></p>
|
||||
<p class="text-slate-500" x-text="m.disco?.usado_gb + '/' + m.disco?.total_gb + 'GB'"></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Token section -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h3 class="font-bold text-slate-700 text-sm">🔑 Token del servidor</h3>
|
||||
<button @click="generarToken()" :disabled="agentTokenCargando"
|
||||
class="text-xs px-3 py-1.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-colors">
|
||||
<span x-show="!agentTokenCargando" x-text="agentModalServidor?.agent_token ? '🔄 Regenerar' : '✨ Generar Token'"></span>
|
||||
<span x-show="agentTokenCargando">Generando...</span>
|
||||
</button>
|
||||
</div>
|
||||
<template x-if="agentModalServidor?.agent_token">
|
||||
<div class="flex items-center gap-2">
|
||||
<code class="flex-1 bg-slate-100 rounded-lg px-3 py-2 text-xs font-mono text-slate-700 break-all"
|
||||
x-text="agentModalServidor.agent_token"></code>
|
||||
<button @click="copiar(agentModalServidor.agent_token)" title="Copiar token"
|
||||
class="p-2 rounded-lg bg-slate-100 hover:bg-slate-200 text-slate-600 transition-colors shrink-0">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="!agentModalServidor?.agent_token">
|
||||
<p class="text-xs text-slate-400 bg-slate-50 rounded-lg px-3 py-2">Genera un token para poder instalar el agente en este servidor.</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Comando de instalación -->
|
||||
<template x-if="agentModalServidor?.agent_token">
|
||||
<div>
|
||||
<h3 class="font-bold text-slate-700 text-sm mb-2">📦 Instalación en 1 comando (Linux)</h3>
|
||||
<div class="bg-slate-900 rounded-xl p-4 relative group">
|
||||
<pre class="text-green-400 text-xs font-mono whitespace-pre-wrap break-all leading-relaxed"
|
||||
x-text="`curl -sL https://admin.u-site.app/agent/install.sh | bash -s -- \\\n --token=${agentModalServidor.agent_token} \\\n --url=https://admin.u-site.app \\\n --interval=30`"></pre>
|
||||
<button @click="copiar(`curl -sL https://admin.u-site.app/agent/install.sh | bash -s -- --token=${agentModalServidor.agent_token} --url=https://admin.u-site.app --interval=30`)"
|
||||
class="absolute top-3 right-3 opacity-0 group-hover:opacity-100 transition-opacity bg-slate-700 hover:bg-slate-600 text-slate-300 rounded-lg p-1.5">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-slate-400 mt-2">⚡ El script descarga el binario, crea el archivo de configuración e instala el servicio systemd automáticamente.</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Pasos manuales -->
|
||||
<template x-if="agentModalServidor?.agent_token">
|
||||
<details class="group">
|
||||
<summary class="cursor-pointer text-sm font-bold text-slate-600 hover:text-slate-800 select-none flex items-center gap-2">
|
||||
<svg class="w-4 h-4 transition-transform group-open:rotate-90" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
Instalación manual / configuración avanzada
|
||||
</summary>
|
||||
<div class="mt-3 space-y-3 pl-6 text-xs text-slate-600">
|
||||
<p><span class="font-bold">1.</span> Descarga el binario para tu arquitectura desde el panel</p>
|
||||
<p><span class="font-bold">2.</span> Crea el archivo <code class="bg-slate-100 px-1 rounded">agent.yml</code>:</p>
|
||||
<pre class="bg-slate-900 text-green-400 rounded-lg p-3 text-xs font-mono"
|
||||
x-text="`api_url: https://admin.u-site.app\ntoken: ${agentModalServidor.agent_token}\ninterval: 30\ndebug: false`"></pre>
|
||||
<p><span class="font-bold">3.</span> Ejecuta: <code class="bg-slate-100 px-1 rounded">./usite-agent --config=agent.yml</code></p>
|
||||
<p><span class="font-bold">Ver logs:</span> <code class="bg-slate-100 px-1 rounded">journalctl -u usite-agent -f</code></p>
|
||||
</div>
|
||||
</details>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function servidorDashboard() {
|
||||
return {
|
||||
servidores: [],
|
||||
servidorSeleccionado: null,
|
||||
agentModalServidor: null,
|
||||
agentTokenCargando: false,
|
||||
cargando: true,
|
||||
cargandoConexiones: false,
|
||||
pingCargando: {},
|
||||
@@ -311,7 +508,6 @@
|
||||
const r = await fetch(`/app/servidor-dashboard/${servidor.ID}`);
|
||||
const data = await r.json();
|
||||
this.servidorSeleccionado._conexiones = data.conexiones || [];
|
||||
// Actualizar también el contador en la card
|
||||
const idx = this.servidores.findIndex(s => s.ID === servidor.ID);
|
||||
if (idx >= 0) this.servidores[idx]._conexiones = data.conexiones || [];
|
||||
} catch (e) {
|
||||
@@ -326,12 +522,61 @@
|
||||
this.servidorSeleccionado = null;
|
||||
},
|
||||
|
||||
abrirAgentModal(servidor) {
|
||||
// Buscar el servidor actualizado en la lista
|
||||
const s = this.servidores.find(sv => sv.ID === servidor.ID) || servidor;
|
||||
this.agentModalServidor = { ...s };
|
||||
},
|
||||
|
||||
async generarToken() {
|
||||
if (!this.agentModalServidor) return;
|
||||
this.agentTokenCargando = true;
|
||||
try {
|
||||
const r = await fetch(`/app/servidor/${this.agentModalServidor.ID}/agent-token`, { method: 'POST' });
|
||||
const data = await r.json();
|
||||
if (data.ok) {
|
||||
this.agentModalServidor.agent_token = data.agent_token;
|
||||
// Actualizar también en la lista de cards
|
||||
const idx = this.servidores.findIndex(s => s.ID === this.agentModalServidor.ID);
|
||||
if (idx >= 0) this.servidores[idx].agent_token = data.agent_token;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error generando token:', e);
|
||||
} finally {
|
||||
this.agentTokenCargando = false;
|
||||
}
|
||||
},
|
||||
|
||||
agentOnline(servidor) {
|
||||
if (!servidor?.agent_last_seen) return false;
|
||||
const last = new Date(servidor.agent_last_seen);
|
||||
const diffMs = Date.now() - last.getTime();
|
||||
return diffMs < 3 * 60 * 1000; // online si reportó en últimos 3 minutos
|
||||
},
|
||||
|
||||
tiempoDesde(isoStr) {
|
||||
if (!isoStr) return '—';
|
||||
const diff = Math.floor((Date.now() - new Date(isoStr).getTime()) / 1000);
|
||||
if (diff < 60) return 'hace ' + diff + 's';
|
||||
if (diff < 3600) return 'hace ' + Math.floor(diff / 60) + 'm';
|
||||
if (diff < 86400) return 'hace ' + Math.floor(diff / 3600) + 'h';
|
||||
return 'hace ' + Math.floor(diff / 86400) + 'd';
|
||||
},
|
||||
|
||||
parseMetricas(jsonStr) {
|
||||
try { return JSON.parse(jsonStr) || {}; }
|
||||
catch { return {}; }
|
||||
},
|
||||
|
||||
copiar(texto) {
|
||||
navigator.clipboard.writeText(texto).catch(() => {});
|
||||
},
|
||||
|
||||
async hacerPing(cx) {
|
||||
const id = cx.ID || cx.id;
|
||||
if (!id) return;
|
||||
// Usar mutación directa para garantizar reactividad en Alpine.js
|
||||
this.pingCargando[id] = true;
|
||||
this.pingResultado[id] = undefined; // resetear resultado previo
|
||||
this.pingResultado[id] = undefined;
|
||||
try {
|
||||
const r = await fetch(`/app/conx-ping/${id}`);
|
||||
if (!r.ok) {
|
||||
|
||||
Reference in New Issue
Block a user