Update the main template and client scripts for the revised navigation and zone/device panels, and add bundled help SVG assets under static. Co-authored-by: Cursor <cursoragent@cursor.com>
604 lines
23 KiB
JavaScript
604 lines
23 KiB
JavaScript
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 = '<option value="">— select adapter —</option>';
|
||
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 = '<option value="">— select network —</option>';
|
||
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 = '<option value="">— select port —</option>';
|
||
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 = '<li>No saved bridge profiles.</li>';
|
||
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();
|
||
});
|
||
}
|
||
});
|