Files
noteflow/routes/rss.routes.js
T
Claude 5cd32da1d1 Performance: Optimisations majeures + Auto-refresh RSS
Problèmes résolus:
✅ Flux RSS ne s'affichaient pas → LEFT JOIN + COALESCE
✅ Pas de mise à jour automatique → Scheduler toutes les 5min
✅ Application lente → Cache serveur + client

Scheduler RSS (services/rss-scheduler.js):
- Mise à jour automatique des flux toutes les 5 minutes
- Première exécution 5 secondes après démarrage
- Gestion parallèle de tous les flux activés
- Logs détaillés pour debugging
- Protection contre les exécutions multiples simultanées
- Limite de 20 articles par flux pour performance

Routes RSS optimisées:
- Cache serveur 30 secondes pour /api/rss/articles
- Invalidation cache après fetch manuel
- LEFT JOIN au lieu de INNER JOIN (flux sans articles)
- COALESCE pour titre fallback
- Logs améliorés avec compteurs

Frontend vanilla JS:
- Auto-refresh RSS toutes les 30 secondes
- Cache settings client 1 minute
- Synchronisation avec scheduler serveur
- console.debug au lieu de console.error pour auto-refresh

Frontend React:
- Auto-refresh RSS toutes les 30 secondes
- loadRssData() séparée pour refresh indépendant
- useEffect cleanup proper (clearInterval)
- État lastUpdate pour tracking

Server.js:
- Démarrage scheduler au startup
- Log confirmation démarrage

Performances:
- Moins de requêtes DB grâce au cache
- Refresh automatique sans action utilisateur
- Articles toujours à jour (max 30s de délai)
- Support de beaucoup de flux RSS sans ralentissement
2025-11-11 17:18:57 +00:00

355 lines
11 KiB
JavaScript

