Skip to content

Commit 03c7b5a

Browse files
fhirschmannclaude
andcommitted
feat: RFID settings sub-tab (learn modification cards + list all learned cards)
Replaces the old inline RFID-assignment form (hidden, kept in DOM only so the context menu can still read the #playMode option list). Music cards are learned from the file-browser context menu; this adds a dedicated RFID settings sub-tab: - Learn modification cards: pick a modification action -> Learn card -> the shared scan popup -> POST rfidMod. Reuses startRfidScan(). - Lists ALL learned cards (music = path + play mode, modification = action) with a per-row delete button; refreshes when the sub-tab is shown and after learn/delete. - The websocket rfid handler already routes scans to the active scan session. - i18n de/en/fr (nav.rfid + settingsex.rfid.*). Verified on the test device via Playwright (CMD_VIRTUAL_RFID_CARD_02): old form hidden, mod dropdown populated (43 actions), learn -> saved (modId 110) -> shown in list -> deleted; explorer context menu still builds its RFID submenu (12 modes); no console errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a900234 commit 03c7b5a

5 files changed

Lines changed: 124 additions & 9 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ neon logo that doubles as the SVG favicon ([`7be5254`](../../commit/7be5254)):
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 → a **popup** asks you to lay the card (scan or cancel) and 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) |
140140
| **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) |
141+
| **RFID settings sub-tab + learned-cards list**: the old inline RFID-assignment form (id field + play-mode + submit) is gone — music cards are learned from the file-browser context menu (scan popup). A new **RFID** settings sub-tab learns **modification cards** (pick a modification action → **Learn card** → scan popup) and lists **all learned cards** (music = path + play mode, modification = action) with a delete button each. i18n de/en/fr | [`rfid-settings-tab`](../../releases/tag/rfid-settings-tab) |
141142
| **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) |
142143

143144
#### Feature highlights

