feat: activer boutons Groupes et ajouter export logs/config desktop
- 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)
This commit is contained in:
+151
-6
@@ -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 `
|
||||
<div class="group-item">
|
||||
<div class="group-info">
|
||||
<h4>${group.name}</h4>
|
||||
<p>Bitrate: ${group.audioBitrate || 96} kbps • ID: ${group.id}</p>
|
||||
<h4>${escapeHtml(group.name)}</h4>
|
||||
<p>Bitrate: ${group.audioBitrate || 96} kbps • ID: ${escapeHtml(id)}</p>
|
||||
</div>
|
||||
<div class="group-actions">
|
||||
<button class="btn btn-small btn-secondary">Modifier</button>
|
||||
<button class="btn btn-small btn-secondary">Supprimer</button>
|
||||
<button class="btn btn-small btn-secondary"
|
||||
data-action="edit"
|
||||
data-id="${escapeHtml(id)}"
|
||||
data-name="${escapeHtml(group.name)}"
|
||||
data-bitrate="${group.audioBitrate || 96}">Modifier</button>
|
||||
<button class="btn btn-small btn-danger"
|
||||
data-action="delete"
|
||||
data-id="${escapeHtml(id)}"
|
||||
data-name="${escapeHtml(group.name)}">Supprimer</button>
|
||||
</div>
|
||||
</div>
|
||||
`).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 '--';
|
||||
|
||||
|
||||
@@ -123,6 +123,15 @@
|
||||
<button class="btn btn-primary" id="btn-save-device">Appliquer</button>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>💾 Sauvegarde de configuration</h3>
|
||||
<div class="config-actions">
|
||||
<button class="btn btn-secondary" id="btn-export-config">Exporter config.yaml</button>
|
||||
<button class="btn btn-secondary" id="btn-import-config">Importer config.yaml</button>
|
||||
</div>
|
||||
<p class="config-note">L'import remplace config.yaml (backup automatique en .bak). Redémarrez le serveur pour appliquer.</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>🎚️ Paramètres Audio</h3>
|
||||
<div class="form-group">
|
||||
@@ -170,6 +179,7 @@
|
||||
<h2>Logs Serveur</h2>
|
||||
<div class="logs-controls">
|
||||
<button class="btn btn-small" id="btn-clear-logs">Effacer</button>
|
||||
<button class="btn btn-small btn-secondary" id="btn-export-logs">Exporter JSON</button>
|
||||
<select id="log-level-filter" class="form-control form-control-small">
|
||||
<option value="">Tous les niveaux</option>
|
||||
<option value="error">Erreurs</option>
|
||||
|
||||
@@ -121,6 +121,15 @@ body {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--accent-error);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover:not(:disabled) {
|
||||
background: #d32f2f;
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
.main-content {
|
||||
display: flex;
|
||||
@@ -370,6 +379,18 @@ body {
|
||||
border-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
/* Config actions */
|
||||
.config-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.config-note {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Groups List */
|
||||
.groups-list {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user