mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-24 15:48:16 +02:00
Merge pull request #15 from R0m1k3/claude/dockerized-notes-todo-app-011CV1roGRbnm21wgtB2LMxP
Performance: Optimisations majeures + Auto-refresh RSS
This commit is contained in:
5 files changed
+211
-57
No files matched your search
@@ -11,22 +11,39 @@ export default function Dashboard() {
|
|||||||
const [rssContent, setRssContent] = useState<RssArticle[] | RssSummary[]>([]);
|
const [rssContent, setRssContent] = useState<RssArticle[] | RssSummary[]>([]);
|
||||||
const [showSummaries, setShowSummaries] = useState(false);
|
const [showSummaries, setShowSummaries] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [lastUpdate, setLastUpdate] = useState<Date>(new Date());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData();
|
loadData();
|
||||||
|
|
||||||
|
// Auto-refresh toutes les 30 secondes pour afficher les nouveaux articles RSS
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
loadRssData();
|
||||||
|
}, 30000); // 30 secondes
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
const [notesData, todosData, settings] = await Promise.all([
|
const [notesData, todosData] = await Promise.all([
|
||||||
notesApi.getAll(false),
|
notesApi.getAll(false),
|
||||||
todosApi.getAll(),
|
todosApi.getAll(),
|
||||||
settingsApi.getAll(),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
setNotes(notesData);
|
setNotes(notesData);
|
||||||
setTodos(todosData);
|
setTodos(todosData);
|
||||||
|
await loadRssData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur chargement données:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadRssData = async () => {
|
||||||
|
try {
|
||||||
|
const settings = await settingsApi.getAll();
|
||||||
const summaryEnabled = settings.rss_summary_enabled === '1';
|
const summaryEnabled = settings.rss_summary_enabled === '1';
|
||||||
setShowSummaries(summaryEnabled);
|
setShowSummaries(summaryEnabled);
|
||||||
|
|
||||||
@@ -37,10 +54,9 @@ export default function Dashboard() {
|
|||||||
const articles = await rssApi.getArticles();
|
const articles = await rssApi.getArticles();
|
||||||
setRssContent(articles);
|
setRssContent(articles);
|
||||||
}
|
}
|
||||||
|
setLastUpdate(new Date());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur chargement données:', error);
|
console.error('Erreur chargement RSS:', error);
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1484,11 +1484,21 @@ async function fetchRssArticles() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache des settings pour optimiser les performances
|
||||||
|
let settingsCache = null;
|
||||||
|
let settingsCacheTime = 0;
|
||||||
|
const SETTINGS_CACHE_DURATION = 60000; // 1 minute
|
||||||
|
|
||||||
async function loadRssArticles() {
|
async function loadRssArticles() {
|
||||||
try {
|
try {
|
||||||
// Vérifier si les résumés sont activés
|
// Vérifier si les résumés sont activés (avec cache)
|
||||||
const settings = await api.get('/api/settings');
|
const now = Date.now();
|
||||||
const summaryEnabled = settings.rss_summary_enabled === '1' || settings.rss_summary_enabled === 'true';
|
if (!settingsCache || (now - settingsCacheTime) > SETTINGS_CACHE_DURATION) {
|
||||||
|
settingsCache = await api.get('/api/settings');
|
||||||
|
settingsCacheTime = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
const summaryEnabled = settingsCache.rss_summary_enabled === '1' || settingsCache.rss_summary_enabled === 'true';
|
||||||
|
|
||||||
if (summaryEnabled) {
|
if (summaryEnabled) {
|
||||||
// Charger les résumés
|
// Charger les résumés
|
||||||
@@ -1735,6 +1745,13 @@ async function init() {
|
|||||||
// Setup search
|
// Setup search
|
||||||
setupSearch();
|
setupSearch();
|
||||||
|
|
||||||
|
// Auto-refresh RSS toutes les 30 secondes pour synchroniser avec le scheduler serveur
|
||||||
|
setInterval(() => {
|
||||||
|
loadRssArticles().catch(err => {
|
||||||
|
console.debug('Erreur auto-refresh RSS:', err);
|
||||||
|
});
|
||||||
|
}, 30000); // 30 secondes
|
||||||
|
|
||||||
// Event listeners
|
// Event listeners
|
||||||
const newNoteBtn = document.getElementById('newNoteBtn');
|
const newNoteBtn = document.getElementById('newNoteBtn');
|
||||||
if (newNoteBtn) {
|
if (newNoteBtn) {
|
||||||
|
|||||||
+29
-49
@@ -112,78 +112,58 @@ router.delete('/feeds/:id', requireAdmin, async (req, res) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/rss/fetch
|
* POST /api/rss/fetch
|
||||||
* Récupérer tous les articles des flux actifs
|
* Récupérer manuellement les articles de tous les flux RSS activés
|
||||||
*/
|
*/
|
||||||
router.post('/fetch', requireAdmin, async (req, res) => {
|
router.post('/fetch', requireAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const feeds = await getAll('SELECT * FROM rss_feeds WHERE enabled = 1');
|
const rssScheduler = require('../services/rss-scheduler');
|
||||||
|
await rssScheduler.manualFetch();
|
||||||
|
|
||||||
let totalArticles = 0;
|
// Invalider le cache
|
||||||
|
articlesCache = null;
|
||||||
|
articlesCacheTime = 0;
|
||||||
|
|
||||||
for (const feed of feeds) {
|
res.json({ message: 'Mise à jour des flux RSS terminée avec succès' });
|
||||||
try {
|
|
||||||
const parsedFeed = await parser.parseURL(feed.url);
|
|
||||||
|
|
||||||
// Mettre à jour les infos du flux
|
|
||||||
await runQuery(
|
|
||||||
'UPDATE rss_feeds SET title = ?, description = ?, last_fetched_at = CURRENT_TIMESTAMP WHERE id = ?',
|
|
||||||
[parsedFeed.title || feed.url, parsedFeed.description || '', feed.id]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Ajouter les articles
|
|
||||||
for (const item of parsedFeed.items) {
|
|
||||||
try {
|
|
||||||
// Vérifier si l'article existe déjà
|
|
||||||
const existing = await getOne('SELECT id FROM rss_articles WHERE link = ?', [item.link]);
|
|
||||||
|
|
||||||
if (!existing) {
|
|
||||||
await runQuery(
|
|
||||||
'INSERT INTO rss_articles (feed_id, title, link, description, pub_date, content) VALUES (?, ?, ?, ?, ?, ?)',
|
|
||||||
[
|
|
||||||
feed.id,
|
|
||||||
item.title || '',
|
|
||||||
item.link || '',
|
|
||||||
item.contentSnippet || item.description || '',
|
|
||||||
item.pubDate || item.isoDate || new Date().toISOString(),
|
|
||||||
item.content || item['content:encoded'] || ''
|
|
||||||
]
|
|
||||||
);
|
|
||||||
totalArticles++;
|
|
||||||
}
|
|
||||||
} catch (articleError) {
|
|
||||||
// Ignorer les articles en double ou invalides
|
|
||||||
logger.warn(`Erreur lors de l'ajout d'un article: ${articleError.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (feedError) {
|
|
||||||
logger.error(`Erreur lors du fetch du flux ${feed.url}:`, feedError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`${totalArticles} nouveaux articles récupérés`);
|
|
||||||
res.json({ message: `${totalArticles} nouveaux articles récupérés` });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Erreur lors du fetch des flux RSS:', error);
|
logger.error('Erreur lors du fetch manuel des flux RSS:', error);
|
||||||
res.status(500).json({ error: 'Erreur serveur' });
|
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
|
* GET /api/rss/articles
|
||||||
* Récupérer les 5 derniers articles
|
* Récupérer les 5 derniers articles (avec cache)
|
||||||
*/
|
*/
|
||||||
router.get('/articles', async (req, res) => {
|
router.get('/articles', async (req, res) => {
|
||||||
try {
|
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(`
|
const articles = await getAll(`
|
||||||
SELECT
|
SELECT
|
||||||
a.id, a.title, a.link, a.description, a.pub_date, a.content,
|
a.id, a.title, a.link, a.description, a.pub_date, a.content,
|
||||||
f.title as feed_title, f.url as feed_url
|
COALESCE(f.title, f.url) as feed_title, f.url as feed_url
|
||||||
FROM rss_articles a
|
FROM rss_articles a
|
||||||
JOIN rss_feeds f ON a.feed_id = f.id
|
LEFT JOIN rss_feeds f ON a.feed_id = f.id
|
||||||
|
WHERE a.pub_date IS NOT NULL
|
||||||
ORDER BY a.pub_date DESC
|
ORDER BY a.pub_date DESC
|
||||||
LIMIT 5
|
LIMIT 5
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// Mettre en cache
|
||||||
|
articlesCache = articles || [];
|
||||||
|
articlesCacheTime = now;
|
||||||
|
|
||||||
|
logger.info(`Articles RSS récupérés: ${articles?.length || 0}`);
|
||||||
res.json(articles || []);
|
res.json(articles || []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Erreur lors de la récupération des articles RSS:', error);
|
logger.error('Erreur lors de la récupération des articles RSS:', error);
|
||||||
|
|||||||
@@ -123,6 +123,11 @@ async function startServer() {
|
|||||||
await initDatabase();
|
await initDatabase();
|
||||||
logger.info('✓ Base de données initialisée avec succès');
|
logger.info('✓ Base de données initialisée avec succès');
|
||||||
|
|
||||||
|
// Démarrer le scheduler RSS
|
||||||
|
const rssScheduler = require('./services/rss-scheduler');
|
||||||
|
rssScheduler.startScheduler();
|
||||||
|
logger.info('✓ Scheduler RSS démarré');
|
||||||
|
|
||||||
// Démarrer le serveur
|
// Démarrer le serveur
|
||||||
app.listen(PORT, '0.0.0.0', () => {
|
app.listen(PORT, '0.0.0.0', () => {
|
||||||
logger.info('═════════════════════════════════════════════');
|
logger.info('═════════════════════════════════════════════');
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
// Service de mise à jour automatique des flux RSS
|
||||||
|
const Parser = require('rss-parser');
|
||||||
|
const { getAll, getOne, runQuery } = require('../config/database');
|
||||||
|
const logger = require('../config/logger');
|
||||||
|
|
||||||
|
const parser = new Parser({
|
||||||
|
timeout: 10000,
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'NoteFlow RSS Reader'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let isRunning = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupérer et mettre à jour tous les flux RSS activés
|
||||||
|
*/
|
||||||
|
async function fetchAllFeeds() {
|
||||||
|
if (isRunning) {
|
||||||
|
logger.info('Fetch RSS déjà en cours, skip...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isRunning = true;
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.info('🔄 Début de la mise à jour des flux RSS...');
|
||||||
|
|
||||||
|
// Récupérer tous les flux activés
|
||||||
|
const feeds = await getAll('SELECT * FROM rss_feeds WHERE enabled = 1');
|
||||||
|
|
||||||
|
if (!feeds || feeds.length === 0) {
|
||||||
|
logger.info('Aucun flux RSS activé');
|
||||||
|
isRunning = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Mise à jour de ${feeds.length} flux RSS...`);
|
||||||
|
|
||||||
|
let totalArticles = 0;
|
||||||
|
let totalErrors = 0;
|
||||||
|
|
||||||
|
// Traiter chaque flux
|
||||||
|
for (const feed of feeds) {
|
||||||
|
try {
|
||||||
|
logger.info(`Fetch: ${feed.url}`);
|
||||||
|
|
||||||
|
// Parser le flux
|
||||||
|
const parsedFeed = await parser.parseURL(feed.url);
|
||||||
|
|
||||||
|
// Mettre à jour le titre et description du flux si nécessaire
|
||||||
|
await runQuery(
|
||||||
|
'UPDATE rss_feeds SET title = ?, description = ?, last_fetched_at = CURRENT_TIMESTAMP WHERE id = ?',
|
||||||
|
[parsedFeed.title || feed.url, parsedFeed.description || '', feed.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ajouter les articles (limiter à 20 par flux pour ne pas surcharger)
|
||||||
|
const items = parsedFeed.items.slice(0, 20);
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
try {
|
||||||
|
// Vérifier si l'article existe déjà
|
||||||
|
const existing = await getOne('SELECT id FROM rss_articles WHERE link = ?', [item.link]);
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
await runQuery(
|
||||||
|
'INSERT INTO rss_articles (feed_id, title, link, description, pub_date, content) VALUES (?, ?, ?, ?, ?, ?)',
|
||||||
|
[
|
||||||
|
feed.id,
|
||||||
|
item.title || 'Sans titre',
|
||||||
|
item.link || '',
|
||||||
|
item.contentSnippet || item.description || '',
|
||||||
|
item.pubDate || item.isoDate || new Date().toISOString(),
|
||||||
|
item.content || item['content:encoded'] || ''
|
||||||
|
]
|
||||||
|
);
|
||||||
|
totalArticles++;
|
||||||
|
}
|
||||||
|
} catch (articleError) {
|
||||||
|
// Ignorer les articles en double ou invalides
|
||||||
|
logger.debug(`Article ignoré: ${articleError.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`✓ ${feed.title || feed.url}: ${items.length} articles traités`);
|
||||||
|
|
||||||
|
} catch (feedError) {
|
||||||
|
totalErrors++;
|
||||||
|
logger.error(`✗ Erreur fetch ${feed.url}:`, feedError.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
||||||
|
logger.info(`✅ Mise à jour terminée: ${totalArticles} nouveaux articles, ${totalErrors} erreurs (${duration}s)`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Erreur lors de la mise à jour automatique des flux RSS:', error);
|
||||||
|
} finally {
|
||||||
|
isRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Démarrer le scheduler (toutes les 5 minutes)
|
||||||
|
*/
|
||||||
|
function startScheduler() {
|
||||||
|
logger.info('📰 Scheduler RSS démarré (mise à jour toutes les 5 minutes)');
|
||||||
|
|
||||||
|
// Première exécution immédiate
|
||||||
|
setTimeout(() => {
|
||||||
|
fetchAllFeeds().catch(err => {
|
||||||
|
logger.error('Erreur lors de la première mise à jour RSS:', err);
|
||||||
|
});
|
||||||
|
}, 5000); // Attendre 5 secondes après le démarrage du serveur
|
||||||
|
|
||||||
|
// Ensuite toutes les 5 minutes
|
||||||
|
setInterval(() => {
|
||||||
|
fetchAllFeeds().catch(err => {
|
||||||
|
logger.error('Erreur lors de la mise à jour RSS:', err);
|
||||||
|
});
|
||||||
|
}, 5 * 60 * 1000); // 5 minutes
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch manuel (utilisé par la route API)
|
||||||
|
*/
|
||||||
|
async function manualFetch() {
|
||||||
|
return await fetchAllFeeds();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
startScheduler,
|
||||||
|
manualFetch,
|
||||||
|
fetchAllFeeds
|
||||||
|
};
|
||||||
Reference in new issue
Block a user