‎html/locales/de.json‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"updateAvailable": "Update verfügbar",
3131
"clickToUpdate": "zum Installieren klicken",
3232
"control": "Steuerung",
33-
"rfid": "Dateien",
33+
"rfid": "RFID",
3434
"wifi": "WLAN",
3535
"mqtt": "MQTT",
3636
"ftp": "FTP",
@@ -115,7 +115,14 @@
115115
"title": "Drehimpulsgeber",
116116
"reverse": "Drehrichtung umkehren"
117117
},
118-
"autosaveHint": "Änderungen werden automatisch gespeichert"
118+
"autosaveHint": "Änderungen werden automatisch gespeichert",
119+
"rfid": {
120+
"modTitle": "Modifikationskarte anlernen",
121+
"modDesc": "Wähle eine Modifikations-Aktion und lege dann die Karte auf. Musik-Karten lernst du direkt im Datei-Browser (Rechtsklick → RFID anlernen).",
122+
"learnBtn": "Karte anlernen",
123+
"learnedTitle": "Gelernte Karten",
124+
"empty": "Noch keine Karten gelernt."
125+
}
119126
},
120127
"security": {
121128
"title": "Webinterface-Passwort",

‎html/locales/en.json‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"updateAvailable": "Update available",
3131
"clickToUpdate": "click to install",
3232
"control": "Control",
33-
"rfid": "Files",
33+
"rfid": "RFID",
3434
"wifi": "WiFi",
3535
"mqtt": "MQTT",
3636
"ftp": "FTP",
@@ -115,7 +115,14 @@
115115
"title": "Rotary encoder",
116116
"reverse": "Reverse direction of rotation"
117117
},
118-
"autosaveHint": "Changes are saved automatically"
118+
"autosaveHint": "Changes are saved automatically",
119+
"rfid": {
120+
"modTitle": "Learn modification card",
121+
"modDesc": "Pick a modification action, then lay the card. Music cards are learned directly in the file browser (right-click → Learn RFID).",
122+
"learnBtn": "Learn card",
123+
"learnedTitle": "Learned cards",
124+
"empty": "No cards learned yet."
125+
}
119126
},
120127
"security": {
121128
"title": "Web interface password",

‎html/locales/fr.json‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"updateAvailable": "Mise à jour disponible",
3131
"clickToUpdate": "cliquer pour installer",
3232
"control": "Contrôle",
33-
"rfid": "Fichiers",
33+
"rfid": "RFID",
3434
"wifi": "WiFi",
3535
"mqtt": "MQTT",
3636
"ftp": "FTP",
@@ -108,7 +108,14 @@
108108
"title": "Codeur rotatif",
109109
"reverse": "Inversion du sens de rotation"
110110
},
111-
"autosaveHint": "Les modifications sont enregistrées automatiquement"
111+
"autosaveHint": "Les modifications sont enregistrées automatiquement",
112+
"rfid": {
113+
"modTitle": "Apprendre une carte de modification",
114+
"modDesc": "Choisissez une action de modification, puis posez la carte. Les cartes musicales s'apprennent dans l'explorateur de fichiers (clic droit → Associer RFID).",
115+
"learnBtn": "Apprendre la carte",
116+
"learnedTitle": "Cartes apprises",
117+
"empty": "Aucune carte apprise pour le moment."
118+
}
112119
},
113120
"security": {
114121
"title": "Mot de passe de l'interface web",

‎html/management.html‎

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2026,7 +2026,10 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
20262026
</fieldset>
20272027
<hr>
20282028
</div>
2029-
<div class="container" id="rfidMusicTags">
2029+
<!-- Old inline RFID-assignment form: hidden. Music cards are learned via the file-browser
2030+
context menu now; modification cards via the RFID settings sub-tab. Kept in the DOM
2031+
(hidden) so the context menu can still read the #playMode option list. -->
2032+
<div class="container" id="rfidMusicTags" hidden>
20302033
<fieldset>
20312034
<legend id="rfidMusicTagsAssignment" data-i18n="files.rfid.title"></legend>
20322035
<form action="#rfidMusicTags" method="POST" onsubmit="rfidAssign('rfidMusicTags'); return false">
@@ -2110,6 +2113,7 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
21102113
<div class="nav nav-tabs justify-content-center" id="settings-subnav" role="tablist">
21112114
<a class="nav-item nav-link active" id="settings-general-subtab" data-bs-toggle="tab" data-bs-target="#settings-general" href="#settings-general" role="tab" aria-controls="settings-general" aria-selected="true"><i class="fas fa-sliders-h"></i> <span class="d-none d-md-inline" data-i18n="nav.general"></span></a>
21122115
<a class="nav-item nav-link" id="settings-buttons-subtab" data-bs-toggle="tab" data-bs-target="#settings-buttons" href="#settings-buttons" role="tab" aria-controls="settings-buttons"><i class="fas fa-keyboard"></i> <span class="d-none d-md-inline" data-i18n="nav.buttons"></span></a>
2116+
<a class="nav-item nav-link" id="settings-rfid-subtab" data-bs-toggle="tab" data-bs-target="#settings-rfid" href="#settings-rfid" role="tab" aria-controls="settings-rfid"><i class="fas fa-id-card"></i> <span class="d-none d-md-inline" data-i18n="nav.rfid"></span></a>
21132117
<a class="nav-item nav-link" id="nav-wifi-tab" data-bs-toggle="tab" data-bs-target="#nav-wifi" href="#nav-wifi" role="tab" aria-controls="nav-wifi"><i class="fas fa-wifi"></i> <span class="d-none d-md-inline" data-i18n="nav.wifi"></span></a>
21142118
<a class="nav-item nav-link" id="nav-mqtt-tab" data-visible="false" data-bs-toggle="tab" data-bs-target="#nav-mqtt" href="#nav-mqtt" role="tab" aria-controls="nav-mqtt"><i class="fas fa-network-wired"></i> <span class="d-none d-md-inline" data-i18n="nav.mqtt"></span></a>
21152119
<a class="nav-item nav-link" id="nav-ftp-tab" data-visible="false" data-bs-toggle="tab" data-bs-target="#nav-ftp" href="#nav-ftp" role="tab" aria-controls="nav-ftp"><i class="fas fa-folder"></i> <span class="d-none d-md-inline" data-i18n="nav.ftp"></span></a>
@@ -2705,6 +2709,27 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
27052709
</div>
27062710
</div>
27072711
</div>
2712+
<div class="tab-pane fade" id="settings-rfid" role="tabpanel" aria-labelledby="settings-rfid-subtab">
2713+
<div class="container">
2714+
<fieldset>
2715+
<legend class="w-auto" data-i18n="settingsex.rfid.modTitle"></legend>
2716+
<p class="small text-secondary" data-i18n="settingsex.rfid.modDesc"></p>
2717+
<div class="row gx-2 align-items-end mb-2">
2718+
<div class="col-12 col-md-8 mb-2">
2719+
<label class="form-label small mb-0" for="modIdLearn" data-i18n="files.rfid.mod.title"></label>
2720+
<select id="modIdLearn" class="form-select form-select-sm"></select>
2721+
</div>
2722+
<div class="col-12 col-md-4 mb-2">
2723+
<button type="button" class="btn btn-primary w-100" onclick="learnModCard()"><i class="fas fa-id-card"></i> <span data-i18n="settingsex.rfid.learnBtn"></span></button>
2724+
</div>
2725+
</div>
2726+
</fieldset>
2727+
<fieldset>
2728+
<legend class="w-auto" data-i18n="settingsex.rfid.learnedTitle"></legend>
2729+
<ul class="list-group" id="rfidLearnedList"></ul>
2730+
</fieldset>
2731+
</div>
2732+
</div>
27082733
<div class="tab-pane fade" id="nav-wifi" role="tabpanel" aria-labelledby="nav-wifi-tab">
27092734
<div class="container" id="wifiConfig">
27102735
<div>
@@ -4905,6 +4930,9 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
49054930
// Auto-save the General + Buttons tabs on any change (delegated, so the dynamically
49064931
// populated button dropdowns are covered too). No submit button on these tabs.
49074932
$('#settings-general, #settings-buttons').on('change slideStop', 'input, select, textarea', autoSaveGeneralSettings);
4933+
// Refresh the learned-cards list whenever the RFID settings sub-tab is shown.
4934+
var rfidSubtab = document.getElementById('settings-rfid-subtab');
4935+
if (rfidSubtab) { rfidSubtab.addEventListener('shown.bs.tab', rebuildLearnedList); }
49084936
});
49094937
const rfidListSavedAssignments = document.getElementById("rfidListSavedAssignments");
49104938
async function rebuildRFIDList() {
@@ -6310,6 +6338,71 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
63106338
toaster.info(i18next.t("files.rfid.learn.done", { rfidId: scannedId }));
63116339
refreshRfidBadgesAround(nodeId);
63126340
}
6341+
// Learn a modification card (RFID settings tab): pick a modification action, then lay a card.
6342+
function learnModCard() {
6343+
var modId = Number(document.getElementById('modIdLearn').value);
6344+
startRfidScan(i18next.t("files.rfid.mod.cmd." + modId), function (scannedId) {
6345+
completeModLearn(scannedId, modId);
6346+
});
6347+
}
6348+
async function completeModLearn(scannedId, modId) {
6349+
await fetch("http://" + host + "/settings", {
6350+
method: "POST",
6351+
headers: { "Content-Type": "application/json" },
6352+
body: JSON.stringify({ rfidMod: { rfidIdMod: scannedId, modId: modId } })
6353+
});
6354+
toaster.info(i18next.t("files.rfid.learn.done", { rfidId: scannedId }));
6355+
rebuildLearnedList();
6356+
}
6357+
// Render the full list of learned cards (music + modification) in the RFID settings tab.
6358+
async function rebuildLearnedList() {
6359+
var list = document.getElementById('rfidLearnedList');
6360+
if (!list) return;
6361+
var cards;
6362+
try { cards = await (await fetch("http://" + host + "/rfid")).json(); } catch (e) { cards = []; }
6363+
list.innerHTML = "";
6364+
if (!Array.isArray(cards) || cards.length === 0) {
6365+
var empty = document.createElement('li');
6366+
empty.className = "list-group-item text-secondary";
6367+
empty.setAttribute('data-i18n', "settingsex.rfid.empty");
6368+
empty.textContent = i18next.t("settingsex.rfid.empty");
6369+
list.appendChild(empty);
6370+
return;
6371+
}
6372+
cards.sort(function (a, b) { return String(a.id).localeCompare(String(b.id)); });
6373+
cards.forEach(function (c) {
6374+
if (!c || !c.id) return;
6375+
var li = document.createElement('li');
6376+
li.className = "list-group-item d-flex justify-content-between align-items-start gap-2";
6377+
var info = document.createElement('div');
6378+
var idLine = document.createElement('div');
6379+
idLine.className = "fw-bold";
6380+
idLine.textContent = c.id;
6381+
info.appendChild(idLine);
6382+
var desc = document.createElement('div');
6383+
desc.className = "small text-secondary text-break";
6384+
if (c.modId) {
6385+
desc.textContent = i18next.t("files.rfid.modification") + ": " + i18next.t("files.rfid.mod.cmd." + c.modId);
6386+
} else {
6387+
var mode = c.playMode ? i18next.t("files.rfid.playmode.mode." + c.playMode) : "";
6388+
desc.textContent = (c.fileOrUrl || "") + (mode ? " — " + mode : "");
6389+
}
6390+
info.appendChild(desc);
6391+
li.appendChild(info);
6392+
var del = document.createElement('button');
6393+
del.className = "btn btn-sm btn-danger fa fa-trash flex-shrink-0";
6394+
del.setAttribute('title', i18next.t("delete"));
6395+
del.addEventListener('click', function () { deleteLearnedCard(c.id); });
6396+
li.appendChild(del);
6397+
list.appendChild(li);
6398+
});
6399+
}
6400+
async function deleteLearnedCard(id) {
6401+
var resp = await fetch("http://" + host + "/rfid?id=" + encodeURIComponent(id), { method: "DELETE" });
6402+
if (resp.ok) { flashActionOk(); } else { toaster.error(resp.statusText); }
6403+
rebuildLearnedList();
6404+
loadRfidPaths(updateSelectedNodeBadges); // keep explorer badges in sync
6405+
}
63136406
// Remove every RFID tag assigned to this exact path (usually one) straight from the explorer.
63146407
async function removeRfidForPath(path, nodeId) {
63156408
var ids = (window.rfidPathIds && window.rfidPathIds[path]) || [];
@@ -6673,8 +6766,8 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
66736766
}
66746767
}
66756768

6676-
/* fill command selectors at the control page and the rfid assignment */
6677-
document.querySelectorAll("#modId, #commandId").forEach(e => replaceCommandSelect(e, modElem));
6769+
/* fill command selectors at the control page, the rfid assignment and the RFID settings tab */
6770+
document.querySelectorAll("#modId, #commandId, #modIdLearn").forEach(e => replaceCommandSelect(e, modElem));
66786771

66796772
darkmodeSwitchSetup();
66806773
selectActiveTabFromLocalStorage();

0 commit comments

Comments
 (0)