Invert log order to display the most recent entries first in the API response. Enhanced Logs.vue with auto-refresh functionality, allowing users to set refresh intervals and follow the latest logs. Updated loadLogs method to support scrolling behavior based on user preferences.
This commit is contained in:
@@ -475,8 +475,10 @@ app.get('/api/logs', (req, res) => {
|
|||||||
const lines = logs.split('\n').filter(l => l.trim());
|
const lines = logs.split('\n').filter(l => l.trim());
|
||||||
const limit = parseInt(req.query.limit) || 100;
|
const limit = parseInt(req.query.limit) || 100;
|
||||||
const lastLines = lines.slice(-limit);
|
const lastLines = lines.slice(-limit);
|
||||||
|
// Invertir orden para mostrar los más recientes primero
|
||||||
|
const reversedLines = lastLines.reverse();
|
||||||
|
|
||||||
res.json({ logs: lastLines });
|
res.json({ logs: reversedLines });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,67 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3 sm:gap-4 mb-4 sm:mb-6">
|
<div class="flex flex-col gap-4 mb-4 sm:mb-6">
|
||||||
<h1 class="text-2xl sm:text-3xl font-bold text-gray-900">Logs del Sistema</h1>
|
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3 sm:gap-4">
|
||||||
<div class="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 sm:space-x-4">
|
<h1 class="text-2xl sm:text-3xl font-bold text-gray-900">Logs del Sistema</h1>
|
||||||
<select v-model="logLevel" @change="loadLogs" class="input text-sm sm:text-base" style="width: 100%; min-width: 160px;">
|
<div class="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 sm:space-x-4">
|
||||||
<option value="">Todos los niveles</option>
|
<select v-model="logLevel" @change="loadLogs(followLatestLog)" class="input text-sm sm:text-base" style="width: 100%; min-width: 160px;">
|
||||||
<option value="INFO">INFO</option>
|
<option value="">Todos los niveles</option>
|
||||||
<option value="WARNING">WARNING</option>
|
<option value="INFO">INFO</option>
|
||||||
<option value="ERROR">ERROR</option>
|
<option value="WARNING">WARNING</option>
|
||||||
<option value="DEBUG">DEBUG</option>
|
<option value="ERROR">ERROR</option>
|
||||||
</select>
|
<option value="DEBUG">DEBUG</option>
|
||||||
<button @click="loadLogs" class="btn btn-primary text-sm sm:text-base whitespace-nowrap">
|
</select>
|
||||||
Actualizar
|
<button @click="loadLogs(followLatestLog)" class="btn btn-primary text-sm sm:text-base whitespace-nowrap">
|
||||||
</button>
|
Actualizar
|
||||||
<button @click="autoRefresh = !autoRefresh" class="btn btn-secondary text-sm sm:text-base whitespace-nowrap">
|
</button>
|
||||||
{{ autoRefresh ? '⏸ Pausar' : '▶ Auto-refresh' }}
|
</div>
|
||||||
</button>
|
</div>
|
||||||
|
|
||||||
|
<!-- Panel de configuración de auto-refresh -->
|
||||||
|
<div class="card p-4 bg-gray-50 border border-gray-200">
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-4">
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
v-model="autoRefresh"
|
||||||
|
@change="handleAutoRefreshChange"
|
||||||
|
class="w-4 h-4 text-blue-600 rounded focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<span class="font-medium text-gray-700">Auto-refresh</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div v-if="autoRefresh" class="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4 flex-1">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<label class="text-sm text-gray-600 whitespace-nowrap">Intervalo:</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
v-model.number="refreshIntervalSeconds"
|
||||||
|
@change="updateRefreshInterval"
|
||||||
|
min="1"
|
||||||
|
max="300"
|
||||||
|
class="input text-sm w-20"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-gray-600 whitespace-nowrap">segundos</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
v-model="followLatestLog"
|
||||||
|
class="w-4 h-4 text-blue-600 rounded focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-gray-700">Seguir último log</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card p-2 sm:p-6">
|
<div class="card p-2 sm:p-6">
|
||||||
<div class="bg-gray-900 text-green-400 font-mono text-xs sm:text-sm p-3 sm:p-4 rounded-lg overflow-x-auto max-h-[400px] sm:max-h-[600px] overflow-y-auto">
|
<div
|
||||||
|
ref="logsContainer"
|
||||||
|
class="bg-gray-900 text-green-400 font-mono text-xs sm:text-sm p-3 sm:p-4 rounded-lg overflow-x-auto max-h-[400px] sm:max-h-[600px] overflow-y-auto"
|
||||||
|
>
|
||||||
<div v-if="loading" class="text-center py-8">
|
<div v-if="loading" class="text-center py-8">
|
||||||
<div class="inline-block animate-spin rounded-full h-6 w-6 border-b-2 border-green-400"></div>
|
<div class="inline-block animate-spin rounded-full h-6 w-6 border-b-2 border-green-400"></div>
|
||||||
<p class="mt-2 text-gray-400">Cargando logs...</p>
|
<p class="mt-2 text-gray-400">Cargando logs...</p>
|
||||||
@@ -44,13 +85,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
|
|
||||||
const logs = ref([]);
|
const logs = ref([]);
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const logLevel = ref('');
|
const logLevel = ref('');
|
||||||
const autoRefresh = ref(false);
|
const autoRefresh = ref(false);
|
||||||
|
const refreshIntervalSeconds = ref(5);
|
||||||
|
const followLatestLog = ref(true);
|
||||||
|
const logsContainer = ref(null);
|
||||||
let refreshInterval = null;
|
let refreshInterval = null;
|
||||||
|
|
||||||
const filteredLogs = computed(() => {
|
const filteredLogs = computed(() => {
|
||||||
@@ -68,11 +112,20 @@ function getLogColor(log) {
|
|||||||
return 'text-green-400';
|
return 'text-green-400';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadLogs() {
|
async function loadLogs(shouldScroll = null) {
|
||||||
|
// Si shouldScroll es null, usar la configuración de followLatestLog
|
||||||
|
const shouldAutoScroll = shouldScroll !== null ? shouldScroll : followLatestLog.value;
|
||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const data = await api.getLogs(500);
|
const data = await api.getLogs(500);
|
||||||
logs.value = data.logs || [];
|
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;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error cargando logs:', error);
|
console.error('Error cargando logs:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -80,25 +133,53 @@ async function loadLogs() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleAutoRefreshChange() {
|
||||||
|
updateRefreshInterval();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRefreshInterval() {
|
||||||
|
// Limpiar intervalo anterior
|
||||||
|
if (refreshInterval) {
|
||||||
|
clearInterval(refreshInterval);
|
||||||
|
refreshInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crear nuevo intervalo si auto-refresh está activado
|
||||||
|
if (autoRefresh.value && refreshIntervalSeconds.value > 0) {
|
||||||
|
refreshInterval = setInterval(() => {
|
||||||
|
if (autoRefresh.value) {
|
||||||
|
// Usar followLatestLog para determinar si hacer scroll
|
||||||
|
loadLogs(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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function handleWSMessage(event) {
|
function handleWSMessage(event) {
|
||||||
const data = event.detail;
|
const data = event.detail;
|
||||||
if (data.type === 'logs_updated') {
|
if (data.type === 'logs_updated') {
|
||||||
loadLogs();
|
// Al recibir actualización de WebSocket, cargar logs con seguimiento si está activado
|
||||||
|
loadLogs(followLatestLog.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadLogs();
|
loadLogs(true); // Primera carga siempre hace scroll
|
||||||
window.addEventListener('ws-message', handleWSMessage);
|
window.addEventListener('ws-message', handleWSMessage);
|
||||||
|
|
||||||
// Auto-refresh
|
// Inicializar auto-refresh si está activado
|
||||||
const checkAutoRefresh = () => {
|
if (autoRefresh.value) {
|
||||||
if (autoRefresh.value) {
|
updateRefreshInterval();
|
||||||
loadLogs();
|
}
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
refreshInterval = setInterval(checkAutoRefresh, 5000);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user