Oberflaeche fuer Auskunft und Loeschung

Sitzt im Kunden-Bereich, zugeklappt. Ein eigener Reiter waere zu
prominent fuer etwas, das man vielleicht zweimal im Jahr braucht --
ganz weglassen hiesse, im Ernstfall von Hand durch ein Dutzend
Tabellen zu suchen, bei laufender Monatsfrist.

DREI SCHRITTE, IN DIESER REIHENFOLGE

  Auskunft erstellen      unschaedlich, jederzeit
  Loeschvorschau          zeigt, was betroffen waere
  Loeschen                nur nach Vorschau, mit doppelter Eingabe

Die Auskunft laesst sich als Datei herunterladen, nicht nur ansehen.
Man muss sie der Person schicken koennen, und zwar vollstaendig --
abtippen waere der sicherste Weg, etwas zu vergessen. JSON, weil
Art. 20 DSGVO ein "gaengiges, maschinenlesbares Format" verlangt.

Die Vorschau zeigt beides getrennt: was geloescht wuerde, und was
bleiben muss -- mit Grund und mit dem Datum, ab dem es weg darf. Ohne
diese Angabe muesste man bei jeder Anfrage neu nachschlagen, welche
Frist gilt.

Vor dem Loeschen wird die Adresse ein zweites Mal verlangt. Der
Unterschied zwischen .de und .com ist ein Buchstabe, die Folge
unwiederbringlich.

GEPRUEFT

Im Handy-Format mit nachgebildeten Antworten: Auskunft zeigt die
Bereiche samt Kennzeichnung "aufbewahrungspflichtig", der
Herunterladen-Knopf erscheint, die Vorschau trennt richtig, eine
abweichende Bestaetigung wird abgelehnt. Kein Ueberlauf, keine
Skriptfehler. Die Handy-Suite bleibt bei 25/25, keine Namenskollision.

Ein Fehler lag dabei im Test selbst: Playwright prueft die ZULETZT
registrierte Route zuerst, und meine allgemeine Auffangregel
ueberdeckte die spezifischen. Die Auskunft meldete "nichts
gespeichert", obwohl die Oberflaeche richtig arbeitete.

