`;
// Auto-save sur changement
setTimeout(() => {
const titleInput = card.querySelector('.note-title-input-inline');
const contentTextarea = card.querySelector('.note-content-textarea-inline');
if (titleInput) titleInput.oninput = utils.debounce(() => saveNoteInline(note.id), 1000);
if (contentTextarea) contentTextarea.oninput = utils.debounce(() => saveNoteInline(note.id), 1000);
}, 0);
} else {
// Mode compact
let html = `
${escapeHtml(note.title)}
`;
if (note.content) {
html += `
${escapeHtml(utils.truncate(note.content, 100))}
`;
}
if (note.image_filename) {
html += ``;
}
// Afficher les todos
if (note.todos && note.todos.length > 0) {
const displayTodos = note.todos.slice(0, 3);
html += `
`).join('');
}
function renderRssSummaries(summaries) {
const container = document.getElementById('rssArticlesList');
if (!container) return;
if (!summaries || summaries.length === 0) {
container.innerHTML = '
Aucun résumé pour le moment
';
return;
}
// Nouvelle structure : afficher les résumés PAR ARTICLE avec titre + résumé + lien
container.innerHTML = summaries.map(summary => {
// 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('🔗')) {
// Nouveau format : résumé par article avec titre et lien
return `
`;
}
}).join('');
}
function formatSummary(summary) {
// Convertir le Markdown de base en HTML
let html = escapeHtml(summary);
// Titres
html = html.replace(/\n## (.*?)\n/g, '
$1
');
html = html.replace(/\n# (.*?)\n/g, '
$1
');
// Listes à puces
html = html.replace(/\n- (.*?)(?=\n|$)/g, '
$1
');
html = html.replace(/(
.*<\/li>)/s, '
$1
');
// Listes numérotées
html = html.replace(/\n\d+\. (.*?)(?=\n|$)/g, '
$1
');
// Gras (avant les liens pour éviter les conflits)
html = html.replace(/\*\*(.*?)\*\*/g, '$1');
// Italique
html = html.replace(/\*(.*?)\*/g, '$1');
// Liens markdown [texte](url) - convertir en liens cliquables
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1');
// Emoji lien avec texte - gérer 🔗 suivi d'espace
html = html.replace(/🔗\s*/g, '🔗 ');
// Sauts de ligne
html = html.replace(/\n\n/g, '
');
html = html.replace(/\n/g, ' ');
return html;
}
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', {});
// Vérifier si les résumés sont activés
const settings = await api.get('/api/settings');
const summaryEnabled = settings.rss_summary_enabled === '1' || settings.rss_summary_enabled === 'true';
if (summaryEnabled) {
// Si les résumés sont activés, juste recharger le bloc RSS
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
if (result.summaries && result.summaries.length > 0) {
const allSummaries = result.summaries.map(s =>
`**${s.title}**\n_${s.feed_title} - ${new Date(s.pub_date).toLocaleDateString('fr-FR')}_\n\n${s.summary}\n\n🔗 [Lire l'article](${s.link})`
).join('\n\n---\n\n');
const newNote = await api.post('/api/notes', {
title: `📰 Résumés RSS - ${new Date().toLocaleDateString('fr-FR')}`,
content: allSummaries
});
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 {
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')) {
showNotification('Veuillez configurer votre clé API OpenRouter dans l\'administration');
} else {
showNotification('Erreur lors de la génération du résumé RSS', 'error');
}
} 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 });
showNotification('Paramètres OpenRouter sauvegardés avec succès', 'success');
} catch (error) {
console.error('Erreur sauvegarde paramètres OpenRouter:', error);
showNotification('Erreur lors de la sauvegarde des paramètres', 'error');
}
}
// ==================== INIT ====================
async function init() {
// Vérifier l'authentification
await initAuth();
// Charger les données
await Promise.all([loadNotes(), loadTodos(), loadRssArticles()]);
// Setup search
setupSearch();
// Auto-refresh RSS toutes les 30 secondes pour synchroniser avec le scheduler serveur
setInterval(() => {
loadRssArticles().catch(err => {
console.debug('Erreur auto-refresh RSS:', err);
});
}, 30000); // 30 secondes
// Event listeners
const newNoteBtn = document.getElementById('newNoteBtn');
if (newNoteBtn) {
newNoteBtn.addEventListener('click', createNewNote);
}
// View filters (Active/Archived)
document.querySelectorAll('.view-filter').forEach(btn => {
btn.addEventListener('click', () => setNoteView(btn.dataset.view));
});
// Admin button
const adminBtn = document.getElementById('adminBtn');
if (adminBtn) {
adminBtn.addEventListener('click', openAdminModal);
}
const closeAdminModalBtn = document.getElementById('closeAdminModal');
if (closeAdminModalBtn) {
closeAdminModalBtn.addEventListener('click', closeAdminModal);
}
const createUserBtn = document.getElementById('createUserBtn');
if (createUserBtn) {
createUserBtn.addEventListener('click', createUser);
}
const adminModalBackdrop = document.getElementById('adminModal');
if (adminModalBackdrop) {
adminModalBackdrop.addEventListener('click', (e) => {
if (e.target === adminModalBackdrop) closeAdminModal();
});
}
const closeEditor = document.getElementById('closeEditor');
if (closeEditor) {
closeEditor.addEventListener('click', closeNoteModal);
}
const saveNoteBtn = document.getElementById('saveNote');
if (saveNoteBtn) {
saveNoteBtn.addEventListener('click', async () => {
if (currentNoteId) {
await saveCurrentNote();
showNotification('Note enregistrée !', 'success');
}
});
}
const deleteNote = document.getElementById('deleteNote');
if (deleteNote) {
deleteNote.addEventListener('click', deleteCurrentNote);
}
const addImageBtn = document.getElementById('addImageBtn');
if (addImageBtn) {
addImageBtn.addEventListener('click', addNoteImage);
}
const imageInput = document.getElementById('imageInput');
if (imageInput) {
imageInput.addEventListener('change', handleImageUploadInline);
}
const fileInput = document.getElementById('fileInput');
if (fileInput) {
fileInput.addEventListener('change', handleFileUploadInline);
}
const removeImage = document.getElementById('removeImage');
if (removeImage) {
removeImage.addEventListener('click', removeNoteImage);
}
const addNoteTodoBtn = document.getElementById('addNoteTodo');
if (addNoteTodoBtn) {
addNoteTodoBtn.addEventListener('click', addNoteTodo);
}
const addTodoBtn = document.getElementById('addTodoBtn');
if (addTodoBtn) {
addTodoBtn.addEventListener('click', addTodo);
}
const todoInput = document.getElementById('todoInput');
if (todoInput) {
todoInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') addTodo();
});
}
// Todo filters
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', () => setTodoFilter(btn.dataset.filter));
});
// Event delegation pour les todos de la sidebar
const todoList = document.getElementById('todoList');
if (todoList) {
todoList.addEventListener('change', (e) => {
if (e.target.classList.contains('todo-checkbox')) {
const todoId = parseInt(e.target.dataset.todoId);
toggleTodo(todoId, e.target.checked);
}
});
todoList.addEventListener('click', (e) => {
if (e.target.classList.contains('todo-delete')) {
const todoId = parseInt(e.target.dataset.todoId);
deleteTodo(todoId);
}
});
}
// Event delegation pour les todos des notes
const noteTodosList = document.getElementById('noteTodosList');
if (noteTodosList) {
noteTodosList.addEventListener('change', (e) => {
if (e.target.classList.contains('todo-checkbox')) {
const todoId = parseInt(e.target.dataset.noteTodoId);
toggleNoteTodo(todoId, e.target.checked);
}
});
noteTodosList.addEventListener('click', (e) => {
if (e.target.classList.contains('todo-delete')) {
const todoId = parseInt(e.target.dataset.noteTodoId);
deleteNoteTodo(todoId);
}
});
}
// Event delegation pour les boutons d'administration des utilisateurs
const usersTableBody = document.getElementById('usersTableBody');
if (usersTableBody) {
usersTableBody.addEventListener('click', (e) => {
// Bouton changer mot de passe
if (e.target.classList.contains('btn-edit-user') || e.target.closest('.btn-edit-user')) {
const btn = e.target.classList.contains('btn-edit-user') ? e.target : e.target.closest('.btn-edit-user');
const userId = parseInt(btn.dataset.userId);
const username = btn.dataset.username;
changeUserPassword(userId, username);
}
// Bouton supprimer utilisateur
else if (e.target.classList.contains('btn-delete-user') || e.target.closest('.btn-delete-user')) {
const btn = e.target.classList.contains('btn-delete-user') ? e.target : e.target.closest('.btn-delete-user');
const userId = parseInt(btn.dataset.userId);
const username = btn.dataset.username;
deleteUser(userId, username);
}
});
}
// Close editor on backdrop click
const noteEditorBackdrop = document.getElementById('noteEditorBackdrop');
if (noteEditorBackdrop) {
noteEditorBackdrop.addEventListener('click', (e) => {
if (e.target === noteEditorBackdrop) closeNoteModal();
});
}
// Event delegation for notes grid
const notesGrid = document.getElementById('notesGrid');
if (notesGrid) {
// Click on compact note to expand
notesGrid.addEventListener('click', (e) => {
const compactNote = e.target.closest('.note-card-compact');
if (compactNote) {
const noteId = parseInt(compactNote.dataset.noteId);
expandNote(noteId);
return;
}
// Collapse note button
if (e.target.closest('.btn-collapse-note')) {
collapseNote();
return;
}
// Archive note button
const archiveBtn = e.target.closest('.btn-archive-note');
if (archiveBtn) {
const noteId = parseInt(archiveBtn.dataset.noteId);
const archived = archiveBtn.dataset.archived === 'true';
archiveNote(noteId, !archived);
return;
}
// Delete note button
const deleteBtn = e.target.closest('.btn-delete-note-inline');
if (deleteBtn) {
const noteId = parseInt(deleteBtn.dataset.noteId);
deleteNoteInline(noteId);
return;
}
// Add image button
const addImageBtn = e.target.closest('.btn-add-image-inline');
if (addImageBtn) {
const noteId = parseInt(addImageBtn.dataset.noteId);
triggerImageUpload(noteId);
return;
}
// Remove image button
const removeImageBtn = e.target.closest('.btn-remove-image-inline');
if (removeImageBtn) {
const noteId = parseInt(removeImageBtn.dataset.noteId);
removeImageInline(noteId);
return;
}
// Add todo button
const addTodoBtn = e.target.closest('.btn-add-todo-inline');
if (addTodoBtn) {
const noteId = parseInt(addTodoBtn.dataset.noteId);
const inputElement = document.querySelector(`.add-todo-inline-input[data-note-id="${noteId}"]`);
if (inputElement) {
addNoteTodoInline(noteId, inputElement);
}
return;
}
// Add file button
const addFileBtn = e.target.closest('.btn-add-file-inline');
if (addFileBtn) {
const noteId = parseInt(addFileBtn.dataset.noteId);
triggerFileUpload(noteId);
return;
}
// Delete todo inline button
if (e.target.classList.contains('todo-delete-inline')) {
const todoId = parseInt(e.target.dataset.todoInlineId);
deleteNoteTodoInline(todoId);
return;
}
// Download file button
const downloadBtn = e.target.closest('.btn-download-file');
if (downloadBtn) {
const fileId = parseInt(downloadBtn.dataset.fileId);
downloadFile(fileId);
return;
}
// Delete file button
const deleteFileBtn = e.target.closest('.btn-delete-file');
if (deleteFileBtn) {
const fileId = parseInt(deleteFileBtn.dataset.fileId);
deleteFile(fileId);
return;
}
});
// Checkbox change for inline todos
notesGrid.addEventListener('change', (e) => {
if (e.target.classList.contains('todo-checkbox-inline')) {
const todoId = parseInt(e.target.dataset.todoInlineId);
toggleNoteTodoInline(todoId, e.target.checked);
}
});
// Enter key on add todo input
notesGrid.addEventListener('keypress', (e) => {
if (e.target.classList.contains('add-todo-inline-input') && e.key === 'Enter') {
const noteId = parseInt(e.target.dataset.noteId);
addNoteTodoInline(noteId, e.target);
}
});
}
// Close modal on Escape
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
if (state.expandedNoteId) {
collapseNote();
} else {
closeNoteModal();
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);
}
// ==================== MODAL IMAGE ====================
// Fermer le modal avec le bouton X
const imageModalClose = document.getElementById('imageModalClose');
if (imageModalClose) {
imageModalClose.addEventListener('click', closeImageModal);
}
// Fermer le modal en cliquant sur le fond
const imageModal = document.getElementById('imageModal');
if (imageModal) {
imageModal.addEventListener('click', (e) => {
if (e.target === imageModal) {
closeImageModal();
}
});
}
// Fermer le modal avec la touche Escape
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeImageModal();
}
});
// Délégation d'événements pour les clics sur les images
// Cela permettra de gérer les images même après un re-render
document.addEventListener('click', (e) => {
const target = e.target;
// Si on clique sur une image de note (miniature)
if (target.classList.contains('note-card-image') || target.classList.contains('note-image')) {
e.preventDefault();
e.stopPropagation();
openImageModal(target.src);
}
});
}
// Démarrer l'application
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}