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