Remove asks in the page, not in a dialog the browser may refuse to show

Removing a charger was gated behind window.confirm(). A browser that
suppresses native dialogs — an embedded webview, a blocked-dialogs setting
— does not show it and hands back false, so the click answered "no" on the
user's behalf: no request, no error, nothing. The button looked broken and
the endpoint was never reached. It always had been fine; a delete with an
unknown id still answers 404 and the ownership path still resolves.

The prompt is part of the row now — the warning, Cancel, Remove — the way
the charger reset in the same view already asks. Remove disables while the
delete is in flight, and a failure lands in the error line the list
already has.

Fifteen other confirm() call sites share the flaw and are left for their
own change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tajniak81
2026-09-01 11:56:04 +02:00
co-authored by Claude Opus 5
parent 21a54c4cfa
commit 749f42f481
+48 -5
View File
@@ -232,14 +232,34 @@ function onChargerImported(charger) {
loadChargerLive(true);
}
async function removeHomeCharger(c) {
if (!confirm(t("charging.home.removeConfirm", { name: c.name }))) return;
// Removing asks first, inline. A window.confirm() would be shorter, but a
// browser that suppresses dialogs — an embedded webview, a blocked-popups
// setting — hands back false, and the button then does nothing at all with
// nothing said about why. The prompt is part of the page instead, the way the
// charger reset above already asks.
const pendingRemoval = ref(""); // charger id awaiting a yes
const removing = ref("");
function askRemoveHomeCharger(c) {
homeChargersError.value = "";
pendingRemoval.value = c.id;
}
function cancelRemoveHomeCharger() {
pendingRemoval.value = "";
}
async function removeHomeCharger(c) {
homeChargersError.value = "";
removing.value = c.id;
try {
await api.deleteHomeCharger(c.id);
homeChargers.value = homeChargers.value.filter((x) => x.id !== c.id);
pendingRemoval.value = "";
} catch (e) {
homeChargersError.value = e.message;
} finally {
removing.value = "";
}
}
@@ -654,9 +674,8 @@ onMounted(async () => {
</button>
</div>
<div v-for="c in homeChargers" :key="c.id">
<div
v-for="c in homeChargers"
:key="c.id"
class="flex w-full items-center gap-3 rounded-control p-3 text-left transition-colors"
:class="selected === c.id ? 'bg-brand-100' : 'hover:bg-sunken'"
>
@@ -677,12 +696,36 @@ onMounted(async () => {
type="button"
class="shrink-0 text-xs font-medium text-muted hover:text-danger"
:title="t('charging.home.remove')"
@click="removeHomeCharger(c)"
@click="askRemoveHomeCharger(c)"
>
{{ t("charging.home.remove") }}
</button>
</div>
<!-- Asked here rather than in a dialog the browser may never show. -->
<div
v-if="pendingRemoval === c.id"
class="mx-3 mb-2 rounded-control border border-danger/40 bg-danger-soft p-3"
>
<p class="text-xs font-medium text-danger">
{{ t("charging.home.removeConfirm", { name: c.name }) }}
</p>
<div class="mt-2 flex gap-2">
<button type="button" class="dh-btn dh-btn-ghost grow" @click="cancelRemoveHomeCharger">
{{ t("common.cancel") }}
</button>
<button
type="button"
class="dh-btn dh-btn-danger grow"
:disabled="removing === c.id"
@click="removeHomeCharger(c)"
>
{{ t("charging.home.remove") }}
</button>
</div>
</div>
</div>
<!-- Nothing imported yet: say what this list is for and offer the import. -->
<div v-if="homeChargers.length === 0" class="px-3 pb-3 pt-1">
<p class="text-sm text-muted">{{ t("charging.home.empty") }}</p>