mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-11 17:29:37 +02:00
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:
5 files changed
+260
-47
No files matched your search
@@ -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);
|
||||||
|
|||||||
Vendored
+119
@@ -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
@@ -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
@@ -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');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user