diff --git a/public/css/skeleton.css b/public/css/skeleton.css new file mode 100644 index 0000000..b861d41 --- /dev/null +++ b/public/css/skeleton.css @@ -0,0 +1,176 @@ +/* Skeleton Loaders pour améliorer la fluidité perçue */ + +.skeleton { + background: linear-gradient( + 90deg, + var(--color-border) 25%, + #f0f0f0 50%, + var(--color-border) 75% + ); + background-size: 200% 100%; + animation: loading 1.5s ease-in-out infinite; + border-radius: var(--radius-md); +} + +@keyframes loading { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +.skeleton-note-card { + height: 200px; + margin-bottom: var(--spacing-lg); +} + +.skeleton-todo { + height: 40px; + margin-bottom: var(--spacing-sm); +} + +.skeleton-rss-article { + height: 80px; + margin-bottom: var(--spacing-md); +} + +/* Loading indicators */ +.loading-spinner { + display: inline-block; + width: 20px; + height: 20px; + border: 3px solid rgba(102, 126, 234, 0.1); + border-radius: 50%; + border-top-color: var(--color-accent); + animation: spinner 0.6s linear infinite; +} + +@keyframes spinner { + to { + transform: rotate(360deg); + } +} + +.loading-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s; +} + +.loading-overlay.active { + opacity: 1; + pointer-events: all; +} + +.loading-content { + text-align: center; + padding: 30px; + background: white; + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); +} + +.loading-content .loading-spinner { + width: 50px; + height: 50px; + border-width: 4px; + margin-bottom: 15px; +} + +.loading-content p { + margin: 0; + color: var(--color-text-secondary); + font-size: var(--font-size-sm); +} + +/* Refresh indicator */ +.refresh-indicator { + position: fixed; + top: 80px; + right: 20px; + background: var(--color-accent); + color: white; + padding: 8px 16px; + border-radius: var(--radius-lg); + font-size: var(--font-size-xs); + box-shadow: var(--shadow-md); + display: flex; + align-items: center; + gap: 8px; + opacity: 0; + transform: translateY(-10px); + transition: all 0.3s; + pointer-events: none; + z-index: 1000; +} + +.refresh-indicator.active { + opacity: 1; + transform: translateY(0); +} + +.refresh-indicator .loading-spinner { + width: 14px; + height: 14px; + border-width: 2px; + border-color: rgba(255, 255, 255, 0.3); + border-top-color: white; +} + +/* Optimistic UI */ +.optimistic-update { + opacity: 0.6; + pointer-events: none; +} + +/* Smooth fade in */ +.fade-in { + animation: fadeIn 0.3s ease-in; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Empty state avec animation */ +.empty-state { + text-align: center; + padding: 60px 20px; + color: var(--color-text-secondary); + animation: fadeIn 0.5s ease-in; +} + +.empty-state-icon { + font-size: 48px; + margin-bottom: 16px; + opacity: 0.5; +} + +.empty-state-text { + font-size: var(--font-size-base); + margin-bottom: 8px; +} + +.empty-state-hint { + font-size: var(--font-size-sm); + opacity: 0.7; +} diff --git a/public/index.html b/public/index.html index e377ebd..e81e975 100644 --- a/public/index.html +++ b/public/index.html @@ -14,6 +14,7 @@ + @@ -295,6 +296,12 @@ + +
+
+ Mise à jour... +
+ diff --git a/public/js/complete-app.js b/public/js/complete-app.js index f850d22..adde63e 100644 --- a/public/js/complete-app.js +++ b/public/js/complete-app.js @@ -1489,8 +1489,27 @@ let settingsCache = null; let settingsCacheTime = 0; const SETTINGS_CACHE_DURATION = 60000; // 1 minute -async function loadRssArticles() { +// Fonction pour afficher/cacher l'indicateur de refresh +function showRefreshIndicator() { + const indicator = document.getElementById('refreshIndicator'); + if (indicator) { + indicator.classList.add('active'); + } +} + +function hideRefreshIndicator() { + const indicator = document.getElementById('refreshIndicator'); + if (indicator) { + indicator.classList.remove('active'); + } +} + +async function loadRssArticles(showIndicator = false) { try { + if (showIndicator) { + showRefreshIndicator(); + } + // Vérifier si les résumés sont activés (avec cache) const now = Date.now(); if (!settingsCache || (now - settingsCacheTime) > SETTINGS_CACHE_DURATION) { @@ -1509,8 +1528,14 @@ async function loadRssArticles() { const articles = await api.get('/api/rss/articles'); renderRssArticles(articles); } + + if (showIndicator) { + // Cacher l'indicateur après un court délai pour que l'utilisateur le voie + setTimeout(hideRefreshIndicator, 500); + } } catch (error) { console.error('Erreur chargement articles RSS:', error); + hideRefreshIndicator(); } } diff --git a/scripts/diagnose-rss.js b/scripts/diagnose-rss.js new file mode 100644 index 0000000..7d75dc1 --- /dev/null +++ b/scripts/diagnose-rss.js @@ -0,0 +1,80 @@ +// Script de diagnostic RSS +const { getAll, initDatabase } = require('../config/database'); +const logger = require('../config/logger'); + +async function diagnoseRSS() { + console.log('\n==================== DIAGNOSTIC RSS ====================\n'); + + try { + // Initialize database first + await initDatabase(); + // Vérifier les flux RSS + const feeds = await getAll('SELECT * FROM rss_feeds'); + console.log(`📰 Flux RSS en base: ${feeds?.length || 0}`); + if (feeds && feeds.length > 0) { + feeds.forEach(feed => { + console.log(` - ${feed.title || feed.url} (${feed.enabled ? 'activé' : 'désactivé'})`); + console.log(` Dernière mise à jour: ${feed.last_fetched_at || 'jamais'}`); + }); + } else { + console.log(' ⚠️ Aucun flux RSS configuré'); + console.log(' 💡 Ajoutez un flux via l\'interface admin'); + } + + console.log(''); + + // Vérifier les articles + const articles = await getAll('SELECT COUNT(*) as count FROM rss_articles'); + console.log(`📄 Articles RSS en base: ${articles[0]?.count || 0}`); + + if (articles[0]?.count > 0) { + const recentArticles = await getAll(` + SELECT + a.title, + a.pub_date, + f.title as feed_title + FROM rss_articles a + LEFT JOIN rss_feeds f ON a.feed_id = f.id + ORDER BY a.pub_date DESC + LIMIT 5 + `); + + console.log('\n5 derniers articles:'); + recentArticles.forEach((article, i) => { + console.log(` ${i + 1}. ${article.title}`); + console.log(` Source: ${article.feed_title || 'Inconnu'}`); + console.log(` Date: ${article.pub_date}`); + }); + } + + console.log(''); + + // Vérifier les résumés + const summaries = await getAll('SELECT COUNT(*) as count FROM rss_summaries'); + console.log(`📝 Résumés RSS en base: ${summaries[0]?.count || 0}`); + + console.log(''); + + // Vérifier les settings + const settings = await getAll('SELECT * FROM settings WHERE key LIKE "rss%" OR key LIKE "openrouter%"'); + console.log(`⚙️ Settings RSS/OpenRouter: ${settings?.length || 0}`); + if (settings && settings.length > 0) { + settings.forEach(setting => { + const value = setting.key.includes('api_key') + ? (setting.value ? '***' + setting.value.slice(-4) : 'non défini') + : setting.value; + console.log(` - ${setting.key}: ${value}`); + }); + } + + console.log('\n========================================================\n'); + + } catch (error) { + console.error('❌ Erreur lors du diagnostic:', error); + } + + process.exit(0); +} + +// Exécuter le diagnostic +diagnoseRSS(); diff --git a/services/rss-scheduler.js b/services/rss-scheduler.js index 1c942d3..ca8970f 100644 --- a/services/rss-scheduler.js +++ b/services/rss-scheduler.js @@ -12,6 +12,46 @@ const parser = new Parser({ let isRunning = false; +/** + * Initialiser des flux RSS par défaut + */ +async function initializeDefaultFeeds() { + try { + const existingFeeds = await getAll('SELECT COUNT(*) as count FROM rss_feeds'); + + if (existingFeeds[0].count === 0) { + logger.info('🔧 Aucun flux RSS trouvé, ajout de flux par défaut...'); + + const defaultFeeds = [ + 'https://www.lemonde.fr/rss/une.xml', + 'https://feeds.bbci.co.uk/news/world/rss.xml', + 'https://www.lefigaro.fr/rss/figaro_actualites.xml' + ]; + + for (const url of defaultFeeds) { + try { + const feed = await parser.parseURL(url); + await runQuery( + 'INSERT INTO rss_feeds (url, title, description, enabled) VALUES (?, ?, ?, 1)', + [url, feed.title || url, feed.description || ''] + ); + logger.info(`✓ Flux ajouté: ${feed.title || url}`); + } catch (error) { + logger.warn(`⚠️ Impossible d'ajouter ${url}: ${error.message}`); + } + } + + logger.info('✓ Flux RSS par défaut initialisés'); + return true; + } + + return false; + } catch (error) { + logger.error('Erreur lors de l\'initialisation des flux par défaut:', error); + return false; + } +} + /** * Récupérer et mettre à jour tous les flux RSS activés */ @@ -31,12 +71,18 @@ async function fetchAllFeeds() { const feeds = await getAll('SELECT * FROM rss_feeds WHERE enabled = 1'); if (!feeds || feeds.length === 0) { - logger.info('Aucun flux RSS activé'); + logger.info('⚠️ Aucun flux RSS activé, initialisation...'); + const initialized = await initializeDefaultFeeds(); + if (initialized) { + // Réessayer avec les nouveaux flux + isRunning = false; + return await fetchAllFeeds(); + } isRunning = false; return; } - logger.info(`Mise à jour de ${feeds.length} flux RSS...`); + logger.info(`📰 Mise à jour de ${feeds.length} flux RSS...`); let totalArticles = 0; let totalErrors = 0; @@ -44,12 +90,17 @@ async function fetchAllFeeds() { // Traiter chaque flux for (const feed of feeds) { try { - logger.info(`Fetch: ${feed.url}`); + logger.info(`⏳ Fetch: ${feed.url}`); - // Parser le flux - const parsedFeed = await parser.parseURL(feed.url); + // Parser le flux avec timeout + const parsedFeed = await Promise.race([ + parser.parseURL(feed.url), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Timeout')), 15000) + ) + ]); - // Mettre à jour le titre et description du flux si nécessaire + // Mettre à jour le titre et description 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] @@ -57,9 +108,12 @@ async function fetchAllFeeds() { // Ajouter les articles (limiter à 20 par flux pour ne pas surcharger) const items = parsedFeed.items.slice(0, 20); + let feedArticles = 0; for (const item of items) { try { + if (!item.link) continue; // Skip articles sans lien + // Vérifier si l'article existe déjà const existing = await getOne('SELECT id FROM rss_articles WHERE link = ?', [item.link]); @@ -69,25 +123,28 @@ async function fetchAllFeeds() { [ feed.id, item.title || 'Sans titre', - item.link || '', + item.link, item.contentSnippet || item.description || '', item.pubDate || item.isoDate || new Date().toISOString(), item.content || item['content:encoded'] || '' ] ); + feedArticles++; totalArticles++; } } catch (articleError) { - // Ignorer les articles en double ou invalides - logger.debug(`Article ignoré: ${articleError.message}`); + // Ignorer les articles en double + if (!articleError.message.includes('UNIQUE')) { + logger.debug(`Article ignoré: ${articleError.message}`); + } } } - logger.info(`✓ ${feed.title || feed.url}: ${items.length} articles traités`); + logger.info(`✓ ${feed.title || feed.url}: ${feedArticles} nouveaux articles`); } catch (feedError) { totalErrors++; - logger.error(`✗ Erreur fetch ${feed.url}:`, feedError.message); + logger.error(`✗ Erreur fetch ${feed.url}: ${feedError.message}`); } } @@ -107,8 +164,9 @@ async function fetchAllFeeds() { function startScheduler() { logger.info('📰 Scheduler RSS démarré (mise à jour toutes les 5 minutes)'); - // Première exécution immédiate - setTimeout(() => { + // Initialiser les flux par défaut si nécessaire, puis première exécution + setTimeout(async () => { + await initializeDefaultFeeds(); fetchAllFeeds().catch(err => { logger.error('Erreur lors de la première mise à jour RSS:', err); }); @@ -132,5 +190,6 @@ async function manualFetch() { module.exports = { startScheduler, manualFetch, - fetchAllFeeds + fetchAllFeeds, + initializeDefaultFeeds };