UX: Titre 'À faire' + Source RSS + Tri todos + Notifications internes

1. Titre 'À faire' au lieu de 'Quick Tasks'
   - Changé public/index.html ligne 126
   - Meilleure localisation française

2. Affichage source RSS au lieu du modèle IA
   - Ajout colonne feed_title à rss_summaries (migration auto)
   - Backend stocke feed_title lors de la génération
   - GET /api/rss/summaries retourne feed_title
   - Frontend affiche 📰 Source + 📅 Date au lieu du modèle
   - Plus pertinent pour l'utilisateur

3. Tri automatique des todos : actives en premier
   - renderTodos() trie : actives avant complétées
   - Si même statut : tri par ID décroissant (récentes en premier)
   - Sort: a.completed ? 1 : -1

4. Système de notifications internes
   - Remplacé TOUS les alert() par showNotification()
   - Types : success, error, warning, info
   - Animations : slide-in/slide-out depuis la droite
   - Auto-fermeture après 4 secondes
   - Bouton de fermeture manuelle
   - Position : top-right, z-index 300
   - Icônes SVG selon le type
   - Couleurs : vert (success), rouge (error), jaune (warning), bleu (info)

Frontend - Notifications:
   - showNotification(message, type, duration)
   - closeNotification(notification)
   - Container fixe en haut à droite
   - Empilage vertical des notifications
   - Escape HTML automatique

CSS - Nouveaux styles:
   - .notifications-container (fixed, top-right)
   - .notification + variations par type
   - Animations slideInRight / slideOutRight
   - Border-left colorée selon le type
   - Responsive et accessible

Impact utilisateur:
   ✓ Plus de popups système alert() invasives
   ✓ Notifications élégantes et non-bloquantes
   ✓ Feedback visuel clair avec icônes et couleurs
   ✓ Source RSS affichée au lieu du modèle IA
   ✓ Todos actives toujours en haut
   ✓ Interface 100% en français

Files:
   - public/index.html: Titre + container notifications
   - public/css/skeleton.css: Styles notifications
   - public/js/complete-app.js: showNotification() + remplacement 40+ alert()
   - config/database.js: Migration feed_title
   - routes/rss.routes.js: Stockage et retour feed_title
This commit is contained in:
Claude committed 2025-11-11 19:35:14 +00:00
1 parent 73391def6c
commit 7354818d3e
5 files changed
+260 -47

No files matched your search

