feat: ajout nommage canaux physiques (Phase 2.5)

- API GET /admin/audio/channels/names
- API PUT /admin/audio/channels/names
- Interface admin : nommage 8 inputs/outputs
- Mode édition avec sauvegarde/annulation
- Stockage dans config.yaml (section audio.channelNames)
- Formulaire organisé en 2 colonnes (inputs/outputs)
This commit is contained in:
2026-05-25 09:54:43 +02:00
parent c1202a63a5
commit ccfdd54e2c
2 changed files with 177 additions and 2 deletions
+115 -2
View File
@@ -19,6 +19,10 @@ function Admin() {
const [selectedOutputDevice, setSelectedOutputDevice] = useState(null); const [selectedOutputDevice, setSelectedOutputDevice] = useState(null);
const [selectedSampleRate, setSelectedSampleRate] = useState(48000); 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 // Gestion formulaire nouveau groupe
const [showGroupForm, setShowGroupForm] = useState(false); const [showGroupForm, setShowGroupForm] = useState(false);
const [editingGroup, setEditingGroup] = useState(null); const [editingGroup, setEditingGroup] = useState(null);
@@ -85,16 +89,19 @@ function Admin() {
}; };
const loadAudioDevices = async () => { const loadAudioDevices = async () => {
const [devicesRes, currentDeviceRes] = await Promise.all([ const [devicesRes, currentDeviceRes, channelNamesRes] = await Promise.all([
fetch(`${API_URL}/admin/audio/devices`), fetch(`${API_URL}/admin/audio/devices`),
fetch(`${API_URL}/admin/audio/device`) fetch(`${API_URL}/admin/audio/device`),
fetch(`${API_URL}/admin/audio/channels/names`)
]); ]);
const devicesData = await devicesRes.json(); const devicesData = await devicesRes.json();
const currentData = await currentDeviceRes.json(); const currentData = await currentDeviceRes.json();
const channelNamesData = await channelNamesRes.json();
setAudioDevices(devicesData.devices || []); setAudioDevices(devicesData.devices || []);
setCurrentDevice(currentData.device || {}); setCurrentDevice(currentData.device || {});
setChannelNames(channelNamesData.channelNames || { inputs: {}, outputs: {} });
// Initialiser les sélections avec les valeurs actuelles // Initialiser les sélections avec les valeurs actuelles
setSelectedInputDevice(currentData.device?.inputDeviceId ?? null); setSelectedInputDevice(currentData.device?.inputDeviceId ?? null);
@@ -221,6 +228,38 @@ function Admin() {
// ========== Gestion audio devices (Phase 2.5) ========== // ========== 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 () => { const handleSaveAudioDevice = async () => {
try { try {
const res = await fetch(`${API_URL}/admin/audio/device`, { const res = await fetch(`${API_URL}/admin/audio/device`, {
@@ -529,6 +568,80 @@ function Admin() {
</button> </button>
</div> </div>
<div className="audio-section">
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 'var(--spacing-md)'}}>
<h3>Nommage des canaux physiques</h3>
{!editingChannelNames ? (
<button onClick={() => setEditingChannelNames(true)} className="btn-secondary">
Modifier les noms
</button>
) : (
<div style={{display: 'flex', gap: 'var(--spacing-sm)'}}>
<button onClick={handleSaveChannelNames} className="btn-primary">
Sauvegarder
</button>
<button onClick={() => { setEditingChannelNames(false); loadAudioDevices(); }} className="btn-secondary">
Annuler
</button>
</div>
)}
</div>
<div style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--spacing-xl)'}}>
<div>
<h4 style={{marginBottom: 'var(--spacing-md)', color: 'var(--color-text-secondary)'}}>Entrées (Inputs)</h4>
<div style={{display: 'grid', gap: 'var(--spacing-sm)'}}>
{Array.from({length: 8}, (_, i) => (
<div key={`input-${i}`} style={{display: 'grid', gridTemplateColumns: '40px 1fr', gap: 'var(--spacing-sm)', alignItems: 'center'}}>
<span style={{color: 'var(--color-text-secondary)', fontSize: '0.85rem'}}>{i}</span>
<input
type="text"
value={channelNames.inputs?.[i] || ''}
onChange={(e) => 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'
}}
/>
</div>
))}
</div>
</div>
<div>
<h4 style={{marginBottom: 'var(--spacing-md)', color: 'var(--color-text-secondary)'}}>Sorties (Outputs)</h4>
<div style={{display: 'grid', gap: 'var(--spacing-sm)'}}>
{Array.from({length: 8}, (_, i) => (
<div key={`output-${i}`} style={{display: 'grid', gridTemplateColumns: '40px 1fr', gap: 'var(--spacing-sm)', alignItems: 'center'}}>
<span style={{color: 'var(--color-text-secondary)', fontSize: '0.85rem'}}>{i}</span>
<input
type="text"
value={channelNames.outputs?.[i] || ''}
onChange={(e) => 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'
}}
/>
</div>
))}
</div>
</div>
</div>
</div>
{currentDevice && Object.keys(currentDevice).length > 0 && ( {currentDevice && Object.keys(currentDevice).length > 0 && (
<div className="current-config"> <div className="current-config">
<h3>Configuration actuelle</h3> <h3>Configuration actuelle</h3>
+62
View File
@@ -516,6 +516,68 @@ router.get('/audio/device', (req, res) => {
} }
}); });
/**
* GET /admin/audio/channels/names
* Récupère les noms personnalisés des canaux physiques
*/
router.get('/audio/channels/names', (req, res) => {
try {
const config = configManager.get();
const channelNames = config.audio?.channelNames || { inputs: {}, outputs: {} };
res.json({
channelNames
});
} catch (error) {
console.error('Erreur GET /admin/audio/channels/names:', error);
res.status(500).json({ error: 'Failed to load channel names' });
}
});
/**
* PUT /admin/audio/channels/names
* Sauvegarde les noms personnalisés des canaux physiques
* Body: { inputs: { "0": "Micro Principal", ... }, outputs: { "0": "Retour Scène", ... } }
*/
router.put('/audio/channels/names', (req, res) => {
try {
const { inputs, outputs } = req.body;
if (!inputs && !outputs) {
return res.status(400).json({
error: 'Missing required fields: inputs or outputs'
});
}
const config = configManager.get();
if (!config.audio.channelNames) {
config.audio.channelNames = { inputs: {}, outputs: {} };
}
if (inputs) {
config.audio.channelNames.inputs = inputs;
}
if (outputs) {
config.audio.channelNames.outputs = outputs;
}
configManager.save(config);
addLog('info', 'Channel names updated', { inputCount: Object.keys(inputs || {}).length, outputCount: Object.keys(outputs || {}).length });
res.json({
message: 'Channel names updated',
channelNames: config.audio.channelNames
});
} catch (error) {
console.error('Erreur PUT /admin/audio/channels/names:', error);
res.status(500).json({ error: 'Failed to update channel names' });
}
});
/** /**
* POST /admin/audio/device * POST /admin/audio/device
* Sélectionne et configure une carte son * Sélectionne et configure une carte son