What a charger is, readable without a CSMS to control it with

The Home chargers tab put a card where the control panel goes and, with
neither Own nor Proxy CSMS turned on, filled it with a single sentence
pointing at Settings — an empty box beside a list of two real chargers the
app already knew the vendor, model and serial of.

That knowledge now has somewhere to be. Charger information lists every
imported charger with the fields its record actually holds; a field the
service never sent is left out rather than shown as a dash, and the
provider's own id appears only when it is something other than the serial.
The selected charger carries the same accent ring the list beside it does,
so picking one still reads as picking the one control drives. The card
stands on its own either way: with control on it sits beneath it, and with
control off the Settings hint becomes a footnote under something worth
reading instead of the whole card.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tajniak81
2026-09-01 11:12:17 +02:00
co-authored by Claude Opus 5
parent d71c1b4691
commit 4203ca93db
4 changed files with 102 additions and 9 deletions
+12
View File
@@ -94,6 +94,18 @@
"resetConfirm": "Genstart denne lader nu? En igangværende opladning bliver afbrudt. Indtast din adgangskode igen for at bekræfte.",
"resetPassword": "Din kontoadgangskode",
"connectHint": "Indtast laderens serienummer og opdater. Laderen skal være forbundet til DriverVaults OCPP-backend (opsættes under Indstillinger → Integrationer)."
},
"info": {
"title": "Laderoplysninger",
"empty": "Intet importeret endnu — en lader, du importerer, viser alt, den melder, her.",
"vendor": "Producent",
"model": "Model",
"serial": "Serienummer",
"site": "Placering",
"power": "Effekt",
"connector": "Stik",
"providerId": "Id hos tjenesten",
"added": "Tilføjet"
}
},
+12
View File
@@ -81,6 +81,18 @@
"resetPassword": "Your account password",
"connectHint": "Enter your charger's serial and refresh. The charger must be connected to DriverVault's OCPP backend (set up in Settings → Integrations)."
},
"info": {
"title": "Charger information",
"empty": "Nothing imported yet — a charger you import shows everything it reports here.",
"vendor": "Vendor",
"model": "Model",
"serial": "Serial",
"site": "Site",
"power": "Power",
"connector": "Connector",
"providerId": "Service id",
"added": "Added"
},
"stations": {
"heading": "Nearby",
"homeHeading": "Your chargers",
+12
View File
@@ -96,6 +96,18 @@
"resetConfirm": "Zrestartować teraz tę ładowarkę? Trwająca sesja ładowania zostanie przerwana. Wpisz ponownie hasło, aby potwierdzić.",
"resetPassword": "Hasło do Twojego konta",
"connectHint": "Wpisz numer seryjny ładowarki i odśwież. Ładowarka musi być połączona z backendem OCPP DriverVault (konfiguracja w Ustawienia → Integracje)."
},
"info": {
"title": "Informacje o ładowarce",
"empty": "Nic jeszcze nie zaimportowano — zaimportowana ładowarka pokaże tutaj wszystko, co zgłasza.",
"vendor": "Producent",
"model": "Model",
"serial": "Numer seryjny",
"site": "Lokalizacja",
"power": "Moc",
"connector": "Złącze",
"providerId": "Identyfikator w usłudze",
"added": "Dodano"
}
},
+66 -9
View File
@@ -2,6 +2,7 @@
import { ref, computed, onMounted } from "vue";
import { t } from "../i18n";
import { api } from "../api";
import { formatDateTime } from "../lib/format.js";
import ChargerImportModal from "../components/ChargerImportModal.vue";
// Charging & map screen, mirroring the web-dashboard UI kit. There is no live
@@ -103,9 +104,16 @@ function chargerLabel(c) {
const homeChargers = ref([]);
const homeChargersError = ref("");
const showChargerImport = ref(false);
// Importing only makes sense once a charger service is connected, so the button
// appears only then — same rule the garage's import button follows.
const canImportCharger = ref(false);
// The charger services this user could import from. Importing only makes sense
// once one of them is connected, so the button appears only then — same rule the
// garage's import button follows — and the list doubles as the id → label map
// the information card names a charger's origin with.
const chargerProviders = ref([]);
const canImportCharger = computed(() => chargerProviders.value.some((p) => p.connected));
function providerLabel(id) {
return chargerProviders.value.find((p) => p.id === id)?.label || id;
}
async function loadHomeChargers() {
homeChargersError.value = "";
@@ -145,6 +153,26 @@ function homeChargerSubtitle(c) {
return [c.serial, c.model, c.siteName].filter(Boolean).join(" · ");
}
// Everything the record holds about one charger, as label/value rows. A field
// the service never sent is left out rather than shown as a dash: the point of
// the card is what is known. The provider's own id for the charger is the serial
// for both services we speak to, so it appears only when it is something else.
function chargerInfoRows(c) {
const rows = [
["vendor", c.vendor],
["model", c.model],
["serial", c.serial],
["site", c.siteName],
["power", c.powerKw ? `${c.powerKw} kW` : ""],
["connector", c.connector],
["providerId", c.providerChargerId === c.serial ? "" : c.providerChargerId],
["added", c.created ? formatDateTime(c.created) : ""],
];
return rows
.filter(([, value]) => value !== "" && value != null)
.map(([key, value]) => ({ key, label: t(`charging.info.${key}`), value }));
}
async function refreshCtl() {
const sn = ctlSerial.value.trim();
if (!sn) {
@@ -204,8 +232,8 @@ onMounted(async () => {
await loadHomeChargers();
api
.listChargerProviders()
.then((list) => (canImportCharger.value = list.some((p) => p.connected)))
.catch(() => (canImportCharger.value = false));
.then((list) => (chargerProviders.value = list))
.catch(() => (chargerProviders.value = []));
await loadCtlMode();
if (ctlActive.value) await loadChargers();
await refreshCtl();
@@ -442,10 +470,39 @@ onMounted(async () => {
<p v-if="ctlError" class="mt-2 text-sm text-danger">{{ ctlError }}</p>
</div>
<!-- No control mode active: point the user at Settings to enable it. -->
<div v-else class="dh-card p-4">
<p class="text-sm font-semibold text-strong">{{ t("charging.control.title") }}</p>
<p class="mt-2 text-xs text-muted">{{ t("charging.stations.noControlHint") }}</p>
<!-- Charger information: everything the record holds about each imported
charger. It stands on its own control needs Own/Proxy CSMS, but
what the charger *is* is known either way, so with control off this
card is what fills the column instead of a bare hint. -->
<div class="dh-card p-4">
<p class="text-sm font-semibold text-strong">{{ t("charging.info.title") }}</p>
<div
v-for="c in homeChargers"
:key="c.id"
class="mt-3 rounded-control bg-sunken p-3"
:class="selected === c.id ? 'ring-2 ring-accent' : ''"
>
<div class="flex items-center justify-between gap-2">
<p class="truncate text-sm font-semibold text-strong">{{ c.name }}</p>
<span v-if="c.provider" class="dh-badge dh-badge-neutral shrink-0">
{{ providerLabel(c.provider) }}
</span>
</div>
<dl class="mt-2 grid grid-cols-[auto_1fr] gap-x-4 gap-y-1">
<template v-for="row in chargerInfoRows(c)" :key="row.key">
<dt class="text-[11px] text-muted">{{ row.label }}</dt>
<dd class="data break-all text-[11px] text-body">{{ row.value }}</dd>
</template>
</dl>
</div>
<p v-if="homeChargers.length === 0" class="mt-2 text-xs text-muted">
{{ t("charging.info.empty") }}
</p>
<!-- Control mode off: say why the card above is missing, here, where
there is now something to read it against. -->
<p v-if="!ctlActive" class="mt-3 text-xs text-muted">{{ t("charging.stations.noControlHint") }}</p>
</div>
</div>