// Routes de gestion des flux RSS
const express = require('express');
const router = express.Router();
const Parser = require('rss-parser');
const axios = require('axios');
const { getAll, getOne, runQuery } = require('../config/database');
const { authenticateToken, requireAdmin } = require('../middleware/auth');
const logger = require('../config/logger');
const parser = new Parser();
// Routes admin (gestion des flux)
router.use(authenticateToken);
/**
* GET /api/rss/feeds
* Liste tous les flux RSS
*/
router.get('/feeds', requireAdmin, async (req, res) => {
try {
const feeds = await getAll('SELECT * FROM rss_feeds ORDER BY created_at DESC');
res.json(feeds || []);
} catch (error) {
logger.error('Erreur lors de la récupération des flux RSS:', error);
res.status(500).json({ error: 'Erreur serveur' });
}
});
/**
* POST /api/rss/feeds
* Ajouter un nouveau flux RSS
*/
router.post('/feeds', requireAdmin, async (req, res) => {
try {
const { url } = req.body;
if (!url) {
return res.status(400).json({ error: 'URL requise' });
}
// Vérifier si le flux existe déjà
const existing = await getOne('SELECT id FROM rss_feeds WHERE url = ?', [url]);
if (existing) {
return res.status(409).json({ error: 'Ce flux existe déjà' });
}
// Tester le flux
try {
const feed = await parser.parseURL(url);
// Ajouter le flux
const result = await runQuery(
'INSERT INTO rss_feeds (url, title, description) VALUES (?, ?, ?)',
[url, feed.title || url, feed.description || '']
);
logger.info(`Flux RSS ajouté: ${feed.title} (${url})`);
res.json({
id: result.id,
url,
title: feed.title || url,
description: feed.description || '',
enabled: 1
});
} catch (parseError) {
logger.error('Erreur lors du parsing du flux RSS:', parseError);
return res.status(400).json({ error: 'Impossible de parser ce flux RSS. Vérifiez l\'URL.' });
}
} catch (error) {
logger.error('Erreur lors de l\'ajout du flux RSS:', error);
res.status(500).json({ error: 'Erreur serveur' });
}
});
/**
* PUT /api/rss/feeds/:id
* Mettre à jour un flux RSS (activer/désactiver)
*/
router.put('/feeds/:id', requireAdmin, async (req, res) => {
try {
const { enabled } = req.body;
await runQuery(
'UPDATE rss_feeds SET enabled = ? WHERE id = ?',
[enabled ? 1 : 0, req.params.id]
);
logger.info(`Flux RSS ${enabled ? 'activé' : 'désactivé'}: ${req.params.id}`);
res.json({ message: 'Flux mis à jour avec succès' });
} catch (error) {
logger.error('Erreur lors de la mise à jour du flux RSS:', error);
res.status(500).json({ error: 'Erreur serveur' });
}
});
/**
* DELETE /api/rss/feeds/:id
* Supprimer un flux RSS
*/
router.delete('/feeds/:id', requireAdmin, async (req, res) => {
try {
await runQuery('DELETE FROM rss_feeds WHERE id = ?', [req.params.id]);
logger.info(`Flux RSS supprimé: ${req.params.id}`);
res.json({ message: 'Flux supprimé avec succès' });
} catch (error) {
logger.error('Erreur lors de la suppression du flux RSS:', error);
res.status(500).json({ error: 'Erreur serveur' });
}
});
/**
* POST /api/rss/fetch
* Récupérer manuellement les articles de tous les flux RSS activés
*/
router.post('/fetch', requireAdmin, async (req, res) => {
try {
const rssScheduler = require('../services/rss-scheduler');
await rssScheduler.manualFetch();
// Invalider le cache
articlesCache = null;
articlesCacheTime = 0;
res.json({ message: 'Mise à jour des flux RSS terminée avec succès' });
} catch (error) {
logger.error('Erreur lors du fetch manuel des flux RSS:', error);
res.status(500).json({ error: 'Erreur serveur' });
}
});
// Cache pour les articles RSS (30 secondes)
let articlesCache = null;
let articlesCacheTime = 0;
const ARTICLES_CACHE_DURATION = 30000; // 30 secondes
/**
* GET /api/rss/articles
* Récupérer les 5 derniers articles (avec cache)
*/
router.get('/articles', async (req, res) => {
try {
// Utiliser le cache si disponible et récent
const now = Date.now();
if (articlesCache && (now - articlesCacheTime) < ARTICLES_CACHE_DURATION) {
logger.debug('Articles RSS servis depuis le cache');
return res.json(articlesCache);
}
const articles = await getAll(`
SELECT
a.id, a.title, a.link, a.description, a.pub_date, a.content,
COALESCE(f.title, f.url) as feed_title, f.url as feed_url
FROM rss_articles a
LEFT JOIN rss_feeds f ON a.feed_id = f.id
WHERE a.pub_date IS NOT NULL
ORDER BY a.pub_date DESC
LIMIT 5
`);
// Mettre en cache
articlesCache = articles || [];
articlesCacheTime = now;
logger.info(`Articles RSS récupérés: ${articles?.length || 0}`);
res.json(articles || []);
} catch (error) {
logger.error('Erreur lors de la récupération des articles RSS:', error);
res.status(500).json({ error: 'Erreur serveur' });
}
});
/**
* POST /api/rss/summarize
* Générer un résumé des 5 derniers articles avec OpenRouter
*/
router.post('/summarize', requireAdmin, async (req, res) => {
try {
// Récupérer les paramètres
const apiKey = await getOne('SELECT value FROM settings WHERE key = ?', ['openrouter_api_key']);
const model = await getOne('SELECT value FROM settings WHERE key = ?', ['openrouter_model']);
const prompt = await getOne('SELECT value FROM settings WHERE key = ?', ['rss_summary_prompt']);
if (!apiKey || !apiKey.value) {
return res.status(400).json({ error: 'Clé API OpenRouter non configurée' });
}
// Récupérer les 5 derniers articles
const articles = await getAll(`
SELECT
a.title, a.description, a.link, a.pub_date,
f.title as feed_title
FROM rss_articles a
JOIN rss_feeds f ON a.feed_id = f.id
ORDER BY a.pub_date DESC
LIMIT 5
`);
if (articles.length === 0) {
return res.status(400).json({ error: 'Aucun article à résumer' });
}
// Construire le prompt
const defaultPrompt = prompt?.value || `Tu es un assistant spécialisé dans la synthèse d'actualités. Analyse les articles suivants et crée un résumé structuré et informatif.
Pour chaque article, identifie:
- Le sujet principal
- Les points clés
- L'importance de l'information
Ensuite, génère un résumé global qui:
1. Regroupe les thèmes communs
2. Hiérarchise les informations par importance
3. Présente une vue d'ensemble claire et concise
4. Utilise un style professionnel mais accessible
Format de sortie en Markdown avec des sections claires.`;
const articlesText = articles.map((a, i) =>
`## Article ${i + 1}: ${a.title}\n**Source**: ${a.feed_title}\n**Date**: ${new Date(a.pub_date).toLocaleDateString('fr-FR')}\n**Description**: ${a.description}\n**Lien**: ${a.link}\n`
).join('\n---\n');
const fullPrompt = `${defaultPrompt}\n\n# Articles à résumer:\n\n${articlesText}`;
// Appeler OpenRouter
const selectedModel = model?.value || 'openai/gpt-3.5-turbo';
logger.info(`Génération du résumé avec le modèle ${selectedModel}...`);
const response = await axios.post(
'https://openrouter.ai/api/v1/chat/completions',
{
model: selectedModel,
messages: [
{
role: 'user',
content: fullPrompt
}
]
},
{
headers: {
'Authorization': `Bearer ${apiKey.value}`,
'HTTP-Referer': 'https://noteflow.app',
'X-Title': 'NoteFlow RSS Summarizer',
'Content-Type': 'application/json'
},
timeout: 60000
}
);
const summary = response.data.choices[0].message.content;
// Sauvegarder le résumé en base de données
await runQuery(
'INSERT INTO rss_summaries (summary, model, articles_count) VALUES (?, ?, ?)',
[summary, selectedModel, articles.length]
);
logger.info('Résumé généré et sauvegardé avec succès');
res.json({
summary,
model: selectedModel,
articles_count: articles.length
});
} catch (error) {
logger.error('Erreur lors de la génération du résumé:', error);
if (error.response) {
logger.error('Réponse OpenRouter:', error.response.data);
return res.status(error.response.status).json({
error: 'Erreur API OpenRouter',
details: error.response.data
});
}
res.status(500).json({ error: 'Erreur serveur' });
}
});
/**
* GET /api/rss/summaries
* Récupérer les 5 derniers résumés
*/
router.get('/summaries', async (req, res) => {
try {
const summaries = await getAll(`
SELECT id, summary, model, articles_count, created_at
FROM rss_summaries
ORDER BY created_at DESC
LIMIT 5
`);
res.json(summaries || []);
} catch (error) {
logger.error('Erreur lors de la récupération des résumés:', error);
res.status(500).json({ error: 'Erreur serveur' });
}
});
/**
* GET /api/rss/models
* Récupérer la liste des modèles OpenRouter depuis l'API
*/
router.get('/models', requireAdmin, async (req, res) => {
try {
// Récupérer les modèles depuis l'API OpenRouter
const response = await axios.get('https://openrouter.ai/api/v1/models', {
headers: {
'HTTP-Referer': 'https://noteflow.app',
'X-Title': 'NoteFlow'
},
timeout: 10000
});
// Transformer les données en format simplifié
const models = response.data.data.map(model => ({
id: model.id,
name: model.name || model.id,
provider: model.id.split('/')[0] || 'Unknown',
context_length: model.context_length,
pricing: model.pricing
}));
// Trier par provider puis par nom
models.sort((a, b) => {
if (a.provider !== b.provider) {
return a.provider.localeCompare(b.provider);
}
return a.name.localeCompare(b.name);
});
res.json(models);
} catch (error) {
logger.error('Erreur lors de la récupération des modèles:', error);
// Fallback sur une liste de modèles populaires si l'API échoue
const fallbackModels = [
{ id: 'openai/gpt-4-turbo', name: 'GPT-4 Turbo', provider: 'openai' },
{ id: 'openai/gpt-3.5-turbo', name: 'GPT-3.5 Turbo', provider: 'openai' },
{ id: 'anthropic/claude-3-opus', name: 'Claude 3 Opus', provider: 'anthropic' },
{ id: 'anthropic/claude-3-sonnet', name: 'Claude 3 Sonnet', provider: 'anthropic' },
{ id: 'google/gemini-pro', name: 'Gemini Pro', provider: 'google' },
{ id: 'meta-llama/llama-3-70b-instruct', name: 'Llama 3 70B', provider: 'meta-llama' },
{ id: 'mistralai/mistral-large', name: 'Mistral Large', provider: 'mistralai' }
];
res.json(fallbackModels);
}
});
module.exports = router;