Tap the card, and that is the whole enrolment

The reader already filled the number in; adding it still meant walking
back to the keyboard and pressing Add. That walk was the entire cost of
the two-step version, and the card is in your hand at the charger. One
press now opens the reader and writes whatever is held against it.

The name is the server's own convention — RFID and the card's last four
digits — because the name is left empty and rfidSaveCard fills it in.
Deriving the same pattern here would have been a second place for it to
drift; every card already on the account reads that way. A name typed
into the box still wins, since throwing away what somebody typed is
worse than the convention.

The reader is a value now rather than a side effect on the form, and the
write is shared with the typed path so the two cannot judge their answers
differently. The caller holds the busy flag across both halves: nothing
re-enables in the gap, where a second press would have opened a second
twenty-second window. The number lands in the box on the way past, so a
write that fails leaves something to retry rather than a card nobody can
name.

The old button stays for the times the number is wanted without the card
being added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tajniak81
2026-09-03 20:10:13 +02:00
co-authored by Claude Opus 5
parent e190364c77
commit 7b339d3dac
4 changed files with 91 additions and 19 deletions
+2
View File
@@ -72,6 +72,8 @@
"tap": "Hold kortet mod laderen",
"tapping": "Hold kortet mod læseren… {n}s",
"tapHint": "Læseren er åben. Hold kortet mod laderen.",
"tapSave": "Hold kortet mod laderen, og tilføj det",
"tapSaveHint": "Tilføjer kortet, så snart det holdes mod læseren, med navnet RFID og kortets sidste fire cifre.",
"tapNone": "Der blev ikke holdt et kort mod læseren, før den lukkede.",
"addTitle": "Tilføj et kort",
"remove": "Fjern",
+2
View File
@@ -353,6 +353,8 @@
"tap": "Tap card at the charger",
"tapping": "Hold the card against the reader… {n}s",
"tapHint": "The reader is open. Hold the card against the charger.",
"tapSave": "Tap card and add it",
"tapSaveHint": "Adds the card as soon as it is tapped, named RFID and its last four digits.",
"tapNone": "No card was tapped before the reader closed.",
"addTitle": "Add a card",
"remove": "Remove",
+2
View File
@@ -72,6 +72,8 @@
"tap": "Przyłóż kartę do ładowarki",
"tapping": "Przytrzymaj kartę przy czytniku… {n}s",
"tapHint": "Czytnik jest otwarty. Przytrzymaj kartę przy ładowarce.",
"tapSave": "Przyłóż kartę i dodaj ją",
"tapSaveHint": "Dodaje kartę zaraz po przyłożeniu, pod nazwą RFID i cztery ostatnie znaki numeru.",
"tapNone": "Nie przyłożono karty, zanim czytnik się zamknął.",
"addTitle": "Dodaj kartę",
"remove": "Usuń",
+85 -19
View File
@@ -1501,14 +1501,7 @@ async function addRfidCard() {
rfidBusy.value = "new";
rfidError.value = "";
try {
const res = await api.saveAnkerRfidCard(sn, number, newCardName.value.trim());
rfidWritten.value = { ...rfidWritten.value, [sn]: res?.cards || [] };
if (res?.present === false) {
rfidError.value = t("charging.rfid.notAdded");
} else {
newCardNumber.value = "";
newCardName.value = "";
}
await writeCard(sn, number);
} catch (e) {
rfidError.value = e.message;
} finally {
@@ -1516,6 +1509,44 @@ async function addRfidCard() {
}
}
// Writing one card and then reading the list back, which is the only thing that
// says whether the write landed. Shared by both ways of adding one, so the tap
// and the typed number cannot end up judging their answers differently.
//
// The name is whatever is in the box, and an empty box is not a missing name: it
// is the server's own convention — "RFID" and the card's last four digits — and
// leaving it to the server is what keeps the two ways of adding a card from
// drifting into two naming conventions.
async function writeCard(sn, number) {
const res = await api.saveAnkerRfidCard(sn, number, newCardName.value.trim());
rfidWritten.value = { ...rfidWritten.value, [sn]: res?.cards || [] };
if (res?.present === false) {
rfidError.value = t("charging.rfid.notAdded");
return false;
}
newCardNumber.value = "";
newCardName.value = "";
return true;
}
// The reader's own twenty seconds, as a value rather than as a side effect on
// the form: one caller wants the number in the box, the other wants to write it.
// The caller owns rfidBusy, so the tap-and-save button can hold it across the
// write that follows and nothing re-enables between the two.
async function readCardAtCharger(sn) {
rfidCountdown.value = 20;
const tick = setInterval(() => {
rfidCountdown.value = Math.max(0, rfidCountdown.value - 1);
}, 1000);
try {
const res = await api.scanAnkerRfidCard(sn);
return res?.tapped && res.card ? res.card : "";
} finally {
clearInterval(tick);
rfidCountdown.value = 0;
}
}
// Asking the charger to read a card, which is what the Anker app's second way of
// adding one does: the reader opens for twenty seconds, and whatever is held
// against it comes back as a number. Nothing is written by this — the card lands
@@ -1525,22 +1556,42 @@ async function scanRfidCard() {
if (!sn || rfidBusy.value) return;
rfidBusy.value = "scan";
rfidError.value = "";
rfidCountdown.value = 20;
const tick = setInterval(() => {
rfidCountdown.value = Math.max(0, rfidCountdown.value - 1);
}, 1000);
try {
const res = await api.scanAnkerRfidCard(sn);
if (res?.tapped && res.card) {
newCardNumber.value = res.card;
} else {
const card = await readCardAtCharger(sn);
if (card) newCardNumber.value = card;
else rfidError.value = t("charging.rfid.tapNone");
} catch (e) {
rfidError.value = e.message;
} finally {
rfidBusy.value = "";
}
}
// The same tap, carried through to the end: the reader opens, and whatever is
// held against it is written without a second press. Enrolling a card happens at
// the charger with the card in your hand — the walk back to the keyboard to
// press Add was the whole cost of the two-step version.
//
// Nothing new is written by this that the two buttons above could not write
// between them; it is the same read and the same write, with nothing to do in
// between. The number lands in the box on the way past, so a write that fails
// leaves something to look at and retry rather than a card nobody can name.
async function tapAndSaveRfidCard() {
const sn = detailSn.value;
if (!sn || rfidBusy.value) return;
rfidBusy.value = "tapSave";
rfidError.value = "";
try {
const card = await readCardAtCharger(sn);
if (!card) {
rfidError.value = t("charging.rfid.tapNone");
return;
}
newCardNumber.value = card;
await writeCard(sn, card);
} catch (e) {
rfidError.value = e.message;
} finally {
clearInterval(tick);
rfidCountdown.value = 0;
rfidBusy.value = "";
}
}
@@ -2271,7 +2322,22 @@ onMounted(async () => {
>
{{ rfidBusy === "scan" ? t("charging.rfid.tapping", { n: rfidCountdown }) : t("charging.rfid.tap") }}
</button>
<p v-if="rfidBusy === 'scan'" class="mt-1 text-[11px] text-muted">{{ t("charging.rfid.tapHint") }}</p>
<!-- The same tap without the second press. Primary, because it is
the one somebody standing at the charger wants; the button
above stays for the times the number is wanted without the
card being added. -->
<button
type="button"
class="dh-btn dh-btn-primary mt-2 w-full text-xs"
:disabled="rfidBusy !== ''"
@click="tapAndSaveRfidCard"
>
{{ rfidBusy === "tapSave" ? t("charging.rfid.tapping", { n: rfidCountdown }) : t("charging.rfid.tapSave") }}
</button>
<p v-if="rfidBusy === 'scan' || rfidBusy === 'tapSave'" class="mt-1 text-[11px] text-muted">
{{ t("charging.rfid.tapHint") }}
</p>
<p v-else class="mt-1 text-[11px] text-muted">{{ t("charging.rfid.tapSaveHint") }}</p>
</form>
<p class="mt-3 text-[11px] text-muted">{{ t("charging.rfid.inferred") }}</p>
</div>