From 9702edb4decc9da0475fb6872ab05bffe1abfe89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Omar=20S=C3=A1nchez=20Pizarro?= Date: Mon, 19 Jan 2026 22:54:50 +0100 Subject: [PATCH] refactor: maintain natural log order in API response and enhance log loading logic - Updated the API to return logs in their natural order (oldest to newest). - Modified the loadLogs function in Logs.vue to support incremental updates and maintain scroll position based on user preferences. - Introduced a hash function to track the last loaded log for efficient updates. --- web/backend/server.js | 5 +- web/frontend/src/views/Logs.vue | 104 +++++++++++++++++++++++++------- 2 files changed, 84 insertions(+), 25 deletions(-) diff --git a/web/backend/server.js b/web/backend/server.js index 17d4c6d..178d1dd 100644 --- a/web/backend/server.js +++ b/web/backend/server.js @@ -475,10 +475,9 @@ app.get('/api/logs', (req, res) => { const lines = logs.split('\n').filter(l => l.trim()); const limit = parseInt(req.query.limit) || 100; const lastLines = lines.slice(-limit); - // Invertir orden para mostrar los más recientes primero - const reversedLines = lastLines.reverse(); + // Mantener orden natural: más antiguo a más reciente - res.json({ logs: reversedLines }); + res.json({ logs: lastLines }); } catch (error) { res.status(500).json({ error: error.message }); } diff --git a/web/frontend/src/views/Logs.vue b/web/frontend/src/views/Logs.vue index 542193f..5bf37bb 100644 --- a/web/frontend/src/views/Logs.vue +++ b/web/frontend/src/views/Logs.vue @@ -4,14 +4,14 @@

Logs del Sistema

- -
@@ -95,6 +95,7 @@ const autoRefresh = ref(false); const refreshIntervalSeconds = ref(5); const followLatestLog = ref(true); const logsContainer = ref(null); +const lastLoadedLogHash = ref(null); // Para rastrear el último log cargado let refreshInterval = null; const filteredLogs = computed(() => { @@ -112,19 +113,78 @@ function getLogColor(log) { return 'text-green-400'; } -async function loadLogs(shouldScroll = null) { +async function loadLogs(forceReload = false, shouldScroll = null) { // Si shouldScroll es null, usar la configuración de followLatestLog const shouldAutoScroll = shouldScroll !== null ? shouldScroll : followLatestLog.value; + // Guardar la posición del scroll actual antes de actualizar + const previousScrollTop = logsContainer.value?.scrollTop || 0; + const previousScrollHeight = logsContainer.value?.scrollHeight || 0; + loading.value = true; try { const data = await api.getLogs(500); - logs.value = data.logs || []; - // Hacer scroll al inicio (arriba) donde están los logs más recientes - // Solo si followLatestLog está activado o si se fuerza - await nextTick(); - if (logsContainer.value && shouldAutoScroll) { - logsContainer.value.scrollTop = 0; + const newLogs = data.logs || []; + + if (forceReload || logs.value.length === 0 || lastLoadedLogHash.value === null) { + // Carga inicial o recarga forzada: reemplazar todo + logs.value = newLogs; + lastLoadedLogHash.value = newLogs.length > 0 ? hashLog(newLogs[newLogs.length - 1]) : null; + + await nextTick(); + if (logsContainer.value && shouldAutoScroll) { + // Scroll al final (abajo) donde están los logs más recientes + logsContainer.value.scrollTop = logsContainer.value.scrollHeight; + } + } else { + // Actualización incremental: añadir solo las líneas nuevas al final + if (newLogs.length > 0) { + const currentLastLog = logs.value.length > 0 ? logs.value[logs.value.length - 1] : null; + const newLastLog = newLogs[newLogs.length - 1]; + const newLastLogHash = hashLog(newLastLog); + const lastLoadedHash = lastLoadedLogHash.value; + + // Si el último log cambió, hay nuevos logs + if (currentLastLog !== newLastLog && newLastLogHash !== lastLoadedHash) { + // Crear un Set de los logs actuales para búsqueda rápida + const currentLogsSet = new Set(logs.value); + + // Encontrar qué logs son nuevos (no están en los logs actuales) + const logsToAdd = []; + // Recorrer desde el final para encontrar los nuevos + for (let i = newLogs.length - 1; i >= 0; i--) { + const log = newLogs[i]; + if (!currentLogsSet.has(log)) { + logsToAdd.unshift(log); // Añadir al principio del array para mantener orden + } else { + // Si encontramos un log que ya existe, no hay más logs nuevos antes + break; + } + } + + // Si hay logs nuevos, añadirlos al final + if (logsToAdd.length > 0) { + // Limitar el número total de logs para evitar crecimiento infinito + const maxLogs = 1000; + logs.value = [...logs.value, ...logsToAdd].slice(-maxLogs); // Mantener los últimos maxLogs + + lastLoadedLogHash.value = newLastLogHash; + + await nextTick(); + + // Ajustar el scroll para mantener la posición visual + if (logsContainer.value) { + if (shouldAutoScroll) { + // Si debe seguir el último log, ir al final (abajo) + logsContainer.value.scrollTop = logsContainer.value.scrollHeight; + } else { + // Mantener la posición del scroll sin cambios + logsContainer.value.scrollTop = previousScrollTop; + } + } + } + } + } } } catch (error) { console.error('Error cargando logs:', error); @@ -133,6 +193,13 @@ async function loadLogs(shouldScroll = null) { } } +// Función auxiliar para crear un hash simple de un log +function hashLog(log) { + if (!log) return null; + // Usar los primeros 100 caracteres como identificador + return log.substring(0, 100); +} + function handleAutoRefreshChange() { updateRefreshInterval(); } @@ -148,35 +215,28 @@ function updateRefreshInterval() { if (autoRefresh.value && refreshIntervalSeconds.value > 0) { refreshInterval = setInterval(() => { if (autoRefresh.value) { - // Usar followLatestLog para determinar si hacer scroll - loadLogs(followLatestLog.value); + // Actualización incremental (no forzada) + loadLogs(false, followLatestLog.value); } }, refreshIntervalSeconds.value * 1000); } } -// Hacer scroll al inicio cuando cambian los logs filtrados (solo si followLatestLog está activado y está cerca del inicio) -watch(filteredLogs, async () => { - await nextTick(); - if (logsContainer.value && followLatestLog.value && logsContainer.value.scrollTop < 10) { - // Solo auto-scroll si el usuario está cerca del inicio y sigue el último log - logsContainer.value.scrollTop = 0; - } -}); +// Nota: El scroll ahora se maneja dentro de loadLogs para mejor control function handleWSMessage(event) { const data = event.detail; if (data.type === 'logs_updated') { // Solo actualizar si auto-refresh está activado if (autoRefresh.value) { - // Al recibir actualización de WebSocket, cargar logs con seguimiento si está activado - loadLogs(followLatestLog.value); + // Actualización incremental (no forzada) cuando llega WebSocket + loadLogs(false, followLatestLog.value); } } } onMounted(() => { - loadLogs(true); // Primera carga siempre hace scroll + loadLogs(true, true); // Primera carga forzada siempre hace scroll window.addEventListener('ws-message', handleWSMessage); // Inicializar auto-refresh si está activado