import { useState, useEffect } from 'react'; import './Admin.css'; const API_URL = import.meta.env.VITE_API_URL || '/api'; function Admin() { const [activeTab, setActiveTab] = useState('groups'); const [groups, setGroups] = useState([]); const [users, setUsers] = useState([]); const [stats, setStats] = useState(null); const [logs, setLogs] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // Audio devices (Phase 2.5) const [audioDevices, setAudioDevices] = useState([]); const [currentDevice, setCurrentDevice] = useState(null); const [selectedInputDevice, setSelectedInputDevice] = useState(null); const [selectedOutputDevice, setSelectedOutputDevice] = useState(null); const [selectedSampleRate, setSelectedSampleRate] = useState(48000); // Channel names (Phase 2.5) const [channelNames, setChannelNames] = useState({ inputs: {}, outputs: {} }); const [editingChannelNames, setEditingChannelNames] = useState(false); // Gestion formulaire nouveau groupe const [showGroupForm, setShowGroupForm] = useState(false); const [editingGroup, setEditingGroup] = useState(null); const [groupForm, setGroupForm] = useState({ name: '', audioBitrate: 96, channels: [] }); // Rafraîchissement automatique useEffect(() => { loadData(); const interval = setInterval(loadData, 3000); // Refresh toutes les 3s return () => clearInterval(interval); }, [activeTab]); const loadData = async () => { try { setLoading(true); if (activeTab === 'groups') { await loadGroups(); } else if (activeTab === 'users') { await loadUsers(); } else if (activeTab === 'stats') { await loadStats(); } else if (activeTab === 'logs') { await loadLogs(); } else if (activeTab === 'audio') { await loadAudioDevices(); } setError(null); } catch (err) { console.error('Erreur chargement données:', err); setError('Erreur de connexion au serveur'); } finally { setLoading(false); } }; const loadGroups = async () => { const res = await fetch(`${API_URL}/admin/groups`); const data = await res.json(); setGroups(data.groups || []); }; const loadUsers = async () => { const res = await fetch(`${API_URL}/admin/users`); const data = await res.json(); setUsers(data.users || []); }; const loadStats = async () => { const res = await fetch(`${API_URL}/admin/stats`); const data = await res.json(); setStats(data); }; const loadLogs = async () => { const res = await fetch(`${API_URL}/admin/logs?limit=50`); const data = await res.json(); setLogs(data.logs || []); }; const loadAudioDevices = async () => { const [devicesRes, currentDeviceRes, channelNamesRes] = await Promise.all([ fetch(`${API_URL}/admin/audio/devices`), fetch(`${API_URL}/admin/audio/device`), fetch(`${API_URL}/admin/audio/channels/names`) ]); const devicesData = await devicesRes.json(); const currentData = await currentDeviceRes.json(); const channelNamesData = await channelNamesRes.json(); setAudioDevices(devicesData.devices || []); setCurrentDevice(currentData.device || {}); setChannelNames(channelNamesData.channelNames || { inputs: {}, outputs: {} }); // Initialiser les sélections avec les valeurs actuelles setSelectedInputDevice(currentData.device?.inputDeviceId ?? null); setSelectedOutputDevice(currentData.device?.outputDeviceId ?? null); setSelectedSampleRate(currentData.device?.sampleRate || 48000); }; // ========== Gestion groupes ========== const handleCreateGroup = async (e) => { e.preventDefault(); try { const res = await fetch(`${API_URL}/admin/groups`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(groupForm) }); if (res.ok) { setShowGroupForm(false); resetGroupForm(); await loadGroups(); } else { const error = await res.json(); alert(`Erreur: ${error.error}`); } } catch (err) { console.error('Erreur création groupe:', err); alert('Erreur lors de la création du groupe'); } }; const handleUpdateGroup = async (e) => { e.preventDefault(); try { const res = await fetch(`${API_URL}/admin/groups/${editingGroup}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(groupForm) }); if (res.ok) { setEditingGroup(null); resetGroupForm(); await loadGroups(); } else { const error = await res.json(); alert(`Erreur: ${error.error}`); } } catch (err) { console.error('Erreur modification groupe:', err); alert('Erreur lors de la modification du groupe'); } }; const handleDeleteGroup = async (groupId) => { if (!confirm('Êtes-vous sûr de vouloir supprimer ce groupe ?')) { return; } try { const res = await fetch(`${API_URL}/admin/groups/${groupId}`, { method: 'DELETE' }); if (res.ok) { await loadGroups(); } else { const error = await res.json(); alert(`Erreur: ${error.error}`); } } catch (err) { console.error('Erreur suppression groupe:', err); alert('Erreur lors de la suppression du groupe'); } }; const startEditGroup = (group) => { setEditingGroup(group.id); setGroupForm({ name: group.name, audioBitrate: group.audioBitrate || 96, channels: group.channels || [] }); setShowGroupForm(true); }; const resetGroupForm = () => { setGroupForm({ name: '', audioBitrate: 96, channels: [] }); setShowGroupForm(false); setEditingGroup(null); }; const addChannel = () => { setGroupForm({ ...groupForm, channels: [ ...groupForm.channels, { name: '', audioInput: 0, audioOutput: 0 } ] }); }; const updateChannel = (index, field, value) => { const newChannels = [...groupForm.channels]; newChannels[index] = { ...newChannels[index], [field]: field === 'audioInput' || field === 'audioOutput' ? parseInt(value) : value }; setGroupForm({ ...groupForm, channels: newChannels }); }; const removeChannel = (index) => { const newChannels = [...groupForm.channels]; newChannels.splice(index, 1); setGroupForm({ ...groupForm, channels: newChannels }); }; // ========== Gestion audio devices (Phase 2.5) ========== const handleSaveChannelNames = async () => { try { const res = await fetch(`${API_URL}/admin/audio/channels/names`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(channelNames) }); if (res.ok) { alert('Noms de canaux sauvegardés avec succès!'); setEditingChannelNames(false); await loadAudioDevices(); } else { const error = await res.json(); alert(`Erreur: ${error.error}`); } } catch (err) { console.error('Erreur sauvegarde noms canaux:', err); alert('Erreur lors de la sauvegarde'); } }; const updateChannelName = (type, channelId, name) => { setChannelNames(prev => ({ ...prev, [type]: { ...prev[type], [channelId]: name } })); }; const handleSaveAudioDevice = async () => { try { const res = await fetch(`${API_URL}/admin/audio/device`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ inputDeviceId: selectedInputDevice !== null ? parseInt(selectedInputDevice) : undefined, outputDeviceId: selectedOutputDevice !== null ? parseInt(selectedOutputDevice) : undefined, sampleRate: parseInt(selectedSampleRate) }) }); if (res.ok) { alert('Configuration audio sauvegardée avec succès!'); await loadAudioDevices(); } else { const error = await res.json(); alert(`Erreur: ${error.error}`); } } catch (err) { console.error('Erreur sauvegarde configuration audio:', err); alert('Erreur lors de la sauvegarde'); } }; // ========== Gestion utilisateurs ========== const handleDisconnectUser = async (identity) => { if (!confirm('Déconnecter cet utilisateur ?')) { return; } try { const res = await fetch(`${API_URL}/admin/users/${identity}`, { method: 'DELETE' }); if (res.ok) { await loadUsers(); } else { const error = await res.json(); alert(`Erreur: ${error.error}`); } } catch (err) { console.error('Erreur déconnexion utilisateur:', err); alert('Erreur lors de la déconnexion'); } }; // ========== Render ========== const formatDate = (dateString) => { const date = new Date(dateString); return date.toLocaleString('fr-FR'); }; const formatUptime = (seconds) => { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); return `${hours}h ${minutes}m`; }; return (

PTT Live - Administration

← Retour
{error && (
{error}
)} {/* TAB: Groupes */} {activeTab === 'groups' && (

Gestion des groupes

{!showGroupForm && ( )}
{showGroupForm && (

{editingGroup ? 'Modifier' : 'Nouveau'} groupe

Canaux audio

{groupForm.channels.map((channel, index) => (
updateChannel(index, 'name', e.target.value)} required /> updateChannel(index, 'audioInput', e.target.value)} min="0" /> updateChannel(index, 'audioOutput', e.target.value)} min="0" />
))}
)}
{groups.map(group => (

{group.name}

Bitrate: {group.audioBitrate || 96} kbps Canaux: {group.channels?.length || 0}
{group.channels && group.channels.length > 0 && (
{group.channels.map(channel => (
{channel.name} (I/O: {channel.audioInput}/{channel.audioOutput})
))}
)}
))}
)} {/* TAB: Audio (Phase 2.5) */} {activeTab === 'audio' && (

Configuration audio

Carte son d'entrée (Input)

{selectedInputDevice !== null && (

Device ID {selectedInputDevice} sélectionné

)}

Carte son de sortie (Output)

{selectedOutputDevice !== null && (

Device ID {selectedOutputDevice} sélectionné

)}

Sample Rate

Nommage des canaux physiques

{!editingChannelNames ? ( ) : (
)}

Entrées (Inputs)

{Array.from({length: 8}, (_, i) => (
{i} updateChannelName('inputs', i, e.target.value)} placeholder={`Input ${i}`} disabled={!editingChannelNames} style={{ padding: 'var(--spacing-sm)', background: editingChannelNames ? 'var(--color-bg)' : 'var(--color-surface-hover)', border: '1px solid var(--color-border)', borderRadius: '6px', color: 'var(--color-text)', fontSize: '0.9rem' }} />
))}

Sorties (Outputs)

{Array.from({length: 8}, (_, i) => (
{i} updateChannelName('outputs', i, e.target.value)} placeholder={`Output ${i}`} disabled={!editingChannelNames} style={{ padding: 'var(--spacing-sm)', background: editingChannelNames ? 'var(--color-bg)' : 'var(--color-surface-hover)', border: '1px solid var(--color-border)', borderRadius: '6px', color: 'var(--color-text)', fontSize: '0.9rem' }} />
))}
{currentDevice && Object.keys(currentDevice).length > 0 && (

Configuration actuelle

Input Device ID: {currentDevice.inputDeviceId ?? 'Non configuré'}

Output Device ID: {currentDevice.outputDeviceId ?? 'Non configuré'}

Sample Rate: {currentDevice.sampleRate ?? 48000} Hz

)}

