// Zone management JavaScript let currentZoneId = null; let brightnessSendTimeout = null; const UI_BRIGHTNESS_STORAGE_KEY = "led_controller_ui_brightness"; function clamp255(n) { const v = parseInt(n, 10); if (Number.isNaN(v)) return null; return Math.max(0, Math.min(255, v)); } function loadSavedUiBrightness() { try { const raw = localStorage.getItem(UI_BRIGHTNESS_STORAGE_KEY); if (raw == null) return null; return clamp255(raw); } catch (_) { return null; } } function persistUiBrightness(value) { const v = clamp255(value); if (v === null) return; try { localStorage.setItem(UI_BRIGHTNESS_STORAGE_KEY, String(v)); } catch (_) {} } function applyBrightnessSliders(val) { const v = clamp255(val); if (v === null) return; const headerSlider = document.getElementById("header-brightness-slider"); const menuSlider = document.getElementById("menu-brightness-slider"); if (headerSlider) headerSlider.value = String(v); if (menuSlider) menuSlider.value = String(v); } function sendZoneBrightness(value) { const val = Math.max(0, Math.min(255, parseInt(value, 10) || 0)); persistUiBrightness(val); const headerSlider = document.getElementById('header-brightness-slider'); const menuSlider = document.getElementById('menu-brightness-slider'); if (headerSlider && String(headerSlider.value) !== String(val)) { headerSlider.value = String(val); } if (menuSlider && String(menuSlider.value) !== String(val)) { menuSlider.value = String(val); } if (brightnessSendTimeout) { clearTimeout(brightnessSendTimeout); } brightnessSendTimeout = setTimeout(() => { (async () => { try { const section = document.querySelector('.presets-section[data-zone-id]'); const names = typeof window.parseTabDeviceNames === 'function' ? window.parseTabDeviceNames(section) : []; const targetMacs = names.length > 0 && typeof window.tabsManager !== 'undefined' && typeof window.tabsManager.resolveTabDeviceMacs === 'function' ? await window.tabsManager.resolveTabDeviceMacs(names) : []; if (typeof window.postDriverSequence === 'function') { await window.postDriverSequence([{ v: '1', b: val, save: true }], targetMacs, 0); return; } // Fallback to raw websocket sender if presets.js helper isn't available yet. if (typeof window.sendEspnowRaw === 'function') { window.sendEspnowRaw({ v: '1', b: val, save: true }); } } catch (err) { console.error('Failed to send brightness via driver sequence:', err); } })(); }, 150); } const isEditModeActive = () => { const toggle = document.querySelector('.ui-mode-toggle'); return !!(toggle && toggle.getAttribute('aria-pressed') === 'true'); }; // Get current zone from cookie function getCurrentZoneFromCookie() { const cookies = document.cookie.split(';'); for (let cookie of cookies) { const [name, value] = cookie.trim().split('='); if (name === 'current_zone') { return value; } } return null; } async function fetchDevicesMap() { try { const response = await fetch("/devices", { headers: { Accept: "application/json" } }); if (!response.ok) return {}; const data = await response.json(); return data && typeof data === "object" ? data : {}; } catch (e) { console.error("fetchDevicesMap:", e); return {}; } } /** Registry MACs for zone device names (order matches zone names; skips unknown names). */ async function resolveZoneDeviceMacs(zoneNames) { const dm = await fetchDevicesMap(); const rows = namesToRows(Array.isArray(zoneNames) ? zoneNames : [], dm); const macs = rows.map((r) => r.mac).filter(Boolean); return [...new Set(macs)]; } function namesToRows(zoneNames, devicesMap) { const usedMacs = new Set(); const list = Array.isArray(zoneNames) ? zoneNames : []; return list.map((name) => { const n = String(name || "").trim(); const matches = Object.entries(devicesMap || {}).filter( ([mac, d]) => d && String((d.name || "").trim()) === n && !usedMacs.has(mac), ); if (matches.length === 0) { return { mac: null, name: n || "unknown" }; } const [mac] = matches[0]; usedMacs.add(mac); return { mac, name: n }; }); } function rowsToNames(rows) { return (rows || []).map((r) => String(r.name || "").trim()).filter((n) => n.length > 0); } function renderZoneDevicesEditor(containerEl, rows, devicesMap) { if (!containerEl) return; containerEl.innerHTML = ""; const entries = Object.entries(devicesMap || {}).sort(([a], [b]) => a.localeCompare(b)); rows.forEach((row, idx) => { const div = document.createElement("div"); div.className = "zone-device-row profiles-row"; const label = document.createElement("span"); label.className = "zone-device-row-label"; const strong = document.createElement("strong"); strong.textContent = row.name || "—"; label.appendChild(strong); label.appendChild(document.createTextNode(" ")); const sub = document.createElement("span"); sub.className = "muted-text"; sub.textContent = row.mac ? row.mac : "(not in registry)"; label.appendChild(sub); const rm = document.createElement("button"); rm.type = "button"; rm.className = "btn btn-danger btn-small"; rm.textContent = "Remove"; rm.addEventListener("click", () => { rows.splice(idx, 1); renderZoneDevicesEditor(containerEl, rows, devicesMap); }); div.appendChild(label); div.appendChild(rm); containerEl.appendChild(div); }); const macsInRows = new Set(rows.map((r) => r.mac).filter(Boolean)); const addWrap = document.createElement("div"); addWrap.className = "zone-devices-add profiles-actions"; const sel = document.createElement("select"); sel.className = "zone-device-add-select"; sel.appendChild(new Option("Add device…", "")); entries.forEach(([mac, d]) => { if (macsInRows.has(mac)) return; const labelName = d && d.name ? String(d.name).trim() : ""; const optLabel = labelName ? `${labelName} — ${mac}` : mac; sel.appendChild(new Option(optLabel, mac)); }); const addBtn = document.createElement("button"); addBtn.type = "button"; addBtn.className = "btn btn-primary btn-small"; addBtn.textContent = "Add"; addBtn.addEventListener("click", () => { const mac = sel.value; if (!mac || !devicesMap[mac]) return; const n = String((devicesMap[mac].name || "").trim() || mac); rows.push({ mac, name: n }); sel.value = ""; renderZoneDevicesEditor(containerEl, rows, devicesMap); }); addWrap.appendChild(sel); addWrap.appendChild(addBtn); containerEl.appendChild(addWrap); } /** Default device name list when creating a zone (refined in Edit zone). */ async function defaultDeviceNamesForNewTab() { const dm = await fetchDevicesMap(); const macs = Object.keys(dm); if (macs.length > 0) { const m0 = macs[0]; return [String((dm[m0].name || "").trim() || m0)]; } return ["1"]; } /** Read zone device names from the presets section (JSON attr preferred; legacy comma list fallback). */ function parseTabDeviceNames(section) { if (!section) return []; const enc = section.getAttribute("data-device-names-json"); if (enc) { try { const arr = JSON.parse(decodeURIComponent(enc)); return Array.isArray(arr) ? arr.map((n) => String(n).trim()).filter((n) => n.length > 0) : []; } catch (e) { /* ignore */ } } const legacy = section.getAttribute("data-device-names"); if (legacy) { return legacy.split(",").map((n) => n.trim()).filter((n) => n.length > 0); } return []; } window.parseTabDeviceNames = parseTabDeviceNames; window.parseZoneDeviceNames = parseTabDeviceNames; function escapeHtmlAttr(s) { return String(s) .replace(/&/g, "&") .replace(/"/g, """) .replace(/ 0 ? zoneIds[0] : null; // Clear stale cookie document.cookie = 'current_zone=; path=/; max-age=0'; } currentZoneId = candidateId; renderZonesList(data.zones, data.zone_order, currentZoneId); // Load current zone content if available if (currentZoneId) { await loadZoneContent(currentZoneId); } else if (data.zone_order && data.zone_order.length > 0) { // Set first zone as current if none is set const firstTabId = data.zone_order[0]; await setCurrentZone(firstTabId); await loadZoneContent(firstTabId); } } catch (error) { console.error('Failed to load zones:', error); const container = document.getElementById('zones-list'); if (container) { container.innerHTML = '
Failed to load zones
'; } } } // Render tabs list in the main UI function renderZonesList(tabs, tabOrder, currentZoneId) { const container = document.getElementById('zones-list'); if (!container) return; if (!tabOrder || tabOrder.length === 0) { container.innerHTML = '
No zones available
'; return; } const editMode = isEditModeActive(); let html = '
'; for (const zoneId of tabOrder) { const zone = tabs[zoneId]; if (zone) { const activeClass = zoneId === currentZoneId ? 'active' : ''; const tabName = zone.name || `Zone ${zoneId}`; html += ` `; } } html += '
'; container.innerHTML = html; } // Render tabs list in modal (like profiles) function renderZonesListModal(tabs, tabOrder, currentZoneId) { const container = document.getElementById('zones-list-modal'); if (!container) return; container.innerHTML = ""; let entries = []; if (Array.isArray(tabOrder)) { entries = tabOrder.map((zoneId) => [zoneId, tabs[zoneId] || {}]); } else if (tabs && typeof tabs === "object") { entries = Object.entries(tabs).filter(([key]) => { return key !== 'current_zone_id' && key !== 'zones' && key !== 'zone_order'; }); } if (entries.length === 0) { const empty = document.createElement("p"); empty.className = "muted-text"; empty.textContent = "No zones found."; container.appendChild(empty); return; } const editMode = isEditModeActive(); entries.forEach(([zoneId, zone]) => { const row = document.createElement("div"); row.className = "profiles-row"; row.dataset.zoneId = String(zoneId); const label = document.createElement("span"); label.textContent = (zone && zone.name) || zoneId; if (String(zoneId) === String(currentZoneId)) { label.textContent = `✓ ${label.textContent}`; label.style.fontWeight = "bold"; label.style.color = "#FFD700"; } const applyButton = document.createElement("button"); applyButton.className = "btn btn-secondary btn-small"; applyButton.textContent = "Select"; applyButton.addEventListener("click", async () => { await selectZone(zoneId); document.getElementById('zones-modal').classList.remove('active'); }); const editButton = document.createElement("button"); editButton.className = "btn btn-secondary btn-small"; editButton.textContent = "Edit"; editButton.addEventListener("click", async () => { await openEditZoneModal(zoneId, zone); }); const cloneButton = document.createElement("button"); cloneButton.className = "btn btn-secondary btn-small"; cloneButton.textContent = "Clone"; cloneButton.addEventListener("click", async () => { const baseName = (zone && zone.name) || zoneId; const suggested = `${baseName} Copy`; const name = prompt("New zone name:", suggested); if (name === null) { return; } const trimmed = String(name).trim(); if (!trimmed) { alert("Zone name cannot be empty."); return; } try { const response = await fetch(`/zones/${zoneId}/clone`, { method: "POST", headers: { "Content-Type": "application/json", "Accept": "application/json", }, body: JSON.stringify({ name: trimmed }), }); if (!response.ok) { const errorData = await response.json().catch(() => ({ error: "Failed to clone zone" })); throw new Error(errorData.error || "Failed to clone zone"); } const data = await response.json().catch(() => null); let newTabId = null; if (data && typeof data === "object") { if (data.id) { newTabId = String(data.id); } else { const ids = Object.keys(data); if (ids.length > 0) { newTabId = String(ids[0]); } } } await loadZonesModal(); if (newTabId) { await selectZone(newTabId); } else { await loadZones(); } } catch (error) { console.error("Clone zone failed:", error); alert("Failed to clone zone: " + error.message); } }); const deleteButton = document.createElement("button"); deleteButton.className = "btn btn-danger btn-small"; deleteButton.textContent = "Delete"; deleteButton.addEventListener("click", async () => { const confirmed = confirm(`Delete zone "${label.textContent}"?`); if (!confirmed) { return; } try { const response = await fetch(`/zones/${zoneId}`, { method: "DELETE", headers: { Accept: "application/json" }, }); if (!response.ok) { const errorData = await response.json().catch(() => ({ error: "Failed to delete zone" })); throw new Error(errorData.error || "Failed to delete zone"); } // Clear cookie if deleted zone was current if (zoneId === currentZoneId) { document.cookie = 'current_zone=; path=/; max-age=0'; currentZoneId = null; } await loadZonesModal(); await loadZones(); // Reload main tabs list } catch (error) { console.error("Delete zone failed:", error); alert("Failed to delete zone: " + error.message); } }); row.appendChild(label); row.appendChild(applyButton); if (editMode) { row.appendChild(editButton); row.appendChild(cloneButton); row.appendChild(deleteButton); } container.appendChild(row); }); } // Load tabs in modal async function loadZonesModal() { const container = document.getElementById('zones-list-modal'); if (!container) return; container.innerHTML = ""; const loading = document.createElement("p"); loading.className = "muted-text"; loading.textContent = "Loading zones..."; container.appendChild(loading); try { const response = await fetch("/zones", { headers: { Accept: "application/json" }, }); if (!response.ok) { throw new Error("Failed to load zones"); } const data = await response.json(); const tabs = data.zones || data; const currentZoneId = getCurrentZoneFromCookie() || data.current_zone_id || null; renderZonesListModal(tabs, data.zone_order || [], currentZoneId); } catch (error) { console.error("Load tabs failed:", error); container.innerHTML = ""; const errorMessage = document.createElement("p"); errorMessage.className = "muted-text"; errorMessage.textContent = "Failed to load zones."; container.appendChild(errorMessage); } } // Select a zone async function selectZone(zoneId) { // Update active state document.querySelectorAll('.zone-button').forEach(btn => { btn.classList.remove('active'); }); const btn = document.querySelector(`[data-zone-id="${zoneId}"]`); if (btn) { btn.classList.add('active'); } // Set as current zone await setCurrentZone(zoneId); // Load zone content loadZoneContent(zoneId); } // Set current zone in cookie async function setCurrentZone(zoneId) { try { const response = await fetch(`/zones/${zoneId}/set-current`, { method: 'POST' }); const data = await response.json(); if (response.ok) { currentZoneId = zoneId; // Also set cookie on client side document.cookie = `current_zone=${zoneId}; path=/; max-age=31536000`; } else { console.error('Failed to set current zone:', data.error); } } catch (error) { console.error('Error setting current zone:', error); } } // Load zone content async function loadZoneContent(zoneId) { const container = document.getElementById('zone-content'); if (!container) return; try { const response = await fetch(`/zones/${zoneId}`); const zone = await response.json(); if (zone.error) { container.innerHTML = `
${zone.error}
`; return; } // Render zone content (presets section) const tabName = zone.name || `Zone ${zoneId}`; const names = Array.isArray(zone.names) ? zone.names : []; const namesJsonAttr = encodeURIComponent(JSON.stringify(names)); const legacyOk = names.length > 0 && !names.some((n) => /[",]/.test(String(n))); const legacyAttr = legacyOk ? ` data-device-names="${escapeHtmlAttr(names.join(","))}"` : ""; container.innerHTML = `
`; // Keep header and menu brightness controls in sync. const brightnessSlider = document.getElementById('header-brightness-slider'); const menuBrightnessSlider = document.getElementById('menu-brightness-slider'); if (menuBrightnessSlider && brightnessSlider) { menuBrightnessSlider.value = brightnessSlider.value; } // Trigger presets loading if the function exists if (typeof renderTabPresets === 'function') { renderTabPresets(zoneId); } } catch (error) { console.error('Failed to load zone content:', error); container.innerHTML = '
Failed to load zone content
'; } } // Send all presets used by all tabs in the current profile via /presets/send. async function sendProfilePresets() { try { // Load current profile to get its tabs const profileRes = await fetch('/profiles/current', { headers: { Accept: 'application/json' }, }); if (!profileRes.ok) { alert('Failed to load current profile.'); return; } const profileData = await profileRes.json(); const profile = profileData.profile || {}; let zoneList = null; if (Array.isArray(profile.zones)) { zoneList = profile.zones; } else if (profile.zones) { zoneList = [profile.zones]; } if (!zoneList || zoneList.length === 0) { if (Array.isArray(profile.zones)) { zoneList = profile.zones; } else if (profile.zones) { zoneList = [profile.zones]; } } if (!zoneList || zoneList.length === 0) { console.warn('sendProfilePresets: no zones found', { profileData, profile, }); } if (!zoneList.length) { alert('Current profile has no zones to send presets for.'); return; } let totalSent = 0; let totalMessages = 0; let zonesWithPresets = 0; for (const zoneId of zoneList) { try { const tabResp = await fetch(`/zones/${zoneId}`, { headers: { Accept: 'application/json' }, }); if (!tabResp.ok) { continue; } const tabData = await tabResp.json(); let presetIds = []; if (Array.isArray(tabData.presets_flat)) { presetIds = tabData.presets_flat; } else if (Array.isArray(tabData.presets)) { if (tabData.presets.length && typeof tabData.presets[0] === 'string') { presetIds = tabData.presets; } else if (tabData.presets.length && Array.isArray(tabData.presets[0])) { presetIds = tabData.presets.flat(); } } presetIds = (presetIds || []).filter(Boolean); if (!presetIds.length) { continue; } zonesWithPresets += 1; const zoneNames = Array.isArray(tabData.names) ? tabData.names : []; const targets = await resolveZoneDeviceMacs(zoneNames); const payload = { preset_ids: presetIds }; if (tabData.default_preset) { payload.default = tabData.default_preset; } if (targets.length > 0) { payload.targets = targets; } const response = await fetch('/presets/send', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, body: JSON.stringify(payload), }); const data = await response.json().catch(() => ({})); if (!response.ok) { const msg = (data && data.error) || `Failed to send presets for zone ${zoneId}.`; console.warn(msg); continue; } totalSent += typeof data.presets_sent === 'number' ? data.presets_sent : presetIds.length; totalMessages += typeof data.messages_sent === 'number' ? data.messages_sent : 0; } catch (e) { console.error('Failed to send profile presets for zone:', zoneId, e); } } if (!zonesWithPresets) { alert('No presets to send for the current profile.'); return; } const messagesLabel = totalMessages ? totalMessages : '?'; alert(`Sent ${totalSent} preset(s) across ${zonesWithPresets} zone(s) (${messagesLabel} driver send(s)).`); } catch (error) { console.error('Failed to send profile presets:', error); alert('Failed to send profile presets.'); } } function tabPresetIdsInOrder(tabData) { let ids = []; if (Array.isArray(tabData.presets_flat)) { ids = tabData.presets_flat.slice(); } else if (Array.isArray(tabData.presets)) { if (tabData.presets.length && typeof tabData.presets[0] === "string") { ids = tabData.presets.slice(); } else if (tabData.presets.length && Array.isArray(tabData.presets[0])) { ids = tabData.presets.flat(); } } return (ids || []).filter(Boolean); } // Presets already on the zone (remove) and presets available to add (select). async function refreshEditTabPresetsUi(zoneId) { const currentEl = document.getElementById("edit-zone-presets-current"); const addEl = document.getElementById("edit-zone-presets-list"); if (!zoneId || !currentEl || !addEl) return; currentEl.innerHTML = 'Loading…'; addEl.innerHTML = 'Loading…'; try { const tabRes = await fetch(`/zones/${zoneId}`, { headers: { Accept: "application/json" } }); if (!tabRes.ok) { const msg = 'Failed to load zone presets.'; currentEl.innerHTML = msg; addEl.innerHTML = msg; return; } const tabData = await tabRes.json(); const inTabIds = tabPresetIdsInOrder(tabData); const inTabSet = new Set(inTabIds.map((id) => String(id))); const presetsRes = await fetch("/presets", { headers: { Accept: "application/json" } }); const allPresets = presetsRes.ok ? await presetsRes.json() : {}; const makeRow = () => { const row = document.createElement("div"); row.className = "profiles-row"; row.style.display = "flex"; row.style.alignItems = "center"; row.style.justifyContent = "space-between"; row.style.gap = "0.5rem"; return row; }; currentEl.innerHTML = ""; if (inTabIds.length === 0) { currentEl.innerHTML = 'No presets on this zone yet.'; } else { for (const presetId of inTabIds) { const preset = allPresets[presetId] || {}; const name = preset.name || presetId; const row = makeRow(); const label = document.createElement("span"); label.textContent = name; const removeBtn = document.createElement("button"); removeBtn.type = "button"; removeBtn.className = "btn btn-danger btn-small"; removeBtn.textContent = "Remove"; removeBtn.addEventListener("click", async () => { if (typeof window.removePresetFromTab !== "function") return; if (!window.confirm(`Remove this preset from the zone?\n\n${name}`)) return; await window.removePresetFromTab(zoneId, presetId); await refreshEditTabPresetsUi(zoneId); }); row.appendChild(label); row.appendChild(removeBtn); currentEl.appendChild(row); } } const allIds = Object.keys(allPresets); const availableToAdd = allIds.filter((id) => !inTabSet.has(String(id))); addEl.innerHTML = ""; if (availableToAdd.length === 0) { addEl.innerHTML = 'No presets to add. All presets are already on this zone.'; } else { const addWrap = document.createElement("div"); addWrap.className = "zone-devices-add profiles-actions"; const sel = document.createElement("select"); sel.className = "zone-device-add-select"; sel.setAttribute("aria-label", "Preset to add to this zone"); sel.appendChild(new Option("Add preset…", "")); const sorted = availableToAdd.slice().sort((a, b) => { const na = (allPresets[a] && allPresets[a].name) || a; const nb = (allPresets[b] && allPresets[b].name) || b; return String(na).localeCompare(String(nb), undefined, { sensitivity: "base" }); }); sorted.forEach((presetId) => { const preset = allPresets[presetId] || {}; const name = preset.name || presetId; sel.appendChild(new Option(`${name} — ${presetId}`, presetId)); }); const addBtn = document.createElement("button"); addBtn.type = "button"; addBtn.className = "btn btn-primary btn-small"; addBtn.textContent = "Add"; addBtn.addEventListener("click", async () => { const presetId = sel.value; if (!presetId) return; if (typeof window.addPresetToTab === "function") { await window.addPresetToTab(presetId, zoneId); sel.value = ""; await refreshEditTabPresetsUi(zoneId); } }); addWrap.appendChild(sel); addWrap.appendChild(addBtn); addEl.appendChild(addWrap); } } catch (e) { console.error("refreshEditTabPresetsUi:", e); const msg = 'Failed to load presets.'; currentEl.innerHTML = msg; addEl.innerHTML = msg; } } async function populateEditTabPresetsList(zoneId) { await refreshEditTabPresetsUi(zoneId); } // Open edit zone modal async function openEditZoneModal(zoneId, zone) { const modal = document.getElementById("edit-zone-modal"); const idInput = document.getElementById("edit-zone-id"); const nameInput = document.getElementById("edit-zone-name"); const editor = document.getElementById("edit-zone-devices-editor"); let tabData = zone; if (!tabData || typeof tabData !== "object" || tabData.error) { try { const response = await fetch(`/zones/${zoneId}`); if (response.ok) { tabData = await response.json(); } } catch (e) { console.error("openEditZoneModal fetch zone:", e); } } tabData = tabData || {}; if (idInput) idInput.value = zoneId; if (nameInput) nameInput.value = tabData.name || ""; const devicesMap = await fetchDevicesMap(); const zoneNames = Array.isArray(tabData.names) && tabData.names.length > 0 ? tabData.names : ["1"]; window.__editTabDeviceRows = namesToRows(zoneNames, devicesMap); renderZoneDevicesEditor(editor, window.__editTabDeviceRows, devicesMap); if (modal) modal.classList.add("active"); await refreshEditTabPresetsUi(zoneId); } function normalizeTabNamesArg(namesOrString) { if (Array.isArray(namesOrString)) { return namesOrString.map((n) => String(n).trim()).filter((n) => n.length > 0); } if (typeof namesOrString === "string" && namesOrString.trim()) { return namesOrString.split(",").map((id) => id.trim()).filter((id) => id.length > 0); } return ["1"]; } // Update an existing zone async function updateZone(zoneId, name, namesOrString) { try { let names = normalizeTabNamesArg(namesOrString); if (!names.length) names = ["1"]; const response = await fetch(`/zones/${zoneId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: name, names: names }) }); const data = await response.json(); if (response.ok) { // Reload tabs list await loadZonesModal(); await loadZones(); // Close modal document.getElementById('edit-zone-modal').classList.remove('active'); return true; } else { alert(`Error: ${data.error || 'Failed to update zone'}`); return false; } } catch (error) { console.error('Failed to update zone:', error); alert('Failed to update zone'); return false; } } // Create a new zone async function createZone(name, namesOrString) { try { let names = normalizeTabNamesArg(namesOrString); if (!names.length) names = ["1"]; const response = await fetch('/zones', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: name, names: names }) }); const data = await response.json(); if (response.ok) { // Reload tabs list await loadZonesModal(); await loadZones(); // Select the new zone if (data && Object.keys(data).length > 0) { const newTabId = Object.keys(data)[0]; await selectZone(newTabId); } return true; } else { alert(`Error: ${data.error || 'Failed to create zone'}`); return false; } } catch (error) { console.error('Failed to create zone:', error); alert('Failed to create zone'); return false; } } // Initialize on page load document.addEventListener('DOMContentLoaded', () => { loadZones(); // Set up tabs modal const tabsButton = document.getElementById('zones-btn'); const zonesModal = document.getElementById('zones-modal'); const tabsCloseButton = document.getElementById('zones-close-btn'); const newTabNameInput = document.getElementById("new-zone-name"); const createZoneButton = document.getElementById("create-zone-btn"); if (tabsButton && zonesModal) { tabsButton.addEventListener("click", async () => { zonesModal.classList.add("active"); await loadZonesModal(); }); } if (tabsCloseButton) { tabsCloseButton.addEventListener('click', () => { zonesModal.classList.remove('active'); }); } // Right-click on a zone button in the main header bar to edit that zone document.addEventListener('contextmenu', async (event) => { if (!isEditModeActive()) { return; } const btn = event.target.closest('.zone-button'); if (!btn || !btn.dataset.zoneId) { return; } event.preventDefault(); const zoneId = btn.dataset.zoneId; try { const response = await fetch(`/zones/${zoneId}`); if (response.ok) { const zone = await response.json(); await openEditZoneModal(zoneId, zone); } else { alert('Failed to load zone for editing'); } } catch (error) { console.error('Failed to load zone:', error); alert('Failed to load zone for editing'); } }); // Set up create zone const createZoneHandler = async () => { if (!newTabNameInput) return; const name = newTabNameInput.value.trim(); if (name) { const deviceNames = await defaultDeviceNamesForNewTab(); await createZone(name, deviceNames); if (newTabNameInput) newTabNameInput.value = ""; } }; if (createZoneButton) { createZoneButton.addEventListener('click', createZoneHandler); } if (newTabNameInput) { newTabNameInput.addEventListener('keypress', (event) => { if (event.key === 'Enter') { createZoneHandler(); } }); } // Set up edit zone form const editZoneForm = document.getElementById('edit-zone-form'); if (editZoneForm) { editZoneForm.addEventListener("submit", async (e) => { e.preventDefault(); const idInput = document.getElementById("edit-zone-id"); const nameInput = document.getElementById("edit-zone-name"); const zoneId = idInput ? idInput.value : null; const name = nameInput ? nameInput.value.trim() : ""; const rows = window.__editTabDeviceRows || []; const deviceNames = rowsToNames(rows); if (zoneId && name) { if (deviceNames.length === 0) { alert("Add at least one device."); return; } await updateZone(zoneId, name, deviceNames); editZoneForm.reset(); } }); } // Profile-wide "Send Presets" button in header const sendProfilePresetsBtn = document.getElementById('send-profile-presets-btn'); if (sendProfilePresetsBtn) { sendProfilePresetsBtn.addEventListener('click', async () => { await sendProfilePresets(); }); } const menuBrightnessSlider = document.getElementById('menu-brightness-slider'); const headerBrightnessSlider = document.getElementById('header-brightness-slider'); const savedBr = loadSavedUiBrightness(); if (savedBr !== null) { applyBrightnessSliders(savedBr); } if (menuBrightnessSlider) { menuBrightnessSlider.addEventListener('input', (e) => { sendZoneBrightness(e.target.value); }); } if (headerBrightnessSlider) { headerBrightnessSlider.addEventListener('input', (e) => { sendZoneBrightness(e.target.value); }); // Apply saved (or default) level to devices once the page is ready. sendZoneBrightness(headerBrightnessSlider.value); } // When run/edit mode toggles, refresh tabs UI so edit actions show/hide immediately. document.querySelectorAll('.ui-mode-toggle').forEach((btn) => { btn.addEventListener('click', async () => { await loadZones(); if (zonesModal && zonesModal.classList.contains("active")) { await loadZonesModal(); } }); }); }); // Export for use in other scripts window.zonesManager = { loadZones, loadZonesModal, selectZone, createZone, updateZone, openEditZoneModal, resolveZoneDeviceMacs, resolveTabDeviceMacs: resolveZoneDeviceMacs, getCurrentZoneId: () => currentZoneId, }; window.tabsManager = window.zonesManager; window.tabsManager.getCurrentTabId = () => currentZoneId; window.tabsManager.loadTabs = loadZones; window.tabsManager.loadTabsModal = loadZonesModal; window.tabsManager.openEditTabModal = openEditZoneModal;