Merge pull request #16 from R0m1k3/claude/dockerized-notes-todo-app-011CV1roGRbnm21wgtB2LMxP

Fluidity: Visual feedback system + RSS auto-init + Diagnostic
This commit is contained in:
LogiFlow authored and GitHub committed 2025-11-11 19:10:45 +01:00
commit e9227d8b25
5 files changed
+362 -15

No files matched your search

+176
View File
@@ -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;
}
+7
View File
@@ -14,6 +14,7 @@
<link rel="stylesheet" href="/css/layout.css">
<link rel="stylesheet" href="/css/components.css">
<link rel="stylesheet" href="/css/animations.css">
<link rel="stylesheet" href="/css/skeleton.css">
</head>
<body>
<!-- Header -->
@@ -295,6 +296,12 @@
<input type="file" id="imageInput" accept="image/*" style="display: none;">
<input type="file" id="fileInput" style="display: none;">
<!-- Refresh indicator -->
<div class="refresh-indicator" id="refreshIndicator">
<div class="loading-spinner"></div>
<span>Mise à jour...</span>
</div>
<!-- JavaScript -->
<script src="/js/complete-app.js"></script>
</body>
+26 -1
View File
@@ -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();
}
}
+80
View File
@@ -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();
+73 -14
View File
@@ -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
};