+21
View File
@@ -149,6 +149,27 @@ function initDatabase() {
logger.info('✓ Tables de base de données créées avec succès'); logger.info('✓ Tables de base de données créées avec succès');
// Migrations automatiques // 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) => { db.all("PRAGMA table_info(notes)", (err, columns) => {
if (err) { if (err) {
logger.error('Erreur lors de la vérification de la structure de la table notes:', err); logger.error('Erreur lors de la vérification de la structure de la table notes:', err);
+119
View File
@@ -174,3 +174,122 @@
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
opacity: 0.7; 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);
}
}
+4 -1
View File
@@ -123,7 +123,7 @@
<!-- Todo Box --> <!-- Todo Box -->
<div class="todo-box"> <div class="todo-box">
<div class="todo-box-header"> <div class="todo-box-header">
<h2 class="todo-box-title">Quick Tasks</h2> <h2 class="todo-box-title">À faire</h2>
<span class="todo-counter" id="todoCounter">0 tâches</span> <span class="todo-counter" id="todoCounter">0 tâches</span>
</div> </div>
@@ -302,6 +302,9 @@
<span>Mise à jour...</span> <span>Mise à jour...</span>
</div> </div>
<!-- Conteneur de notifications -->
<div class="notifications-container" id="notificationsContainer"></div>
<!-- JavaScript --> <!-- JavaScript -->
<script src="/js/complete-app.js"></script> <script src="/js/complete-app.js"></script>
</body> </body>
+112 -42
View File
@@ -474,6 +474,66 @@ function escapeHtml(text) {
return div.innerHTML; 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 = '<svg class="notification-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>';
break;
case 'error':
iconSvg = '<svg class="notification-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>';
break;
case 'warning':
iconSvg = '<svg class="notification-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>';
break;
default: // info
iconSvg = '<svg class="notification-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>';
}
notification.innerHTML = `
${iconSvg}
<div class="notification-content">${escapeHtml(message)}</div>
<button class="notification-close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
`;
// 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 ==================== // ==================== INLINE NOTE EXPANSION ====================
function expandNote(noteId) { function expandNote(noteId) {
state.expandedNoteId = noteId; state.expandedNoteId = noteId;
@@ -519,7 +579,7 @@ async function archiveNote(noteId, archived) {
await loadNotes(); await loadNotes();
} catch (error) { } catch (error) {
console.error('Erreur archivage note:', 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(); await loadNotes();
} catch (error) { } catch (error) {
console.error('Erreur suppression note:', 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(); inputElement.focus();
} catch (error) { } catch (error) {
console.error('Erreur ajout todo:', 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(); renderNotes();
} catch (error) { } catch (error) {
console.error('Erreur suppression image:', 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(); renderNotes();
} catch (error) { } catch (error) {
console.error('Erreur upload image:', 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 // Reset input
@@ -707,7 +767,7 @@ async function handleFileUploadInline(event) {
renderNotes(); renderNotes();
} catch (error) { } catch (error) {
console.error('Erreur upload fichier:', 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 // Reset input
@@ -721,7 +781,7 @@ async function downloadFile(fileId) {
window.open(`/api/notes/files/${fileId}/download`, '_blank'); window.open(`/api/notes/files/${fileId}/download`, '_blank');
} catch (error) { } catch (error) {
console.error('Erreur téléchargement fichier:', 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) { } catch (error) {
console.error('Erreur suppression fichier:', 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); }, 100);
} catch (error) { } catch (error) {
console.error('Erreur création note:', 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(); await loadNotes();
} catch (error) { } catch (error) {
console.error('Erreur upload image:', 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; 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 => { filtered.forEach(todo => {
const item = document.createElement('div'); const item = document.createElement('div');
item.className = 'todo-item' + (todo.completed ? ' completed' : ''); item.className = 'todo-item' + (todo.completed ? ' completed' : '');
@@ -1194,7 +1264,7 @@ async function loadUsers() {
renderUsersTable(users); renderUsersTable(users);
} catch (error) { } catch (error) {
console.error('Erreur chargement utilisateurs:', 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() { async function createUser() {
const username = prompt('Nom d\'utilisateur:'); const username = prompt('Nom d\'utilisateur:');
if (!username || username.trim().length < 3) { 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; return;
} }
const password = prompt('Mot de passe:'); const password = prompt('Mot de passe:');
if (!password || password.length < 4) { 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; return;
} }
@@ -1270,11 +1340,11 @@ async function createUser() {
password, password,
is_admin: isAdmin is_admin: isAdmin
}); });
alert('Utilisateur créé avec succès'); showNotification('Utilisateur créé avec succès', 'success');
await loadUsers(); await loadUsers();
} catch (error) { } catch (error) {
console.error('Erreur création utilisateur:', 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; const confirmPassword = confirmPasswordInput.value;
if (!newPassword || newPassword.length < 6) { 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; return;
} }
if (newPassword !== confirmPassword) { if (newPassword !== confirmPassword) {
alert('Les mots de passe ne correspondent pas'); showNotification('Les mots de passe ne correspondent pas', 'warning');
return; return;
} }
try { try {
await api.put(`/api/users/${currentPasswordChangeUserId}`, { password: newPassword }); 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(); closePasswordChangeModal();
} catch (error) { } catch (error) {
console.error('Erreur modification mot de passe:', 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 { try {
await api.delete(`/api/users/${id}`); await api.delete(`/api/users/${id}`);
alert('Utilisateur supprimé avec succès'); showNotification('Utilisateur supprimé avec succès', 'success');
await loadUsers(); await loadUsers();
} catch (error) { } catch (error) {
console.error('Erreur suppression utilisateur:', 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); renderRssFeeds(feeds);
} catch (error) { } catch (error) {
console.error('Erreur chargement flux RSS:', 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(); const url = input.value.trim();
if (!url) { if (!url) {
alert('Veuillez entrer une URL de flux RSS'); showNotification('Veuillez entrer une URL de flux RSS', 'warning');
return; return;
} }
if (!url.startsWith('http://') && !url.startsWith('https://')) { 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; return;
} }
try { try {
await api.post('/api/rss/feeds', { url }); await api.post('/api/rss/feeds', { url });
input.value = ''; input.value = '';
alert('Flux RSS ajouté avec succès'); showNotification('Flux RSS ajouté avec succès', 'success');
await loadRssFeeds(); await loadRssFeeds();
} catch (error) { } catch (error) {
console.error('Erreur ajout flux RSS:', 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 }); await api.put(`/api/rss/feeds/${feedId}`, { enabled });
} catch (error) { } catch (error) {
console.error('Erreur toggle flux RSS:', 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 { try {
await api.delete(`/api/rss/feeds/${feedId}`); await api.delete(`/api/rss/feeds/${feedId}`);
alert('Flux RSS supprimé avec succès'); showNotification('Flux RSS supprimé avec succès', 'success');
await loadRssFeeds(); await loadRssFeeds();
} catch (error) { } catch (error) {
console.error('Erreur suppression flux RSS:', 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 { try {
const result = await api.post('/api/rss/fetch', {}); 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(); await loadRssArticles();
} catch (error) { } catch (error) {
console.error('Erreur fetch articles RSS:', 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 { } finally {
btn.disabled = false; btn.disabled = false;
btn.textContent = originalText; 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) // 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 // Si c'est un résumé DB, il contient déjà le titre et le lien formatés
if (summary.summary && summary.summary.includes('🔗')) { 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 ` return `
<div class="rss-summary rss-summary-article"> <div class="rss-summary rss-summary-article">
<div class="rss-summary-content">${formatSummary(summary.summary)}</div> <div class="rss-summary-content">${formatSummary(summary.summary)}</div>
<div class="rss-summary-meta"> <div class="rss-summary-meta">
${summary.created_at ? `<span>${new Date(summary.created_at).toLocaleDateString('fr-FR', { hour: '2-digit', minute: '2-digit' })}</span>` : ''} ${summary.feed_title ? `<span>📰 ${escapeHtml(summary.feed_title)}</span>` : ''}
${summary.model ? `<span>${escapeHtml(summary.model.split('/')[1] || summary.model)}</span>` : ''} ${summary.created_at ? `<span>📅 ${new Date(summary.created_at).toLocaleDateString('fr-FR', { hour: '2-digit', minute: '2-digit' })}</span>` : ''}
</div> </div>
</div> </div>
`; `;
@@ -1653,7 +1723,7 @@ async function summarizeRss() {
if (summaryEnabled) { if (summaryEnabled) {
// Si les résumés sont activés, juste recharger le bloc RSS // 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(); await loadRssArticles();
} else { } else {
// Sinon créer une note avec TOUS les résumés par article // Sinon créer une note avec TOUS les résumés par article
@@ -1667,22 +1737,22 @@ async function summarizeRss() {
content: allSummaries 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(); await loadNotes();
// Expand the new note // Expand the new note
state.expandedNoteId = newNote.id; state.expandedNoteId = newNote.id;
renderNotes(); renderNotes();
} else { } else {
alert('Aucun résumé généré'); showNotification('Aucun résumé généré', 'warning');
} }
} }
} catch (error) { } catch (error) {
console.error('Erreur génération résumé RSS:', error); console.error('Erreur génération résumé RSS:', error);
if (error.message && error.message.includes('400')) { 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 { } 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 { } finally {
btn.disabled = false; 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_enabled', { value: summaryEnabled.checked ? '1' : '0' });
await api.put('/api/settings/rss_summary_prompt', { value: summaryPrompt.value }); 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) { } catch (error) {
console.error('Erreur sauvegarde paramètres OpenRouter:', 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 () => { saveNoteBtn.addEventListener('click', async () => {
if (currentNoteId) { if (currentNoteId) {
await saveCurrentNote(); await saveCurrentNote();
alert('Note enregistrée !'); showNotification('Note enregistrée !', 'success');
} }
}); });
} }
+4 -4
View File
@@ -241,10 +241,10 @@ Résumé (100 mots max):`;
const summary = response.data.choices[0].message.content; const summary = response.data.choices[0].message.content;
// Sauvegarder le résumé individuel // Sauvegarder le résumé individuel avec feed_title
await runQuery( await runQuery(
'INSERT INTO rss_summaries (summary, model, articles_count, created_at) VALUES (?, ?, ?, datetime("now"))', '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.title}**\n\n${summary}\n\n🔗 [Lire l'article](${article.link})`, selectedModel, 1, article.feed_title]
); );
return { return {
@@ -300,7 +300,7 @@ Résumé (100 mots max):`;
router.get('/summaries', async (req, res) => { router.get('/summaries', async (req, res) => {
try { try {
const summaries = await getAll(` 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 FROM rss_summaries
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT 5 LIMIT 5