fix:logs and new articles
Signed-off-by: Omar Sánchez Pizarro <omar.sanchez@pistacero.net>
This commit is contained in:
@@ -63,6 +63,15 @@ async function initRedis() {
|
|||||||
redisClient.on('error', (err) => console.error('Redis Client Error', err));
|
redisClient.on('error', (err) => console.error('Redis Client Error', err));
|
||||||
await redisClient.connect();
|
await redisClient.connect();
|
||||||
console.log('✅ Conectado a Redis');
|
console.log('✅ Conectado a Redis');
|
||||||
|
|
||||||
|
// Inicializar claves conocidas para evitar notificar artículos existentes
|
||||||
|
try {
|
||||||
|
const initialKeys = await redisClient.keys('notified:*');
|
||||||
|
notifiedArticleKeys = new Set(initialKeys);
|
||||||
|
console.log(`📋 ${notifiedArticleKeys.size} artículos ya notificados detectados`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error inicializando claves de artículos:', error.message);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('ℹ️ Redis no configurado, usando modo memoria');
|
console.log('ℹ️ Redis no configurado, usando modo memoria');
|
||||||
}
|
}
|
||||||
@@ -446,7 +455,7 @@ app.get('/api/articles/search', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Obtener logs (últimas líneas)
|
// Obtener logs (últimas líneas o nuevas líneas desde un número de línea)
|
||||||
app.get('/api/logs', (req, res) => {
|
app.get('/api/logs', (req, res) => {
|
||||||
try {
|
try {
|
||||||
// Intentar múltiples ubicaciones posibles
|
// Intentar múltiples ubicaciones posibles
|
||||||
@@ -457,7 +466,7 @@ app.get('/api/logs', (req, res) => {
|
|||||||
if (existsSync(altPath)) {
|
if (existsSync(altPath)) {
|
||||||
logFile = altPath;
|
logFile = altPath;
|
||||||
} else {
|
} else {
|
||||||
return res.json({ logs: [] });
|
return res.json({ logs: [], totalLines: 0, lastLineNumber: 0 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -465,19 +474,37 @@ app.get('/api/logs', (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const stats = statSync(logFile);
|
const stats = statSync(logFile);
|
||||||
if (stats.isDirectory()) {
|
if (stats.isDirectory()) {
|
||||||
return res.json({ logs: ['Error: monitor.log es un directorio. Por favor, elimínalo y reinicia.'] });
|
return res.json({ logs: ['Error: monitor.log es un directorio. Por favor, elimínalo y reinicia.'], totalLines: 0, lastLineNumber: 0 });
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return res.json({ logs: [] });
|
return res.json({ logs: [], totalLines: 0, lastLineNumber: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const logs = readFileSync(logFile, 'utf8');
|
const logsContent = readFileSync(logFile, 'utf8');
|
||||||
const lines = logs.split('\n').filter(l => l.trim());
|
const allLines = logsContent.split('\n').filter(l => l.trim());
|
||||||
const limit = parseInt(req.query.limit) || 100;
|
const totalLines = allLines.length;
|
||||||
const lastLines = lines.slice(-limit);
|
|
||||||
// Mantener orden natural: más antiguo a más reciente
|
|
||||||
|
|
||||||
res.json({ logs: lastLines });
|
// Si se proporciona since (número de línea desde el que empezar), devolver solo las nuevas
|
||||||
|
const sinceLine = parseInt(req.query.since) || 0;
|
||||||
|
|
||||||
|
if (sinceLine > 0 && sinceLine < totalLines) {
|
||||||
|
// Devolver solo las líneas nuevas después de sinceLine
|
||||||
|
const newLines = allLines.slice(sinceLine);
|
||||||
|
return res.json({
|
||||||
|
logs: newLines,
|
||||||
|
totalLines: totalLines,
|
||||||
|
lastLineNumber: totalLines - 1 // Índice de la última línea
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Carga inicial: devolver las últimas líneas
|
||||||
|
const limit = parseInt(req.query.limit) || 500;
|
||||||
|
const lastLines = allLines.slice(-limit);
|
||||||
|
return res.json({
|
||||||
|
logs: lastLines,
|
||||||
|
totalLines: totalLines,
|
||||||
|
lastLineNumber: totalLines - 1 // Índice de la última línea
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
@@ -580,8 +607,8 @@ if (!existsSync(watchLogPath)) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch files for changes (ya no vigilamos FAVORITES_PATH porque usa Redis)
|
// Watch files for changes (ya no vigilamos logs porque usa polling)
|
||||||
const watcher = watch([WORKERS_PATH, watchLogPath].filter(p => existsSync(p)), {
|
const watcher = watch([WORKERS_PATH].filter(p => existsSync(p)), {
|
||||||
persistent: true,
|
persistent: true,
|
||||||
ignoreInitial: true,
|
ignoreInitial: true,
|
||||||
});
|
});
|
||||||
@@ -591,17 +618,100 @@ watcher.on('change', async (path) => {
|
|||||||
if (path === WORKERS_PATH) {
|
if (path === WORKERS_PATH) {
|
||||||
const workers = readJSON(WORKERS_PATH);
|
const workers = readJSON(WORKERS_PATH);
|
||||||
broadcast({ type: 'workers_updated', data: workers });
|
broadcast({ type: 'workers_updated', data: workers });
|
||||||
} else if (path === LOG_PATH) {
|
|
||||||
broadcast({ type: 'logs_updated' });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Rastrear artículos ya notificados para detectar nuevos
|
||||||
|
let notifiedArticleKeys = new Set();
|
||||||
|
let articlesCheckInterval = null;
|
||||||
|
|
||||||
|
// Función para detectar y enviar artículos nuevos
|
||||||
|
async function checkForNewArticles() {
|
||||||
|
if (!redisClient) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const currentKeys = await redisClient.keys('notified:*');
|
||||||
|
const currentKeysSet = new Set(currentKeys);
|
||||||
|
|
||||||
|
// Encontrar claves nuevas
|
||||||
|
const newKeys = currentKeys.filter(key => !notifiedArticleKeys.has(key));
|
||||||
|
|
||||||
|
if (newKeys.length > 0) {
|
||||||
|
// Obtener los artículos nuevos
|
||||||
|
const newArticles = [];
|
||||||
|
for (const key of newKeys) {
|
||||||
|
try {
|
||||||
|
const value = await redisClient.get(key);
|
||||||
|
if (value) {
|
||||||
|
// Intentar parsear como JSON
|
||||||
|
let articleData = {};
|
||||||
|
try {
|
||||||
|
articleData = JSON.parse(value);
|
||||||
|
} catch (e) {
|
||||||
|
// Si no es JSON válido, extraer información de la key
|
||||||
|
const parts = key.split(':');
|
||||||
|
if (parts.length >= 3) {
|
||||||
|
articleData = {
|
||||||
|
platform: parts[1],
|
||||||
|
id: parts.slice(2).join(':'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Añadir información adicional si está disponible
|
||||||
|
if (articleData.platform && articleData.id) {
|
||||||
|
newArticles.push({
|
||||||
|
platform: articleData.platform || 'unknown',
|
||||||
|
id: articleData.id || 'unknown',
|
||||||
|
title: articleData.title || null,
|
||||||
|
price: articleData.price || null,
|
||||||
|
currency: articleData.currency || '€',
|
||||||
|
url: articleData.url || null,
|
||||||
|
images: articleData.images || [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error obteniendo artículo de Redis (${key}):`, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enviar artículos nuevos por WebSocket
|
||||||
|
if (newArticles.length > 0) {
|
||||||
|
broadcast({
|
||||||
|
type: 'new_articles',
|
||||||
|
data: newArticles
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualizar el set de claves notificadas
|
||||||
|
notifiedArticleKeys = currentKeysSet;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error verificando artículos nuevos:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inicializar el check de artículos cuando Redis esté listo
|
||||||
|
async function startArticleMonitoring() {
|
||||||
|
if (redisClient) {
|
||||||
|
// Iniciar intervalo para verificar nuevos artículos cada 3 segundos
|
||||||
|
articlesCheckInterval = setInterval(checkForNewArticles, 3000);
|
||||||
|
console.log('✅ Monitoreo de artículos nuevos iniciado');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Inicializar servidor
|
// Inicializar servidor
|
||||||
const PORT = process.env.PORT || 3001;
|
const PORT = process.env.PORT || 3001;
|
||||||
|
|
||||||
async function startServer() {
|
async function startServer() {
|
||||||
await initRedis();
|
await initRedis();
|
||||||
|
|
||||||
|
// Iniciar monitoreo de artículos nuevos
|
||||||
|
await startArticleMonitoring();
|
||||||
|
|
||||||
server.listen(PORT, () => {
|
server.listen(PORT, () => {
|
||||||
console.log(`🚀 Servidor backend ejecutándose en http://localhost:${PORT}`);
|
console.log(`🚀 Servidor backend ejecutándose en http://localhost:${PORT}`);
|
||||||
console.log(`📡 WebSocket disponible en ws://localhost:${PORT}`);
|
console.log(`📡 WebSocket disponible en ws://localhost:${PORT}`);
|
||||||
|
|||||||
@@ -83,6 +83,65 @@
|
|||||||
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 sm:py-6">
|
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 sm:py-6">
|
||||||
<router-view />
|
<router-view />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<!-- Toast notifications container -->
|
||||||
|
<div class="fixed bottom-4 right-4 z-50 space-y-2">
|
||||||
|
<div
|
||||||
|
v-for="toast in toasts"
|
||||||
|
:key="toast.id"
|
||||||
|
class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg p-4 max-w-md min-w-[320px] animate-slide-in"
|
||||||
|
>
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<img
|
||||||
|
v-if="toast.image"
|
||||||
|
:src="toast.image"
|
||||||
|
:alt="toast.title"
|
||||||
|
class="w-16 h-16 object-cover rounded"
|
||||||
|
@error="($event) => $event.target.style.display = 'none'"
|
||||||
|
/>
|
||||||
|
<div v-else class="w-16 h-16 bg-gray-200 dark:bg-gray-700 rounded flex items-center justify-center">
|
||||||
|
<span class="text-gray-400 text-xs">📦</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="flex items-start justify-between gap-2">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex items-center gap-2 mb-1">
|
||||||
|
<span
|
||||||
|
class="px-2 py-0.5 text-xs font-semibold rounded"
|
||||||
|
:class="toast.platform === 'wallapop' ? 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200' : 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200'"
|
||||||
|
>
|
||||||
|
{{ toast.platform?.toUpperCase() }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h4 class="font-semibold text-gray-900 dark:text-gray-100 text-sm mb-1 line-clamp-2">
|
||||||
|
{{ toast.title || 'Nuevo artículo' }}
|
||||||
|
</h4>
|
||||||
|
<p v-if="toast.price" class="text-lg font-bold text-primary-600 dark:text-primary-400 mb-2">
|
||||||
|
{{ toast.price }} {{ toast.currency || '€' }}
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
v-if="toast.url"
|
||||||
|
:href="toast.url"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="text-xs text-primary-600 dark:text-primary-400 hover:underline inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
Ver artículo →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="removeToast(toast.id)"
|
||||||
|
class="flex-shrink-0 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -111,10 +170,32 @@ const navItems = [
|
|||||||
const wsConnected = ref(false);
|
const wsConnected = ref(false);
|
||||||
const mobileMenuOpen = ref(false);
|
const mobileMenuOpen = ref(false);
|
||||||
const darkMode = ref(false);
|
const darkMode = ref(false);
|
||||||
|
const toasts = ref([]);
|
||||||
let ws = null;
|
let ws = null;
|
||||||
|
let toastIdCounter = 0;
|
||||||
|
|
||||||
const isDark = computed(() => darkMode.value);
|
const isDark = computed(() => darkMode.value);
|
||||||
|
|
||||||
|
function addToast(article) {
|
||||||
|
const id = ++toastIdCounter;
|
||||||
|
toasts.value.push({
|
||||||
|
id,
|
||||||
|
...article,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-remover después de 8 segundos
|
||||||
|
setTimeout(() => {
|
||||||
|
removeToast(id);
|
||||||
|
}, 8000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeToast(id) {
|
||||||
|
const index = toasts.value.findIndex(t => t.id === id);
|
||||||
|
if (index > -1) {
|
||||||
|
toasts.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toggleDarkMode() {
|
function toggleDarkMode() {
|
||||||
darkMode.value = !darkMode.value;
|
darkMode.value = !darkMode.value;
|
||||||
if (darkMode.value) {
|
if (darkMode.value) {
|
||||||
@@ -173,7 +254,16 @@ function connectWebSocket() {
|
|||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
// Los componentes individuales manejarán los mensajes
|
|
||||||
|
// Manejar notificaciones de artículos nuevos
|
||||||
|
if (data.type === 'new_articles' && data.data) {
|
||||||
|
// Mostrar toasts para cada artículo nuevo
|
||||||
|
for (const article of data.data) {
|
||||||
|
addToast(article);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Los componentes individuales manejarán otros mensajes
|
||||||
window.dispatchEvent(new CustomEvent('ws-message', { detail: data }));
|
window.dispatchEvent(new CustomEvent('ws-message', { detail: data }));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,10 +57,12 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Logs
|
// Logs
|
||||||
async getLogs(limit = 100) {
|
async getLogs(limit = 500, sinceLine = null) {
|
||||||
const response = await api.get('/logs', {
|
const params = { limit };
|
||||||
params: { limit },
|
if (sinceLine !== null && sinceLine > 0) {
|
||||||
});
|
params.since = sinceLine;
|
||||||
|
}
|
||||||
|
const response = await api.get('/logs', { params });
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -42,3 +42,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes slide-in {
|
||||||
|
from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-in {
|
||||||
|
animation: slide-in 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ const autoRefresh = ref(false);
|
|||||||
const refreshIntervalSeconds = ref(5);
|
const refreshIntervalSeconds = ref(5);
|
||||||
const followLatestLog = ref(true);
|
const followLatestLog = ref(true);
|
||||||
const logsContainer = ref(null);
|
const logsContainer = ref(null);
|
||||||
const lastLoadedLogHash = ref(null); // Para rastrear el último log cargado
|
const lastLineNumber = ref(-1); // Número de la última línea leída
|
||||||
let refreshInterval = null;
|
let refreshInterval = null;
|
||||||
|
|
||||||
const filteredLogs = computed(() => {
|
const filteredLogs = computed(() => {
|
||||||
@@ -117,24 +117,31 @@ async function loadLogs(forceReload = false, shouldScroll = null) {
|
|||||||
// Si shouldScroll es null, usar la configuración de followLatestLog
|
// Si shouldScroll es null, usar la configuración de followLatestLog
|
||||||
const shouldAutoScroll = shouldScroll !== null ? shouldScroll : followLatestLog.value;
|
const shouldAutoScroll = shouldScroll !== null ? shouldScroll : followLatestLog.value;
|
||||||
|
|
||||||
// Guardar la posición del scroll actual antes de actualizar
|
// Guardar la posición del scroll antes de actualizar
|
||||||
const previousScrollTop = logsContainer.value?.scrollTop || 0;
|
const previousScrollTop = logsContainer.value?.scrollTop || 0;
|
||||||
const previousScrollHeight = logsContainer.value?.scrollHeight || 0;
|
const wasAtBottom = logsContainer.value
|
||||||
|
? logsContainer.value.scrollTop + logsContainer.value.clientHeight >= logsContainer.value.scrollHeight - 10
|
||||||
|
: false;
|
||||||
|
|
||||||
// Solo mostrar loader en carga inicial o recarga forzada
|
// Solo mostrar loader en carga inicial o recarga forzada
|
||||||
const isInitialLoad = logs.value.length === 0 || lastLoadedLogHash.value === null;
|
const isInitialLoad = forceReload || lastLineNumber.value === -1;
|
||||||
if (forceReload || isInitialLoad) {
|
if (isInitialLoad) {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await api.getLogs(500);
|
// Si es carga inicial o forzada, no enviar sinceLine (cargar últimas líneas)
|
||||||
const newLogs = data.logs || [];
|
// Si es actualización incremental, enviar lastLineNumber + 1 para obtener solo las nuevas
|
||||||
|
const sinceLine = isInitialLoad ? null : lastLineNumber.value + 1;
|
||||||
|
const data = await api.getLogs(500, sinceLine);
|
||||||
|
|
||||||
if (forceReload || isInitialLoad) {
|
const newLogs = data.logs || [];
|
||||||
|
const newLastLineNumber = data.lastLineNumber !== undefined ? data.lastLineNumber : -1;
|
||||||
|
|
||||||
|
if (isInitialLoad) {
|
||||||
// Carga inicial o recarga forzada: reemplazar todo
|
// Carga inicial o recarga forzada: reemplazar todo
|
||||||
logs.value = newLogs;
|
logs.value = newLogs;
|
||||||
lastLoadedLogHash.value = newLogs.length > 0 ? hashLog(newLogs[newLogs.length - 1]) : null;
|
lastLineNumber.value = newLastLineNumber;
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
if (logsContainer.value && shouldAutoScroll) {
|
if (logsContainer.value && shouldAutoScroll) {
|
||||||
@@ -143,73 +150,40 @@ async function loadLogs(forceReload = false, shouldScroll = null) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Actualización incremental: añadir solo las líneas nuevas al final
|
// Actualización incremental: añadir solo las líneas nuevas al final
|
||||||
if (newLogs.length > 0) {
|
if (newLogs.length > 0 && newLastLineNumber > lastLineNumber.value) {
|
||||||
const currentLastLog = logs.value.length > 0 ? logs.value[logs.value.length - 1] : null;
|
// Añadir las nuevas líneas al final
|
||||||
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
|
// Limitar el número total de logs para evitar crecimiento infinito
|
||||||
const maxLogs = 1000;
|
const maxLogs = 1000;
|
||||||
logs.value = [...logs.value, ...logsToAdd].slice(-maxLogs); // Mantener los últimos maxLogs
|
logs.value = [...logs.value, ...newLogs].slice(-maxLogs);
|
||||||
|
|
||||||
lastLoadedLogHash.value = newLastLogHash;
|
// Actualizar el número de la última línea leída
|
||||||
|
lastLineNumber.value = newLastLineNumber;
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
// Ajustar el scroll para mantener la posición visual
|
// Ajustar el scroll
|
||||||
if (logsContainer.value) {
|
if (logsContainer.value) {
|
||||||
if (shouldAutoScroll) {
|
if (shouldAutoScroll) {
|
||||||
// Si debe seguir el último log, ir al final (abajo)
|
// Si debe seguir el último log, ir al final (abajo)
|
||||||
logsContainer.value.scrollTop = logsContainer.value.scrollHeight;
|
logsContainer.value.scrollTop = logsContainer.value.scrollHeight;
|
||||||
} else {
|
} else if (wasAtBottom) {
|
||||||
// Mantener la posición del scroll sin cambios
|
// Si estaba abajo, mantenerlo abajo
|
||||||
logsContainer.value.scrollTop = previousScrollTop;
|
logsContainer.value.scrollTop = logsContainer.value.scrollHeight;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// Si no estaba abajo y no sigue logs, mantener posición (no hacer nada)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error cargando logs:', error);
|
console.error('Error cargando logs:', error);
|
||||||
// Asegurar que el loader se oculte incluso si hay error
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
} finally {
|
} finally {
|
||||||
// Solo ocultar loader si se mostró
|
if (isInitialLoad) {
|
||||||
if (forceReload || isInitialLoad) {
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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() {
|
function handleAutoRefreshChange() {
|
||||||
updateRefreshInterval();
|
updateRefreshInterval();
|
||||||
}
|
}
|
||||||
@@ -236,13 +210,7 @@ function updateRefreshInterval() {
|
|||||||
|
|
||||||
function handleWSMessage(event) {
|
function handleWSMessage(event) {
|
||||||
const data = event.detail;
|
const data = event.detail;
|
||||||
if (data.type === 'logs_updated') {
|
// Ya no escuchamos logs_updated porque usamos polling con números de línea
|
||||||
// Solo actualizar si auto-refresh está activado
|
|
||||||
if (autoRefresh.value) {
|
|
||||||
// Actualización incremental (no forzada) cuando llega WebSocket
|
|
||||||
loadLogs(false, followLatestLog.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user