diff --git a/resources/views/servidor_dashboard.html b/resources/views/servidor_dashboard.html
index fbd8b66..d3dde01 100644
--- a/resources/views/servidor_dashboard.html
+++ b/resources/views/servidor_dashboard.html
@@ -1,4 +1,5 @@
+
+
+
+
+
+
Historial de Métricas
+
+
+
+
+
+
+
+
+
+
+ Cargando historial...
+
+
+
+ Sin datos en este período
+
+
+
+
+
+
+ CPU %
+ RAM %
+ Disco %
+
+
+
+
+
@@ -601,6 +639,10 @@
syncCargando: {},
pingCargando: {},
pingResultado: {},
+ historialHoras: 24,
+ historialCargando: false,
+ historialVacio: false,
+ _chart: null,
async init() {
await this.cargarServidores();
@@ -624,12 +666,13 @@
this.servidorSeleccionado = { ...servidor };
this.pingCargando = {};
this.pingResultado = {};
+ this.historialHoras = 24;
+ this.historialVacio = false;
this.cargandoConexiones = true;
+ if (this._chart) { this._chart.destroy(); this._chart = null; }
try {
const r = await fetch(`/app/servidor-dashboard/${servidor.ID}`);
const data = await r.json();
- // Inyectar estado de ping directamente en cada conexión para que
- // Alpine v3 lo rastreé correctamente dentro del x-for
const conexiones = (data.conexiones || []).map(c => ({
...c,
_pingCargando: false,
@@ -644,12 +687,112 @@
} finally {
this.cargandoConexiones = false;
}
+ if (servidor.agent_token) {
+ await this.$nextTick();
+ this.cargarHistorial(24);
+ }
},
cerrarModal() {
+ if (this._chart) { this._chart.destroy(); this._chart = null; }
this.servidorSeleccionado = null;
},
+ async cargarHistorial(horas) {
+ if (!this.servidorSeleccionado) return;
+ this.historialHoras = horas;
+ this.historialCargando = true;
+ this.historialVacio = false;
+ try {
+ const r = await fetch(`/app/servidor/${this.servidorSeleccionado.ID}/metricas-history?horas=${horas}`);
+ const data = await r.json();
+ if (!data || data.length === 0) { this.historialVacio = true; return; }
+ // Downsample si hay muchos puntos (>300 para fluidez)
+ const pts = data.length > 300
+ ? data.filter((_, i) => i % Math.ceil(data.length / 300) === 0)
+ : data;
+ const labels = pts.map(p => {
+ const d = new Date(p.created_at);
+ return horas <= 6
+ ? d.toLocaleTimeString('es', {hour:'2-digit', minute:'2-digit', second:'2-digit'})
+ : horas <= 24
+ ? d.toLocaleTimeString('es', {hour:'2-digit', minute:'2-digit'})
+ : d.toLocaleDateString('es', {month:'short', day:'numeric'}) + ' ' + d.toLocaleTimeString('es', {hour:'2-digit', minute:'2-digit'});
+ });
+ await this.$nextTick();
+ const canvas = document.getElementById('metricasChart');
+ if (!canvas) return;
+ if (this._chart) this._chart.destroy();
+ this._chart = new Chart(canvas, {
+ type: 'line',
+ data: {
+ labels,
+ datasets: [
+ {
+ label: 'CPU %',
+ data: pts.map(p => p.cpu_pct),
+ borderColor: '#3b82f6',
+ backgroundColor: 'rgba(59,130,246,0.08)',
+ borderWidth: 1.5,
+ pointRadius: 0,
+ tension: 0.3,
+ fill: true,
+ },
+ {
+ label: 'RAM %',
+ data: pts.map(p => p.ram_pct),
+ borderColor: '#22c55e',
+ backgroundColor: 'rgba(34,197,94,0.08)',
+ borderWidth: 1.5,
+ pointRadius: 0,
+ tension: 0.3,
+ fill: true,
+ },
+ {
+ label: 'Disco %',
+ data: pts.map(p => p.disco_pct),
+ borderColor: '#f59e0b',
+ backgroundColor: 'rgba(245,158,11,0.06)',
+ borderWidth: 1.5,
+ pointRadius: 0,
+ tension: 0.1,
+ fill: false,
+ },
+ ],
+ },
+ options: {
+ responsive: true,
+ interaction: { mode: 'index', intersect: false },
+ plugins: {
+ legend: { display: false },
+ tooltip: {
+ callbacks: {
+ label: ctx => `${ctx.dataset.label}: ${ctx.parsed.y?.toFixed(1)}%`
+ }
+ }
+ },
+ scales: {
+ x: {
+ ticks: { maxTicksLimit: 8, font: { size: 10 }, color: '#94a3b8' },
+ grid: { color: '#f1f5f9' },
+ },
+ y: {
+ min: 0, max: 100,
+ ticks: { stepSize: 25, font: { size: 10 }, color: '#94a3b8',
+ callback: v => v + '%' },
+ grid: { color: '#f1f5f9' },
+ },
+ },
+ },
+ });
+ } catch (e) {
+ console.error('Error cargando historial:', e);
+ this.historialVacio = true;
+ } finally {
+ this.historialCargando = false;
+ }
+ },
+
abrirAgentModal(servidor) {
// Buscar el servidor actualizado en la lista
const s = this.servidores.find(sv => sv.ID === servidor.ID) || servidor;