The cards that open the charger get a card of their own

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tajniak81
2026-09-03 10:36:52 +02:00
co-authored by Claude Opus 5
parent 4188d049cb
commit a5842201c0
4 changed files with 114 additions and 2 deletions
+6
View File
@@ -64,6 +64,12 @@
"idle": "Oplader ikke",
"idleHint": "Tilslut ved en station for at starte en session."
},
"rfid": {
"title": "RFID-kortindstillinger",
"none": "Ingen kort er godkendt til denne lader.",
"unsupported": "Tjenesten, som denne lader kommer fra, rapporterer ikke RFID-kort.",
"readOnly": "Kort tilføjes og fjernes i Anker-appen. Ankers endpoints til det er udokumenterede, så DriverVault læser listen i stedet for at gætte sig til en skrivning."
},
"stations": {
"heading": "I nærheden",
"homeHeading": "Dine ladere",
+6
View File
@@ -333,6 +333,12 @@
"rawTitle": "As the service reports it",
"rawHint": "Every other field the service sent about this charger, under its own field names. They are undocumented, so they are shown as they arrive rather than renamed."
},
"rfid": {
"title": "RFID cards settings",
"none": "No cards are authorised on this charger.",
"unsupported": "The service this charger came from does not report RFID cards.",
"readOnly": "Cards are added and removed in the Anker app. Anker's endpoints for that are undocumented, so DriverVault reads the list rather than guessing at a write."
},
"stations": {
"heading": "Nearby",
"homeHeading": "Your chargers",
+6
View File
@@ -64,6 +64,12 @@
"idle": "Brak ładowania",
"idleHint": "Podłącz na stacji, aby rozpocząć sesję."
},
"rfid": {
"title": "Ustawienia kart RFID",
"none": "Na tej ładowarce nie autoryzowano żadnej karty.",
"unsupported": "Usługa, z której pochodzi ta ładowarka, nie zgłasza kart RFID.",
"readOnly": "Karty dodaje się i usuwa w aplikacji Anker. Endpointy Ankera do tego są nieudokumentowane, więc DriverVault odczytuje listę, zamiast zgadywać zapis."
},
"stations": {
"heading": "W pobliżu",
"homeHeading": "Twoje ładowarki",
+96 -2
View File
@@ -177,7 +177,7 @@ const sessionMetrics = computed(() =>
// buttons just did: the limit that was applied, whether boost took. An account
// that has already arranged this column gets it at the end instead, the same
// rule a tab added in a later release follows, and one drag puts it anywhere.
const ALL_CHARGER_CARDS = ["control", "settings", "connection", "readings", "info"];
const ALL_CHARGER_CARDS = ["control", "rfid", "settings", "connection", "readings", "info"];
const cardKeys = ref([...ALL_CHARGER_CARDS]);
watch(
@@ -187,7 +187,19 @@ watch(
for (const key of order || []) {
if (ALL_CHARGER_CARDS.includes(key) && !arranged.includes(key)) arranged.push(key);
}
cardKeys.value = [...arranged, ...ALL_CHARGER_CARDS.filter((k) => !arranged.includes(k))];
// A card added after this order was saved goes where the default order puts
// it — behind the neighbour it was written to sit under — rather than at the
// bottom of the column. Somebody who has arranged their cards once should not
// have to go looking for the new one.
for (const key of ALL_CHARGER_CARDS) {
if (arranged.includes(key)) continue;
const before = ALL_CHARGER_CARDS.slice(0, ALL_CHARGER_CARDS.indexOf(key))
.reverse()
.find((k) => arranged.includes(k));
const at = before ? arranged.indexOf(before) + 1 : 0;
arranged.splice(at, 0, key);
}
cardKeys.value = arranged;
},
{ immediate: true }
);
@@ -1174,6 +1186,13 @@ const chargerDetailViews = computed(() => {
}));
});
// The RFID cards authorised on the charger, out of the view that answers with
// them. A card of its own: a list of physical cards somebody holds in their hand
// is worth more than a block in a diagnostic dump. The information card keeps
// relaying this view as well — it relays every view the account answers, and a
// hole in that list would be the one thing it cannot say.
const rfidView = computed(() => chargerDetailViews.value.find((v) => v.id === "rfid") || null);
// Everything else the service said about this charger, under its own field
// names. The rows above are the ones DriverVault has a name for; these are the
// remainder — the service documents none of them, so its own key is the only
@@ -1647,6 +1666,81 @@ onMounted(async () => {
</div>
</div>
<!-- The cards that may start a charge without a phone. Directly under
the control card because it is the same question who may use this
charger asked of a person rather than of a button. It reads the
list the account holds; adding and removing them is the Anker app's
job, and the card says so rather than offering a button that cannot
work. -->
<div
v-if="selectedHomeCharger"
class="dh-card p-4 transition-shadow duration-150"
:class="dropCard === 'rfid' ? 'ring-2 ring-accent' : ''"
:style="{ order: cardOrder('rfid') }"
@dragenter.prevent="onCardDragEnter('rfid')"
@dragover.prevent
@drop.prevent="commitCardOrder"
>
<div class="flex items-center justify-between gap-2">
<button
type="button"
:draggable="canArrangeCards"
:title="canArrangeCards ? t('charging.cards.dragHint') : ''"
class="flex grow items-center gap-3 text-left"
:class="[canArrangeCards ? 'cursor-grab active:cursor-grabbing' : '', dragCard === 'rfid' ? 'opacity-50' : '']"
:aria-expanded="isOpen('rfid')"
@click="toggleCard('rfid')"
@dragstart="onCardDragStart('rfid', $event)"
@dragend="commitCardOrder"
>
<p class="text-sm font-semibold text-strong">{{ t("charging.rfid.title") }}</p>
<span v-if="rfidView?.items.length" class="dh-badge dh-badge-neutral">{{ rfidView.items.length }}</span>
</button>
<button
type="button"
class="dh-btn dh-btn-ghost !px-2 !py-1 text-xs"
:disabled="chargerDetailsLoading !== ''"
@click="loadChargerDetails(true)"
>
{{ chargerDetailsLoading ? t("common.loading") : t("charging.info.refresh") }}
</button>
<button
type="button"
class="flex shrink-0 items-center"
:aria-expanded="isOpen('rfid')"
:aria-label="t('charging.rfid.title')"
@click="toggleCard('rfid')"
>
<svg
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
class="h-4 w-4 shrink-0 text-muted transition-transform" :class="isOpen('rfid') ? '' : '-rotate-90'"
><path stroke-linecap="round" stroke-linejoin="round" d="m6 9 6 6 6-6" /></svg>
</button>
</div>
<div v-show="isOpen('rfid')">
<p class="mt-1 text-[11px] text-muted">{{ selectedHomeCharger.name }}</p>
<!-- A service that does not answer for cards, an account that may
not read them, and a charger with none on it are three different
answers, and each is said in its own words. -->
<p v-if="!rfidView" class="mt-3 text-xs text-muted">{{ t("charging.rfid.unsupported") }}</p>
<p v-else-if="rfidView.error" class="mt-3 text-xs text-muted">{{ rfidView.error }}</p>
<p v-else-if="!rfidView.items.length" class="mt-3 text-xs text-muted">{{ t("charging.rfid.none") }}</p>
<div v-else class="mt-3 flex flex-col gap-2">
<div v-for="card in rfidView.items" :key="card.key" class="rounded-control bg-sunken p-3">
<p class="text-sm font-semibold text-strong">{{ card.label }}</p>
<dl class="mt-1 grid grid-cols-[auto_1fr] gap-x-4 gap-y-1">
<template v-for="row in card.rows" :key="row.key">
<dt class="break-all text-[11px] text-muted" :class="row.named ? '' : 'data'">{{ row.label }}</dt>
<dd class="data break-all text-[11px] text-body">{{ row.value }}</dd>
</template>
</dl>
</div>
</div>
<p class="mt-3 text-[11px] text-muted">{{ t("charging.rfid.readOnly") }}</p>
</div>
</div>
<!-- What the charger is set to, as it reports it back. Its own card
under the control one: these are the values those buttons write, so
they are read right after pressing them, and they were buried at