Abmelden meldet jetzt wirklich ab

Rueckmeldung: 'der abmelde button klappt auch nicht auf dem handy'. Die
Ursache war nicht das Handy -- der Fehler war auf beiden Geraeten
derselbe, auf dem Handy sieht man das kurze Aufblitzen nur eher.

Der Knopf beendete die Team-Sitzung, loeschte den Schluessel und lud neu.
Das Merkmal der Zugangswand blieb dabei im Browser stehen. Beim
Neuladen holt sich die Seite damit sofort wieder einen Ausweis -- man
war nach einer Zehntelsekunde erneut angemeldet, und der Knopf schien
nichts zu tun.

Jetzt werden BEIDE Sitzungen beendet (der Endpunkt dafuer gab es
laengst, die Verwaltung rief ihn nur nie auf), und man landet an der
Zugangswand statt in einer Codeeingabe. Das ist auch die ehrliche
Bedeutung des Wortes: Wer sich abmeldet, will draussen sein.

Beide Aufrufe sind einzeln abgesichert -- faellt einer aus, laeuft der
andere trotzdem. Ein halbes Abmelden waere schlimmer als keins: Man
hielte sich fuer abgemeldet und waere es nicht.

Geprueft mit 16 Pruefungen auf Computer und Handy, die messen, was
tatsaechlich hinausgeht statt ob sich etwas auf dem Schirm bewegt.
Dabei ein Artefakt im eigenen Test gefunden und behoben: Das
Vorbereitungsskript lief bei jeder Navigation und setzte den Schluessel
auf der Zugangswand gleich wieder -- zwei Pruefungen massen also sich
selbst.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-24 11:29:28 +02:00
co-authored by Claude Opus 5
parent 2ae77fc54f
commit 6b818fd6f6
3 changed files with 163 additions and 5 deletions
+124
View File
@@ -0,0 +1,124 @@
/* =====================================================================
pruef-abmelden.mjs — meldet "Abmelden" wirklich ab?
Rückmeldung vom 23.08.2026: "der abmelde button klappt auch nicht auf
dem handy". Er tat scheinbar nichts.
Die Ursache war nicht das Handy: Der Knopf beendete die Team-Sitzung
und lud neu -- das Merkmal der Zugangswand blieb aber stehen, und beim
Neuladen holte sich die Seite sofort wieder einen Ausweis. Man war
nach einer Zehntelsekunde erneut angemeldet. Auf dem Handy sieht man
das Aufblitzen eher, deshalb fiel es dort auf.
Geprüft wird deshalb, was TATSÄCHLICH hinausgeht -- nicht, ob sich
etwas auf dem Schirm bewegt.
===================================================================== */
import { chromium, devices } from "playwright";
import { createServer } from "node:http";
import { readFileSync, existsSync } from "node:fs";
import { join, extname } from "node:path";
const W = process.cwd();
const T = { ".html": "text/html", ".css": "text/css", ".js": "text/javascript",
".png": "image/png", ".jpg": "image/jpeg", ".svg": "image/svg+xml",
".webp": "image/webp", ".ico": "image/x-icon", ".json": "application/json" };
const srv = createServer((q, r) => {
const p = join(W, decodeURIComponent(q.url.split("?")[0]));
if (existsSync(p) && !p.endsWith("/")) {
r.writeHead(200, { "content-type": T[extname(p)] || "text/plain" });
return r.end(readFileSync(p));
}
r.writeHead(404).end("");
});
await new Promise((r) => srv.listen(4357, r));
const ergebnisse = [];
const pruefe = (n, ok, z) => {
ergebnisse.push({ n, ok });
console.log((ok ? " ok " : " FEHL ") + n + (z !== undefined ? " -> " + z : ""));
};
for (const [wo, angaben] of [
["Computer", { viewport: { width: 1440, height: 950 } }],
["Handy", { ...devices["iPhone 14 Pro"] }],
]) {
const browser = await chromium.launch();
const kontext = await browser.newContext(angaben);
const seite = await kontext.newPage();
const protokoll = [];
seite.on("pageerror", (e) => protokoll.push(e.message));
const gerufen = [];
await seite.route("**/*", async (route) => {
const u = new URL(route.request().url());
const m = route.request().method();
if (m === "POST") gerufen.push(u.pathname);
if (u.pathname === "/webdesign/zugang-beenden") return route.fulfill({ json: { ok: true } });
if (u.pathname.startsWith("/auth/")) return route.fulfill({ json: { ok: true } });
if (u.pathname.includes("/webdesign/admin/uebersicht")) {
return route.fulfill({ json: { ok: true,
beiMir: { anfragen: { n: 0 }, nachrichten: { n: 0 }, wuensche: { n: 0 }, widerrufe: { n: 0 }, projekte: 0 },
beiKunde: { projekte: 0, aufgaben: 0, zahlungen: { n: 0, summe: 0 }, zahlungenText: "0,00 €" },
geld: { monat: { n: 0, brutto: 0, netto: 0, bruttoText: "0,00 €", nettoText: "0,00 €" },
gesamt: { n: 0, bruttoText: "0,00 €" }, gebuehrenText: "0,00 €" },
projekte: [], verlauf: [], bestand: {} } });
}
if (u.pathname.includes("/webdesign/admin/")) {
return route.fulfill({ json: { ok: true, anfragen: [], kunden: [], projekte: [], zahlungen: [], verlaeufe: [] } });
}
return route.continue();
});
/* Nur auf der VERWALTUNGSSEITE vorbelegen.
addInitScript laeuft bei jeder Navigation erneut. Ohne diese
Einschraenkung setzte es den Schluessel auf der Zugangswand sofort
wieder -- und die Pruefung "der Schluessel ist weg" mass dann das
eigene Vorbereitungsskript statt des Abmeldens. Erst beim
Nachsehen aufgefallen: Zwei Zeilen meldeten Fehler, obwohl das
Abmelden voellig richtig funktionierte. */
await seite.addInitScript(() => {
if (!location.pathname.includes("verwaltung")) return;
sessionStorage.setItem("wd-vw-token", "t");
sessionStorage.setItem("wd-vw-token-quelle", "ausweis");
sessionStorage.setItem("wd-sitzung-aktiv", "1");
});
await seite.goto("http://127.0.0.1:4357/webdesign/verwaltung.html");
await seite.waitForTimeout(700);
console.log("\n=== " + wo + " ===");
const knopf = seite.locator("#vw-abmelden");
pruefe(wo + ": der Abmelden-Knopf ist sichtbar", await knopf.isVisible());
const kasten = await knopf.boundingBox();
pruefe(wo + ": er ist gross genug zum Antippen",
kasten && kasten.height >= 44, kasten && Math.round(kasten.height) + "px");
await knopf.click();
await seite.waitForTimeout(900);
/* Das Entscheidende: Beide Sitzungen muessen beendet werden. Vorher
ging nur der erste Aufruf hinaus -- und genau deshalb war man
sofort wieder drin. */
pruefe(wo + ": die Team-Sitzung wird beendet",
gerufen.some((x) => x.includes("/auth/logout")), gerufen.join(", "));
pruefe(wo + ": die ZUGANGSWAND wird ebenfalls beendet",
gerufen.some((x) => x === "/webdesign/zugang-beenden"), gerufen.join(", "));
pruefe(wo + ": der gespeicherte Schluessel ist weg",
await seite.evaluate(() => !sessionStorage.getItem("wd-vw-token")));
pruefe(wo + ": auch dessen Herkunft",
await seite.evaluate(() => !sessionStorage.getItem("wd-vw-token-quelle")));
/* Und man landet an der Zugangswand, nicht wieder in der Verwaltung. */
pruefe(wo + ": man landet an der Zugangswand",
/zugang\.html/.test(seite.url()), seite.url());
pruefe(wo + ": kein Skriptfehler", protokoll.length === 0, protokoll.join(" | "));
await browser.close();
}
srv.close();
const f = ergebnisse.filter((e) => !e.ok).length;
console.log(`\n===== ${ergebnisse.length - f} bestanden, ${f} fehlgeschlagen =====`);
process.exitCode = f ? 1 : 0;
+1 -1
View File
@@ -34,7 +34,7 @@
Start alle alten Zwischenspeicher weg. Muss bei jeder Änderung an den
Dateien unten hochgezählt werden, sonst hängen Nutzer auf einem alten
Stand fest. */
const CACHE_NAME = "dogfather-webdesign-v25";
const CACHE_NAME = "dogfather-webdesign-v26";
/* Bausteine, die die Oberfläche zum Anzeigen braucht. Bewusst KEINE
HTML-Datei in dieser Liste. */
+38 -4
View File
@@ -1492,10 +1492,44 @@
}
});
document.getElementById("vw-abmelden").addEventListener("click", async function () {
try { await api("/auth/logout", { method: "POST", body: {} }); } catch (e) { /* egal */ }
token = null; tokenSchreiben(null);
location.reload();
document.getElementById("vw-abmelden").addEventListener("click", async function (e) {
/* ABMELDEN MUSS AUCH DIE ZUGANGSWAND BEENDEN.
Vorher tat der Knopf scheinbar nichts (Rueckmeldung 23.08.2026:
"der abmelde button klappt auch nicht auf dem handy"). Er beendete
die Team-Sitzung, loeschte den Schluessel und lud neu -- aber das
Merkmal der Zugangswand blieb im Browser stehen. Beim Neuladen holt
sich die Seite damit sofort wieder einen Ausweis und ist nach einer
Zehntelsekunde erneut angemeldet. Auf dem Handy faellt es mehr auf,
weil man das kurze Aufblitzen dort eher sieht -- der Fehler war auf
beiden Geraeten derselbe.
Deshalb jetzt BEIDE Sitzungen beenden. Das ist auch die ehrliche
Bedeutung des Wortes: Wer sich abmeldet, will draussen sein und
nicht in einem halb offenen Zustand zurueckbleiben.
Die Reihenfolge ist wichtig: erst die Zugangswand, dann neu laden.
Umgekehrt waere die Seite schon weg, bevor der Aufruf hinausgeht. */
window.WD.knopfSperren(e.target, true, "Wird abgemeldet…");
/* Beide Aufrufe einzeln absichern: Faellt einer aus, soll der andere
trotzdem laufen. Ein halbes Abmelden waere schlimmer als keins --
man haelte sich fuer abgemeldet und waere es nicht. */
try { await api("/auth/logout", { method: "POST", body: {} }); } catch (f) { /* egal */ }
try {
await fetch("/webdesign/zugang-beenden", {
method: "POST", credentials: "same-origin",
headers: { "content-type": "application/json" }, body: "{}",
});
} catch (f) { /* egal */ }
token = null; tokenQuelle = null; tokenSchreiben(null);
try { sessionStorage.removeItem("wd-sitzung-aktiv"); } catch (f) { /* egal */ }
/* Zur Zugangswand statt neu laden. Ein Neuladen der Verwaltung
zeigte nur die Codeeingabe -- und liesse offen, ob man nun
draussen ist oder nicht. */
location.href = "/webdesign/zugang.html";
});
/* ---------------- Liste ---------------- */