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 @@
+
+
+
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
};