docs(ui): update help assets and regenerate help pdf

This commit is contained in:
pi
2026-03-26 00:40:40 +13:00
parent ec39df00fc
commit 09a87b79d2
18 changed files with 478 additions and 133 deletions

View File

@@ -60,6 +60,12 @@ document.addEventListener('DOMContentLoaded', () => {
if (nameInput && data && typeof data === 'object') {
nameInput.value = data.device_name || 'led-controller';
}
const chInput = document.getElementById('wifi-channel-input');
if (chInput && data && typeof data === 'object') {
const ch = data.wifi_channel;
chInput.value =
ch !== undefined && ch !== null && ch !== '' ? String(ch) : '6';
}
} catch (error) {
console.error('Error loading device settings:', error);
}
@@ -116,15 +122,29 @@ document.addEventListener('DOMContentLoaded', () => {
showSettingsMessage('Device name is required', 'error');
return;
}
const chRaw = document.getElementById('wifi-channel-input')
? document.getElementById('wifi-channel-input').value
: '6';
const wifiChannel = parseInt(chRaw, 10);
if (Number.isNaN(wifiChannel) || wifiChannel < 1 || wifiChannel > 11) {
showSettingsMessage('WiFi channel must be between 1 and 11', 'error');
return;
}
try {
const response = await fetch('/settings/settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ device_name: deviceName }),
body: JSON.stringify({
device_name: deviceName,
wifi_channel: wifiChannel,
}),
});
const result = await response.json();
if (response.ok) {
showSettingsMessage('Device name saved. It will be used on next restart.', 'success');
showSettingsMessage(
'Device settings saved. They will apply on next restart where relevant.',
'success',
);
} else {
showSettingsMessage(`Error: ${result.error || 'Failed to save device name'}`, 'error');
}

View File

@@ -174,6 +174,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 presetSaveButton = document.getElementById('preset-save-btn');
const presetAddFromPaletteButton = document.getElementById('preset-add-from-palette-btn');
@@ -532,6 +533,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
}
}
updatePresetEditorTabActionsVisibility();
};
const clearForm = () => {
@@ -565,6 +567,7 @@ document.addEventListener('DOMContentLoaded', () => {
presetPatternInput.style.backgroundColor = '';
presetPatternInput.style.cursor = '';
}
updatePresetEditorTabActionsVisibility();
};
const getActiveTabId = () => {
@@ -575,6 +578,12 @@ document.addEventListener('DOMContentLoaded', () => {
return section ? section.dataset.tabId : null;
};
const updatePresetEditorTabActionsVisibility = () => {
if (!presetRemoveFromTabButton) return;
const show = Boolean(currentEditTabId && currentEditId);
presetRemoveFromTabButton.hidden = !show;
};
const updateTabDefaultPreset = async (presetId) => {
const tabId = getActiveTabId();
if (!tabId) {
@@ -786,6 +795,7 @@ document.addEventListener('DOMContentLoaded', () => {
editButton.textContent = 'Edit';
editButton.addEventListener('click', async () => {
currentEditId = presetId;
currentEditTabId = null;
const paletteColors = await getCurrentProfilePaletteColors();
const presetForEditor = {
...(preset || {}),
@@ -1241,6 +1251,16 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
if (presetRemoveFromTabButton) {
presetRemoveFromTabButton.addEventListener('click', async () => {
if (!currentEditTabId || !currentEditId) return;
if (!window.confirm('Remove this preset from this tab?')) return;
await removePresetFromTab(currentEditTabId, currentEditId);
clearForm();
closeEditor();
});
}
presetSaveButton.addEventListener('click', async () => {
const payload = buildPresetPayload();
if (!payload.name) {
@@ -1778,58 +1798,7 @@ const createPresetButton = (presetId, preset, tabId, isSelected = false) => {
editPresetFromTab(presetId, tabId, preset);
});
const defaultBtn = document.createElement('button');
defaultBtn.type = 'button';
defaultBtn.className = 'btn btn-secondary btn-small';
defaultBtn.textContent = 'Default';
defaultBtn.title = 'Set as default preset';
defaultBtn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
if (isDraggingPreset) return;
const section = row.closest('.presets-section');
const namesAttr = section && section.getAttribute('data-device-names');
const deviceNames = namesAttr
? namesAttr.split(',').map((n) => n.trim()).filter((n) => n.length > 0)
: [];
sendDefaultPreset(presetId, deviceNames);
// Persist tab-level default if we know the tab from this tile.
if (tabId) {
try {
const tabResponse = await fetch(`/tabs/${tabId}`, {
headers: { Accept: 'application/json' },
});
if (tabResponse.ok) {
const tabData = await tabResponse.json();
tabData.default_preset = presetId;
await fetch(`/tabs/${tabId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(tabData),
});
}
} catch (error) {
console.warn('Failed to save tab default preset:', error);
}
}
});
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'btn btn-danger btn-small';
removeBtn.textContent = 'Remove';
removeBtn.title = 'Remove from this tab';
removeBtn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
if (isDraggingPreset) return;
if (!window.confirm('Remove this preset from this tab?')) return;
await removePresetFromTab(tabId, presetId);
});
actions.appendChild(editBtn);
actions.appendChild(defaultBtn);
actions.appendChild(removeBtn);
row.appendChild(actions);
}

View File

@@ -620,15 +620,21 @@ body.preset-ui-run .edit-mode-only {
height: 5rem;
}
/* Edit only beside the preset tile in edit mode. */
.preset-tile-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-auto-rows: 1fr;
display: flex;
flex-direction: column;
justify-content: stretch;
gap: 0.2rem;
align-content: stretch;
flex-shrink: 0;
padding: 0.15rem 0 0.15rem 0.25rem;
width: 6.5rem;
width: auto;
min-width: 0;
}
.preset-editor-modal-actions {
flex-wrap: wrap;
gap: 0.35rem;
}
.preset-tile-actions .btn {

View File

@@ -142,13 +142,6 @@ function renderTabsListModal(tabs, tabOrder, currentTabId) {
openEditTabModal(tabId, tab);
});
const sendPresetsButton = document.createElement("button");
sendPresetsButton.className = "btn btn-secondary btn-small";
sendPresetsButton.textContent = "Send Presets";
sendPresetsButton.addEventListener("click", async () => {
await sendTabPresets(tabId);
});
const cloneButton = document.createElement("button");
cloneButton.className = "btn btn-secondary btn-small";
cloneButton.textContent = "Clone";
@@ -233,7 +226,6 @@ function renderTabsListModal(tabs, tabOrder, currentTabId) {
row.appendChild(label);
row.appendChild(applyButton);
row.appendChild(sendPresetsButton);
if (editMode) {
row.appendChild(editButton);
row.appendChild(cloneButton);
@@ -373,69 +365,6 @@ async function loadTabContent(tabId) {
}
}
// Send all presets used by a tab via the /presets/send HTTP endpoint.
async function sendTabPresets(tabId) {
try {
// Load tab data to determine which presets are used
const tabResponse = await fetch(`/tabs/${tabId}`, {
headers: { Accept: 'application/json' },
});
if (!tabResponse.ok) {
alert('Failed to load tab to send presets.');
return;
}
const tabData = await tabResponse.json();
// Extract preset IDs from tab (supports grid, flat, and legacy formats)
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') {
// Flat array of IDs
presetIds = tabData.presets;
} else if (tabData.presets.length && Array.isArray(tabData.presets[0])) {
// 2D grid
presetIds = tabData.presets.flat();
}
}
presetIds = (presetIds || []).filter(Boolean);
if (!presetIds.length) {
alert('This tab has no presets to send.');
return;
}
// Call server-side ESPNow sender with just the IDs; it handles chunking.
const payload = { preset_ids: presetIds };
if (tabData.default_preset) {
payload.default = tabData.default_preset;
}
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.';
alert(msg);
return;
}
const sent = typeof data.presets_sent === 'number' ? data.presets_sent : presetIds.length;
const messages = typeof data.messages_sent === 'number' ? data.messages_sent : '?';
alert(`Sent ${sent} preset(s) in ${messages} ESPNow message(s).`);
} catch (error) {
console.error('Failed to send tab presets:', error);
alert('Failed to send tab presets.');
}
}
// Send all presets used by all tabs in the current profile via /presets/send.
async function sendProfilePresets() {
try {

View File

@@ -179,9 +179,10 @@
<input type="number" id="preset-n8-input" min="0" max="255" value="0" class="n-input">
</div>
</div>
<div class="modal-actions">
<div class="modal-actions preset-editor-modal-actions">
<button class="btn btn-secondary" id="preset-send-btn">Try</button>
<button class="btn btn-secondary" id="preset-default-btn">Default</button>
<button type="button" class="btn btn-danger" id="preset-remove-from-tab-btn" hidden>Remove from tab</button>
<button class="btn btn-primary" id="preset-save-btn">Save &amp; Send</button>
<button class="btn btn-secondary" id="preset-editor-close-btn">Close</button>
</div>
@@ -262,8 +263,13 @@
<input type="text" id="device-name-input" name="device_name" placeholder="e.g. led-controller" required>
<small>This name may be used for mDNS (e.g. <code>name.local</code>) and UI display.</small>
</div>
<div class="form-group">
<label for="wifi-channel-input">WiFi channel (ESP-NOW)</label>
<input type="number" id="wifi-channel-input" name="wifi_channel" min="1" max="11" required>
<small>STA channel (111) for LED drivers and the serial bridge. Use the same value everywhere.</small>
</div>
<div class="btn-group">
<button type="submit" class="btn btn-primary btn-full">Save Name</button>
<button type="submit" class="btn btn-primary btn-full">Save device settings</button>
</div>
</form>
</div>

View File

@@ -170,11 +170,26 @@
<div class="settings-header">
<h1>Device Settings</h1>
<p>Configure WiFi Access Point settings</p>
<p>Configure WiFi Access Point and ESP-NOW options</p>
</div>
<div id="message" class="message"></div>
<!-- ESP-NOW (LED driver / bridge channel) -->
<div class="settings-section">
<h2>ESP-NOW</h2>
<form id="espnow-form">
<div class="form-group">
<label for="wifi-channel-page-input">WiFi channel (ESP-NOW)</label>
<input type="number" id="wifi-channel-page-input" name="wifi_channel" min="1" max="11" required>
<small>STA channel (111) for LED drivers and the serial bridge. Use the same value on every device.</small>
</div>
<div class="btn-group">
<button type="submit" class="btn btn-primary btn-full">Save channel</button>
</div>
</form>
</div>
<!-- WiFi Access Point Settings -->
<div class="settings-section">
<h2>WiFi Access Point Settings</h2>
@@ -222,6 +237,46 @@
}, 5000);
}
async function loadEspnowChannel() {
try {
const response = await fetch('/settings');
const data = await response.json();
const chInput = document.getElementById('wifi-channel-page-input');
if (chInput && data && typeof data === 'object') {
const ch = data.wifi_channel;
chInput.value =
ch !== undefined && ch !== null && ch !== '' ? String(ch) : '6';
}
} catch (error) {
console.error('Error loading ESP-NOW channel:', error);
}
}
document.getElementById('espnow-form').addEventListener('submit', async (e) => {
e.preventDefault();
const chRaw = document.getElementById('wifi-channel-page-input').value;
const wifiChannel = parseInt(chRaw, 10);
if (Number.isNaN(wifiChannel) || wifiChannel < 1 || wifiChannel > 11) {
showMessage('WiFi channel must be between 1 and 11', 'error');
return;
}
try {
const response = await fetch('/settings/settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ wifi_channel: wifiChannel }),
});
const result = await response.json();
if (response.ok) {
showMessage('ESP-NOW channel saved.', 'success');
} else {
showMessage(`Error: ${result.error || 'Failed to save'}`, 'error');
}
} catch (error) {
showMessage(`Error: ${error.message}`, 'error');
}
});
// Load AP status and config
async function loadAPStatus() {
try {
@@ -299,6 +354,7 @@
});
// Load all data on page load
loadEspnowChannel();
loadAPStatus();
// Refresh status every 10 seconds