document.addEventListener('DOMContentLoaded', () => { // Help modal const helpBtn = document.getElementById('help-btn'); const helpModal = document.getElementById('help-modal'); const helpCloseBtn = document.getElementById('help-close-btn'); const mainMenuBtn = document.getElementById('main-menu-btn'); const mainMenuDropdown = document.getElementById('main-menu-dropdown'); if (helpBtn && helpModal) { const openHelp = () => { helpModal.classList.add('active'); switchHelpTab('overview'); }; helpBtn.addEventListener('click', openHelp); } if (helpCloseBtn && helpModal) { helpCloseBtn.addEventListener('click', () => { helpModal.classList.remove('active'); }); } const helpTabButtons = document.querySelectorAll('[data-help-tab]'); const helpTabPanels = document.querySelectorAll('[data-help-panel]'); function switchHelpTab(tabId) { if (!tabId) tabId = 'overview'; for (const btn of helpTabButtons) { const on = btn.getAttribute('data-help-tab') === tabId; btn.classList.toggle('active', on); btn.setAttribute('aria-selected', on ? 'true' : 'false'); } for (const panel of helpTabPanels) { const on = panel.getAttribute('data-help-panel') === tabId; panel.classList.toggle('active', on); panel.hidden = !on; } } for (const btn of helpTabButtons) { btn.addEventListener('click', () => { switchHelpTab(btn.getAttribute('data-help-tab')); }); } // Mobile main menu: forward clicks to existing header buttons if (mainMenuBtn && mainMenuDropdown) { mainMenuBtn.addEventListener('click', () => { mainMenuDropdown.classList.toggle('open'); const zonesMenuDropdown = document.getElementById('zones-menu-dropdown'); const zonesMenuBtn = document.getElementById('zones-menu-btn'); if (zonesMenuDropdown) zonesMenuDropdown.classList.remove('open'); if (zonesMenuBtn) zonesMenuBtn.setAttribute('aria-expanded', 'false'); }); mainMenuDropdown.addEventListener('click', (event) => { const target = event.target; if (target && target.matches('button[data-target]')) { const id = target.getAttribute('data-target'); const realBtn = document.getElementById(id); if (realBtn) { realBtn.click(); } mainMenuDropdown.classList.remove('open'); } }); } // Settings modal wiring (reusing existing settings endpoints). const settingsButton = document.getElementById('settings-btn'); const settingsModal = document.getElementById('settings-modal'); const settingsCloseButton = document.getElementById('settings-close-btn'); const settingsTabButtons = document.querySelectorAll('[data-settings-tab]'); const settingsTabPanels = document.querySelectorAll('[data-settings-panel]'); const ledToolIframe = document.getElementById('led-tool-iframe'); let settingsActiveTab = 'bridge'; function loadLedToolIframe() { if (!ledToolIframe) return; const blank = !ledToolIframe.src || ledToolIframe.src === 'about:blank'; if (blank) { ledToolIframe.src = '/led-tool/editor'; } } function unloadLedToolIframe() { if (ledToolIframe) { ledToolIframe.src = 'about:blank'; } } function switchSettingsTab(tabId) { if (!tabId) tabId = 'bridge'; settingsActiveTab = tabId; for (const btn of settingsTabButtons) { const on = btn.getAttribute('data-settings-tab') === tabId; btn.classList.toggle('active', on); btn.setAttribute('aria-selected', on ? 'true' : 'false'); } for (const panel of settingsTabPanels) { const on = panel.getAttribute('data-settings-panel') === tabId; panel.classList.toggle('active', on); panel.hidden = !on; } if (settingsModal) { settingsModal.classList.toggle('settings-modal--led-tool', tabId === 'led-tool'); } if (tabId === 'led-tool') { loadLedToolIframe(); } } for (const btn of settingsTabButtons) { btn.addEventListener('click', () => { switchSettingsTab(btn.getAttribute('data-settings-tab')); }); } window.openSettingsModal = (tabId) => { if (!settingsModal) return; if (tabId) { switchSettingsTab(tabId); } else { switchSettingsTab(settingsActiveTab); } settingsModal.classList.add('active'); if (!tabId || tabId === 'bridge') { loadBridgeSettings(); } }; const bridgeWsStatus = document.getElementById('bridge-ws-status'); const bridgeConnectionDetails = document.getElementById('bridge-connection-details'); const bridgeProfilesList = document.getElementById('bridge-profiles-list'); let lastBridgeSettings = null; const bridgeSerialPortSelect = document.getElementById('bridge-serial-port'); const bridgeSerialBaudInput = document.getElementById('bridge-serial-baud'); const bridgeSerialConnectBtn = document.getElementById('bridge-serial-connect-btn'); const bridgeSerialSaveProfileBtn = document.getElementById('bridge-serial-save-profile-btn'); const bridgeSerialRefreshBtn = document.getElementById('bridge-serial-refresh-btn'); const bridgeWifiInterfaceSelect = document.getElementById('bridge-wifi-interface'); const bridgeWifiRefreshInterfacesBtn = document.getElementById('bridge-wifi-refresh-interfaces-btn'); const bridgeWifiSsidSelect = document.getElementById('bridge-wifi-ssid'); const bridgeWifiSsidManual = document.getElementById('bridge-wifi-ssid-manual'); const bridgeWifiPassword = document.getElementById('bridge-wifi-password'); const bridgeWifiConnectBtn = document.getElementById('bridge-wifi-connect-btn'); const bridgeWifiSaveProfileBtn = document.getElementById('bridge-wifi-save-profile-btn'); const bridgeWifiScanBtn = document.getElementById('bridge-wifi-scan-btn'); const bridgeWifiApIp = document.getElementById('bridge-wifi-ap-ip'); const bridgeWifiWsPort = document.getElementById('bridge-wifi-ws-port'); function setBridgeWsStatus(text, isError = false) { if (!bridgeWsStatus) return; bridgeWsStatus.textContent = text || ''; bridgeWsStatus.style.color = isError ? '#f44336' : ''; } function connLabel(ok) { return ok ? 'connected' : 'not connected'; } function bridgeStatusLine(data) { if (!data) return ''; const mode = data.bridge_transport === 'serial' ? 'USB serial' : 'Wi‑Fi'; const active = data.active_bridge_id ? (data.bridges || []).find((b) => b.id === data.active_bridge_id) : null; const activeBit = active ? ` — active profile: ${active.label}` : ''; if (data.bridge_transport === 'wifi' && data.bridge_ws_url) { return `${mode}: ${data.bridge_ws_url} (${connLabel(data.bridge_connected)})${activeBit}`; } if (data.bridge_serial_port) { return `${mode}: ${data.bridge_serial_port} (${connLabel(data.bridge_connected)})${activeBit}`; } return `Bridge ${mode} (${connLabel(data.bridge_connected)})${activeBit}`; } function renderBridgeConnectionDetails(data) { if (!bridgeConnectionDetails) return; bridgeConnectionDetails.innerHTML = ''; if (!data) return; const rows = [ ['Transport in use', data.bridge_transport === 'serial' ? 'USB serial' : 'Wi‑Fi'], [ 'Wi‑Fi WebSocket', data.bridge_ws_url ? `${data.bridge_ws_url} (${connLabel(data.bridge_wifi_connected)})` : connLabel(false), ], [ 'USB serial', data.bridge_serial_port ? `${data.bridge_serial_port} (${connLabel(data.bridge_serial_connected)})` : connLabel(false), ], ]; const active = (data.bridges || []).find((b) => b.id === data.active_bridge_id); if (active) { const detail = active.transport === 'wifi' ? `Wi‑Fi ${active.ssid}` : `USB ${active.serial_port}`; rows.push(['Active saved profile', `${active.label} (${detail})`]); } else if (data.bridge_connected) { rows.push(['Active saved profile', '— (connected, no matching saved profile)']); } for (const [k, v] of rows) { const li = document.createElement('li'); li.textContent = `${k}: ${v}`; bridgeConnectionDetails.appendChild(li); } } function resolvedBridgeSsid() { const manual = bridgeWifiSsidManual?.value?.trim(); if (manual) return manual; return bridgeWifiSsidSelect?.value?.trim() || ''; } async function loadBridgeSettings() { try { const bridgesRes = await fetch('/settings/wifi/bridges'); const bridgesData = await bridgesRes.json().catch(() => ({})); lastBridgeSettings = bridgesData; if (bridgeSerialBaudInput && bridgesData.bridge_serial_baudrate) { bridgeSerialBaudInput.value = String(bridgesData.bridge_serial_baudrate); } await loadSerialPorts(bridgesData.bridge_serial_port || ''); await loadWifiInterfaces(bridgesData.wifi_interface || ''); renderBridgeConnectionDetails(bridgesData); setBridgeWsStatus(bridgeStatusLine(bridgesData)); renderBridgeProfiles(bridgesData.bridges || [], bridgesData); } catch (err) { setBridgeWsStatus(err.message, true); } } async function loadWifiInterfaces(selectedDevice) { if (!bridgeWifiInterfaceSelect) return; try { const res = await fetch('/settings/wifi/interfaces'); const data = await res.json().catch(() => ({})); if (!res.ok || !data.ok) { setBridgeWsStatus(data.error || 'Wi‑Fi interfaces unavailable', true); return; } const current = selectedDevice || bridgeWifiInterfaceSelect.value; bridgeWifiInterfaceSelect.innerHTML = ''; for (const iface of data.interfaces || []) { const opt = document.createElement('option'); opt.value = iface.device; const bits = [iface.device]; if (iface.label && iface.label !== iface.device) bits.push(iface.label); if (iface.state) bits.push(`(${iface.state})`); opt.textContent = bits.join(' — '); bridgeWifiInterfaceSelect.appendChild(opt); } if (current) bridgeWifiInterfaceSelect.value = current; } catch (err) { setBridgeWsStatus(err.message, true); } } async function scanBridgeWifi() { const device = bridgeWifiInterfaceSelect?.value?.trim(); if (!device) { setBridgeWsStatus('Select a Wi‑Fi adapter first', true); return; } setBridgeWsStatus('Scanning…'); try { const res = await fetch( `/settings/wifi/scan?device=${encodeURIComponent(device)}` ); const data = await res.json().catch(() => ({})); if (!res.ok || !data.ok) { setBridgeWsStatus(data.error || 'Scan failed', true); return; } if (!bridgeWifiSsidSelect) return; const prev = resolvedBridgeSsid(); bridgeWifiSsidSelect.innerHTML = ''; for (const net of data.networks || []) { const opt = document.createElement('option'); opt.value = net.ssid; opt.textContent = `${net.ssid} (${net.signal}%)`; bridgeWifiSsidSelect.appendChild(opt); } if (prev) { bridgeWifiSsidSelect.value = prev; if (!bridgeWifiSsidSelect.value && bridgeWifiSsidManual) { bridgeWifiSsidManual.value = prev; } } setBridgeWsStatus(`Found ${(data.networks || []).length} network(s)`); } catch (err) { setBridgeWsStatus(err.message, true); } } async function loadSerialPorts(selectedPort) { if (!bridgeSerialPortSelect) return; try { const res = await fetch('/led-tool/ports'); const data = await res.json().catch(() => ({})); const current = selectedPort || bridgeSerialPortSelect.value; bridgeSerialPortSelect.innerHTML = ''; for (const p of data.ports || []) { const opt = document.createElement('option'); opt.value = p.device; opt.textContent = p.description ? `${p.device} — ${p.description}` : p.device; bridgeSerialPortSelect.appendChild(opt); } if (current) bridgeSerialPortSelect.value = current; } catch (err) { setBridgeWsStatus(err.message, true); } } function profileStatusFor(p, data) { const activeId = data.active_bridge_id || ''; const isActive = Boolean(activeId && p.id === activeId && data.bridge_connected); if (isActive) { return { text: 'Connected', className: 'settings-bridge-profile-status--connected' }; } return { text: 'Not connected', className: 'settings-bridge-profile-status--idle' }; } async function deleteBridgeProfile(id, label) { const name = label || id; if (!window.confirm(`Delete saved bridge profile “${name}”?`)) return; setBridgeWsStatus('Deleting…'); try { const res = await fetch(`/settings/wifi/bridges/${encodeURIComponent(id)}`, { method: 'DELETE', headers: { Accept: 'application/json' }, }); const data = await res.json().catch(() => ({})); if (!res.ok || !data.ok) { setBridgeWsStatus(data.error || 'Delete failed', true); return; } setBridgeWsStatus(data.message || 'Profile deleted'); await loadBridgeSettings(); } catch (err) { setBridgeWsStatus(err.message, true); } } function renderBridgeProfiles(profiles, bridgesData) { if (!bridgeProfilesList) return; bridgeProfilesList.innerHTML = ''; const data = bridgesData || lastBridgeSettings || {}; const activeId = data.active_bridge_id || ''; if (!profiles.length) { bridgeProfilesList.innerHTML = '
  • No saved bridge profiles.
  • '; return; } for (const p of profiles) { const li = document.createElement('li'); const isActive = Boolean(activeId && p.id === activeId && data.bridge_connected); li.className = 'settings-bridge-profile-row' + (isActive ? ' settings-bridge-profile-row--active' : ''); const main = document.createElement('div'); main.className = 'settings-bridge-profile-main'; const label = document.createElement('span'); label.className = 'settings-bridge-profile-label'; if (p.transport === 'wifi') { label.textContent = `${p.label} — Wi‑Fi ${p.ssid}`; } else { label.textContent = `${p.label} — USB ${p.serial_port}`; } const status = document.createElement('span'); const st = profileStatusFor(p, data); status.className = 'settings-bridge-profile-status ' + st.className; status.textContent = st.text; main.appendChild(label); main.appendChild(status); const actions = document.createElement('div'); actions.className = 'settings-bridge-profile-actions'; const connectBtn = document.createElement('button'); connectBtn.type = 'button'; connectBtn.className = 'btn btn-secondary btn-small'; connectBtn.textContent = 'Connect'; connectBtn.addEventListener('click', () => connectSavedBridge(p.id)); const deleteBtn = document.createElement('button'); deleteBtn.type = 'button'; deleteBtn.className = 'btn btn-secondary btn-small settings-bridge-profile-delete'; deleteBtn.textContent = 'Delete'; deleteBtn.addEventListener('click', () => deleteBridgeProfile(p.id, p.label)); actions.appendChild(connectBtn); actions.appendChild(deleteBtn); li.appendChild(main); li.appendChild(actions); bridgeProfilesList.appendChild(li); } } async function connectSavedBridge(id) { setBridgeWsStatus('Connecting…'); try { const res = await fetch(`/settings/wifi/bridges/${encodeURIComponent(id)}/connect`, { method: 'POST', headers: { Accept: 'application/json' }, }); const data = await res.json().catch(() => ({})); if (!res.ok || !data.ok) { setBridgeWsStatus(data.error || 'Connect failed', true); return; } setBridgeWsStatus(data.message ? `${data.message} — ${bridgeStatusLine(data)}` : bridgeStatusLine(data)); await loadBridgeSettings(); } catch (err) { setBridgeWsStatus(err.message, true); } } async function connectBridgeWifi(saveProfile) { const device = bridgeWifiInterfaceSelect?.value?.trim(); const ssid = resolvedBridgeSsid(); const password = bridgeWifiPassword?.value || ''; const apIp = bridgeWifiApIp?.value?.trim() || '192.168.4.1'; const wsPort = parseInt(bridgeWifiWsPort?.value, 10) || 80; const label = document.getElementById('bridge-wifi-label')?.value?.trim() || ssid; if (!device) { setBridgeWsStatus('Select a Wi‑Fi adapter', true); return; } if (!ssid) { setBridgeWsStatus('Enter or select a bridge SSID', true); return; } setBridgeWsStatus('Connecting…'); try { const res = await fetch('/settings/wifi/connect', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ device, ssid, password, ap_ip: apIp, ws_port: wsPort, label, save_profile: saveProfile, }), }); const data = await res.json().catch(() => ({})); if (!res.ok || !data.ok) { setBridgeWsStatus(data.error || 'Connect failed', true); return; } setBridgeWsStatus(data.message ? `${data.message} — ${bridgeStatusLine(data)}` : bridgeStatusLine(data)); await loadBridgeSettings(); } catch (err) { setBridgeWsStatus(err.message, true); } } async function connectBridgeSerial(saveProfile) { const port = bridgeSerialPortSelect ? bridgeSerialPortSelect.value : ''; const baud = parseInt(bridgeSerialBaudInput?.value, 10) || 115200; const label = document.getElementById('bridge-serial-label')?.value?.trim() || port; if (!port) { setBridgeWsStatus('Select a USB serial port', true); return; } setBridgeWsStatus('Connecting…'); try { const res = await fetch('/settings/wifi/serial/connect', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ port, baudrate: baud, label, save_profile: saveProfile }), }); const data = await res.json().catch(() => ({})); if (!res.ok || !data.ok) { setBridgeWsStatus(data.error || 'Connect failed', true); return; } setBridgeWsStatus(data.message ? `${data.message} — ${bridgeStatusLine(data)}` : bridgeStatusLine(data)); await loadBridgeSettings(); } catch (err) { setBridgeWsStatus(err.message, true); } } if (bridgeSerialRefreshBtn) { bridgeSerialRefreshBtn.addEventListener('click', () => loadSerialPorts()); } if (bridgeSerialConnectBtn) { bridgeSerialConnectBtn.addEventListener('click', () => connectBridgeSerial(true)); } if (bridgeWifiRefreshInterfacesBtn) { bridgeWifiRefreshInterfacesBtn.addEventListener('click', () => loadWifiInterfaces()); } if (bridgeWifiScanBtn) { bridgeWifiScanBtn.addEventListener('click', () => scanBridgeWifi()); } if (bridgeWifiConnectBtn) { bridgeWifiConnectBtn.addEventListener('click', () => connectBridgeWifi(true)); } if (bridgeWifiSaveProfileBtn) { bridgeWifiSaveProfileBtn.addEventListener('click', async () => { const device = bridgeWifiInterfaceSelect?.value?.trim(); const ssid = resolvedBridgeSsid(); if (!ssid) { setBridgeWsStatus('SSID required to save profile', true); return; } const password = bridgeWifiPassword?.value || ''; const apIp = bridgeWifiApIp?.value?.trim() || '192.168.4.1'; const wsPort = parseInt(bridgeWifiWsPort?.value, 10) || 80; const label = document.getElementById('bridge-wifi-label')?.value?.trim() || ssid; try { const res = await fetch('/settings/wifi/bridges'); const data = await res.json().catch(() => ({})); const bridges = Array.isArray(data.bridges) ? data.bridges : []; bridges.push({ id: crypto.randomUUID ? crypto.randomUUID().slice(0, 12) : String(Date.now()), label, transport: 'wifi', ssid, password, ap_ip: apIp, ws_port: wsPort, }); const putRes = await fetch('/settings/wifi/bridges', { method: 'PUT', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ bridges, wifi_interface: device || data.wifi_interface }), }); const putData = await putRes.json().catch(() => ({})); if (!putRes.ok || !putData.ok) { setBridgeWsStatus(putData.error || 'Save failed', true); return; } setBridgeWsStatus('Wi‑Fi profile saved'); await loadBridgeSettings(); } catch (err) { setBridgeWsStatus(err.message, true); } }); } if (bridgeSerialSaveProfileBtn) { bridgeSerialSaveProfileBtn.addEventListener('click', async () => { const port = bridgeSerialPortSelect ? bridgeSerialPortSelect.value : ''; if (!port) { setBridgeWsStatus('Port required to save profile', true); return; } const baud = parseInt(bridgeSerialBaudInput?.value, 10) || 115200; const label = document.getElementById('bridge-serial-label')?.value?.trim() || port; try { const res = await fetch('/settings/wifi/bridges'); const data = await res.json().catch(() => ({})); const bridges = Array.isArray(data.bridges) ? data.bridges : []; bridges.push({ id: crypto.randomUUID ? crypto.randomUUID().slice(0, 12) : String(Date.now()), label, transport: 'serial', serial_port: port, serial_baudrate: baud, }); const putRes = await fetch('/settings/wifi/bridges', { method: 'PUT', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ bridges }), }); const putData = await putRes.json().catch(() => ({})); if (!putRes.ok || !putData.ok) { setBridgeWsStatus(putData.error || 'Save failed', true); return; } setBridgeWsStatus('Serial profile saved'); await loadBridgeSettings(); } catch (err) { setBridgeWsStatus(err.message, true); } }); } if (settingsButton && settingsModal) { settingsButton.addEventListener('click', () => { switchSettingsTab('bridge'); settingsModal.classList.add('active'); loadBridgeSettings(); }); } if (settingsCloseButton && settingsModal) { settingsCloseButton.addEventListener('click', () => { settingsModal.classList.remove('active'); settingsModal.classList.remove('settings-modal--led-tool'); unloadLedToolIframe(); }); } });