From e053924b63cd647bf346cde06dbfe8e6658588d0 Mon Sep 17 00:00:00 2001 From: Benoit Date: Mon, 25 May 2026 09:56:31 +0200 Subject: [PATCH] feat: matrice de routing audio style Dante Controller (Phase 2.5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - API GET/POST /admin/audio/routing - Composant AudioRoutingMatrix avec 2 matrices : * Inputs vers Groupes (8 inputs x N groupes) * Groupes vers Outputs (N groupes x 8 outputs) - Interface visuelle type grille cliquable - Intégration noms de canaux personnalisés - Stockage routing dans config.yaml - Responsive design avec CSS Grid - Style cohérent avec interface admin --- client/src/Admin.jsx | 3 + client/src/components/AudioRoutingMatrix.css | 163 ++++++++++++++++ client/src/components/AudioRoutingMatrix.jsx | 193 +++++++++++++++++++ server/api/admin.js | 61 ++++++ 4 files changed, 420 insertions(+) create mode 100644 client/src/components/AudioRoutingMatrix.css create mode 100644 client/src/components/AudioRoutingMatrix.jsx diff --git a/client/src/Admin.jsx b/client/src/Admin.jsx index 96e7c86..6f83640 100644 --- a/client/src/Admin.jsx +++ b/client/src/Admin.jsx @@ -1,5 +1,6 @@ import { useState, useEffect } from 'react'; import './Admin.css'; +import AudioRoutingMatrix from './components/AudioRoutingMatrix'; const API_URL = import.meta.env.VITE_API_URL || '/api'; @@ -642,6 +643,8 @@ function Admin() { + + {currentDevice && Object.keys(currentDevice).length > 0 && (

Configuration actuelle

diff --git a/client/src/components/AudioRoutingMatrix.css b/client/src/components/AudioRoutingMatrix.css new file mode 100644 index 0000000..b862d15 --- /dev/null +++ b/client/src/components/AudioRoutingMatrix.css @@ -0,0 +1,163 @@ +.routing-matrix-container { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 12px; + padding: var(--spacing-xl); + margin-top: var(--spacing-lg); +} + +.routing-matrix-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--spacing-xl); +} + +.routing-matrix-header h3 { + margin: 0; + font-size: 1.25rem; + font-weight: 600; +} + +.routing-section { + margin-bottom: var(--spacing-xl); +} + +.routing-section:last-child { + margin-bottom: 0; +} + +.routing-section h4 { + margin: 0 0 var(--spacing-sm) 0; + font-size: 1rem; + font-weight: 600; + color: var(--color-text); +} + +.routing-description { + margin: 0 0 var(--spacing-lg) 0; + color: var(--color-text-secondary); + font-size: 0.9rem; +} + +.routing-matrix { + display: grid; + gap: 2px; + background: var(--color-border); + border: 1px solid var(--color-border); + border-radius: 8px; + overflow: hidden; +} + +.matrix-corner { + background: var(--color-surface-hover); +} + +.matrix-header-row { + display: contents; +} + +.matrix-header-cell { + background: var(--color-surface-hover); + padding: var(--spacing-sm); + font-size: 0.85rem; + font-weight: 600; + color: var(--color-text-secondary); + text-align: center; + display: flex; + align-items: center; + justify-content: center; + min-height: 50px; + word-break: break-word; + hyphens: auto; +} + +.matrix-row { + display: contents; +} + +.matrix-label-cell { + background: var(--color-surface-hover); + padding: var(--spacing-sm); + font-size: 0.85rem; + font-weight: 600; + color: var(--color-text-secondary); + display: flex; + align-items: center; + min-width: 120px; + word-break: break-word; +} + +.matrix-cell { + background: var(--color-bg); + padding: var(--spacing-md); + min-height: 40px; + min-width: 60px; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; + position: relative; +} + +.matrix-cell:hover { + background: var(--color-surface-hover); + border: 1px solid var(--color-primary); +} + +.matrix-cell.active { + background: var(--color-primary); + color: white; +} + +.matrix-cell.active:hover { + background: var(--color-primary-hover); +} + +.checkmark { + font-size: 1.2rem; + font-weight: bold; +} + +@media (max-width: 1024px) { + .matrix-header-cell, + .matrix-label-cell { + font-size: 0.75rem; + padding: var(--spacing-xs); + min-width: 80px; + } + + .matrix-cell { + min-width: 50px; + min-height: 35px; + padding: var(--spacing-sm); + } +} + +@media (max-width: 768px) { + .routing-matrix-container { + padding: var(--spacing-md); + } + + .routing-matrix-header { + flex-direction: column; + gap: var(--spacing-md); + align-items: stretch; + } + + .matrix-header-cell, + .matrix-label-cell { + font-size: 0.7rem; + min-width: 60px; + } + + .matrix-cell { + min-width: 40px; + min-height: 30px; + } + + .checkmark { + font-size: 1rem; + } +} diff --git a/client/src/components/AudioRoutingMatrix.jsx b/client/src/components/AudioRoutingMatrix.jsx new file mode 100644 index 0000000..f88ab2f --- /dev/null +++ b/client/src/components/AudioRoutingMatrix.jsx @@ -0,0 +1,193 @@ +import { useState, useEffect } from 'react'; +import './AudioRoutingMatrix.css'; + +const API_URL = import.meta.env.VITE_API_URL || '/api'; + +function AudioRoutingMatrix({ groups, channelNames }) { + const [routing, setRouting] = useState({ inputToGroup: {}, groupToOutput: {}, gains: {} }); + const [loading, setLoading] = useState(true); + + useEffect(() => { + loadRouting(); + }, []); + + const loadRouting = async () => { + try { + const res = await fetch(`${API_URL}/admin/audio/routing`); + const data = await res.json(); + setRouting(data.routing || { inputToGroup: {}, groupToOutput: {}, gains: {} }); + } catch (error) { + console.error('Erreur chargement routing:', error); + } finally { + setLoading(false); + } + }; + + const saveRouting = async () => { + try { + const res = await fetch(`${API_URL}/admin/audio/routing`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(routing) + }); + + if (res.ok) { + alert('Configuration de routing sauvegardée!'); + } else { + const error = await res.json(); + alert(`Erreur: ${error.error}`); + } + } catch (error) { + console.error('Erreur sauvegarde routing:', error); + alert('Erreur lors de la sauvegarde'); + } + }; + + const toggleInputToGroup = (inputId, groupId) => { + setRouting(prev => { + const inputToGroup = { ...prev.inputToGroup }; + if (!inputToGroup[inputId]) { + inputToGroup[inputId] = []; + } + + const groupArray = [...inputToGroup[inputId]]; + const index = groupArray.indexOf(groupId); + + if (index > -1) { + groupArray.splice(index, 1); + } else { + groupArray.push(groupId); + } + + inputToGroup[inputId] = groupArray; + + return { ...prev, inputToGroup }; + }); + }; + + const toggleGroupToOutput = (groupId, outputId) => { + setRouting(prev => { + const groupToOutput = { ...prev.groupToOutput }; + if (!groupToOutput[groupId]) { + groupToOutput[groupId] = []; + } + + const outputArray = [...groupToOutput[groupId]]; + const index = outputArray.indexOf(outputId); + + if (index > -1) { + outputArray.splice(index, 1); + } else { + outputArray.push(outputId); + } + + groupToOutput[groupId] = outputArray; + + return { ...prev, groupToOutput }; + }); + }; + + const isInputRoutedToGroup = (inputId, groupId) => { + return routing.inputToGroup[inputId]?.includes(groupId) || false; + }; + + const isGroupRoutedToOutput = (groupId, outputId) => { + return routing.groupToOutput[groupId]?.includes(outputId) || false; + }; + + const getChannelName = (type, id) => { + const name = channelNames?.[type]?.[id]; + return name || `${type === 'inputs' ? 'Input' : 'Output'} ${id}`; + }; + + if (loading) { + return
Chargement...
; + } + + return ( +
+
+

Matrice de routing audio

+ +
+ +
+

Inputs vers Groupes

+

+ Sélectionnez quels inputs audio alimentent chaque groupe +

+ +
+
+ +
+ {groups.map(group => ( +
+ {group.name} +
+ ))} +
+ + {Array.from({length: 8}, (_, i) => ( +
+
+ {getChannelName('inputs', i)} +
+ + {groups.map(group => ( +
toggleInputToGroup(String(i), group.id)} + > + {isInputRoutedToGroup(String(i), group.id) && } +
+ ))} +
+ ))} +
+
+ +
+

Groupes vers Outputs

+

+ Sélectionnez vers quels outputs chaque groupe envoie son audio +

+ +
+
+ +
+ {Array.from({length: 8}, (_, i) => ( +
+ {getChannelName('outputs', i)} +
+ ))} +
+ + {groups.map(group => ( +
+
+ {group.name} +
+ + {Array.from({length: 8}, (_, i) => ( +
toggleGroupToOutput(group.id, String(i))} + > + {isGroupRoutedToOutput(group.id, String(i)) && } +
+ ))} +
+ ))} +
+
+
+ ); +} + +export default AudioRoutingMatrix; diff --git a/server/api/admin.js b/server/api/admin.js index 64c559e..0df127e 100644 --- a/server/api/admin.js +++ b/server/api/admin.js @@ -578,6 +578,67 @@ router.put('/audio/channels/names', (req, res) => { } }); +/** + * GET /admin/audio/routing + * Récupère la configuration de routing actuelle + * Format: { inputToGroup: { "0": ["production"], "1": ["technique"] }, groupToOutput: { "production": ["0", "1"] } } + */ +router.get('/audio/routing', (req, res) => { + try { + const config = configManager.get(); + const routing = config.audio?.routing || { inputToGroup: {}, groupToOutput: {}, gains: {} }; + + res.json({ + routing + }); + } catch (error) { + console.error('Erreur GET /admin/audio/routing:', error); + res.status(500).json({ error: 'Failed to load routing' }); + } +}); + +/** + * POST /admin/audio/routing + * Sauvegarde la configuration de routing + * Body: { inputToGroup: {...}, groupToOutput: {...}, gains: {...} } + */ +router.post('/admin/audio/routing', (req, res) => { + try { + const { inputToGroup, groupToOutput, gains } = req.body; + + const config = configManager.get(); + + if (!config.audio.routing) { + config.audio.routing = { inputToGroup: {}, groupToOutput: {}, gains: {} }; + } + + if (inputToGroup !== undefined) { + config.audio.routing.inputToGroup = inputToGroup; + } + + if (groupToOutput !== undefined) { + config.audio.routing.groupToOutput = groupToOutput; + } + + if (gains !== undefined) { + config.audio.routing.gains = gains; + } + + configManager.save(config); + + addLog('info', 'Audio routing updated'); + + res.json({ + message: 'Audio routing updated', + routing: config.audio.routing + }); + + } catch (error) { + console.error('Erreur POST /admin/audio/routing:', error); + res.status(500).json({ error: 'Failed to update routing' }); + } +}); + /** * POST /admin/audio/device * Sélectionne et configure une carte son