Files
soft_usite/resources/views/servidor_dashboard.html
T
Lizandro GuarnizoandCopilot 29820c8706 Add Servidor Dashboard with connection monitoring
- Created servidor_dashboard.html with beautiful visual dashboard
- Shows all servers in grid cards with basic info
- Click button opens detailed modal with:
  - Server full information (IP, CPU, RAM, Storage, OS, Expiration)
  - All connected databases in a clean diagram layout
  - Database connection status (active/inactive)
  - Connection details (Host, Port, User, Type)
  - Ping button to test connection with response time
  - View Connection button to open query-runner
- Backend updates:
  - Added ServidorDashboard() controller to render the dashboard view
  - Added GetServidorDashboard() API endpoint to fetch server + connections
  - Added PingConexion() endpoint to test database connections (TCP + DB test)
  - Color-coded status badges and database type indicators
  - Responsive design for mobile and desktop
- Routes:
  - GET /app/servidor-dashboard (render view)
  - GET /api/app/servidor-dashboard/:id (get server details API)
  - GET /api/app/conx-ping/:id (ping connection endpoint)

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

412 lines
20 KiB
HTML

<!-- Servidor Dashboard -->
<div x-data="servidorDashboard()" x-init="init()">
<!-- Header -->
<div class="mb-8">
<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 conexiones de base de datos en un solo lugar</p>
</div>
<!-- Servidor List -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" x-show="!servidorSeleccionado">
<template x-for="servidor in servidores" :key="servidor.ID">
<div class="bg-white rounded-lg border border-slate-200 shadow-sm hover:shadow-md transition-all overflow-hidden">
<!-- Card Header -->
<div class="bg-gradient-to-r from-blue-500 to-blue-600 p-4">
<h3 class="text-white font-bold text-lg" x-text="servidor.Nombre"></h3>
<p class="text-blue-100 text-xs mt-1" x-text="servidor.Descripcion"></p>
</div>
<!-- Card Body -->
<div class="p-4 space-y-3">
<!-- Server Info -->
<div class="space-y-2 pb-3 border-b border-slate-100">
<div class="flex items-center gap-2">
<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="M9 12l2 2 4-4m7-4a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span class="text-sm text-slate-600">
<strong>IP:</strong> <span x-text="servidor.IP"></span>
</span>
</div>
<div class="flex items-center gap-2">
<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="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span class="text-sm text-slate-600">
<strong>Estado:</strong>
<span class="ml-1 inline-block px-2 py-0.5 rounded-full text-xs font-medium"
:class="servidor.Activo ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'"
x-text="servidor.Activo ? 'Activo' : 'Inactivo'">
</span>
</span>
</div>
</div>
<!-- Connections Count -->
<div class="flex items-center justify-between py-2">
<span class="text-sm text-slate-600">
<strong>Conexiones BD:</strong>
<span class="ml-1 font-bold text-blue-600" x-text="servidor.conexiones?.length || 0"></span>
</span>
</div>
<!-- Action Button -->
<button @click="abrirModal(servidor)"
class="w-full mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors flex items-center justify-center gap-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>
<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"></path>
</svg>
Ver Detalles
</button>
</div>
</div>
</template>
<!-- Empty State -->
<template x-if="servidores.length === 0">
<div 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="M9 3v12m6-12v12m.938-6h2.016A2.25 2.25 0 0121 12a2.25 2.25 0 01-2.25 2.25h-2.016m0 0h2.016a2.25 2.25 0 100-4.5h-2.016m0 0V7.5A2.25 2.25 0 109.75 9.75"></path>
</svg>
<p class="text-lg font-medium">No hay servidores disponibles</p>
</div>
</template>
</div>
<!-- Server Detail Modal -->
<template x-if="servidorSeleccionado">
<div class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4" @click.self="cerrarModal()">
<div class="bg-white rounded-lg max-w-6xl w-full max-h-[90vh] overflow-y-auto">
<!-- Modal Header -->
<div class="sticky top-0 bg-gradient-to-r from-blue-600 to-blue-700 p-6 border-b border-blue-800 flex items-center justify-between">
<div class="flex items-center gap-4">
<button @click="cerrarModal()" class="text-white hover:bg-blue-500 p-2 rounded-lg 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="M15 19l-7-7 7-7"></path>
</svg>
</button>
<div>
<h2 class="text-2xl font-bold text-white" x-text="servidorSeleccionado.Nombre"></h2>
<p class="text-blue-100 text-sm" x-text="servidorSeleccionado.Descripcion"></p>
</div>
</div>
<div class="text-right">
<span class="text-white text-sm font-medium">
<span class="inline-block px-3 py-1 rounded-full text-xs font-bold"
:class="servidorSeleccionado.Activo ? 'bg-green-400 text-green-900' : 'bg-red-400 text-red-900'"
x-text="servidorSeleccionado.Activo ? '● Activo' : '● Inactivo'">
</span>
</span>
</div>
</div>
<!-- Modal Body -->
<div class="p-8 space-y-8">
<!-- Server Information Grid -->
<div>
<h3 class="text-lg font-bold text-slate-900 mb-4 flex items-center gap-2">
<svg class="w-5 h-5 text-blue-600" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"></path>
</svg>
Información del Servidor
</h3>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<!-- IP -->
<div class="bg-slate-50 p-4 rounded-lg border border-slate-200">
<p class="text-xs font-medium text-slate-500 uppercase mb-2">Dirección IP</p>
<p class="text-lg font-bold text-slate-900 font-mono" x-text="servidorSeleccionado.IP"></p>
</div>
<!-- Tipo -->
<div class="bg-slate-50 p-4 rounded-lg border border-slate-200">
<p class="text-xs font-medium text-slate-500 uppercase mb-2">Tipo</p>
<p class="text-lg font-bold text-slate-900" x-text="servidorSeleccionado.TipoServidor?.Nombre || 'N/A'"></p>
</div>
<!-- Provider -->
<div class="bg-slate-50 p-4 rounded-lg border border-slate-200">
<p class="text-xs font-medium text-slate-500 uppercase mb-2">Proveedor</p>
<p class="text-lg font-bold text-slate-900" x-text="servidorSeleccionado.ProvServidor?.Nombre || 'N/A'"></p>
</div>
<!-- RAM -->
<div class="bg-slate-50 p-4 rounded-lg border border-slate-200">
<p class="text-xs font-medium text-slate-500 uppercase mb-2">RAM</p>
<p class="text-lg font-bold text-slate-900" x-text="servidorSeleccionado.Ram + ' GB' || 'N/A'"></p>
</div>
<!-- CPU -->
<div class="bg-slate-50 p-4 rounded-lg border border-slate-200">
<p class="text-xs font-medium text-slate-500 uppercase mb-2">CPU</p>
<p class="text-lg font-bold text-slate-900" x-text="servidorSeleccionado.CPU || 'N/A'"></p>
</div>
<!-- Almacenamiento -->
<div class="bg-slate-50 p-4 rounded-lg border border-slate-200">
<p class="text-xs font-medium text-slate-500 uppercase mb-2">Almacenamiento</p>
<p class="text-lg font-bold text-slate-900" x-text="servidorSeleccionado.Almacenamiento + ' GB' || 'N/A'"></p>
</div>
<!-- OS -->
<div class="bg-slate-50 p-4 rounded-lg border border-slate-200">
<p class="text-xs font-medium text-slate-500 uppercase mb-2">Sistema Operativo</p>
<p class="text-lg font-bold text-slate-900" x-text="servidorSeleccionado.SO || 'N/A'"></p>
</div>
<!-- Fecha Expiración -->
<div class="bg-slate-50 p-4 rounded-lg border border-slate-200">
<p class="text-xs font-medium text-slate-500 uppercase mb-2">Expiración</p>
<p class="text-lg font-bold text-slate-900" x-text="formatearFecha(servidorSeleccionado.FechaExpiracion)"></p>
</div>
</div>
</div>
<!-- Databases Connection Diagram -->
<div>
<h3 class="text-lg font-bold text-slate-900 mb-4 flex items-center gap-2">
<svg class="w-5 h-5 text-blue-600" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"></path>
</svg>
Conexiones de Base de Datos
</h3>
<template x-if="servidorSeleccionado.conexiones && servidorSeleccionado.conexiones.length > 0">
<div class="space-y-4">
<!-- Server node in center -->
<div class="relative">
<div class="flex justify-center mb-8">
<div class="bg-gradient-to-r from-blue-600 to-blue-700 text-white px-6 py-3 rounded-lg shadow-lg font-bold flex items-center gap-2">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2m0 2a8 8 0 100 16 8 8 0 000-16z"></path>
</svg>
<span x-text="servidorSeleccionado.Nombre"></span>
</div>
</div>
<!-- Databases Grid below server -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mt-8">
<template x-for="conexion in servidorSeleccionado.conexiones" :key="conexion.ID">
<div class="bg-white border-2 rounded-lg overflow-hidden hover:shadow-lg transition-all"
:class="getEstadoColorClass(conexion.Estado)">
<!-- Connection Header -->
<div class="px-4 py-3" :class="getEstadoHeaderClass(conexion.Estado)">
<div class="flex items-center justify-between">
<h4 class="font-bold text-white text-sm" x-text="conexion.Nombre"></h4>
<span class="inline-flex items-center gap-1 text-xs font-bold text-white"
:class="conexion.Estado === 'activo' ? 'text-green-100' : 'text-red-100'">
<span class="w-2 h-2 rounded-full" :class="conexion.Estado === 'activo' ? 'bg-green-400' : 'bg-red-400'"></span>
<span x-text="conexion.Estado || 'desconocido'"></span>
</span>
</div>
</div>
<!-- Connection Body -->
<div class="p-4 space-y-3">
<!-- DB Type Badge -->
<div class="flex items-center gap-2">
<span class="text-xs font-bold px-2 py-1 rounded-full text-white" :class="getTipoDbColor(conexion.TipoDb?.Nombre)">
<span x-text="conexion.TipoDb?.Nombre || 'UNKNOWN'"></span>
</span>
<span class="text-xs text-slate-500 font-medium" x-text="`v${conexion.Version || 'N/A'}`"></span>
</div>
<!-- Connection Details -->
<div class="space-y-2 text-xs">
<div class="flex justify-between items-start">
<span class="text-slate-600 font-medium">Host:</span>
<span class="text-slate-900 font-mono text-right" x-text="conexion.Host"></span>
</div>
<div class="flex justify-between items-start">
<span class="text-slate-600 font-medium">Puerto:</span>
<span class="text-slate-900 font-mono" x-text="conexion.Puerto"></span>
</div>
<div class="flex justify-between items-start">
<span class="text-slate-600 font-medium">Usuario:</span>
<span class="text-slate-900 font-mono" x-text="conexion.Usuario"></span>
</div>
</div>
<!-- Connection Status Section -->
<div class="pt-3 border-t border-slate-200">
<div class="flex items-center justify-between">
<span class="text-xs font-medium text-slate-600">Ping:</span>
<button @click="hacerPingConexion(conexion)"
:disabled="pingCargando[conexion.ID]"
class="text-xs px-2 py-1 rounded bg-slate-100 text-slate-700 hover:bg-slate-200 disabled:opacity-50 transition-colors">
<span x-show="!pingCargando[conexion.ID]">Verificar</span>
<span x-show="pingCargando[conexion.ID]">...</span>
</button>
</div>
<template x-if="pingResultado[conexion.ID]">
<div class="mt-2 p-2 rounded-lg text-xs font-mono"
:class="pingResultado[conexion.ID].exitoso ? 'bg-green-50 text-green-800' : 'bg-red-50 text-red-800'">
<template x-if="pingResultado[conexion.ID].exitoso">
<span><span x-text="`${pingResultado[conexion.ID].tiempo}ms`"></span></span>
</template>
<template x-if="!pingResultado[conexion.ID].exitoso">
<span>✗ Conexión fallida</span>
</template>
</div>
</template>
</div>
<!-- Action Button -->
<button @click="abrirVcard(conexion)"
class="w-full mt-3 px-3 py-2 bg-blue-50 text-blue-700 rounded-lg font-medium text-xs hover:bg-blue-100 transition-colors flex items-center justify-center gap-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="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
</svg>
Ver Conexión
</button>
</div>
</div>
</template>
</div>
</div>
</div>
</template>
<!-- No connections -->
<template x-if="!servidorSeleccionado.conexiones || servidorSeleccionado.conexiones.length === 0">
<div class="text-center py-12 bg-slate-50 rounded-lg border border-slate-200">
<svg class="w-12 h-12 mx-auto mb-3 opacity-30 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.658 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path>
</svg>
<p class="text-slate-600 font-medium">Este servidor no tiene conexiones de base de datos configuradas</p>
</div>
</template>
</div>
<!-- Last Updated -->
<div class="text-center py-4 border-t border-slate-200">
<p class="text-xs text-slate-500" x-text="`Última actualización: ${formatearFechaHora(new Date())}`"></p>
</div>
</div>
</div>
</div>
</template>
<script>
function servidorDashboard() {
return {
servidores: [],
servidorSeleccionado: null,
pingCargando: {},
pingResultado: {},
async init() {
await this.cargarServidores();
},
async cargarServidores() {
try {
const response = await fetch('/api/app/loadservidor?limit=100');
const data = await response.json();
this.servidores = data.registros || [];
// Cargar detalles de conexiones para cada servidor
for (let servidor of this.servidores) {
await this.cargarConexionesServidor(servidor);
}
} catch (error) {
console.error('Error cargando servidores:', error);
}
},
async cargarConexionesServidor(servidor) {
try {
const response = await fetch(`/api/app/servidor-dashboard/${servidor.ID}`);
const data = await response.json();
if (data.conexiones) {
servidor.conexiones = data.conexiones;
}
} catch (error) {
console.error('Error cargando conexiones:', error);
servidor.conexiones = [];
}
},
abrirModal(servidor) {
this.servidorSeleccionado = JSON.parse(JSON.stringify(servidor));
this.pingResultado = {};
},
cerrarModal() {
this.servidorSeleccionado = null;
this.pingCargando = {};
this.pingResultado = {};
},
async hacerPingConexion(conexion) {
this.pingCargando[conexion.ID] = true;
try {
const response = await fetch(`/api/app/conx-ping/${conexion.ID}`);
const data = await response.json();
this.pingResultado[conexion.ID] = data;
} catch (error) {
this.pingResultado[conexion.ID] = {
exitoso: false,
error: error.message
};
} finally {
this.pingCargando[conexion.ID] = false;
}
},
abrirVcard(conexion) {
window.open(`/app/conx_db?conx_db_id=${conexion.ID}`, '_blank');
},
getTipoDbColor(tipo) {
const colores = {
'MySQL': 'bg-blue-600',
'PostgreSQL': 'bg-blue-700',
'MongoDB': 'bg-green-600',
'Redis': 'bg-red-600',
'SQLite': 'bg-yellow-600',
'SQLServer': 'bg-purple-600'
};
return colores[tipo] || 'bg-slate-500';
},
getEstadoHeaderClass(estado) {
if (estado === 'activo') return 'bg-green-600';
if (estado === 'inactivo') return 'bg-red-600';
return 'bg-slate-600';
},
getEstadoColorClass(estado) {
if (estado === 'activo') return 'border-green-300';
if (estado === 'inactivo') return 'border-red-300';
return 'border-slate-300';
},
formatearFecha(fecha) {
if (!fecha) return 'N/A';
try {
return new Date(fecha).toLocaleDateString('es-CO', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
} catch {
return 'N/A';
}
},
formatearFechaHora(fecha) {
return fecha.toLocaleString('es-CO', {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
};
}
</script>
</div>