Die Endpunkte dazu liegen in server-internal und brauchen noch einen
Deploy durch Filipe (/home/dogiintern, Rechte 700). Bis dahin meldet
die Oberflaeche einen 404 -- mit dem Hinweis, dass der Server einen
Neustart mit dem neuen Stand braucht, statt nur "Fehler" zu sagen.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-26 20:03:35 +02:00
co-authored by Claude Opus 5
parent 7b09d752f8
commit c03793b25f
3 changed files with 195 additions and 2 deletions
+1 -1
View File
@@ -863,7 +863,7 @@
Stand haben. Die Nummer wird zusammen mit CACHE_NAME in sw.js Stand haben. Die Nummer wird zusammen mit CACHE_NAME in sw.js
hochgezaehlt -- beide gehoeren zusammen. */ hochgezaehlt -- beide gehoeren zusammen. */
navigator.serviceWorker navigator.serviceWorker
.register("/webdesign/sw.js?v=60", { scope: "/webdesign/" }) .register("/webdesign/sw.js?v=61", { scope: "/webdesign/" })
.catch(function () { /* still schlucken: ohne SW funktioniert alles weiter */ }); .catch(function () { /* still schlucken: ohne SW funktioniert alles weiter */ });
}); });
} }
+1 -1
View File
@@ -34,7 +34,7 @@
Start alle alten Zwischenspeicher weg. Muss bei jeder Änderung an den Start alle alten Zwischenspeicher weg. Muss bei jeder Änderung an den
Dateien unten hochgezählt werden, sonst hängen Nutzer auf einem alten Dateien unten hochgezählt werden, sonst hängen Nutzer auf einem alten
Stand fest. */ Stand fest. */
const CACHE_NAME = "dogfather-webdesign-v60"; const CACHE_NAME = "dogfather-webdesign-v61";
/* Bausteine, die die Oberfläche zum Anzeigen braucht. Bewusst KEINE /* Bausteine, die die Oberfläche zum Anzeigen braucht. Bewusst KEINE
HTML-Datei in dieser Liste. */ HTML-Datei in dieser Liste. */
+193
View File
@@ -2275,6 +2275,43 @@
<h2 style="font-size:1.15rem">Bestehende Zugänge</h2> <h2 style="font-size:1.15rem">Bestehende Zugänge</h2>
<div id="vw-kunden-liste"><div class="vw-laedt">Wird geladen…</div></div> <div id="vw-kunden-liste"><div class="vw-laedt">Wird geladen…</div></div>
<!-- Auskunft und Löschung (DSGVO Art. 15/17).
Steht bewusst hier unten und aufgeklappt-verborgen: Es ist
ein seltener Vorgang, aber wenn er kommt, läuft eine Frist
von einem Monat. Ein eigener Reiter wäre zu prominent für
etwas, das man vielleicht zweimal im Jahr braucht — ganz
weglassen hieße, im Ernstfall von Hand durch ein Dutzend
Tabellen zu suchen. -->
<details class="wd-karte" id="vw-ds" style="margin-top:1.6rem">
<summary style="cursor:pointer;font-weight:700;min-height:44px;display:flex;align-items:center">
Auskunft und Löschung (DSGVO)
</summary>
<p class="wd-klein wd-leise" style="margin-top:.8rem">
Für Anfragen nach Art. 15 („Welche Daten haben Sie über mich?“)
und Art. 17 („Bitte löschen Sie meine Daten“). Beides ist
binnen eines Monats zu beantworten.
</p>
<div class="wd-feld" style="margin-top:1rem">
<label for="vw-ds-mail">E-Mail-Adresse der Person</label>
<input type="email" id="vw-ds-mail" autocomplete="off"
placeholder="[email protected]" />
</div>
<div class="wd-btn-reihe" style="margin-top:.8rem">
<button type="button" class="wd-btn wd-btn--klein" id="vw-ds-auskunft">
Auskunft erstellen
</button>
<button type="button" class="wd-btn wd-btn--zweit wd-btn--klein" id="vw-ds-vorschau">
Was würde eine Löschung betreffen?
</button>
</div>
<div id="vw-ds-ergebnis" style="margin-top:1rem"></div>
</details>
</div> </div>
<!-- ============ PROJEKTE ============ --> <!-- ============ PROJEKTE ============ -->
@@ -2966,6 +3003,162 @@
return browser + (system ? " auf " + system : ""); return browser + (system ? " auf " + system : "");
} }
/* =====================================================================
AUSKUNFT UND LÖSCHUNG (DSGVO Art. 15/17)
Drei Schritte, und die Reihenfolge ist Absicht: sehen, Vorschau,
erst dann ausführen. Eine Löschung ist endgültig — sie gehört nicht
hinter einen Knopf, der sofort zuschlägt.
Die Auskunft wird als Datei zum Herunterladen angeboten, nicht nur
angezeigt. Man muss sie der Person schicken können, und zwar
vollständig; abtippen wäre der sicherste Weg, etwas zu vergessen.
===================================================================== */
function dsMail() {
var f = document.getElementById("vw-ds-mail");
return f ? f.value.trim() : "";
}
function dsMeldung(html, art) {
var z = document.getElementById("vw-ds-ergebnis");
if (!z) return;
var farbe = art === "warn" ? "var(--wd-lila-hell)"
: art === "gut" ? "var(--wd-aqua)" : "var(--wd-rand)";
z.innerHTML = '<div style="border-left:3px solid ' + farbe +
';padding:.8rem 1rem;background:rgb(255 255 255 / .03);border-radius:8px">' + html + '</div>';
}
async function dsAuskunft() {
var mail = dsMail();
if (!mail) return dsMeldung("Bitte zuerst eine E-Mail-Adresse eintragen.", "warn");
dsMeldung("Wird zusammengetragen…");
var d;
try {
d = await api("/webdesign/admin/datenschutz/auskunft", { method: "POST", body: { email: mail } });
} catch (fehler) {
return dsMeldung("Nicht möglich: " + s((fehler && fehler.message) || "unbekannter Fehler") +
(fehler && fehler.status === 404
? '<br /><span class="wd-klein wd-leise">Der Server kennt diesen Zugang noch nicht — er braucht einen Neustart mit dem neuen Stand.</span>'
: ""), "warn");
}
if (!d || !d.ok) return dsMeldung("Nicht möglich: " + s((d && d.error) || "unbekannt"), "warn");
if (!d.datensaetze) {
return dsMeldung("Zu <strong>" + s(mail) + "</strong> ist nichts gespeichert.", "gut");
}
var liste = (d.bereiche || []).map(function (b) {
return "<li>" + s(String(b.anzahl)) + "× " + s(b.was) +
(b.aufbewahren ? ' <span class="wd-klein wd-leise">(aufbewahrungspflichtig)</span>' : "") +
"</li>";
}).join("");
dsMeldung(
"<strong>" + s(String(d.datensaetze)) + " Datensätze</strong> zu " + s(mail) +
'<ul class="wd-liste" style="margin:.6rem 0">' + liste + "</ul>" +
'<div class="wd-btn-reihe">' +
'<button type="button" class="wd-btn wd-btn--zweit wd-btn--klein" id="vw-ds-laden">' +
"Als Datei herunterladen</button></div>", "gut");
var knopf = document.getElementById("vw-ds-laden");
if (knopf) {
knopf.addEventListener("click", function () {
/* Die vollständige Auskunft, wie sie herausgegeben wird. JSON,
weil Art. 20 ein "gängiges, maschinenlesbares Format"
verlangt — und weil nichts verloren geht. */
var blob = new Blob([JSON.stringify(d, null, 2)], { type: "application/json" });
var url = URL.createObjectURL(blob);
var a = document.createElement("a");
a.href = url;
a.download = "auskunft-" + mail.replace(/[^a-z0-9]/gi, "-") + "-" +
new Date().toISOString().slice(0, 10) + ".json";
a.click();
URL.revokeObjectURL(url);
});
}
}
async function dsVorschau() {
var mail = dsMail();
if (!mail) return dsMeldung("Bitte zuerst eine E-Mail-Adresse eintragen.", "warn");
dsMeldung("Wird geprüft…");
var d;
try {
d = await api("/webdesign/admin/datenschutz/vorschau", { method: "POST", body: { email: mail } });
} catch (fehler) {
return dsMeldung("Nicht möglich: " + s((fehler && fehler.message) || "unbekannt"), "warn");
}
if (!d || !d.ok) return dsMeldung("Nicht möglich: " + s((d && d.error) || "unbekannt"), "warn");
var weg = (d.loeschbar || []).map(function (x) {
return "<li>" + s(String(x.anzahl)) + "× " + s(x.was) + "</li>";
}).join("") || '<li class="wd-leise">nichts</li>';
/* Der wichtigere Teil: was NICHT gelöscht werden darf, mit Grund
und Datum. Ohne diese Angabe müsste man bei jeder Anfrage neu
nachschlagen, welche Frist gilt. */
var bleibt = (d.bleibt || []).map(function (x) {
return "<li>" + s(String(x.anzahl)) + "× " + s(x.was) +
'<br /><span class="wd-klein wd-leise">' + s(x.grund) +
" · frei ab " + s(x.freiAb) + "</span></li>";
}).join("");
dsMeldung(
"<strong>Würde gelöscht</strong>" +
'<ul class="wd-liste" style="margin:.4rem 0 .8rem">' + weg + "</ul>" +
(bleibt
? "<strong>Bleibt (gesetzliche Aufbewahrung)</strong>" +
'<ul class="wd-liste" style="margin:.4rem 0 .8rem">' + bleibt + "</ul>"
: "") +
'<p class="wd-klein">' + s(d.hinweis || "") + "</p>" +
((d.loeschbar || []).length
? '<div class="wd-feld" style="margin-top:.8rem">' +
'<label for="vw-ds-best">Zum Löschen die Adresse noch einmal eintippen</label>' +
'<input type="email" id="vw-ds-best" autocomplete="off" placeholder="' + s(mail) + '" />' +
"</div>" +
'<div class="wd-btn-reihe" style="margin-top:.6rem">' +
'<button type="button" class="wd-btn wd-btn--klein" id="vw-ds-loeschen">' +
"Jetzt löschen</button></div>"
: ""), "warn");
var lk = document.getElementById("vw-ds-loeschen");
if (lk) lk.addEventListener("click", dsLoeschen);
}
async function dsLoeschen() {
var mail = dsMail();
var best = document.getElementById("vw-ds-best");
var bestaetigung = best ? best.value.trim() : "";
if (mail.toLowerCase() !== bestaetigung.toLowerCase()) {
return dsMeldung("Die beiden Adressen stimmen nicht überein. " +
"Zum Schutz vor einem Vertipper wird sie ein zweites Mal verlangt — " +
"der Unterschied zwischen .de und .com ist ein Buchstabe, die Folge unwiederbringlich.", "warn");
}
dsMeldung("Wird gelöscht…");
var d;
try {
d = await api("/webdesign/admin/datenschutz/loeschen",
{ method: "POST", body: { email: mail, bestaetigung: bestaetigung } });
} catch (fehler) {
return dsMeldung("Nicht möglich: " + s((fehler && fehler.message) || "unbekannt"), "warn");
}
if (!d || !d.ok) return dsMeldung("Nicht möglich: " + s((d && d.error) || "unbekannt"), "warn");
dsMeldung("<strong>" + s(String(d.geloescht)) + " Datensätze gelöscht.</strong>" +
'<p class="wd-klein" style="margin-top:.5rem">' + s(d.hinweis || "") + "</p>", "gut");
}
(function dsVerdrahten() {
var a = document.getElementById("vw-ds-auskunft");
if (a) a.addEventListener("click", dsAuskunft);
var v = document.getElementById("vw-ds-vorschau");
if (v) v.addEventListener("click", dsVorschau);
})();
async function ladeUebersicht() { async function ladeUebersicht() {
var laedt = document.getElementById("vw-ub-laedt"); var laedt = document.getElementById("vw-ub-laedt");
var inhalt = document.getElementById("vw-ub-inhalt"); var inhalt = document.getElementById("vw-ub-inhalt");