feat: gestion groupes sans serveur via IPC + lecture YAML directe
- 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
This commit is contained in:
@@ -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');
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"electron-store": "^8.1.0",
|
||||
"qrcode": "^1.5.4"
|
||||
"qrcode": "^1.5.4",
|
||||
"yaml": "^2.9.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
+59
-29
@@ -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 = '<p class="empty-state">Aucun groupe configuré</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = data.groups.map(group => {
|
||||
const serverNote = serverRunning ? '' : '<p class="config-note" style="margin-bottom:1rem">Serveur arrêté — les modifications seront appliquées au prochain démarrage.</p>';
|
||||
|
||||
container.innerHTML = serverNote + data.groups.map(group => {
|
||||
const id = slugify(group.name);
|
||||
return `
|
||||
<div class="group-item">
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user