Skip to content

Commit c284d0e

Browse files
fhirschmannclaude
andcommitted
feat: auto-save General + Buttons settings tabs (no submit button)
The General and Buttons settings sub-tabs persist changes automatically instead of requiring a submit press: - Debounced (~0.6s) genSettings() on any change in #settings-general / #settings-buttons (delegated, so the dynamically populated button dropdowns are covered too). - A time-window guard suppresses auto-save while fillSettings() programmatically populates the form (immune to early returns), so loading never triggers a save. - Submit buttons on both tabs replaced with a small 'changes saved automatically' hint; Reset now applies defaults immediately (persists after repopulating). - Other tabs (WiFi, security, MQTT, FTP, Bluetooth) deliberately keep submit (half-typed credentials / reboot on BT-mode change). i18n de/en/fr. Verified on the test device via Playwright: toggling a General checkbox and changing a Buttons dropdown both persist without submit; populating the form triggers no save; values restored; no console errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 0863f75 commit c284d0e

5 files changed

Lines changed: 28 additions & 6 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ neon logo that doubles as the SVG favicon ([`7be5254`](../../commit/7be5254)):
137137
| **M3U playlist builder**: a "Create playlist" button in the Files tab opens a two-pane builder — browse the SD card on the left, an ordered (drag-to-reorder) track list on the right — to assemble an `.m3u` from SD files, whole folders, and webradio URLs (which `LOCAL_M3U` plays interchangeably). Saving writes `/Playlists/<name>.m3u` via a new `POST /playlist` endpoint; the file can then be assigned to an RFID tag with play mode 11. i18n de/en/fr | [`af041fc`](../../commit/af041fc) |
138138
| **File browser: assignment badges + context-menu RFID learn (scan-after-select)**: in the Files-tab tree each row is badged (right-aligned, after the name) when that exact path has an **RFID tag** assigned (tag icon) and/or a **per-path EQ rule** (sliders icon), so you can see at a glance what is mapped where. The explorer **context menu** learns/removes RFID assignments directly: right-click a file/folder → **RFID anlernen** → pick a play mode, then **lay the card** — the next scanned card is assigned to that path (no tab switch / submit); or **remove** an existing assignment. Mirrors the existing EQ-profile submenu. Works with physical cards and the [virtual cards](#virtual-rfid-cards) (`CMD_VIRTUAL_RFID_CARD_*`) alike. Badges refresh when a folder is (re)opened and live after assigning/removing. i18n de/en/fr | [`7b8e303`](../../commit/7b8e303) |
139139
| **No standby while on external power**: optional setting (General tab) that keeps the device from entering the inactivity standby while it is on external power. The `complete` board has no charge/USB sense, so "on power" is inferred heuristically from the battery voltage against a configurable threshold (default **3.5 V** — the LiFePO4 charge voltage sits above its resting voltage). The manual sleep timer is unaffected. i18n de/en/fr | [`no-sleep-on-power`](../../releases/tag/no-sleep-on-power) |
140+
| **Auto-save settings (General + Buttons)**: the **General** and **Buttons** settings sub-tabs no longer have a submit button — changes are persisted automatically (debounced ~0.6 s) as soon as you toggle/select or leave a field. Populating the form on load is suppressed so it never triggers a spurious save; the **Reset** button now applies the defaults right away. The credential/connection tabs (WiFi, security, MQTT, FTP, Bluetooth) keep their explicit submit button on purpose (half-typed passwords / reboot on Bluetooth-mode change). i18n de/en/fr | [`settings-autosave`](../../releases/tag/settings-autosave) |
140141
| **OpenAPI/Swagger spec updated**: the bundled `REST_API.yaml` (served at `/swagger.html`) now documents all fork-added endpoints (`/sync`, `/syncstop`, `/playlist`, `/githubupdate`, `/version`, `/login`, `/logout`, `/security`, `/eqrule(s)`, `/activeequalizer`, `/bluetooth*`, `/sdclean`) and declares the security schemes (`X-API-Key` header / `apikey` query param, or the `ESPUINO_SESSION` login cookie) so the Swagger "Authorize" button works. The web-interface footer links to the Swagger UI (`/swagger.html`) | [`f8298d9`](../../commit/f8298d9) |
141142

142143
#### Feature highlights

‎html/locales/de.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@
114114
"rotary": {
115115
"title": "Drehimpulsgeber",
116116
"reverse": "Drehrichtung umkehren"
117-
}
117+
},
118+
"autosaveHint": "Änderungen werden automatisch gespeichert"
118119
},
119120
"security": {
120121
"title": "Webinterface-Passwort",

‎html/locales/en.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@
114114
"rotary": {
115115
"title": "Rotary encoder",
116116
"reverse": "Reverse direction of rotation"
117-
}
117+
},
118+
"autosaveHint": "Changes are saved automatically"
118119
},
119120
"security": {
120121
"title": "Web interface password",

‎html/locales/fr.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@
107107
"rotary": {
108108
"title": "Codeur rotatif",
109109
"reverse": "Inversion du sens de rotation"
110-
}
110+
},
111+
"autosaveHint": "Les modifications sont enregistrées automatiquement"
111112
},
112113
"security": {
113114
"title": "Mot de passe de l'interface web",

‎html/management.html‎

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2540,8 +2540,8 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
25402540
</span>
25412541
<div class="text-center">
25422542
<button type="button" class="btn btn-warning" data-i18n="reset"
2543-
onclick="resetSettings()"></button>&nbsp;<button type="submit" class="btn btn-primary"
2544-
data-i18n="submit"></button>
2543+
onclick="resetSettings()"></button>
2544+
<span class="ms-2 small text-secondary" data-i18n="settingsex.autosaveHint"></span>
25452545
</div>
25462546
</form>
25472547
</div>
@@ -2681,7 +2681,7 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
26812681
</div>
26822682
</fieldset>
26832683
<div class="text-center mt-3">
2684-
<button type="button" class="btn btn-primary" onclick="genSettings('generalConfig')" data-i18n="submit"></button>
2684+
<span class="small text-secondary" data-i18n="settingsex.autosaveHint"></span>
26852685
</div>
26862686
</div>
26872687
</div>
@@ -4539,10 +4539,24 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
45394539
toaster.info("Connecting to " + address + "...");
45404540
}
45414541

