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 (
{error && (
{error}
)}
{/* TAB: Groupes */}
{activeTab === 'groups' && (
Gestion des groupes
{!showGroupForm && (
)}
{showGroupForm && (
)}
{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
| ID |
Nom |
Entrées |
Sorties |
Sample Rate |
API |
{audioDevices.map(device => (
| {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;