From 7dd01f6113d7e0a7a21713f3b921f7d7f01760f5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Nov 2025 15:59:38 +0000 Subject: [PATCH] =?UTF-8?q?JavaScript:=20Impl=C3=A9mentation=20compl=C3=A8?= =?UTF-8?q?te=20RSS,=20OpenRouter=20et=20administration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Admin: Système de tabs pour Users/RSS/OpenRouter - Admin: Modal de changement de mot de passe (sans prompts navigateur) - RSS: CRUD complet des flux RSS (ajout, toggle, suppression) - RSS: Fetch et affichage des 10 derniers articles - RSS: Click sur article ouvre dans nouvel onglet - RSS: Bouton résumé IA avec OpenRouter (crée une note) - OpenRouter: Chargement/sauvegarde des paramètres (API key, modèle, prompt) - OpenRouter: Sélecteur de modèles avec recherche/filtre - OpenRouter: Prompt par défaut optimisé pour synthèse d'articles - Animations: Ajout spinner pour états de chargement - Event delegation pour toutes les nouvelles interactions --- public/css/animations.css | 6 + public/js/complete-app.js | 480 +++++++++++++++++++++++++++++++++++++- 2 files changed, 475 insertions(+), 11 deletions(-) diff --git a/public/css/animations.css b/public/css/animations.css index 08d2b7c..8024541 100644 --- a/public/css/animations.css +++ b/public/css/animations.css @@ -116,6 +116,12 @@ animation: spin 1s linear infinite; } +.spinner { + animation: spin 1s linear infinite; + width: 20px; + height: 20px; +} + /* Smooth transitions */ .smooth-transition { transition: all var(--transition-base); diff --git a/public/js/complete-app.js b/public/js/complete-app.js index 88620dd..ac1bd7e 100644 --- a/public/js/complete-app.js +++ b/public/js/complete-app.js @@ -1160,6 +1160,34 @@ function setupSearch() { } // ==================== ADMIN ==================== + +// Tab switching +function switchAdminTab(tabName) { + // Update tab buttons + document.querySelectorAll('.admin-tab').forEach(btn => { + btn.classList.toggle('active', btn.dataset.tab === tabName); + }); + + // Update tab content + document.querySelectorAll('.admin-tab-content').forEach(content => { + content.style.display = 'none'; + }); + + const targetTab = document.getElementById(`tab-${tabName}`); + if (targetTab) { + targetTab.style.display = 'block'; + } + + // Load data for the selected tab + if (tabName === 'users') { + loadUsers(); + } else if (tabName === 'rss') { + loadRssFeeds(); + } else if (tabName === 'openrouter') { + loadOpenRouterSettings(); + } +} + async function loadUsers() { try { const users = await api.get('/api/users'); @@ -1200,7 +1228,9 @@ async function openAdminModal() { modal.style.display = 'flex'; setTimeout(() => modal.classList.add('active'), 10); - await loadUsers(); + + // Switch to users tab by default + switchAdminTab('users'); } function closeAdminModal() { @@ -1248,32 +1278,74 @@ async function createUser() { } } -async function changeUserPassword(id, username) { - const password = prompt(`Nouveau mot de passe pour "${username}" :`); +// Password Change Modal +let currentPasswordChangeUserId = null; +let currentPasswordChangeUsername = null; - if (!password) return; +function openPasswordChangeModal(userId, username) { + currentPasswordChangeUserId = userId; + currentPasswordChangeUsername = username; - if (password.length < 6) { + const modal = document.getElementById('changePasswordModal'); + const usernameDisplay = document.getElementById('changePasswordUsername'); + const newPasswordInput = document.getElementById('newPassword'); + const confirmPasswordInput = document.getElementById('confirmPassword'); + + if (usernameDisplay) { + usernameDisplay.textContent = `Modifier le mot de passe de "${username}"`; + } + + if (newPasswordInput) newPasswordInput.value = ''; + if (confirmPasswordInput) confirmPasswordInput.value = ''; + + modal.style.display = 'flex'; + setTimeout(() => modal.classList.add('active'), 10); + + if (newPasswordInput) newPasswordInput.focus(); +} + +function closePasswordChangeModal() { + const modal = document.getElementById('changePasswordModal'); + modal.classList.remove('active'); + setTimeout(() => { + modal.style.display = 'none'; + }, 300); + + currentPasswordChangeUserId = null; + currentPasswordChangeUsername = null; +} + +async function confirmPasswordChange() { + const newPasswordInput = document.getElementById('newPassword'); + const confirmPasswordInput = document.getElementById('confirmPassword'); + + const newPassword = newPasswordInput.value; + const confirmPassword = confirmPasswordInput.value; + + if (!newPassword || newPassword.length < 6) { alert('Le mot de passe doit contenir au moins 6 caractères'); return; } - const confirmPassword = prompt('Confirmer le nouveau mot de passe :'); - - if (password !== confirmPassword) { + if (newPassword !== confirmPassword) { alert('Les mots de passe ne correspondent pas'); return; } try { - await api.put(`/api/users/${id}`, { password }); + await api.put(`/api/users/${currentPasswordChangeUserId}`, { password: newPassword }); alert('Mot de passe modifié avec succès'); + closePasswordChangeModal(); } catch (error) { console.error('Erreur modification mot de passe:', error); alert('Erreur lors de la modification du mot de passe'); } } +async function changeUserPassword(id, username) { + openPasswordChangeModal(id, username); +} + async function deleteUser(id, username) { const confirmed = await confirmDialog.show({ icon: '⚠️', @@ -1294,13 +1366,299 @@ async function deleteUser(id, username) { } } +// ==================== RSS FEEDS ==================== +async function loadRssFeeds() { + try { + const feeds = await api.get('/api/rss/feeds'); + renderRssFeeds(feeds); + } catch (error) { + console.error('Erreur chargement flux RSS:', error); + alert('Erreur lors du chargement des flux RSS'); + } +} + +function renderRssFeeds(feeds) { + const container = document.getElementById('rssFeedsList'); + if (!container) return; + + if (feeds.length === 0) { + container.innerHTML = '