Toutes les cartes son disponibles

{audioDevices.map(device => ( ))}
ID Nom Entrées Sorties Sample Rate API
{device.id} {device.name} {device.maxInputChannels} {device.maxOutputChannels} {device.defaultSampleRate} Hz {device.hostAPIName}
)} {/* TAB: Utilisateurs */} {activeTab === 'users' && (

Utilisateurs connectés ({users.length})

{users.length === 0 ? (

Aucun utilisateur connecté

) : ( {users.map(user => ( ))}
Utilisateur Groupe Connecté depuis Dernière activité Actions
{user.username} {user.groupId} {formatDate(user.connectedAt)} {formatDate(user.lastActivity)}
)}
)} {/* TAB: Statistiques */} {activeTab === 'stats' && stats && (

Statistiques système

Connexions totales

{stats.totalConnections}

Connexions actives

{stats.activeConnections}

Uptime

{formatUptime(stats.uptime)}

Mémoire

{Math.round(stats.memory.heapUsed / 1024 / 1024)} MB
{stats.audioStats && stats.audioStats.length > 0 && (

Dernières stats audio

{stats.audioStats.map((stat, index) => ( ))}
Timestamp Type Données
{formatDate(stat.timestamp)} {stat.type || 'N/A'} {JSON.stringify(stat, null, 2)}
)}
)} {/* TAB: Logs */} {activeTab === 'logs' && (

Logs serveur ({logs.length})

{logs.length === 0 ? (

Aucun log disponible

) : (
{logs.map((log, index) => (
{formatDate(log.timestamp)} {log.level.toUpperCase()} {log.message} {log.meta && Object.keys(log.meta).length > 0 && ( {JSON.stringify(log.meta)} )}
))}
)}
)}
); } export default Admin;