feat(zones): rename tabs to zones across api, ui, and storage

Made-with: Cursor
This commit is contained in:
pi
2026-04-06 18:22:03 +12:00
parent d1ffb857c8
commit fd618d7714
35 changed files with 1347 additions and 1303 deletions

View File

@@ -175,9 +175,9 @@ async function postDriverSequence(sequence, targetMacs, delayS) {
return res.json().catch(() => ({}));
}
// Send a select message for a preset to all devices on the current tab (ESP-NOW or Wi-Fi).
// Send a select message for a preset to all devices on the current zone (ESP-NOW or Wi-Fi).
const sendSelectForCurrentTabDevices = async (presetId, sectionEl) => {
const section = sectionEl || document.querySelector('.presets-section[data-tab-id]');
const section = sectionEl || document.querySelector('.presets-section[data-zone-id]');
if (!section || !presetId) {
return;
}
@@ -223,7 +223,7 @@ document.addEventListener('DOMContentLoaded', () => {
const presetBrightnessInput = document.getElementById('preset-brightness-input');
const presetDelayInput = document.getElementById('preset-delay-input');
const presetDefaultButton = document.getElementById('preset-default-btn');
const presetRemoveFromTabButton = document.getElementById('preset-remove-from-tab-btn');
const presetRemoveFromTabButton = document.getElementById('preset-remove-from-zone-btn');
const presetSaveButton = document.getElementById('preset-save-btn');
const presetAddFromPaletteButton = document.getElementById('preset-add-from-palette-btn');
@@ -623,8 +623,8 @@ document.addEventListener('DOMContentLoaded', () => {
if (currentEditTabId) {
return currentEditTabId;
}
const section = document.querySelector('.presets-section[data-tab-id]');
return section ? section.dataset.tabId : null;
const section = document.querySelector('.presets-section[data-zone-id]');
return section ? section.dataset.zoneId : null;
};
const updatePresetEditorTabActionsVisibility = () => {
@@ -634,12 +634,12 @@ document.addEventListener('DOMContentLoaded', () => {
};
const updateTabDefaultPreset = async (presetId) => {
const tabId = getActiveTabId();
if (!tabId) {
const zoneId = getActiveTabId();
if (!zoneId) {
return;
}
try {
const tabResponse = await fetch(`/tabs/${tabId}`, {
const tabResponse = await fetch(`/zones/${zoneId}`, {
headers: { Accept: 'application/json' },
});
if (!tabResponse.ok) {
@@ -647,13 +647,13 @@ document.addEventListener('DOMContentLoaded', () => {
}
const tabData = await tabResponse.json();
tabData.default_preset = presetId;
await fetch(`/tabs/${tabId}`, {
await fetch(`/zones/${zoneId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(tabData),
});
} catch (error) {
console.warn('Failed to save tab default preset:', error);
console.warn('Failed to save zone default preset:', error);
}
};
@@ -950,22 +950,22 @@ document.addEventListener('DOMContentLoaded', () => {
}
const showAddPresetToTabModal = async (optionalTabId) => {
let tabId = optionalTabId;
if (!tabId) {
// Get current tab ID from the presets section
const leftPanel = document.querySelector('.presets-section[data-tab-id]');
tabId = leftPanel ? leftPanel.dataset.tabId : null;
let zoneId = optionalTabId;
if (!zoneId) {
// Get current zone ID from the presets section
const leftPanel = document.querySelector('.presets-section[data-zone-id]');
zoneId = leftPanel ? leftPanel.dataset.zoneId : null;
}
if (!tabId) {
if (!zoneId) {
// Fallback: try to get from URL
const pathParts = window.location.pathname.split('/');
const tabIndex = pathParts.indexOf('tabs');
const tabIndex = pathParts.indexOf('zones');
if (tabIndex !== -1 && tabIndex + 1 < pathParts.length) {
tabId = pathParts[tabIndex + 1];
zoneId = pathParts[tabIndex + 1];
}
}
if (!tabId) {
alert('Could not determine current tab.');
if (!zoneId) {
alert('Could not determine current zone.');
return;
}
@@ -980,10 +980,10 @@ document.addEventListener('DOMContentLoaded', () => {
const allPresetsRaw = await response.json();
const allPresets = await filterPresetsForCurrentProfile(allPresetsRaw);
// Load only the current tab's presets so we can avoid duplicates within this tab.
// Load only the current zone's presets so we can avoid duplicates within this zone.
let currentTabPresets = [];
try {
const tabResponse = await fetch(`/tabs/${tabId}`, {
const tabResponse = await fetch(`/zones/${zoneId}`, {
headers: { Accept: 'application/json' },
});
if (tabResponse.ok) {
@@ -999,19 +999,19 @@ document.addEventListener('DOMContentLoaded', () => {
}
}
} catch (e) {
console.warn('Could not load current tab presets:', e);
console.warn('Could not load current zone presets:', e);
}
// Create modal
const modal = document.createElement('div');
modal.className = 'modal active';
modal.id = 'add-preset-to-tab-modal';
modal.id = 'add-preset-to-zone-modal';
modal.innerHTML = `
<div class="modal-content">
<h2>Add Preset to Tab</h2>
<h2>Add Preset to Zone</h2>
<div id="add-preset-list" class="profiles-list" style="max-height: 400px; overflow-y: auto;"></div>
<div class="modal-actions">
<button class="btn btn-secondary" id="add-preset-to-tab-close-btn">Close</button>
<button class="btn btn-secondary" id="add-preset-to-zone-close-btn">Close</button>
</div>
</div>
`;
@@ -1023,7 +1023,7 @@ document.addEventListener('DOMContentLoaded', () => {
const availableToAdd = presetNames.filter(presetId => !currentTabPresets.includes(presetId));
if (availableToAdd.length === 0) {
listContainer.innerHTML = '<p class="muted-text">No presets to add. All presets are already in this tab, or create a preset first.</p>';
listContainer.innerHTML = '<p class="muted-text">No presets to add. All presets are already in this zone, or create a preset first.</p>';
} else {
availableToAdd.forEach(presetId => {
const preset = allPresets[presetId];
@@ -1042,7 +1042,7 @@ document.addEventListener('DOMContentLoaded', () => {
addButton.className = 'btn btn-primary btn-small';
addButton.textContent = 'Add';
addButton.addEventListener('click', async () => {
await addPresetToTab(presetId, tabId);
await addPresetToTab(presetId, zoneId);
modal.remove();
});
@@ -1054,7 +1054,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
// Close button handler
document.getElementById('add-preset-to-tab-close-btn').addEventListener('click', () => {
document.getElementById('add-preset-to-zone-close-btn').addEventListener('click', () => {
modal.remove();
});
@@ -1067,34 +1067,34 @@ document.addEventListener('DOMContentLoaded', () => {
window.showAddPresetToTabModal = showAddPresetToTabModal;
} catch (e) {}
const addPresetToTab = async (presetId, tabId) => {
if (!tabId) {
// Try to get tab ID from the left-panel
const leftPanel = document.querySelector('.presets-section[data-tab-id]');
tabId = leftPanel ? leftPanel.dataset.tabId : null;
const addPresetToTab = async (presetId, zoneId) => {
if (!zoneId) {
// Try to get zone ID from the left-panel
const leftPanel = document.querySelector('.presets-section[data-zone-id]');
zoneId = leftPanel ? leftPanel.dataset.zoneId : null;
if (!tabId) {
if (!zoneId) {
// Fallback: try to get from URL
const pathParts = window.location.pathname.split('/');
const tabIndex = pathParts.indexOf('tabs');
const tabIndex = pathParts.indexOf('zones');
if (tabIndex !== -1 && tabIndex + 1 < pathParts.length) {
tabId = pathParts[tabIndex + 1];
zoneId = pathParts[tabIndex + 1];
}
}
}
if (!tabId) {
alert('Could not determine current tab.');
if (!zoneId) {
alert('Could not determine current zone.');
return;
}
try {
// Get current tab data
const tabResponse = await fetch(`/tabs/${tabId}`, {
// Get current zone data
const tabResponse = await fetch(`/zones/${zoneId}`, {
headers: { Accept: 'application/json' },
});
if (!tabResponse.ok) {
throw new Error('Failed to load tab');
throw new Error('Failed to load zone');
}
const tabData = await tabResponse.json();
@@ -1111,7 +1111,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
if (flat.includes(presetId)) {
alert('Preset is already added to this tab.');
alert('Preset is already added to this zone.');
return;
}
@@ -1120,23 +1120,23 @@ document.addEventListener('DOMContentLoaded', () => {
tabData.presets = newGrid;
tabData.presets_flat = flat;
// Update tab
const updateResponse = await fetch(`/tabs/${tabId}`, {
// Update zone
const updateResponse = await fetch(`/zones/${zoneId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(tabData),
});
if (!updateResponse.ok) {
throw new Error('Failed to update tab');
throw new Error('Failed to update zone');
}
// Reload the tab content to show the new preset
// Reload the zone content to show the new preset
if (typeof renderTabPresets === 'function') {
await renderTabPresets(tabId);
await renderTabPresets(zoneId);
} else if (window.htmx) {
htmx.ajax('GET', `/tabs/${tabId}/content-fragment`, {
target: '#tab-content',
htmx.ajax('GET', `/zones/${zoneId}/content-fragment`, {
target: '#zone-content',
swap: 'innerHTML'
});
} else {
@@ -1144,8 +1144,8 @@ document.addEventListener('DOMContentLoaded', () => {
window.location.reload();
}
} catch (error) {
console.error('Failed to add preset to tab:', error);
alert('Failed to add preset to tab.');
console.error('Failed to add preset to zone:', error);
alert('Failed to add preset to zone.');
}
};
try {
@@ -1269,8 +1269,8 @@ document.addEventListener('DOMContentLoaded', () => {
alert('Preset name is required to send.');
return;
}
// Send current editor values and then select on all devices in the current tab (if any)
const section = document.querySelector('.presets-section[data-tab-id]');
// Send current editor values and then select on all devices in the current zone (if any)
const section = document.querySelector('.presets-section[data-zone-id]');
const deviceNames = tabDeviceNamesFromSection(section);
// Work out the preset ID: for existing presets use currentEditId, otherwise fall back to name
const presetId = currentEditId || payload.name;
@@ -1286,7 +1286,7 @@ document.addEventListener('DOMContentLoaded', () => {
alert('Preset name is required.');
return;
}
const section = document.querySelector('.presets-section[data-tab-id]');
const section = document.querySelector('.presets-section[data-zone-id]');
const deviceNames = tabDeviceNamesFromSection(section);
const presetId = currentEditId || payload.name;
await updateTabDefaultPreset(presetId);
@@ -1297,7 +1297,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (presetRemoveFromTabButton) {
presetRemoveFromTabButton.addEventListener('click', async () => {
if (!currentEditTabId || !currentEditId) return;
if (!window.confirm('Remove this preset from this tab?')) return;
if (!window.confirm('Remove this preset from this zone?')) return;
await removePresetFromTab(currentEditTabId, currentEditId);
clearForm();
closeEditor();
@@ -1348,12 +1348,12 @@ document.addEventListener('DOMContentLoaded', () => {
clearForm();
closeEditor();
// Reload tab presets if we're in a tab view
const leftPanel = document.querySelector('.presets-section[data-tab-id]');
// Reload zone presets if we're in a zone view
const leftPanel = document.querySelector('.presets-section[data-zone-id]');
if (leftPanel) {
const tabId = leftPanel.dataset.tabId;
if (tabId && typeof renderTabPresets !== 'undefined') {
renderTabPresets(tabId);
const zoneId = leftPanel.dataset.zoneId;
if (zoneId && typeof renderTabPresets !== 'undefined') {
renderTabPresets(zoneId);
}
}
} catch (error) {
@@ -1362,11 +1362,11 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
// Listen for edit preset events from tab preset buttons
// Listen for edit preset events from zone preset buttons
document.addEventListener('editPreset', async (event) => {
const { presetId, preset, tabId } = event.detail;
const { presetId, preset, zoneId } = event.detail;
currentEditId = presetId;
currentEditTabId = tabId || null;
currentEditTabId = zoneId || null;
await loadPatterns();
const paletteColors = await getCurrentProfilePaletteColors();
setFormValues({
@@ -1478,11 +1478,11 @@ const sendDefaultPreset = async (presetId, deviceNames) => {
}
};
// Expose for other scripts (tabs.js) so they can reuse the shared WebSocket.
// Expose for other scripts (zones.js) so they can reuse the shared WebSocket.
try {
window.sendPresetViaEspNow = sendPresetViaEspNow;
window.postDriverSequence = postDriverSequence;
// Expose a generic ESPNow sender so other scripts (tabs.js) can send
// Expose a generic ESPNow sender so other scripts (zones.js) can send
// non-preset messages such as global brightness.
window.sendEspnowRaw = sendEspnowMessage;
window.getEspnowSocket = getEspnowSocket;
@@ -1490,9 +1490,9 @@ try {
// window may not exist in some environments; ignore.
}
// Store selected preset per tab
// Store selected preset per zone
const selectedPresets = {};
// Run vs Edit for tab preset strip (in-memory only — each full page load starts in run mode)
// Run vs Edit for zone preset strip (in-memory only — each full page load starts in run mode)
let presetUiMode = 'run';
const getPresetUiMode = () => (presetUiMode === 'edit' ? 'edit' : 'run');
@@ -1559,15 +1559,15 @@ const arrayToGrid = (presetIds, columns = 3) => {
return grid;
};
// Function to save preset grid for a tab
const savePresetGrid = async (tabId, presetGrid) => {
// Function to save preset grid for a zone
const savePresetGrid = async (zoneId, presetGrid) => {
try {
// Get current tab data
const tabResponse = await fetch(`/tabs/${tabId}`, {
// Get current zone data
const tabResponse = await fetch(`/zones/${zoneId}`, {
headers: { Accept: 'application/json' },
});
if (!tabResponse.ok) {
throw new Error('Failed to load tab');
throw new Error('Failed to load zone');
}
const tabData = await tabResponse.json();
@@ -1576,8 +1576,8 @@ const savePresetGrid = async (tabId, presetGrid) => {
// Also store as flat array for backward compatibility
tabData.presets_flat = presetGrid.flat();
// Save updated tab
const updateResponse = await fetch(`/tabs/${tabId}`, {
// Save updated zone
const updateResponse = await fetch(`/zones/${zoneId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(tabData),
@@ -1631,18 +1631,18 @@ const insertDraggingOntoTarget = (presetsList, dragging, dropTarget) => {
}
};
// Function to render presets for a specific tab in 2D grid
const renderTabPresets = async (tabId) => {
const presetsList = document.getElementById('presets-list-tab');
// Function to render presets for a specific zone in 2D grid
const renderTabPresets = async (zoneId) => {
const presetsList = document.getElementById('presets-list-zone');
if (!presetsList) return;
try {
// Get tab data to see which presets are associated
const tabResponse = await fetch(`/tabs/${tabId}`, {
// Get zone data to see which presets are associated
const tabResponse = await fetch(`/zones/${zoneId}`, {
headers: { Accept: 'application/json' },
});
if (!tabResponse.ok) {
throw new Error('Failed to load tab');
throw new Error('Failed to load zone');
}
const tabData = await tabResponse.json();
@@ -1669,7 +1669,7 @@ const renderTabPresets = async (tabId) => {
const paletteColors = await getCurrentProfilePaletteColors();
presetsList.innerHTML = '';
presetsList.dataset.reorderTabId = tabId;
presetsList.dataset.reorderTabId = zoneId;
// Drag-and-drop on the list (wire once — re-render would duplicate listeners otherwise)
if (!presetsList.dataset.dragWired) {
@@ -1719,7 +1719,7 @@ const renderTabPresets = async (tabId) => {
try {
if (!saveId) {
console.warn('No tab id for preset reorder save');
console.warn('No zone id for preset reorder save');
return;
}
await savePresetGrid(saveId, newGrid);
@@ -1733,19 +1733,19 @@ const renderTabPresets = async (tabId) => {
});
}
// Get the currently selected preset for this tab
const selectedPresetId = selectedPresets[tabId];
// Get the currently selected preset for this zone
const selectedPresetId = selectedPresets[zoneId];
// Render presets in grid layout
// Flatten the grid and render all presets (grid CSS will handle layout)
const flatPresets = presetGrid.flat().filter(id => id);
if (flatPresets.length === 0) {
// Show empty message if this tab has no presets
// Show empty message if this zone has no presets
const empty = document.createElement('p');
empty.className = 'muted-text';
empty.style.gridColumn = '1 / -1'; // Span all columns
empty.textContent = 'No presets added to this tab. Open the tab\'s Edit menu and click "Add Preset" to add one.';
empty.textContent = 'No presets added to this zone. Open the zone\'s Edit menu and click "Add Preset" to add one.';
presetsList.appendChild(empty);
} else {
flatPresets.forEach((presetId) => {
@@ -1756,18 +1756,18 @@ const renderTabPresets = async (tabId) => {
...preset,
colors: resolveColorsWithPaletteRefs(preset.colors, preset.palette_refs, paletteColors),
};
const wrapper = createPresetButton(presetId, displayPreset, tabId, isSelected);
const wrapper = createPresetButton(presetId, displayPreset, zoneId, isSelected);
presetsList.appendChild(wrapper);
}
});
}
} catch (error) {
console.error('Failed to render tab presets:', error);
console.error('Failed to render zone presets:', error);
presetsList.innerHTML = '<p class="muted-text">Failed to load presets.</p>';
}
};
const createPresetButton = (presetId, preset, tabId, isSelected = false) => {
const createPresetButton = (presetId, preset, zoneId, isSelected = false) => {
const uiMode = getPresetUiMode();
const row = document.createElement('div');
@@ -1806,12 +1806,12 @@ const createPresetButton = (presetId, preset, tabId, isSelected = false) => {
button.addEventListener('click', () => {
if (isDraggingPreset) return;
const presetsListEl = document.getElementById('presets-list-tab');
const presetsListEl = document.getElementById('presets-list-zone');
if (presetsListEl) {
presetsListEl.querySelectorAll('.pattern-button').forEach((btn) => btn.classList.remove('active'));
}
button.classList.add('active');
selectedPresets[tabId] = presetId;
selectedPresets[zoneId] = presetId;
const section = row.closest('.presets-section');
sendSelectForCurrentTabDevices(presetId, section).catch((err) => {
console.error(err);
@@ -1828,7 +1828,7 @@ const createPresetButton = (presetId, preset, tabId, isSelected = false) => {
row.addEventListener('dragend', () => {
row.classList.remove('dragging');
const presetsListEl = document.getElementById('presets-list-tab');
const presetsListEl = document.getElementById('presets-list-zone');
if (presetsListEl) {
delete presetsListEl.dataset.dropTargetId;
}
@@ -1854,7 +1854,7 @@ const createPresetButton = (presetId, preset, tabId, isSelected = false) => {
e.preventDefault();
e.stopPropagation();
if (isDraggingPreset) return;
editPresetFromTab(presetId, tabId, preset);
editPresetFromTab(presetId, zoneId, preset);
});
actions.appendChild(editBtn);
@@ -1864,7 +1864,7 @@ const createPresetButton = (presetId, preset, tabId, isSelected = false) => {
return row;
};
const editPresetFromTab = async (presetId, tabId, existingPreset) => {
const editPresetFromTab = async (presetId, zoneId, existingPreset) => {
try {
let preset = existingPreset;
if (!preset) {
@@ -1880,7 +1880,7 @@ const editPresetFromTab = async (presetId, tabId, existingPreset) => {
// Dispatch a custom event to trigger the edit in the DOMContentLoaded scope
const editEvent = new CustomEvent('editPreset', {
detail: { presetId, preset, tabId }
detail: { presetId, preset, zoneId }
});
document.dispatchEvent(editEvent);
} catch (error) {
@@ -1889,36 +1889,36 @@ const editPresetFromTab = async (presetId, tabId, existingPreset) => {
}
};
// Remove a preset from a specific tab (does not delete the preset itself)
// Expected call style: removePresetFromTab(tabId, presetId)
const removePresetFromTab = async (tabId, presetId) => {
if (!tabId) {
// Try to get tab ID from the left-panel
const leftPanel = document.querySelector('.presets-section[data-tab-id]');
tabId = leftPanel ? leftPanel.dataset.tabId : null;
// Remove a preset from a specific zone (does not delete the preset itself)
// Expected call style: removePresetFromTab(zoneId, presetId)
const removePresetFromTab = async (zoneId, presetId) => {
if (!zoneId) {
// Try to get zone ID from the left-panel
const leftPanel = document.querySelector('.presets-section[data-zone-id]');
zoneId = leftPanel ? leftPanel.dataset.zoneId : null;
if (!tabId) {
if (!zoneId) {
// Fallback: try to get from URL
const pathParts = window.location.pathname.split('/');
const tabIndex = pathParts.indexOf('tabs');
const tabIndex = pathParts.indexOf('zones');
if (tabIndex !== -1 && tabIndex + 1 < pathParts.length) {
tabId = pathParts[tabIndex + 1];
zoneId = pathParts[tabIndex + 1];
}
}
}
if (!tabId) {
alert('Could not determine current tab.');
if (!zoneId) {
alert('Could not determine current zone.');
return;
}
try {
// Get current tab data
const tabResponse = await fetch(`/tabs/${tabId}`, {
// Get current zone data
const tabResponse = await fetch(`/zones/${zoneId}`, {
headers: { Accept: 'application/json' },
});
if (!tabResponse.ok) {
throw new Error('Failed to load tab');
throw new Error('Failed to load zone');
}
const tabData = await tabResponse.json();
@@ -1937,7 +1937,7 @@ const removePresetFromTab = async (tabId, presetId) => {
const beforeLen = flat.length;
flat = flat.filter(id => String(id) !== String(presetId));
if (flat.length === beforeLen) {
alert('Preset is not in this tab.');
alert('Preset is not in this zone.');
return;
}
@@ -1945,19 +1945,19 @@ const removePresetFromTab = async (tabId, presetId) => {
tabData.presets = newGrid;
tabData.presets_flat = flat;
const updateResponse = await fetch(`/tabs/${tabId}`, {
const updateResponse = await fetch(`/zones/${zoneId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(tabData),
});
if (!updateResponse.ok) {
throw new Error('Failed to update tab presets');
throw new Error('Failed to update zone presets');
}
await renderTabPresets(tabId);
await renderTabPresets(zoneId);
} catch (error) {
console.error('Failed to remove preset from tab:', error);
alert('Failed to remove preset from tab.');
console.error('Failed to remove preset from zone:', error);
alert('Failed to remove preset from zone.');
}
};
try {
@@ -1966,13 +1966,13 @@ try {
// Listen for HTMX swaps to render presets
document.body.addEventListener('htmx:afterSwap', (event) => {
if (event.target && event.target.id === 'tab-content') {
// Get tab ID from the left-panel
const leftPanel = document.querySelector('.presets-section[data-tab-id]');
if (event.target && event.target.id === 'zone-content') {
// Get zone ID from the left-panel
const leftPanel = document.querySelector('.presets-section[data-zone-id]');
if (leftPanel) {
const tabId = leftPanel.dataset.tabId;
if (tabId) {
renderTabPresets(tabId);
const zoneId = leftPanel.dataset.zoneId;
if (zoneId) {
renderTabPresets(zoneId);
}
}
}
@@ -1993,9 +1993,9 @@ document.addEventListener('DOMContentLoaded', () => {
}
const mainMenu = document.getElementById('main-menu-dropdown');
if (mainMenu) mainMenu.classList.remove('open');
const leftPanel = document.querySelector('.presets-section[data-tab-id]');
const leftPanel = document.querySelector('.presets-section[data-zone-id]');
if (leftPanel) {
renderTabPresets(leftPanel.dataset.tabId);
renderTabPresets(leftPanel.dataset.zoneId);
}
});
});