From 955bfdfe07301a23e05f0d6febcbda15c6e98173 Mon Sep 17 00:00:00 2001 From: Benoit Date: Wed, 1 Jul 2026 13:21:59 +0200 Subject: [PATCH] feat: activer boutons Groupes et ajouter export logs/config desktop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Onglet Groupes : boutons Modifier et Supprimer fonctionnels via délégation d'événements et data-attributes ; slugify() côté client synchronisé avec le serveur ; classe CSS btn-danger pour Supprimer - Export logs : bouton "Exporter JSON" dans l'onglet Logs (filtre niveau actif) - Export/Import config.yaml : section dédiée dans Configuration avec dialog système (backup automatique .bak avant import) via IPC Electron (config:export / config:import dans main.js + preload.js) --- DESKTOP-APP.md | 4 +- electron/main.js | 47 ++++++++++++ electron/preload.js | 6 ++ electron/ui/app.js | 157 ++++++++++++++++++++++++++++++++++++-- electron/ui/index.html | 10 +++ electron/ui/styles.css | 21 +++++ server/config/config.yaml | 2 +- 7 files changed, 238 insertions(+), 9 deletions(-) diff --git a/DESKTOP-APP.md b/DESKTOP-APP.md index 9c7cf7c..ba38f9a 100644 --- a/DESKTOP-APP.md +++ b/DESKTOP-APP.md @@ -336,9 +336,9 @@ PORT=3001 npm start ### Priorité moyenne - [ ] **Graphiques monitoring** : Chart.js pour latence/bande passante -- [ ] **Export logs** : bouton télécharger CSV/JSON +- [x] **Export logs** : bouton télécharger JSON (filtre niveau appliqué) - [ ] **Matrice routing** : interface graphique drag & drop -- [ ] **Notifications desktop** : via Electron Notification API +- [x] **Export & import config** : bouton télécharger YAML et charger config (backup auto .bak) ### Priorité basse - [ ] **Thème toggle** : dark/light mode diff --git a/electron/main.js b/electron/main.js index 918a261..edb5131 100644 --- a/electron/main.js +++ b/electron/main.js @@ -5,6 +5,7 @@ const { app, BrowserWindow, ipcMain, Menu, Tray, dialog } = require('electron'); const path = require('path'); +const fs = require('fs'); const { spawn } = require('child_process'); const http = require('http'); const https = require('https'); @@ -365,6 +366,52 @@ app.whenReady().then(async () => { return setupHelper.getNetworkIP(); }); + ipcMain.handle('config:export', async () => { + const configPath = path.join(__dirname, '..', 'server', 'config', 'config.yaml'); + + try { + const content = fs.readFileSync(configPath, 'utf8'); + + const { filePath } = await dialog.showSaveDialog(mainWindow, { + title: 'Exporter la configuration', + defaultPath: 'config.yaml', + filters: [{ name: 'YAML', extensions: ['yaml', 'yml'] }] + }); + + if (!filePath) return { success: false, cancelled: true }; + + fs.writeFileSync(filePath, content, 'utf8'); + return { success: true, filePath }; + } catch (error) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('config:import', async () => { + const { filePaths } = await dialog.showOpenDialog(mainWindow, { + title: 'Importer une configuration', + filters: [{ name: 'YAML', extensions: ['yaml', 'yml'] }], + properties: ['openFile'] + }); + + if (!filePaths || filePaths.length === 0) return { success: false, cancelled: true }; + + try { + const content = fs.readFileSync(filePaths[0], 'utf8'); + const configPath = path.join(__dirname, '..', 'server', 'config', 'config.yaml'); + + // Backup de l'ancienne config avant remplacement + if (fs.existsSync(configPath)) { + fs.copyFileSync(configPath, configPath + '.bak'); + } + + fs.writeFileSync(configPath, content, 'utf8'); + return { success: true }; + } catch (error) { + return { success: false, error: error.message }; + } + }); + // Créer fenêtre createWindow(); createTray(); diff --git a/electron/preload.js b/electron/preload.js index da83344..2dd75d4 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -37,6 +37,12 @@ contextBridge.exposeInMainWorld('electronAPI', { // IP réseau locale (même détection que pour les certificats mkcert) getNetworkIP: () => ipcRenderer.invoke('network:ip'), + // Export/import configuration YAML via dialog système + config: { + export: () => ipcRenderer.invoke('config:export'), + import: () => ipcRenderer.invoke('config:import') + }, + // Helpers platform: process.platform, version: process.env.npm_package_version || '0.3.0' diff --git a/electron/ui/app.js b/electron/ui/app.js index c7048ff..07cd6d4 100644 --- a/electron/ui/app.js +++ b/electron/ui/app.js @@ -312,18 +312,82 @@ async function fetchGroups() { return; } - container.innerHTML = data.groups.map(group => ` + container.innerHTML = data.groups.map(group => { + const id = slugify(group.name); + return `
-

${group.name}

-

Bitrate: ${group.audioBitrate || 96} kbps • ID: ${group.id}

