diff --git a/config/database.js b/config/database.js index e59f87b..b5463ca 100644 --- a/config/database.js +++ b/config/database.js @@ -149,6 +149,27 @@ function initDatabase() { logger.info('✓ Tables de base de données créées avec succès'); // Migrations automatiques + // Migration pour rss_summaries - ajouter feed_title + db.all("PRAGMA table_info(rss_summaries)", (err, columns) => { + if (err) { + logger.error('Erreur lors de la vérification de la structure de la table rss_summaries:', err); + return; + } + + const hasFeedTitle = columns.some(col => col.name === 'feed_title'); + + if (!hasFeedTitle) { + logger.info('Migration: Ajout de la colonne feed_title à rss_summaries...'); + db.run('ALTER TABLE rss_summaries ADD COLUMN feed_title TEXT', (err) => { + if (err) { + logger.error('Erreur lors de l\'ajout de la colonne feed_title:', err); + } else { + logger.info('✓ Colonne feed_title ajoutée avec succès'); + } + }); + } + }); + db.all("PRAGMA table_info(notes)", (err, columns) => { if (err) { logger.error('Erreur lors de la vérification de la structure de la table notes:', err); diff --git a/public/css/skeleton.css b/public/css/skeleton.css index b861d41..3ca81a1 100644 --- a/public/css/skeleton.css +++ b/public/css/skeleton.css @@ -174,3 +174,122 @@ font-size: var(--font-size-sm); opacity: 0.7; } + +/* Système de notifications */ +.notifications-container { + position: fixed; + top: calc(var(--header-height) + 20px); + right: 20px; + z-index: 300; + display: flex; + flex-direction: column; + gap: 12px; + max-width: 400px; + pointer-events: none; +} + +.notification { + background: var(--color-card); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + padding: 16px 20px; + box-shadow: var(--shadow-lg); + display: flex; + align-items: center; + gap: 12px; + opacity: 0; + transform: translateX(400px); + animation: slideInRight 0.3s ease-out forwards; + pointer-events: auto; + min-width: 300px; + position: relative; +} + +.notification.closing { + animation: slideOutRight 0.3s ease-in forwards; +} + +.notification-icon { + width: 24px; + height: 24px; + flex-shrink: 0; +} + +.notification-success { + border-left: 4px solid var(--color-success); +} + +.notification-success .notification-icon { + color: var(--color-success); +} + +.notification-error { + border-left: 4px solid var(--color-danger); +} + +.notification-error .notification-icon { + color: var(--color-danger); +} + +.notification-warning { + border-left: 4px solid var(--color-warning); +} + +.notification-warning .notification-icon { + color: var(--color-warning); +} + +.notification-info { + border-left: 4px solid var(--color-info); +} + +.notification-info .notification-icon { + color: var(--color-info); +} + +.notification-content { + flex: 1; + font-size: var(--font-size-sm); + color: var(--color-text-primary); + line-height: 1.4; +} + +.notification-close { + width: 24px; + height: 24px; + padding: 4px; + border: none; + background: transparent; + color: var(--color-text-secondary); + cursor: pointer; + border-radius: 50%; + transition: all var(--transition-fast); + flex-shrink: 0; +} + +.notification-close:hover { + background: var(--color-hover); + color: var(--color-text-primary); +} + +@keyframes slideInRight { + from { + opacity: 0; + transform: translateX(400px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes slideOutRight { + from { + opacity: 1; + transform: translateX(0); + } + to { + opacity: 0; + transform: translateX(400px); + } +} diff --git a/public/index.html b/public/index.html index e81e975..805c915 100644 --- a/public/index.html +++ b/public/index.html @@ -123,7 +123,7 @@
-

Quick Tasks

+

À faire

0 tâches
@@ -302,6 +302,9 @@ Mise à jour...
+ +
+ diff --git a/public/js/complete-app.js b/public/js/complete-app.js index f6f73a5..fef8fd1 100644 --- a/public/js/complete-app.js +++ b/public/js/complete-app.js @@ -474,6 +474,66 @@ function escapeHtml(text) { return div.innerHTML; } +// Système de notifications internes +function showNotification(message, type = 'info', duration = 4000) { + const container = document.getElementById('notificationsContainer'); + if (!container) return; + + // Créer la notification + const notification = document.createElement('div'); + notification.className = `notification notification-${type}`; + + // Icône selon le type + let iconSvg = ''; + switch (type) { + case 'success': + iconSvg = ''; + break; + case 'error': + iconSvg = ''; + break; + case 'warning': + iconSvg = ''; + break; + default: // info + iconSvg = ''; + } + + notification.innerHTML = ` + ${iconSvg} +
${escapeHtml(message)}
+ + `; + + // Bouton de fermeture + const closeBtn = notification.querySelector('.notification-close'); + closeBtn.addEventListener('click', () => { + closeNotification(notification); + }); + + // Ajouter au container + container.appendChild(notification); + + // Auto-fermeture + if (duration > 0) { + setTimeout(() => { + closeNotification(notification); + }, duration); + } +} + +function closeNotification(notification) { + notification.classList.add('closing'); + setTimeout(() => { + notification.remove(); + }, 300); +} + // ==================== INLINE NOTE EXPANSION ==================== function expandNote(noteId) { state.expandedNoteId = noteId; @@ -519,7 +579,7 @@ async function archiveNote(noteId, archived) { await loadNotes(); } catch (error) { console.error('Erreur archivage note:', error); - alert('Erreur lors de l\'archivage de la note'); + showNotification('Erreur lors de l\'archivage de la note'); } } @@ -539,7 +599,7 @@ async function deleteNoteInline(noteId) { await loadNotes(); } catch (error) { console.error('Erreur suppression note:', error); - alert('Erreur lors de la suppression de la note'); + showNotification('Erreur lors de la suppression de la note', 'error'); } } @@ -568,7 +628,7 @@ async function addNoteTodoInline(noteId, inputElement) { inputElement.focus(); } catch (error) { console.error('Erreur ajout todo:', error); - alert('Erreur lors de l\'ajout du todo'); + showNotification('Erreur lors de l\'ajout du todo'); } } @@ -653,7 +713,7 @@ async function removeImageInline(noteId) { renderNotes(); } catch (error) { console.error('Erreur suppression image:', error); - alert('Erreur lors de la suppression de l\'image'); + showNotification('Erreur lors de la suppression de l\'image'); } } @@ -679,7 +739,7 @@ async function handleImageUploadInline(event) { renderNotes(); } catch (error) { console.error('Erreur upload image:', error); - alert('Erreur lors de l\'upload de l\'image'); + showNotification('Erreur lors de l\'upload de l\'image'); } // Reset input @@ -707,7 +767,7 @@ async function handleFileUploadInline(event) { renderNotes(); } catch (error) { console.error('Erreur upload fichier:', error); - alert('Erreur lors de l\'upload du fichier: ' + (error.message || 'Erreur inconnue')); + showNotification('Erreur lors de l\'upload du fichier: ' + (error.message || 'Erreur inconnue')); } // Reset input @@ -721,7 +781,7 @@ async function downloadFile(fileId) { window.open(`/api/notes/files/${fileId}/download`, '_blank'); } catch (error) { console.error('Erreur téléchargement fichier:', error); - alert('Erreur lors du téléchargement du fichier'); + showNotification('Erreur lors du téléchargement du fichier', 'error'); } } @@ -747,7 +807,7 @@ async function deleteFile(fileId) { } } catch (error) { console.error('Erreur suppression fichier:', error); - alert('Erreur lors de la suppression du fichier'); + showNotification('Erreur lors de la suppression du fichier', 'error'); } } @@ -775,7 +835,7 @@ async function createNewNote() { }, 100); } catch (error) { console.error('Erreur création note:', error); - alert('Erreur lors de la création de la note'); + showNotification('Erreur lors de la création de la note', 'error'); } } @@ -1010,7 +1070,7 @@ async function handleImageUpload(event) { await loadNotes(); } catch (error) { console.error('Erreur upload image:', error); - alert('Erreur lors de l\'upload de l\'image'); + showNotification('Erreur lors de l\'upload de l\'image'); } } @@ -1057,6 +1117,16 @@ function renderTodos() { return true; }); + // Tri : actives en premier, puis complétées + filtered.sort((a, b) => { + // Les actives (completed = false) avant les complétées (completed = true) + if (a.completed !== b.completed) { + return a.completed ? 1 : -1; + } + // Si même statut, tri par ID décroissant (plus récentes en premier) + return b.id - a.id; + }); + filtered.forEach(todo => { const item = document.createElement('div'); item.className = 'todo-item' + (todo.completed ? ' completed' : ''); @@ -1194,7 +1264,7 @@ async function loadUsers() { renderUsersTable(users); } catch (error) { console.error('Erreur chargement utilisateurs:', error); - alert('Erreur lors du chargement des utilisateurs'); + showNotification('Erreur lors du chargement des utilisateurs', 'error'); } } @@ -1246,13 +1316,13 @@ function closeAdminModal() { async function createUser() { const username = prompt('Nom d\'utilisateur:'); if (!username || username.trim().length < 3) { - alert('Le nom d\'utilisateur doit contenir au moins 3 caractères'); + showNotification('Le nom d\'utilisateur doit contenir au moins 3 caractères'); return; } const password = prompt('Mot de passe:'); if (!password || password.length < 4) { - alert('Le mot de passe doit contenir au moins 4 caractères'); + showNotification('Le mot de passe doit contenir au moins 4 caractères', 'warning'); return; } @@ -1270,11 +1340,11 @@ async function createUser() { password, is_admin: isAdmin }); - alert('Utilisateur créé avec succès'); + showNotification('Utilisateur créé avec succès', 'success'); await loadUsers(); } catch (error) { console.error('Erreur création utilisateur:', error); - alert('Erreur lors de la création de l\'utilisateur. Il existe peut-être déjà.'); + showNotification('Erreur lors de la création de l\'utilisateur. Il existe peut-être déjà.'); } } @@ -1323,22 +1393,22 @@ async function confirmPasswordChange() { const confirmPassword = confirmPasswordInput.value; if (!newPassword || newPassword.length < 6) { - alert('Le mot de passe doit contenir au moins 6 caractères'); + showNotification('Le mot de passe doit contenir au moins 6 caractères', 'warning'); return; } if (newPassword !== confirmPassword) { - alert('Les mots de passe ne correspondent pas'); + showNotification('Les mots de passe ne correspondent pas', 'warning'); return; } try { await api.put(`/api/users/${currentPasswordChangeUserId}`, { password: newPassword }); - alert('Mot de passe modifié avec succès'); + showNotification('Mot de passe modifié avec succès', 'success'); closePasswordChangeModal(); } catch (error) { console.error('Erreur modification mot de passe:', error); - alert('Erreur lors de la modification du mot de passe'); + showNotification('Erreur lors de la modification du mot de passe', 'error'); } } @@ -1358,11 +1428,11 @@ async function deleteUser(id, username) { try { await api.delete(`/api/users/${id}`); - alert('Utilisateur supprimé avec succès'); + showNotification('Utilisateur supprimé avec succès', 'success'); await loadUsers(); } catch (error) { console.error('Erreur suppression utilisateur:', error); - alert('Erreur lors de la suppression de l\'utilisateur'); + showNotification('Erreur lors de la suppression de l\'utilisateur'); } } @@ -1373,7 +1443,7 @@ async function loadRssFeeds() { renderRssFeeds(feeds); } catch (error) { console.error('Erreur chargement flux RSS:', error); - alert('Erreur lors du chargement des flux RSS'); + showNotification('Erreur lors du chargement des flux RSS', 'error'); } } @@ -1414,23 +1484,23 @@ async function addRssFeed() { const url = input.value.trim(); if (!url) { - alert('Veuillez entrer une URL de flux RSS'); + showNotification('Veuillez entrer une URL de flux RSS', 'warning'); return; } if (!url.startsWith('http://') && !url.startsWith('https://')) { - alert('L\'URL doit commencer par http:// ou https://'); + showNotification('L\'URL doit commencer par http:// ou https://', 'warning'); return; } try { await api.post('/api/rss/feeds', { url }); input.value = ''; - alert('Flux RSS ajouté avec succès'); + showNotification('Flux RSS ajouté avec succès', 'success'); await loadRssFeeds(); } catch (error) { console.error('Erreur ajout flux RSS:', error); - alert('Erreur lors de l\'ajout du flux RSS. Vérifiez l\'URL.'); + showNotification('Erreur lors de l\'ajout du flux RSS. Vérifiez l\'URL.'); } } @@ -1439,7 +1509,7 @@ async function toggleRssFeed(feedId, enabled) { await api.put(`/api/rss/feeds/${feedId}`, { enabled }); } catch (error) { console.error('Erreur toggle flux RSS:', error); - alert('Erreur lors de la modification du flux RSS'); + showNotification('Erreur lors de la modification du flux RSS', 'error'); } } @@ -1455,11 +1525,11 @@ async function deleteRssFeed(feedId) { try { await api.delete(`/api/rss/feeds/${feedId}`); - alert('Flux RSS supprimé avec succès'); + showNotification('Flux RSS supprimé avec succès', 'success'); await loadRssFeeds(); } catch (error) { console.error('Erreur suppression flux RSS:', error); - alert('Erreur lors de la suppression du flux RSS'); + showNotification('Erreur lors de la suppression du flux RSS', 'error'); } } @@ -1473,11 +1543,11 @@ async function fetchRssArticles() { try { const result = await api.post('/api/rss/fetch', {}); - alert(result.message || 'Articles récupérés avec succès'); + showNotification(result.message || 'Articles récupérés avec succès', 'success'); await loadRssArticles(); } catch (error) { console.error('Erreur fetch articles RSS:', error); - alert('Erreur lors de la récupération des articles RSS'); + showNotification('Erreur lors de la récupération des articles RSS', 'error'); } finally { btn.disabled = false; btn.textContent = originalText; @@ -1574,13 +1644,13 @@ function renderRssSummaries(summaries) { // Le résumé peut être soit l'ancien format (global) soit le nouveau (par article depuis DB) // Si c'est un résumé DB, il contient déjà le titre et le lien formatés if (summary.summary && summary.summary.includes('🔗')) { - // Ancien format sauvegardé en DB avec titre et lien + // Nouveau format : résumé par article avec titre et lien return `
${formatSummary(summary.summary)}
- ${summary.created_at ? `${new Date(summary.created_at).toLocaleDateString('fr-FR', { hour: '2-digit', minute: '2-digit' })}` : ''} - ${summary.model ? `${escapeHtml(summary.model.split('/')[1] || summary.model)}` : ''} + ${summary.feed_title ? `📰 ${escapeHtml(summary.feed_title)}` : ''} + ${summary.created_at ? `📅 ${new Date(summary.created_at).toLocaleDateString('fr-FR', { hour: '2-digit', minute: '2-digit' })}` : ''}
`; @@ -1653,7 +1723,7 @@ async function summarizeRss() { if (summaryEnabled) { // Si les résumés sont activés, juste recharger le bloc RSS - alert(`${result.articles_count} résumés générés avec succès !`); + showNotification(`${result.articles_count} résumés générés avec succès !`, 'success'); await loadRssArticles(); } else { // Sinon créer une note avec TOUS les résumés par article @@ -1667,22 +1737,22 @@ async function summarizeRss() { content: allSummaries }); - alert(`${result.articles_count} résumés générés et sauvegardés dans une nouvelle note !`); + showNotification(`${result.articles_count} résumés générés et sauvegardés dans une nouvelle note !`, 'success'); await loadNotes(); // Expand the new note state.expandedNoteId = newNote.id; renderNotes(); } else { - alert('Aucun résumé généré'); + showNotification('Aucun résumé généré', 'warning'); } } } catch (error) { console.error('Erreur génération résumé RSS:', error); if (error.message && error.message.includes('400')) { - alert('Veuillez configurer votre clé API OpenRouter dans l\'administration'); + showNotification('Veuillez configurer votre clé API OpenRouter dans l\'administration'); } else { - alert('Erreur lors de la génération du résumé RSS'); + showNotification('Erreur lors de la génération du résumé RSS', 'error'); } } finally { btn.disabled = false; @@ -1785,10 +1855,10 @@ async function saveOpenRouterSettings() { await api.put('/api/settings/rss_summary_enabled', { value: summaryEnabled.checked ? '1' : '0' }); await api.put('/api/settings/rss_summary_prompt', { value: summaryPrompt.value }); - alert('Paramètres OpenRouter sauvegardés avec succès'); + showNotification('Paramètres OpenRouter sauvegardés avec succès', 'success'); } catch (error) { console.error('Erreur sauvegarde paramètres OpenRouter:', error); - alert('Erreur lors de la sauvegarde des paramètres'); + showNotification('Erreur lors de la sauvegarde des paramètres', 'error'); } } @@ -1854,7 +1924,7 @@ async function init() { saveNoteBtn.addEventListener('click', async () => { if (currentNoteId) { await saveCurrentNote(); - alert('Note enregistrée !'); + showNotification('Note enregistrée !', 'success'); } }); } diff --git a/routes/rss.routes.js b/routes/rss.routes.js index aa91ce5..6504dad 100644 --- a/routes/rss.routes.js +++ b/routes/rss.routes.js @@ -241,10 +241,10 @@ Résumé (100 mots max):`; const summary = response.data.choices[0].message.content; - // Sauvegarder le résumé individuel + // Sauvegarder le résumé individuel avec feed_title await runQuery( - 'INSERT INTO rss_summaries (summary, model, articles_count, created_at) VALUES (?, ?, ?, datetime("now"))', - [`**${article.title}**\n\n${summary}\n\n🔗 [Lire l'article](${article.link})`, selectedModel, 1] + 'INSERT INTO rss_summaries (summary, model, articles_count, feed_title, created_at) VALUES (?, ?, ?, ?, datetime("now"))', + [`**${article.title}**\n\n${summary}\n\n🔗 [Lire l'article](${article.link})`, selectedModel, 1, article.feed_title] ); return { @@ -300,7 +300,7 @@ Résumé (100 mots max):`; router.get('/summaries', async (req, res) => { try { const summaries = await getAll(` - SELECT id, summary, model, articles_count, created_at + SELECT id, summary, model, articles_count, feed_title, created_at FROM rss_summaries ORDER BY created_at DESC LIMIT 5