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 (
{error && (
{error}
)}
{/* TAB: Groupes */}
{activeTab === 'groups' && (
Gestion des groupes
{!showGroupForm && (
)}
{showGroupForm && (
)}
{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
| ID |
Nom |
Entrées |
Sorties |
Sample Rate |
API |
{audioDevices.map((device, index) => (
| {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é
) : (
| Utilisateur |
Groupe |
Connecté depuis |
Dernière activité |
Actions |
{users.map(user => (
| {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
| Timestamp |
Type |
Données |
{stats.audioStats.map((stat, index) => (
| {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;