From 51245db25687e82124a8450bd600936daa3bbb69 Mon Sep 17 00:00:00 2001 From: Benoit Date: Wed, 1 Jul 2026 13:36:12 +0200 Subject: [PATCH] feat: gestion groupes sans serveur via IPC + lecture YAML directe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ajout du package yaml dans electron pour parser config.yaml - Handlers IPC groups:list/create/update/delete lisent/écrivent config.yaml directement depuis le Main Process (sans serveur requis) - fetchGroups() utilise toujours IPC (plus d'appel REST pour la lecture) - editGroup/deleteGroup/addGroup : REST API si serveur actif, IPC sinon - loadViewData : onglet Groupes chargé même si serveur arrêté - Note affichée quand le serveur est arrêté : modifications appliquées au prochain démarrage --- electron/main.js | 70 ++++++++++++++++++++++++++++++++++ electron/package.json | 3 +- electron/preload.js | 8 ++++ electron/ui/app.js | 88 +++++++++++++++++++++++++++++-------------- 4 files changed, 139 insertions(+), 30 deletions(-) diff --git a/electron/main.js b/electron/main.js index edb5131..7aadda2 100644 --- a/electron/main.js +++ b/electron/main.js @@ -10,8 +10,24 @@ const { spawn } = require('child_process'); const http = require('http'); const https = require('https'); const QRCode = require('qrcode'); +const yaml = require('yaml'); const setupHelper = require('./setup-helper'); +const CONFIG_PATH = path.join(__dirname, '..', 'server', 'config', 'config.yaml'); + +function readConfig() { + return yaml.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); +} + +function writeConfig(config) { + fs.writeFileSync(CONFIG_PATH, yaml.stringify(config), 'utf8'); +} + +function slugify(text) { + return text.toString().normalize('NFD').replace(/[̀-ͯ]/g, '') + .toLowerCase().trim().replace(/\s+/g, '-').replace(/[^\w-]+/g, '').replace(/--+/g, '-'); +} + // État de l'application let mainWindow = null; let tray = null; @@ -366,6 +382,60 @@ app.whenReady().then(async () => { return setupHelper.getNetworkIP(); }); + // ========== Groupes (lecture/écriture YAML directe, sans serveur) ========== + + ipcMain.handle('groups:list', () => { + try { + const config = readConfig(); + return { groups: config.groups || [] }; + } catch (error) { + return { groups: [], error: error.message }; + } + }); + + ipcMain.handle('groups:create', (event, { name, audioBitrate }) => { + try { + const config = readConfig(); + const id = slugify(name); + if ((config.groups || []).find(g => slugify(g.name) === id)) { + return { success: false, error: `Un groupe "${name}" existe déjà` }; + } + const group = { name, ...(audioBitrate ? { audioBitrate } : {}) }; + config.groups = [...(config.groups || []), group]; + writeConfig(config); + return { success: true, group }; + } catch (error) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('groups:update', (event, { id, name, audioBitrate }) => { + try { + const config = readConfig(); + const idx = (config.groups || []).findIndex(g => slugify(g.name) === id); + if (idx === -1) return { success: false, error: `Groupe ${id} introuvable` }; + if (name !== undefined) config.groups[idx].name = name; + if (audioBitrate !== undefined) config.groups[idx].audioBitrate = audioBitrate; + writeConfig(config); + return { success: true, group: config.groups[idx] }; + } catch (error) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('groups:delete', (event, { id }) => { + try { + const config = readConfig(); + const idx = (config.groups || []).findIndex(g => slugify(g.name) === id); + if (idx === -1) return { success: false, error: `Groupe ${id} introuvable` }; + config.groups.splice(idx, 1); + writeConfig(config); + return { success: true }; + } catch (error) { + return { success: false, error: error.message }; + } + }); + ipcMain.handle('config:export', async () => { const configPath = path.join(__dirname, '..', 'server', 'config', 'config.yaml'); diff --git a/electron/package.json b/electron/package.json index bb84e4f..f990cb9 100644 --- a/electron/package.json +++ b/electron/package.json @@ -55,6 +55,7 @@ }, "dependencies": { "electron-store": "^8.1.0", - "qrcode": "^1.5.4" + "qrcode": "^1.5.4", + "yaml": "^2.9.0" } } diff --git a/electron/preload.js b/electron/preload.js index 2dd75d4..a6d2f2f 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -43,6 +43,14 @@ contextBridge.exposeInMainWorld('electronAPI', { import: () => ipcRenderer.invoke('config:import') }, + // Groupes : lecture/écriture YAML directe (fonctionne sans serveur) + groups: { + list: () => ipcRenderer.invoke('groups:list'), + create: (data) => ipcRenderer.invoke('groups:create', data), + update: (data) => ipcRenderer.invoke('groups:update', data), + delete: (data) => ipcRenderer.invoke('groups:delete', data) + }, + // 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 2d24cc4..009fc3f 100644 --- a/electron/ui/app.js +++ b/electron/ui/app.js @@ -302,17 +302,19 @@ async function fetchDevices() { } async function fetchGroups() { - const data = await apiCall('/admin/groups'); - if (!data) return; - const container = document.getElementById('groups-list'); + // Lecture directe depuis config.yaml via IPC (fonctionne sans serveur) + const data = await window.electronAPI.groups.list(); + if (!data.groups || data.groups.length === 0) { container.innerHTML = '

Aucun groupe configuré

'; return; } - container.innerHTML = data.groups.map(group => { + const serverNote = serverRunning ? '' : '

Serveur arrêté — les modifications seront appliquées au prochain démarrage.

'; + + container.innerHTML = serverNote + data.groups.map(group => { const id = slugify(group.name); return `
@@ -358,18 +360,27 @@ async function editGroup(id, currentName, currentBitrate) { } try { - const response = await fetch(`${API_BASE}/admin/groups/${id}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name: newName, audioBitrate: newBitrate }) - }); + let ok, errorMsg; - if (response.ok) { + if (serverRunning) { + const response = await fetch(`${API_BASE}/admin/groups/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: newName, audioBitrate: newBitrate }) + }); + ok = response.ok; + if (!ok) errorMsg = (await response.json().catch(() => ({}))).error; + } else { + const res = await window.electronAPI.groups.update({ id, name: newName, audioBitrate: newBitrate }); + ok = res.success; + errorMsg = res.error; + } + + if (ok) { showNotification('Groupe modifié', 'success'); await fetchGroups(); } else { - const err = await response.json().catch(() => ({})); - showNotification('Erreur: ' + (err.error || 'Modification échouée'), 'error'); + showNotification('Erreur: ' + (errorMsg || 'Modification échouée'), 'error'); } } catch (error) { console.error('Erreur edit group:', error); @@ -388,16 +399,23 @@ async function deleteGroup(id, name) { if (!confirmed) return; try { - const response = await fetch(`${API_BASE}/admin/groups/${id}`, { - method: 'DELETE' - }); + let ok, errorMsg; - if (response.ok) { + if (serverRunning) { + const response = await fetch(`${API_BASE}/admin/groups/${id}`, { method: 'DELETE' }); + ok = response.ok; + if (!ok) errorMsg = (await response.json().catch(() => ({}))).error; + } else { + const res = await window.electronAPI.groups.delete({ id }); + ok = res.success; + errorMsg = res.error; + } + + if (ok) { showNotification('Groupe supprimé', 'success'); await fetchGroups(); } else { - const err = await response.json().catch(() => ({})); - showNotification('Erreur: ' + (err.error || 'Suppression échouée'), 'error'); + showNotification('Erreur: ' + (errorMsg || 'Suppression échouée'), 'error'); } } catch (error) { console.error('Erreur delete group:', error); @@ -554,6 +572,12 @@ async function loadInitialData() { } async function loadViewData(view) { + // Les groupes sont lisibles même sans serveur (config.yaml direct) + if (view === 'groups') { + await fetchGroups(); + return; + } + if (!serverRunning) return; switch (view) { @@ -566,9 +590,6 @@ async function loadViewData(view) { await fetchDevices(); await fetchConfig(); break; - case 'groups': - await fetchGroups(); - break; case 'monitoring': renderVUMeters(); break; @@ -653,18 +674,27 @@ document.addEventListener('DOMContentLoaded', () => { const audioBitrate = parseInt(result.bitrate) || 96; try { - const response = await fetch(`${API_BASE}/admin/groups`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name, audioBitrate }) - }); + let ok, errorMsg; - if (response.ok) { + if (serverRunning) { + const response = await fetch(`${API_BASE}/admin/groups`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name, audioBitrate }) + }); + ok = response.ok; + if (!ok) errorMsg = (await response.json().catch(() => ({}))).error; + } else { + const res = await window.electronAPI.groups.create({ name, audioBitrate }); + ok = res.success; + errorMsg = res.error; + } + + if (ok) { showNotification('Groupe créé', 'success'); await fetchGroups(); } else { - const err = await response.json().catch(() => ({})); - showNotification('Erreur: ' + (err.error || 'Création échouée'), 'error'); + showNotification('Erreur: ' + (errorMsg || 'Création échouée'), 'error'); } } catch (error) { console.error('Erreur add group:', error);