4542+
// Auto-save for the General + Buttons settings tabs (no submit button needed). Debounced,
4543+
// and suppressed for a short window while fillSettings() programmatically populates the form
4544+
// (the window auto-expires, so it is immune to early returns inside fillSettings).
4545+
var settingsAutoSaveTimer = null;
4546+
var settingsAutoSaveSuppressedUntil = 0;
4547+
function autoSaveGeneralSettings() {
4548+
if (Date.now() < settingsAutoSaveSuppressedUntil) {
4549+
return;
4550+
}
4551+
clearTimeout(settingsAutoSaveTimer);
4552+
settingsAutoSaveTimer = setTimeout(function () { genSettings('generalConfig'); }, 600);
4553+
}
4554+
45424555
async function fillSettings(settings) {
45434556
if (!settings) {
45444557
return false;
45454558
}
4559+
settingsAutoSaveSuppressedUntil = Date.now() + 1500; // don't auto-save while populating
45464560
// update global settings
45474561
window.settings = {
45484562
...window.settings,
@@ -4776,6 +4790,7 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
47764790
const obj = await response.json();
47774791
const data = obj.defaults ? obj.defaults : obj;
47784792
await fillSettings(data);
4793+
await genSettings('generalConfig'); // persist the defaults (no submit button anymore)
47794794
} catch (err) {
47804795
toaster.error(i18next.t("toast.error") + ": " + err.message);
47814796
}
@@ -4863,6 +4878,9 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
48634878
document.addEventListener("DOMContentLoaded", function () {
48644879
const c = document.getElementById("eqRulesListContainer");
48654880
if (c) c.addEventListener("shown.bs.collapse", rebuildEqRulesList);
4881+
// Auto-save the General + Buttons tabs on any change (delegated, so the dynamically
4882+
// populated button dropdowns are covered too). No submit button on these tabs.
4883+
$('#settings-general, #settings-buttons').on('change slideStop', 'input, select, textarea', autoSaveGeneralSettings);
48664884
});
48674885
const rfidListSavedAssignments = document.getElementById("rfidListSavedAssignments");
48684886
async function rebuildRFIDList() {

0 commit comments

Comments
 (0)