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