+

${escapeHtml(group.name)}

+

Bitrate: ${group.audioBitrate || 96} kbps • ID: ${escapeHtml(id)}

- - + +
- `).join(''); + `; + }).join(''); +} + +async function editGroup(id, currentName, currentBitrate) { + const newName = prompt('Nom du groupe:', currentName); + if (newName === null || newName.trim() === '') return; + + const newBitrateStr = prompt('Bitrate (kbps, 32-320):', String(currentBitrate)); + if (newBitrateStr === null) return; + + const newBitrate = parseInt(newBitrateStr); + if (isNaN(newBitrate) || newBitrate < 32 || newBitrate > 320) { + showNotification('Bitrate invalide (32-320 kbps)', 'error'); + return; + } + + try { + const response = await fetch(`${API_BASE}/admin/groups/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: newName.trim(), audioBitrate: newBitrate }) + }); + + if (response.ok) { + showNotification('Groupe modifié', 'success'); + await fetchGroups(); + } else { + const err = await response.json().catch(() => ({})); + showNotification('Erreur: ' + (err.error || 'Modification échouée'), 'error'); + } + } catch (error) { + console.error('Erreur edit group:', error); + showNotification('Erreur réseau', 'error'); + } +} + +async function deleteGroup(id, name) { + if (!confirm(`Supprimer le groupe "${name}" ?`)) return; + + try { + const response = await fetch(`${API_BASE}/admin/groups/${id}`, { + method: 'DELETE' + }); + + if (response.ok) { + showNotification('Groupe supprimé', 'success'); + await fetchGroups(); + } else { + const err = await response.json().catch(() => ({})); + showNotification('Erreur: ' + (err.error || 'Suppression échouée'), 'error'); + } + } catch (error) { + console.error('Erreur delete group:', error); + showNotification('Erreur réseau', 'error'); + } } async function fetchConfig() { @@ -581,10 +645,91 @@ document.addEventListener('DOMContentLoaded', () => { } }); } + + // Exporter config.yaml + const btnExportConfig = document.getElementById('btn-export-config'); + if (btnExportConfig) { + btnExportConfig.addEventListener('click', async () => { + const result = await window.electronAPI.config.export(); + if (result.success) { + showNotification('Configuration exportée', 'success'); + } else if (!result.cancelled) { + showNotification('Erreur export: ' + (result.error || 'Échec'), 'error'); + } + }); + } + + // Importer config.yaml + const btnImportConfig = document.getElementById('btn-import-config'); + if (btnImportConfig) { + btnImportConfig.addEventListener('click', async () => { + const result = await window.electronAPI.config.import(); + if (result.success) { + showNotification('Configuration importée - Redémarrez le serveur pour appliquer', 'warning'); + if (serverRunning) { + await fetchConfig(); + } + } else if (!result.cancelled) { + showNotification('Erreur import: ' + (result.error || 'Échec'), 'error'); + } + }); + } + + // Exporter les logs + const btnExportLogs = document.getElementById('btn-export-logs'); + if (btnExportLogs) { + btnExportLogs.addEventListener('click', () => { + const levelFilter = document.getElementById('log-level-filter').value; + const logs = levelFilter ? logsBuffer.filter(l => l.level === levelFilter) : logsBuffer; + + if (logs.length === 0) { + showNotification('Aucun log à exporter', 'info'); + return; + } + + const content = JSON.stringify(logs, null, 2); + const blob = new Blob([content], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = url; + a.download = `ptt-live-logs-${new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-')}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + showNotification(`${logs.length} logs exportés`, 'success'); + }); + } + + // Délégation d'événements pour modifier/supprimer un groupe + const groupsList = document.getElementById('groups-list'); + if (groupsList) { + groupsList.addEventListener('click', async (e) => { + const btn = e.target.closest('[data-action]'); + if (!btn) return; + + const action = btn.dataset.action; + const id = btn.dataset.id; + const name = btn.dataset.name; + + if (action === 'edit') { + await editGroup(id, name, parseInt(btn.dataset.bitrate)); + } else if (action === 'delete') { + await deleteGroup(id, name); + } + }); + } }); // ========== Helpers ========== +function slugify(text) { + return text.toString().normalize('NFD').replace(/[̀-ͯ]/g, '') + .toLowerCase().trim().replace(/\s+/g, '-').replace(/[^\w-]+/g, '').replace(/--+/g, '-'); +} + function formatUptime(seconds) { if (!seconds) return '--'; diff --git a/electron/ui/index.html b/electron/ui/index.html index ee85cea..2e4335d 100644 --- a/electron/ui/index.html +++ b/electron/ui/index.html @@ -123,6 +123,15 @@ +
+

💾 Sauvegarde de configuration

+
+ + +
+

L'import remplace config.yaml (backup automatique en .bak). Redémarrez le serveur pour appliquer.

+
+

🎚️ Paramètres Audio

@@ -170,6 +179,7 @@

Logs Serveur

+