fix:logs and new articles

Signed-off-by: Omar Sánchez Pizarro <omar.sanchez@pistacero.net>
This commit is contained in:
Omar Sánchez Pizarro
2026-01-19 23:06:33 +01:00
parent ec512e2809
commit 08a9a277f5
5 changed files with 273 additions and 88 deletions

View File

@@ -63,6 +63,15 @@ async function initRedis() {
redisClient.on('error', (err) => console.error('Redis Client Error', err));
await redisClient.connect();
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 {
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) => {
try {
// Intentar múltiples ubicaciones posibles
@@ -457,7 +466,7 @@ app.get('/api/logs', (req, res) => {
if (existsSync(altPath)) {
logFile = altPath;
} else {
return res.json({ logs: [] });
return res.json({ logs: [], totalLines: 0, lastLineNumber: 0 });
}
}
@@ -465,19 +474,37 @@ app.get('/api/logs', (req, res) => {
try {
const stats = statSync(logFile);
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) {
return res.json({ logs: [] });
return res.json({ logs: [], totalLines: 0, lastLineNumber: 0 });
}
const logs = readFileSync(logFile, 'utf8');
const lines = logs.split('\n').filter(l => l.trim());
const limit = parseInt(req.query.limit) || 100;
const lastLines = lines.slice(-limit);
// Mantener orden natural: más antiguo a más reciente
const logsContent = readFileSync(logFile, 'utf8');
const allLines = logsContent.split('\n').filter(l => l.trim());
const totalLines = allLines.length;
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) {
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)
const watcher = watch([WORKERS_PATH, watchLogPath].filter(p => existsSync(p)), {
// Watch files for changes (ya no vigilamos logs porque usa polling)
const watcher = watch([WORKERS_PATH].filter(p => existsSync(p)), {
persistent: true,
ignoreInitial: true,
});
@@ -591,17 +618,100 @@ watcher.on('change', async (path) => {
if (path === WORKERS_PATH) {
const workers = readJSON(WORKERS_PATH);
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
const PORT = process.env.PORT || 3001;
async function startServer() {
await initRedis();
// Iniciar monitoreo de artículos nuevos
await startArticleMonitoring();
server.listen(PORT, () => {
console.log(`🚀 Servidor backend ejecutándose en http://localhost:${PORT}`);
console.log(`📡 WebSocket disponible en ws://localhost:${PORT}`);