From fd72eec2818608654894213fabcc86b12fcb1fe2 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Thu, 25 Jun 2026 11:05:10 -0500 Subject: [PATCH] feat(dashboard): add CPU/RAM/disk history chart with 1h/6h/24h/7d range Uses Chart.js to render line charts from servidor_metricas_history. Chart loads when opening the server detail modal and downsamples to max 300 points for performance. Co-Authored-By: Claude Sonnet 4.6 --- resources/views/servidor_dashboard.html | 147 +++++++++++++++++++++++- 1 file changed, 145 insertions(+), 2 deletions(-) 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 @@ +
@@ -355,6 +356,43 @@
+ + +

@@ -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;