// RPi Audio Mixer — Web UI Application let ws = null; let sessionToken = null; let connected = false; let channelCount = 16; let paramState = {}; // channel -> { param_type -> value } // ── Logging ──────────────────────────────────────────────────────────── function log(msg, cls) { const el = document.getElementById('log'); const div = document.createElement('div'); div.textContent = new Date().toLocaleTimeString() + ' ' + msg; div.className = cls || ''; el.prepend(div); if (el.children.length > 50) el.lastChild.remove(); } // ── Connection ────────────────────────────────────────────────────────── async function login() { const apiKey = document.getElementById('apikey-input').value; try { const resp = await fetch('/api/v1/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey }, body: JSON.stringify({ api_key: apiKey }), }); const data = await resp.json(); if (data.token) { sessionToken = data.token; document.getElementById('connect-btn').disabled = false; log('Session obtained (expires in ' + data.expires_in + 's)', 'log-info'); // Auto-connect connectWS(); } else { log('Login failed: ' + JSON.stringify(data), 'log-error'); } } catch (e) { log('Login error: ' + e.message, 'log-error'); } } function connectWS() { if (!sessionToken) { log('No session token — login first', 'log-error'); return; } if (ws) ws.close(); const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; const url = proto + '//' + location.host + '/ws?session=' + sessionToken; setStatus('connecting', '● Connecting...'); ws = new WebSocket(url); ws.onopen = () => { connected = true; setStatus('connected', '● Connected'); document.getElementById('connect-btn').disabled = true; log('WebSocket connected', 'log-state'); }; ws.onclose = (e) => { connected = false; setStatus('disconnected', '● Disconnected'); document.getElementById('connect-btn').disabled = false; log('WebSocket closed: ' + (e.reason || 'code ' + e.code), 'log-error'); }; ws.onerror = () => log('WebSocket error', 'log-error'); ws.onmessage = handleMessage; } function disconnectWS() { if (ws) ws.close(); } function setStatus(cls, text) { const el = document.getElementById('conn-status'); el.className = 'status ' + cls; el.textContent = text; } // ── Message handling ──────────────────────────────────────────────────── function handleMessage(event) { try { const msg = JSON.parse(event.data); switch (msg.type) { case 'full_state': handleFullState(msg.payload); break; case 'parameter_update': handleParameterUpdate(msg.payload); break; case 'transport_command': handleTransportUpdate(msg.payload); break; case 'error': log('Server error: ' + msg.payload.message, 'log-error'); break; default: log('Unknown msg type: ' + msg.type, 'log-info'); } } catch (e) { log('Parse error: ' + e.message, 'log-error'); } } function handleFullState(state) { log('Full state received: ' + (state.channels ? state.channels.length : 0) + ' channels', 'log-state'); if (state.channels) { channelCount = state.channels.length; buildChannelStrips(state.channels); } if (state.transport) { updateTransportUI(state.transport); } if (state.master) { updateMasterUI(state.master); } } function handleParameterUpdate(payload) { const { param_type, value, channel } = payload; if (!paramState[channel]) paramState[channel] = {}; paramState[channel][param_type] = value; updateChannelUI(channel, param_type, value); if (param_type === 'master_volume') { document.getElementById('master-vol-val').textContent = value.toFixed(1) + ' dB'; document.getElementById('master-fader').querySelector('input').value = value; } } function handleTransportUpdate(payload) { updateTransportUI(payload); } // ── Channel strip builder ─────────────────────────────────────────────── function buildChannelStrips(channels) { const container = document.getElementById('channels'); container.innerHTML = ''; channels.forEach((ch, i) => { const strip = document.createElement('div'); strip.className = 'channel-strip'; strip.id = 'ch-' + i; const volDb = ch.volume_db !== undefined ? ch.volume_db : 0; strip.innerHTML = `