Aucun flux RSS configuré

'; + return; + } + + container.innerHTML = feeds.map(feed => ` +
+
+
${escapeHtml(feed.title || feed.url)}
+
${escapeHtml(feed.url)}
+ ${feed.last_fetched_at ? `
Dernière maj: ${new Date(feed.last_fetched_at).toLocaleString('fr-FR')}
` : ''} +
+
+ + +
+
+ `).join(''); +} + +async function addRssFeed() { + const input = document.getElementById('rssUrlInput'); + const url = input.value.trim(); + + if (!url) { + alert('Veuillez entrer une URL de flux RSS'); + return; + } + + if (!url.startsWith('http://') && !url.startsWith('https://')) { + alert('L\'URL doit commencer par http:// ou https://'); + return; + } + + try { + await api.post('/api/rss/feeds', { url }); + input.value = ''; + alert('Flux RSS ajouté avec succès'); + await loadRssFeeds(); + } catch (error) { + console.error('Erreur ajout flux RSS:', error); + alert('Erreur lors de l\'ajout du flux RSS. Vérifiez l\'URL.'); + } +} + +async function toggleRssFeed(feedId, enabled) { + try { + 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'); + } +} + +async function deleteRssFeed(feedId) { + const confirmed = await confirmDialog.show({ + icon: '📰', + title: 'Supprimer ce flux RSS', + message: 'Êtes-vous sûr de vouloir supprimer ce flux RSS ?\n\nTous les articles associés seront également supprimés.', + okText: 'Supprimer' + }); + + if (!confirmed) return; + + try { + await api.delete(`/api/rss/feeds/${feedId}`); + alert('Flux RSS supprimé avec succès'); + await loadRssFeeds(); + } catch (error) { + console.error('Erreur suppression flux RSS:', error); + alert('Erreur lors de la suppression du flux RSS'); + } +} + +async function fetchRssArticles() { + const btn = document.getElementById('fetchRssBtn'); + if (!btn) return; + + const originalText = btn.textContent; + btn.disabled = true; + btn.textContent = '🔄 Récupération en cours...'; + + try { + const result = await api.post('/api/rss/fetch', {}); + alert(result.message || 'Articles récupérés avec succès'); + await loadRssArticles(); + } catch (error) { + console.error('Erreur fetch articles RSS:', error); + alert('Erreur lors de la récupération des articles RSS'); + } finally { + btn.disabled = false; + btn.textContent = originalText; + } +} + +async function loadRssArticles() { + try { + const articles = await api.get('/api/rss/articles'); + renderRssArticles(articles); + } catch (error) { + console.error('Erreur chargement articles RSS:', error); + } +} + +function renderRssArticles(articles) { + const container = document.getElementById('rssArticlesList'); + if (!container) return; + + if (!articles || articles.length === 0) { + container.innerHTML = '

Aucun article pour le moment

'; + return; + } + + container.innerHTML = articles.map(article => ` +
+
${escapeHtml(article.title)}
+ + ${article.description ? `
${escapeHtml(article.description)}
` : ''} +
+ `).join(''); +} + +async function summarizeRss() { + const btn = document.getElementById('summarizeRssBtn'); + if (!btn) return; + + const originalHTML = btn.innerHTML; + btn.disabled = true; + btn.innerHTML = ''; + + try { + const result = await api.post('/api/rss/summarize', {}); + + // Create a new note with the summary + const newNote = await api.post('/api/notes', { + title: `📰 Résumé RSS - ${new Date().toLocaleDateString('fr-FR')}`, + content: result.summary + }); + + alert('Résumé généré et sauvegardé dans une nouvelle note !'); + await loadNotes(); + + // Expand the new note + state.expandedNoteId = newNote.id; + renderNotes(); + } 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'); + } else { + alert('Erreur lors de la génération du résumé RSS'); + } + } finally { + btn.disabled = false; + btn.innerHTML = originalHTML; + } +} + +// ==================== OPENROUTER ==================== +let allModels = []; + +async function loadOpenRouterSettings() { + try { + const settings = await api.get('/api/settings'); + const models = await api.get('/api/rss/models'); + + allModels = models; + + // Populate form + const apiKeyInput = document.getElementById('openrouterApiKey'); + const modelSelect = document.getElementById('openrouterModel'); + const summaryEnabled = document.getElementById('rssSummaryEnabled'); + const summaryPrompt = document.getElementById('rssSummaryPrompt'); + + if (apiKeyInput) { + apiKeyInput.value = settings.openrouter_api_key || ''; + } + + if (summaryEnabled) { + summaryEnabled.checked = settings.rss_summary_enabled === 'true' || settings.rss_summary_enabled === '1'; + } + + if (summaryPrompt) { + summaryPrompt.value = settings.rss_summary_prompt || `Tu es un assistant spécialisé dans la synthèse d'actualités. Analyse les articles suivants et crée un résumé structuré et informatif. + +Pour chaque article, identifie: +- Le sujet principal +- Les points clés +- L'importance de l'information + +Ensuite, génère un résumé global qui: +1. Regroupe les thèmes communs +2. Hiérarchise les informations par importance +3. Présente une vue d'ensemble claire et concise +4. Utilise un style professionnel mais accessible + +Format de sortie en Markdown avec des sections claires.`; + } + + // Populate model selector + renderModelSelect(models, settings.openrouter_model); + + } catch (error) { + console.error('Erreur chargement paramètres OpenRouter:', error); + } +} + +function renderModelSelect(models, selectedModel) { + const select = document.getElementById('openrouterModel'); + if (!select) return; + + select.innerHTML = models.map(model => ` + + `).join(''); +} + +function filterModels() { + const searchInput = document.getElementById('modelSearch'); + const select = document.getElementById('openrouterModel'); + + if (!searchInput || !select) return; + + const query = searchInput.value.toLowerCase(); + + if (!query) { + renderModelSelect(allModels, select.value); + return; + } + + const filtered = allModels.filter(model => + model.name.toLowerCase().includes(query) || + model.provider.toLowerCase().includes(query) || + model.id.toLowerCase().includes(query) + ); + + renderModelSelect(filtered, select.value); +} + +async function saveOpenRouterSettings() { + const apiKeyInput = document.getElementById('openrouterApiKey'); + const modelSelect = document.getElementById('openrouterModel'); + const summaryEnabled = document.getElementById('rssSummaryEnabled'); + const summaryPrompt = document.getElementById('rssSummaryPrompt'); + + try { + // Save all settings + await api.put('/api/settings/openrouter_api_key', { value: apiKeyInput.value }); + await api.put('/api/settings/openrouter_model', { value: modelSelect.value }); + 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'); + } catch (error) { + console.error('Erreur sauvegarde paramètres OpenRouter:', error); + alert('Erreur lors de la sauvegarde des paramètres'); + } +} + // ==================== INIT ==================== async function init() { // Vérifier l'authentification await initAuth(); // Charger les données - await Promise.all([loadNotes(), loadTodos()]); + await Promise.all([loadNotes(), loadTodos(), loadRssArticles()]); // Setup search setupSearch(); @@ -1584,10 +1942,110 @@ async function init() { collapseNote(); } else { closeNoteModal(); - document.getElementById('adminModal').style.display = 'none'; + closeAdminModal(); + closePasswordChangeModal(); } } }); + + // ==================== ADMIN TAB SWITCHING ==================== + document.querySelectorAll('.admin-tab').forEach(tab => { + tab.addEventListener('click', () => { + switchAdminTab(tab.dataset.tab); + }); + }); + + // ==================== PASSWORD CHANGE MODAL ==================== + const closePasswordModal = document.getElementById('closeChangePasswordModal'); + if (closePasswordModal) { + closePasswordModal.addEventListener('click', closePasswordChangeModal); + } + + const cancelPasswordChange = document.getElementById('cancelChangePassword'); + if (cancelPasswordChange) { + cancelPasswordChange.addEventListener('click', closePasswordChangeModal); + } + + const confirmPasswordBtn = document.getElementById('confirmChangePassword'); + if (confirmPasswordBtn) { + confirmPasswordBtn.addEventListener('click', confirmPasswordChange); + } + + // Enter key to confirm password change + const newPasswordInput = document.getElementById('newPassword'); + const confirmPasswordInput = document.getElementById('confirmPassword'); + if (newPasswordInput && confirmPasswordInput) { + [newPasswordInput, confirmPasswordInput].forEach(input => { + input.addEventListener('keypress', (e) => { + if (e.key === 'Enter') confirmPasswordChange(); + }); + }); + } + + // ==================== RSS FEEDS ==================== + const addRssFeedBtn = document.getElementById('addRssFeedBtn'); + if (addRssFeedBtn) { + addRssFeedBtn.addEventListener('click', addRssFeed); + } + + const rssUrlInput = document.getElementById('rssUrlInput'); + if (rssUrlInput) { + rssUrlInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') addRssFeed(); + }); + } + + const fetchRssBtn = document.getElementById('fetchRssBtn'); + if (fetchRssBtn) { + fetchRssBtn.addEventListener('click', fetchRssArticles); + } + + // Event delegation for RSS feeds list + const rssFeedsList = document.getElementById('rssFeedsList'); + if (rssFeedsList) { + rssFeedsList.addEventListener('change', (e) => { + if (e.target.type === 'checkbox' && e.target.dataset.feedId) { + const feedId = parseInt(e.target.dataset.feedId); + toggleRssFeed(feedId, e.target.checked); + } + }); + + rssFeedsList.addEventListener('click', (e) => { + const deleteBtn = e.target.closest('.btn-delete-feed'); + if (deleteBtn) { + const feedId = parseInt(deleteBtn.dataset.feedId); + deleteRssFeed(feedId); + } + }); + } + + // RSS articles click to open in new tab + const rssArticlesList = document.getElementById('rssArticlesList'); + if (rssArticlesList) { + rssArticlesList.addEventListener('click', (e) => { + const article = e.target.closest('.rss-article'); + if (article && article.dataset.link) { + window.open(article.dataset.link, '_blank'); + } + }); + } + + // RSS summarize button + const summarizeRssBtn = document.getElementById('summarizeRssBtn'); + if (summarizeRssBtn) { + summarizeRssBtn.addEventListener('click', summarizeRss); + } + + // ==================== OPENROUTER ==================== + const modelSearch = document.getElementById('modelSearch'); + if (modelSearch) { + modelSearch.addEventListener('input', utils.debounce(filterModels, 300)); + } + + const saveOpenRouterBtn = document.getElementById('saveOpenRouterSettings'); + if (saveOpenRouterBtn) { + saveOpenRouterBtn.addEventListener('click', saveOpenRouterSettings); + } } // Démarrer l'application