Files
soft_usite/resources/views/servidor_dashboard.html
T
Lizandro GuarnizoandCopilot c834e40e83 fix(servidor-dashboard): show DB connection count on server cards
- GetServidor: query conx_count per server with GROUP BY and attach to response
- servidor_dashboard.html: use conx_count from API (fallback to _conexiones.length)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-21 10:25:18 -05:00

360 lines
18 KiB
HTML

<!-- Servidor Dashboard -->
<div x-data="servidorDashboard()" x-init="init()">
<!-- Header -->
<div class="mb-8 flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-slate-800">Dashboard de Servidores</h1>
<p class="text-slate-500 text-sm mt-2">Monitorea todos tus servidores y sus conexiones de base de datos</p>
</div>
<button @click="cargarServidores()" class="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 transition-colors">
<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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
Actualizar
</button>
</div>
<!-- Loading -->
<div x-show="cargando" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<template x-for="i in [1,2,3]">
<div class="bg-white rounded-lg border border-slate-200 h-48 animate-pulse">
<div class="bg-slate-200 h-16 rounded-t-lg"></div>
<div class="p-4 space-y-3">
<div class="h-4 bg-slate-200 rounded w-3/4"></div>
<div class="h-4 bg-slate-200 rounded w-1/2"></div>
</div>
</div>
</template>
</div>
<!-- Servidor List -->
<div x-show="!cargando" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<template x-for="servidor in servidores" :key="servidor.ID">
<div class="bg-white rounded-xl border border-slate-200 shadow-sm hover:shadow-lg transition-all overflow-hidden group">
<!-- Card Header -->
<div class="bg-gradient-to-br from-slate-700 to-slate-900 p-5">
<div class="flex items-start justify-between">
<div>
<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>
</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">
<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 class="font-bold text-blue-600" x-text="servidor.conx_count ?? servidor._conexiones?.length ?? 0"></span>
conexiones 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>
</div>
</div>
</template>
<!-- Empty State -->
<div x-show="!cargando && servidores.length === 0" class="col-span-full text-center py-20 text-slate-400">
<svg class="w-16 h-16 mx-auto mb-4 opacity-20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2"/>
</svg>
<p class="text-lg font-medium">No hay servidores registrados</p>
</div>
</div>
<!-- ===== Modal Detalle Servidor ===== -->
<div x-show="servidorSeleccionado" x-cloak
class="fixed inset-0 bg-black/60 z-50 flex items-start justify-center p-4 overflow-y-auto"
@click.self="cerrarModal()">
<div class="bg-white rounded-xl w-full max-w-5xl my-8 shadow-2xl">
<!-- Modal Header -->
<div class="bg-gradient-to-br from-slate-700 to-slate-900 p-6 rounded-t-xl flex items-center justify-between">
<div>
<h2 class="text-2xl font-bold text-white" x-text="servidorSeleccionado?.nombre"></h2>
<p class="text-slate-300 text-sm mt-1 font-mono" x-text="servidorSeleccionado?.ip_servidor"></p>
</div>
<button @click="cerrarModal()" class="text-slate-300 hover:text-white p-2 rounded-lg hover:bg-white/10 transition-colors">
<svg class="w-6 h-6" 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-8">
<!-- === Info Grid === -->
<div>
<h3 class="text-sm font-bold text-slate-500 uppercase tracking-wider mb-4">Información del Servidor</h3>
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
<div class="bg-slate-50 border border-slate-200 rounded-xl p-4">
<p class="text-xs text-slate-400 font-medium mb-1">🌐 IP Servidor</p>
<p class="font-bold text-slate-900 font-mono text-sm break-all" x-text="servidorSeleccionado?.ip_servidor || '—'"></p>
</div>
<div class="bg-slate-50 border border-slate-200 rounded-xl p-4">
<p class="text-xs text-slate-400 font-medium mb-1">🏢 Proveedor</p>
<p class="font-bold text-slate-900 text-sm" x-text="servidorSeleccionado?.prov_servidor?.nombre || '—'"></p>
</div>
<div class="bg-slate-50 border border-slate-200 rounded-xl p-4">
<p class="text-xs text-slate-400 font-medium mb-1">🖥️ Tipo</p>
<p class="font-bold text-slate-900 text-sm" x-text="servidorSeleccionado?.tipo_servidor?.nombre || '—'"></p>
</div>
<div class="bg-slate-50 border border-slate-200 rounded-xl p-4">
<p class="text-xs text-slate-400 font-medium mb-1">💻 SO</p>
<p class="font-bold text-slate-900 text-sm" x-text="servidorSeleccionado?.so || '—'"></p>
</div>
<div class="bg-slate-50 border border-slate-200 rounded-xl p-4">
<p class="text-xs text-slate-400 font-medium mb-1">🧠 RAM</p>
<p class="font-bold text-slate-900 text-sm" x-text="servidorSeleccionado?.ram || '—'"></p>
</div>
<div class="bg-slate-50 border border-slate-200 rounded-xl p-4">
<p class="text-xs text-slate-400 font-medium mb-1">⚙️ Núcleos CPU</p>
<p class="font-bold text-slate-900 text-sm" x-text="servidorSeleccionado?.nucleos || '—'"></p>
</div>
<div class="bg-slate-50 border border-slate-200 rounded-xl p-4">
<p class="text-xs text-slate-400 font-medium mb-1">💾 Disco</p>
<p class="font-bold text-slate-900 text-sm" x-text="servidorSeleccionado?.disco || '—'"></p>
</div>
<div class="bg-slate-50 border border-slate-200 rounded-xl p-4">
<p class="text-xs text-slate-400 font-medium mb-1">📅 Vencimiento</p>
<p class="font-bold text-slate-900 text-sm" x-text="servidorSeleccionado?.vencimiento || '—'"></p>
</div>
<div x-show="servidorSeleccionado?.ultimo_ping" class="bg-slate-50 border border-slate-200 rounded-xl p-4 md:col-span-2">
<p class="text-xs text-slate-400 font-medium mb-1">📡 Último Ping</p>
<p class="font-bold text-slate-900 text-sm font-mono" x-text="servidorSeleccionado?.ultimo_ping"></p>
</div>
</div>
</div>
<!-- === Conexiones BD === -->
<div>
<h3 class="text-sm font-bold text-slate-500 uppercase tracking-wider mb-4">
Conexiones de Base de Datos
<span class="ml-2 bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full text-xs font-bold normal-case"
x-text="(servidorSeleccionado?._conexiones?.length || 0) + ' conexiones'"></span>
</h3>
<!-- Cargando conexiones -->
<div x-show="cargandoConexiones" class="text-center py-8 text-slate-400">
<div class="inline-block w-6 h-6 border-2 border-blue-500 border-t-transparent rounded-full animate-spin mb-2"></div>
<p class="text-sm">Cargando conexiones...</p>
</div>
<!-- Grid de conexiones -->
<div x-show="!cargandoConexiones && servidorSeleccionado?._conexiones?.length > 0"
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<template x-for="cx in (servidorSeleccionado?._conexiones || [])" :key="cx.ID">
<div class="bg-white border-2 border-slate-200 rounded-xl overflow-hidden hover:shadow-md transition-all">
<!-- DB header with type color -->
<div class="px-4 py-3 flex items-center justify-between" :class="getTipoDbBg(cx.tipo_db?.nombre)">
<div class="flex items-center gap-2">
<span class="text-white font-bold text-sm px-2 py-0.5 rounded bg-black/20"
x-text="cx.tipo_db?.nombre || 'DB'"></span>
<span class="text-white/80 text-xs font-mono" x-text="'#' + cx.ID"></span>
</div>
<!-- Ping badge -->
<div x-show="pingResultado[cx.ID] !== undefined">
<span class="text-xs px-2 py-1 rounded-full font-bold"
:class="pingResultado[cx.ID]?.exitoso ? 'bg-green-400 text-green-900' : 'bg-red-400 text-red-900'"
x-text="pingResultado[cx.ID]?.exitoso ? '✓ ' + pingResultado[cx.ID].tiempo + 'ms' : '✗ offline'">
</span>
</div>
</div>
<!-- DB details -->
<div class="p-4 space-y-2 text-xs">
<div class="flex justify-between">
<span class="text-slate-500 font-medium">Host</span>
<span class="text-slate-900 font-mono" x-text="cx.servidor?.ip_servidor || servidorSeleccionado?.ip_servidor || '—'"></span>
</div>
<div class="flex justify-between">
<span class="text-slate-500 font-medium">Puerto</span>
<span class="text-slate-900 font-mono font-bold" x-text="cx.puerto || '—'"></span>
</div>
<div class="flex justify-between">
<span class="text-slate-500 font-medium">Usuario</span>
<span class="text-slate-900 font-mono" x-text="cx.usuario || '—'"></span>
</div>
<!-- Ping button -->
<div class="pt-3 border-t border-slate-100 flex items-center justify-between">
<button @click="hacerPing(cx)"
:disabled="pingCargando[cx.ID]"
class="flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors"
:class="pingCargando[cx.ID] ? 'bg-slate-100 text-slate-400' : 'bg-blue-50 text-blue-700 hover:bg-blue-100'">
<svg x-show="!pingCargando[cx.ID]" 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.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0"/>
</svg>
<svg x-show="pingCargando[cx.ID]" class="w-3.5 h-3.5 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
<span x-text="pingCargando[cx.ID] ? 'Probando...' : 'Ping'"></span>
</button>
<button @click="abrirQueryRunner(cx)"
class="flex items-center gap-1 px-3 py-1.5 bg-slate-50 text-slate-600 rounded-lg text-xs font-medium hover:bg-slate-100 transition-colors">
<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="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>
</svg>
Query Runner
</button>
</div>
</div>
</div>
</template>
</div>
<!-- Sin conexiones -->
<div x-show="!cargandoConexiones && (!servidorSeleccionado?._conexiones || servidorSeleccionado._conexiones.length === 0)"
class="text-center py-12 bg-slate-50 rounded-xl border border-slate-200">
<svg class="w-10 h-10 mx-auto mb-3 text-slate-300" 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-4"/>
</svg>
<p class="text-slate-500 font-medium text-sm">Este servidor no tiene conexiones de base de datos</p>
</div>
</div>
</div>
</div>
</div>
<script>
function servidorDashboard() {
return {
servidores: [],
servidorSeleccionado: null,
cargando: true,
cargandoConexiones: false,
pingCargando: {},
pingResultado: {},
async init() {
await this.cargarServidores();
},
async cargarServidores() {
this.cargando = true;
try {
const r = await fetch('/app/loadservidor?limit=100');
const data = await r.json();
this.servidores = data.registros || [];
} catch (e) {
console.error('Error cargando servidores:', e);
this.servidores = [];
} finally {
this.cargando = false;
}
},
async abrirModal(servidor) {
this.servidorSeleccionado = { ...servidor };
this.pingCargando = {};
this.pingResultado = {};
this.cargandoConexiones = true;
try {
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) {
console.error('Error cargando conexiones:', e);
this.servidorSeleccionado._conexiones = [];
} finally {
this.cargandoConexiones = false;
}
},
cerrarModal() {
this.servidorSeleccionado = null;
},
async hacerPing(cx) {
this.pingCargando = { ...this.pingCargando, [cx.ID]: true };
try {
const r = await fetch(`/app/conx-ping/${cx.ID}`);
const data = await r.json();
this.pingResultado = { ...this.pingResultado, [cx.ID]: data };
} catch (e) {
this.pingResultado = { ...this.pingResultado, [cx.ID]: { exitoso: false } };
} finally {
this.pingCargando = { ...this.pingCargando, [cx.ID]: false };
}
},
abrirQueryRunner(cx) {
window.open(`/app/query-runner?conx_db_id=${cx.ID}`, '_blank');
},
getTipoDbBg(tipo) {
const m = {
'MySQL': 'bg-blue-500',
'MariaDB': 'bg-blue-600',
'PostgreSQL': 'bg-indigo-600',
'MongoDB': 'bg-green-600',
'Redis': 'bg-red-500',
'SQLite': 'bg-amber-500',
'SQLServer': 'bg-purple-600',
'MSSQL': 'bg-purple-600',
};
return m[tipo] || 'bg-slate-600';
},
};
}
</script>
</div>