import { useState, useEffect, useRef } from 'react'; import './Admin.css'; import AudioRoutingMatrix from './components/AudioRoutingMatrix'; const API_URL = import.meta.env.VITE_API_URL || '/api'; function Admin() { // Lire l'onglet depuis l'URL hash (ex: #audio) ou utiliser 'groups' par défaut const getInitialTab = () => { const hash = window.location.hash.slice(1); // Enlever le # return ['groups', 'audio', 'users', 'stats', 'logs'].includes(hash) ? hash : 'groups'; }; const [activeTab, setActiveTab] = useState(getInitialTab()); 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({ inputChannels: 8, outputChannels: 8 }); const [selectedInputDevice, setSelectedInputDevice] = useState(null); const [selectedOutputDevice, setSelectedOutputDevice] = useState(null); const [selectedSampleRate, setSelectedSampleRate] = useState(48000); const isEditingAudioRef = useRef(false); // Channel names (Phase 2.5) const [channelNames, setChannelNames] = useState({ inputs: {}, outputs: {} }); // Gestion formulaire nouveau groupe const [showGroupForm, setShowGroupForm] = useState(false); const [editingGroup, setEditingGroup] = useState(null); const [groupForm, setGroupForm] = useState({ name: '', audioBitrate: 96 }); // Synchroniser l'onglet avec l'URL hash useEffect(() => { const handleHashChange = () => { const hash = window.location.hash.slice(1); if (['groups', 'audio', 'users', 'stats', 'logs'].includes(hash)) { setActiveTab(hash); } }; window.addEventListener('hashchange', handleHashChange); return () => window.removeEventListener('hashchange', handleHashChange); }, []); // 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, groupsRes] = await Promise.all([ fetch(`${API_URL}/admin/audio/devices`), fetch(`${API_URL}/admin/audio/device`), fetch(`${API_URL}/admin/audio/channels/names`), fetch(`${API_URL}/admin/groups`) ]); const devicesData = await devicesRes.json(); const currentData = await currentDeviceRes.json(); const channelNamesData = await channelNamesRes.json(); const groupsData = await groupsRes.json(); setAudioDevices(devicesData.devices || []); setGroups(groupsData.groups || []); const device = currentData.device || { inputChannels: 8, outputChannels: 8 }; setCurrentDevice(device); setChannelNames(channelNamesData.channelNames || { inputs: {}, outputs: {} }); // Ne réinitialiser les sélections que lors du chargement initial (pas en train d'éditer) if (!isEditingAudioRef.current) { setSelectedInputDevice(device.inputDeviceId ?? null); setSelectedOutputDevice(device.outputDeviceId ?? null); setSelectedSampleRate(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 }); setShowGroupForm(true); }; const resetGroupForm = () => { setGroupForm({ name: '', audioBitrate: 96 }); setShowGroupForm(false); setEditingGroup(null); }; // ========== 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!'); 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 || undefined, outputDeviceId: selectedOutputDevice || undefined, sampleRate: parseInt(selectedSampleRate) }) }); if (res.ok) { isEditingAudioRef.current = false; // Désactiver le mode édition 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

Le routing audio se configure dans l'onglet "Audio" via la matrice de routing.

)}
{groups.map(group => (

{group.name}

Bitrate: {group.audioBitrate || 96} kbps
))}
)} {/* TAB: Audio (Phase 2.5) */} {activeTab === 'audio' && (

Configuration audio

Configuration des cartes son

{selectedInputDevice !== null && selectedInputDevice !== '' && (

Device ID: {selectedInputDevice}

)}
{selectedOutputDevice !== null && selectedOutputDevice !== '' && (

Device ID: {selectedOutputDevice}

)}

Nommage des canaux physiques

Entrées (Inputs) - {currentDevice.inputChannels || 0} canaux disponibles

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

Sorties (Outputs) - {currentDevice.outputChannels || 0} canaux disponibles

{Array.from({length: currentDevice.outputChannels || 8}, (_, i) => (
{i} updateChannelName('outputs', i, e.target.value)} placeholder={`Output ${i}`} style={{ padding: 'var(--spacing-sm)', background: 'var(--color-bg)', border: '1px solid var(--color-border)', borderRadius: '6px', color: 'var(--color-text)', fontSize: '0.9rem' }} />
))}
{currentDevice && currentDevice.inputDeviceId && (

Configuration actuelle

Input Device: {currentDevice.inputDeviceName || currentDevice.inputDeviceId}

Output Device: {currentDevice.outputDeviceName || currentDevice.outputDeviceId}

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

Canaux: {currentDevice.inputChannels} entrées / {currentDevice.outputChannels} sorties

)}

Toutes les cartes son disponibles

{audioDevices.map((device, index) => ( ))}
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;