Compare commits

..
964 Commits
Author SHA1 Message Date
ClaudeandClaude Opus 5.5 594aa9196a Datenschutz Weg B: PDF aus der freigegebenen Fassung (Stand 22.10.2026) gedruckt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 16:01:19 +00:00
ClaudeandClaude Sonnet 5.5 3b5e1d559a Datenschutz Weg B (E4): Forensatz sagt "sind angewiesen" statt "nehmen auf"
Der Hinweis in der E-Mail zu einem Forenbeitrag ist eine Arbeitsanweisung. Der Dienst legt den Baustein nicht in den Pult-Eintrag und
erfasst keine von Hand geschriebene Mail; der Text sagt es darum wie im Absatz davor: Die Mitarbeiter sind angewiesen, den Hinweis
aufzunehmen. Der Test verlangt den neuen Satz und verbietet den alten. Kommentar zur Forenerkennung des Dienstes nachgezogen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 15:41:20 +00:00
ClaudeandClaude Sonnet 5.5 c6e3c47be7 Datenschutz Weg B (E4): Beiträge aus Frage-Foren beantworten wir nicht im Forum
Abschnitt 11 sagt, was der Dienst seit E4 tut: Für Beiträge in den Frage-Foren (gutefrage.net,
forum-hilfe.de, wer-weiss-was.de, wpde.org) entwirft die KI keine Antwort; ein Mitarbeiter sieht
den Beitrag an und meldet sich höchstens per E-Mail, wenn der Beitrag ausdrücklich um Angebote
per E-Mail bittet (mit dem Hinweis am Ende), sonst nicht. Der Satz über öffentlich sichtbare
Antworten in Frage-Foren entfällt, Abschnitt 14 sagt "höchstens eine Antwort". Der Test pinnt
die Sätze, die Forenliste (gleich in Quelle und Regel) und dass nirgends steht, wir antworteten
im Forum.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 15:26:18 +00:00
ClaudeandClaude Sonnet 5.5 b73cd340bc Datenschutz Weg B (E2): aussortierte Anfragen spätestens am Tag nach dem Fund kürzen
Abschnitt 11 sagt jetzt, was der Dienst seit Regel C tut: Anfragen, die die KI als nicht
passend aussortiert, werden spätestens am Tag nach dem Fund gekürzt (erster täglicher
Kürzlauf nach dem Berliner Kalendertag des Fundes). 30 Tage gelten nur noch nach einer
Absage, gerechnet ab dem Lesen. Der Zweck "Mitarbeiter sehen nach, ob die KI zu Unrecht
aussortiert hat" und der Satz "Bis dahin speichern wir sie" entfallen. Die Fristenliste und
der Test legal_datenschutz_anfragen sind angepasst (Liste wörtlich, nur noch ein Eintrag mit
30 Tagen, entfallene Sätze verboten).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 15:24:28 +00:00
ClaudeandClaude Sonnet 5.5 53ae593860 Datenschutz Weg B: zwei Sätze lesbarer (Plattform-Hinweise, Firmenadresse im Widerspruchskasten)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 14:47:06 +00:00
ClaudeandClaude Sonnet 5.5 fd5994c243 Datenschutz Weg B nach zweiter Prüfung: Firmen-Domain, Postfach, Zwecke, verständlichere Fristen
- Prüfwerte nennen die Firmen-Domain (bei Firmenadressen, nicht bei Freemail); der Widerspruchskasten sagt, dass die Sperre dann auch die Domain trifft
- Postfach: der Dienst lädt jede neue Mail zur Prüfung des Absenders, gespeichert werden nur die Mails der Plattformen
- Name: "Wir lesen Ihren Namen nicht gesondert aus"; Zweck der 30 Tage (Fehlurteile der KI prüfen) und der Reste (Auswertung) genannt
- Hinweis: Mitarbeiter nehmen ihn in jede Antwort auf, auch in selbst geschriebene; Frage-Foren: Antwort öffentlich sichtbar
- Widerspruchskasten nennt die Plattform-Mail im Postfach (bis 90 Tage nach Eingang)
- Verständlich: "Fund" erklärt, Fristen als Liste, robots.txt umschrieben, Absätze höchstens 130 Wörter; Hinweis im Dienst folgt (02a0b3c, Nachfolger)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 14:45:32 +00:00
ClaudeandClaude Sonnet 5.5 5c49f63616 Datenschutz Weg B: Alarm-Postfach bei Hetzner, Sicherungen bis zu sieben Tage, keine [OFFEN]-Markierung mehr
Abschnitt 11: Das Alarm-Postfach ist support@webklar.com bei der Hetzner Online GmbH (Abschnitt 8); der Dienst liest dort nur den Posteingang
und übernimmt nur die E-Mails der Plattformen; diese löschen wir im Postfach spätestens 90 Tage nach dem Eingang (keine "Postfach-Regel"
versprochen). Gekürzte Daten bleiben in den Datensicherungen bis zu sieben Tage, wie im übrigen Text. Der Hinweis in der Antwort heißt jetzt
"Am Ende unserer Antwort steht ein Hinweis" und nennt, dass die Mitarbeiter angewiesen sind, ihn beim Bearbeiten nicht zu entfernen; er ist
wörtlich auf Claras Hinweis im Dienst abgestimmt. Abschnitt 12: Hetzner nennt das Postfach, der eigene Listenpunkt entfällt.

Tests: jede [OFFEN]-Markierung (auch im Kommentar) und jeder Span class="offen" macht legalTexts rot; Pins für die neuen Sätze. Das Upload-Skript
bricht weiter ab, solange eine Markierung steht. pdf_pruefsumme bleibt rot bis zum Neudruck des PDF am Pushtag.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 14:18:18 +00:00
ClaudeandClaude Sonnet 5.5 23ab43f9fe Datenschutz Weg B nach Prüfung: Zusagen auf den Dienst begrenzt, [OFFEN]-Markierungen, Upload-Sperren
Abschnitt 11: Sperre und Kürzung ab dem Lesen unserer Antwort, nur je Beitrag und E-Mail-Adresse (neues Gesuch = neuer Beitrag);
aussortierte Anfragen 30 Tage; feste Ausschlussliste der Websuche samt robots.txt; Kriterium für die Dauer der Reste; Kopie der
Standardvertragsklauseln; Hinweis in der ersten Nachricht nennt den Widerspruch; Abschnitt 11 und 14: die KI sortiert aus, ein
Mitarbeiter entscheidet über jede Antwort. Alarm-Postfach-Anbieter und Aufbewahrung der Server-Sicherungen stehen als sichtbare
[OFFEN]-Markierung im Text (auch in Abschnitt 12).

Tests: Pins angepasst, Markierungen nur die zwei erlaubten, neuer Test für Zusagen in Reichweite des Dienstes. Upload-Skript bricht ab,
solange eine [OFFEN]-Markierung steht oder die Prüfsumme neben dem PDF nicht zur HTML passt.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 13:57:18 +00:00
ClaudeandClaude Sonnet 5.5 b3ca84a8e5 Datenschutz-Upload: Vermerk "Abschnitt 11 Interessenten (Antworten auf öffentliche Anfragen)", Test prüft den vollen Wortlaut
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 13:10:22 +00:00
ClaudeandClaude Sonnet 5.5 6a25df22fb Tests: legal_datenschutz_anfragen (Abschnitt 11 Weg B, Stand 22.10.2026), Titelliste und Stand-Pin angepasst
Drei neue Tests: tragende Wendungen von Abschnitt 11, keine Stufe-2-Reste und keine offenen Markierungen, Abschnitt 2, 12 und 14 passend zu 11.
Der alte Pin "Stand 17.10.2026" prüft nur noch "nicht vor der Reichweitenmessung". Die Titelliste kennt die neuen Überschriften von 11 und 14.
pdf_pruefsumme bleibt rot, bis das PDF neu gedruckt ist.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 13:10:22 +00:00
ClaudeandClaude Sonnet 5.5 e554071739 Datenschutz Stand 22.10.2026: Abschnitt 11 Interessenten nur für den Anfrage-Weg (Weg B), Anker #interessenten
Abschnitt 11 beschreibt nur, was heute läuft: öffentliche Gesuche (Alarm-E-Mails der Plattformen, Websuche), KI bewertet und entwirft, ein
Mitarbeiter sendet einmal von Hand. Daten, Rechtsgrundlage, Anthropic, Fristen (90/30 Tage), Hash-Werte, Kopien und Widerspruch.
Abschnitt 2 (auch Privatpersonen aus Foren), 12 (Anthropic-Zweck, Plattformen) und 14 (keine Einzelentscheidung nach Art. 22) angepasst.
Kein Resend, Cloudflare, Overpass und kein Kaltmail-Weg: die volle Fassung bleibt auf feature/workers-datenschutz für Stufe 2.
PDF und Prüfsumme sind noch nicht neu gedruckt.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 13:10:22 +00:00
Claude 0fbeadd0c1 Rechtliches: Skript zum Hochladen der Datenschutzerklärung als neue Version
Der Ablauf des Vorbilds (hochladen) ist jetzt eine exportierte, parametrisierte Funktion; datenschutz-hochladen.mjs ruft sie mit Karte datenschutz, PDF und HTML auf und bricht ab, wenn der Stand in der HTML nicht der Freigabe entspricht.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
(cherry picked from commit 06676c459d0ac4f534aed7d616e2c0d40ac97b97)
2026-10-22 13:03:30 +00:00
ClaudeandClaude Opus 5.5 dd062b1f15 Analyse-Skript: bei SPA-Seitenwechseln die Verweildauer der vorigen Seite senden
Router-Links (z. B. React Router auf webklar.com) verhindern den Standard-Klick, dann griff onLinkClick nicht
und das en der vorigen Seite ging verloren. Jetzt sendet der Seitenwechsel-Timer zuerst en (nur mit
Einwilligung, einmal je Seite) und dann das pv der neuen Seite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 18:37:55 +00:00
ClaudeandClaude Opus 5.5 07a4e153bf Datenschutzerklärung Stand 17.10.2026: Abschnitt 4a Reichweitenmessung auf webklar.com, PDF neu
- Abschnitt 4: eigene Website-Analyse auf webklar.com statt "keine Analyse-Werkzeuge", Eintrag wk-consent.
- Abschnitt 4a: Rolle (WEBklar selbst Verantwortlicher), Daten ohne und mit Einwilligung, Rechtsgrundlagen,
  Widerruf, Fristen, Verweis auf webklar.com/datenschutz-analyse.html.
- Abschnitt 5: Kontaktformular-Anfragen gehen seit 17.10.2026 auch per Mail an das eigene Postfach.
- Abschnitt 13: Einwilligung nur für die erweiterte Reichweitenmessung.
- Archiv: Banner-Fassung eigen 2026-10-17 und Kopie der Zusatzseite von webklar.com.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 18:34:06 +00:00
ClaudeandClaude Opus 5.5 9ed295946c Website-Analyse für webklar.com: Sie-Fassung des Banners, fester Messdienst-Eintrag, Auswertung fürs WOMS
- Messskript: data-variant="eigen" zeigt eine eigene Banner-Fassung (Sie-Form, WEBklar misst selbst,
  BANNER_VERSION_EIGEN 2026-10-17); Kunden-Websites bleiben bei der Fassung 2026-10-02.
- config.analyse.eigeneWebsite: fester Schlüssel und Domains von webklar.com; ensureEigeneWebsite zieht den
  Eintrag im Messdienst täglich nach (aktiv: false schaltet ab). Kein Kunde, kein Abo, keine Tickets.
- /api/admin/analyse-eigen (jeder Mitarbeiter): Kennzahlen 7/30/90 Tage und Heatmap-Link auf webklar.com.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 18:34:06 +00:00
ClaudeandClaude Sonnet 5.5 f3f8ac3777 Kundenansicht neu: doppelte Klasse .kn-nur-lesen (ansichten.css) durch die vorhandene .kn-nur-leser (huelle.css) ersetzt
Die Stufennamen im Fortschrittsbalken der Zeitleiste (nur für Vorlesegeräte) tragen jetzt .kn-nur-leser; die zweite,
gleichwertige Regel .kn-nur-lesen in ansichten.css ist gestrichen. Tests: DOM- und Quelltext-Test (keine Spur von
.kn-nur-lesen in Stil und Modul), Stil-Test sucht die Klassenregeln auch in huelle.css.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 15:48:56 +00:00
ClaudeandClaude Sonnet 5.5 ace4c85e72 Kundenansicht neu (Aufträge): Zeitleiste barrierefrei, "mehr anzeigen" mit Auftragstitel im Namen und aria-controls, "Im Chat antworten" bricht nicht um
Der Knopf "mehr anzeigen"/"weniger anzeigen" bekommt einen aria-label mit dem Auftragstitel (der sichtbare Text steht vorn im
Namen, der Titel nur als Text) und aria-controls auf die Liste seines Auftrags (eindeutige Id je Liste); beides zieht beim Umschalten mit.
Der Linktext "Im Chat antworten" trägt white-space: nowrap und bleibt in einer Zeile beisammen.
Tests: DOM-Test für Name, Verknüpfung, Umschalten, fremden Titel und Ersatztitel; CSS-Test für nowrap.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 15:48:25 +00:00
ClaudeandClaude Sonnet 5.5 14dc2ad1bf Kundenansicht neu (Aufträge): Aufträge mit Rückfrage vor den übrigen laufenden, nie hinter "Ältere Aufträge anzeigen"
sortiereAuftraege stellt unter den laufenden Aufträgen die mit Rückfrage (braucheInfo) nach vorn, danach
wie bisher die jüngste Aktivität, dann die Reihenfolge des Servers. Erledigte und Stornierte bleiben dahinter.
Tests: Reihenfolge mit Rückfragen und Gruppierung (die Rückfrage mit der ältesten Aktivität bleibt sichtbar).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 15:47:21 +00:00
ClaudeandClaude Sonnet 5.5 6bfcaa892c Kundenansicht neu (Meine): Kurzinfo nur auf Anfrage (GET /api/funktionen?kurzinfo=1)
Der Server berechnet kurzinfo (Terminplaner, Webshop, News, Buchungskalender, Öffnungszeiten) nur noch,
wenn die neue Ansicht ?kurzinfo=1 sendet. Das tut allein die Seite "Meine"; Etagen, Einschalt-Ablauf und
Module fragen ohne den Parameter, dann fehlt das Feld und es gibt keine Abfrage auf Termine, Bestellungen,
Beiträge und Anfragen. Mit dem Parameter bleibt die Antwort wie bisher (Text oder null je Funktion).
Nur der Wert "1" gilt; doppelte oder als Liste gesendete Parameter heißen: ohne. Alte Ansicht unverändert.

Tests: kundenansichtKurzinfo (neuer Fall ohne/mit Parameter, Abfragen und Feld), knFunktionenAnsicht
(die Seite lädt genau einmal ?kurzinfo=1, auch nach "Nochmal versuchen").

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 15:46:48 +00:00
ClaudeandClaude Opus 5.5 d7724afc02 Merge kn/meine-pilot (Pilot-Feedback 17.10.)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 15:43:07 +00:00
ClaudeandClaude Opus 5.5 5f4e158660 Merge kn/auftraege-pilot (Pilot-Feedback 17.10.)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 15:43:07 +00:00
ClaudeandClaude Opus 5.5 85808fc115 Merge kn/chat-fix (Pilot-Feedback 17.10.)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 15:43:07 +00:00
ClaudeandClaude Sonnet 5.5 1703dabaaf Kundenansicht neu (Aufträge): Rückfrage-Link als Block, bündige Datumsspalte bei Einträgen aus dem Vorjahr
- Browserlauf: display:flex am Link zerlegte den Satz in zwei Spalten; der Link ist jetzt ein Block, die 44 px Tippfläche kommen aus dem Innenabstand.
- Trägt ein Datum das Jahr (TT.MM.JJ), bekommt die Datumsspalte des Auftrags die Breite dafür (Klasse kn-az-mit-jahr), die Texte bleiben bündig.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 15:31:46 +00:00
ClaudeandClaude Sonnet 5.5 d62f8b805e Kundenansicht neu (Aufträge): Rückfrage-Zeile als ganze Link-Zeile, Einklappen ohne verschachtelte Regel, Balken im Pseudo-Element
- Die Zeile "Wir brauchen eine Info von dir · Im Chat antworten" ist ein Link (#chat) mit großer Tippfläche am Handy; unterstrichen ist nur der zweite Teil.
- Browserlauf: .kn-einklappbar:not(.is-open) [data-extra] verbarg die Zeilen einer aufgeklappten Zeitleiste, wenn der äußere Wrapper zu war.
  Die Wrapper haben jetzt eigene Klassen und Regeln nur für direkte Kinder (mit Regressionstest).
- Die Fläche der Balken-Abschnitte liegt im Pseudo-Element, der Lesetext darunter bleibt auf Weiß (Gate low-contrast).
- Das Modul steht in den Listen der Quelltext- und Stil-Tests der Ansicht-Module.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 15:28:43 +00:00
ClaudeandClaude Sonnet 5.5 ec79defb2a Plan Kundenansicht neu: Nachtrag N52 (Kurzinfo je Funktion in Meine) und Vertrag V5
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 15:28:00 +00:00
ClaudeandClaude Sonnet 5.5 670ead3091 Kundenansicht neu Chat-Fix: Chat als Vollbild-Ansicht (Verlauf scrollt in sich, Eingabe unten, Spalte zentriert, "Chat" klein im Kopf)
Pilot-Feedback 17.10.: Der Chat wirkte durchsichtig und verrutscht.
- Handy: Inhalt steht fest zwischen Kopf und Leiste (wie das Blatt); die Eingabe sitzt höchstens 6 px über der Leiste bzw. dem Widerrufsstreifen und bei
  offener Tastatur am unteren Rand des sichtbaren Bereichs. "Chat" steht klein im Kopf (die H1 bleibt nur für Screenreader), der Kopf ist deckend mit Haarlinie,
  der Verlauf beginnt direkt darunter und scrollt in sich.
- Desktop: Spalte in Fensterhöhe, Chat höchstens 760 px breit und mittig, Verlauf füllt die Höhe, Eingabe am unteren Spaltenrand.
- Verlauf steht unten (neueste Nachricht sichtbar), kurze Verläufe unten bei der Eingabe; Blasen fremd #f8f9fb, eigen #ebeef1.
- "Senden" bleibt schwarz, auch bei leerem Feld (gesperrt im Ansichtsmodus weiter gedämpft).
- haltFokus: Druck auf Senden, Anhang oder Mikrofon nimmt dem Textfeld den Fokus nicht; sonst kommt die Leiste zurück, schiebt die feste Eingabe hoch und der
  Tipp auf "Senden" geht bei offener Tastatur verloren (im Browser nachgestellt). public/chat-composer.js und die alte Ansicht bleiben unverändert.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 15:17:26 +00:00
ClaudeandClaude Sonnet 5.5 9aeb9872be Kundenansicht neu (Meine, Live-Detail): Zeile einer abgeschalteten Funktion kürzer ("Im Chat wieder anfragen")
Die Detailzeile umbrach bei mehreren Websites neben der Pille auf drei Zeilen. Der Abo-Ende-Text mit dem
Löschtag bleibt unverändert.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 15:17:13 +00:00
ClaudeandClaude Sonnet 5.5 db62e320ff Kundenansicht neu (Meine, Live-Detail): Zeile mit Detailzeile und Pfeil, Pille nur wenn die Funktion nicht läuft
Jede Zeile in "Meine" zeigt den Funktionsnamen fett, darunter die Kurzinfo des Servers ("3 Termine heute ·
nächster 10:30") oder "Aktiv", rechts einen Pfeil. Eine Status-Pille steht nur noch bei Funktionen, die nicht
laufen (In Einrichtung, Zahlung offen, Angefragt, Abgeschaltet, Abgelehnt, Gesperrt ...); "Aktiv seit ..." entfällt.
Bei mehreren Websites steht der Websitename vorne in der Detailzeile. Die Zeilen für Funktionen, die nicht laufen,
sind kürzer ("Wir richten sie gerade ein", "Anfrage abgelehnt · Im Chat nachfragen"). Die Kurzinfo gilt nur bei
aktiver Funktion, nur als kurzer nicht leerer Text und wird nie als HTML gelesen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 15:10:43 +00:00
ClaudeandClaude Sonnet 5.5 53a191219e Kundenansicht neu (Meine, Live-Detail): GET /api/funktionen liefert je Projekt und Funktion die Kurzinfo
In der neuen Ansicht trägt jede Funktion unter projects[].features.<key> zusätzlich kurzinfo (Text oder null).
Nur aktive Funktionen bekommen einen Text: Terminplaner "3 Termine heute · nächster 10:30", Webshop "2 neue
Bestellungen", News-Pinnwand "4 Beiträge sichtbar", Buchungskalender "1 offene Anfrage" bzw. "Nächste Anreise
24.10.", Öffnungszeiten live der Status der Website (ohne eigene Abfrage). Berliner Zeit, je Funktion höchstens
zwei schmale Abfragen (limit, select, Index-Felder), alles gleichzeitig; jede Funktion einzeln begrenzt (1,5 s)
und fehlertolerant: Fehler oder Zeitüberschreitung ergibt null. Ohne neue Ansicht bleibt die Antwort wie sie
war und es wird nichts gelesen. Die Analyse hat keine Kurzinfo (Zahlen kommen aus dem Auswertungsdienst).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 15:09:25 +00:00
ClaudeandClaude Sonnet 5.5 2064bddba6 Kundenansicht neu (Aufträge): "Was wir zuletzt erledigt haben" als Zeitleiste auf der Website-Seite
Je Auftrag eine Überschrift (Titel fett, Status-Pille), ein schmaler Balken mit vier
Abschnitten (Stufennamen als Text für Screenreader), bei Rückfrage die Zeile "Wir brauchen
eine Info von dir · Im Chat antworten" und eine Zeitleiste mit Haarlinie (Datum TT.MM. und
Kurztext, drei sichtbar, "mehr anzeigen"). Fünf Aufträge offen, der Rest hinter
"Ältere Aufträge anzeigen". Die Auftragsnummer steht nicht mehr im Text (ruhiger), bleibt
als data-auftrag. Die alte baueAktivitaet und ihre toten CSS-Regeln entfallen; die
Projektdetails (baueAuftrag, baueArbeiten) bleiben unverändert.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 15:07:34 +00:00
ClaudeandClaude Sonnet 5.5 f231f4b887 Kundenansicht neu (Aufträge): Logik der Zeitleiste (Fortschritt, Reihenfolge, Gruppierung, Kürzen, Kurztexte)
Reine Funktionen in public/neu/auftrags-zeitleiste.js: laufende Aufträge vor erledigten
(jeweils jüngste Aktivität zuerst, Storniertes zuletzt), höchstens fünf Aufträge offen,
vier Fortschrittsabschnitte aus status.step, Zeilen der Zeitleiste neueste zuerst mit
Datum TT.MM. und Kurztext. Test zuerst (rot: Modul fehlte), dann grün.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 15:05:48 +00:00
ClaudeandClaude Sonnet 5.5 5cd49d6a4a Kundenansicht neu E-Fix F1 (Nachkorrektur): Widerrufsstreifen auf den Bestellseiten ausgeblendet
Auf Abo bestellen, Wechsel, Zusatzspeicher und Übernahmegutschrift fehlt der feste Widerrufsstreifen, damit Kerninfo und Knopf "Zahlungspflichtig bestellen"
auch bei Privatkunden in laufender Frist auf 375x667 auf einem Bildschirm stehen (Spec Abschnitt 4). Auf den Hauptseiten bleibt er (Spec 5.2),
"Vertrag widerrufen" bleibt im Konto und unter Verträge erreichbar.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 22:44:20 +00:00
ClaudeandClaude Sonnet 5.5 c5f1d9545c Kundenansicht neu E-Fix F1 (Nachkorrektur): Wechsel nennt "mit 1 Paket" / "mit 2 Paketen" Zusatzspeicher (Dativ)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 22:40:53 +00:00
ClaudeandClaude Sonnet 5.5 0ee5c86809 Kundenansicht neu E-Fix F1 (Politur): "Fällig: erst am TT.MM.JJJJ (50,00 €)" statt doppeltem Doppelpunkt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 22:20:43 +00:00
ClaudeandClaude Sonnet 5.5 be34f1be1d Kundenansicht neu E-Fix F1 (Rechtsrunde): R18 nennt den gekürzten Block der Bestellseiten
Die Wortlaute des Blocks "Was du bestellst" sind seit E-Fix F1 knapp, die ausführlichen Sätze stehen unter dem Knopf. Als Entscheidung R18
(Vorschlag und Alternative, Kanzlei-Frage KQ6) in die Liste der Rechtsrunde aufgenommen; der Pin zählt jetzt R1 bis R18.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 22:07:07 +00:00
ClaudeandClaude Sonnet 5.5 fa79e6e10c Kundenansicht neu E-Fix F1 (Nachtrag): Wechsel mit gebuchtem Zusatzspeicher passt auf 375x667
Mit gebuchtem Zusatzspeicher (eine Block-Zeile mehr) maß der Wechsel 598 px gegen 558 nutzbare. Jetzt steckt der Zusatzspeicher im Preis
("52,00 € pro Monat mit 2 Paketen Zusatzspeicher (+10 GB)") und die gutgeschriebenen Tage in "Fällig"; die Einzelposten stehen ausführlich unter dem
Knopf. Gemessen auf 375x667 (Blockanfang bis Knopfunterkante, nutzbar 558 px): Wechsel Betrieb 448, Wechsel privat mit Häkchen 516, mit
Zusatzspeicher 536 px.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 22:06:36 +00:00
ClaudeandClaude Sonnet 5.5 c92ddd9363 Kundenansicht neu E-Fix W1 (Portal): Team-Endpunkt stundenkonto liefert zusätzlich active
GET /api/team/customers/:id/stundenkonto kannte nur Produkt und Konto; WOMS (stundenkontoView.js) kann bei beendetem Abo nicht "verfällt, wird
nicht berechnet" (V8, E17) statt "wird mit den nächsten Gutschriften verrechnet" zeigen, weil das Feld active fehlte. Jetzt trägt jede Antwort
active (Abo läuft: Status active, trialing oder past_due, auch mit vorgemerkter Kündigung; ohne Abo false). WOMS liest antwort.active schon,
nur ein ausdrückliches false ändert den Satz. Test in teamStundenkonto.test.js.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:49:50 +00:00
ClaudeandClaude Sonnet 5.5 6ff63a7a11 Kundenansicht neu E-Fix M10: hatLoeschVermerk prüft den strengen Vermerk der Löschjobs
hatLoeschVermerk erkannte gelöschte Daten an /gelöscht am/ in adminNote. Der Vermerk der Jobs ist strenger (Shopdaten bzw. Buchungsdaten
gelöscht am JJJJ-MM-TT), freier Text des Teams ("Domain gelöscht am Kundenwunsch") traf das lockere Muster: das Abo kam zurück, der Abgleich hielt
die Daten für gelöscht und setzte das Dokument auf off statt die Funktion wieder einzuschalten. Jetzt je Funktion das Muster des Jobs mit Datum.
Test in knLoeschVermerk.test.js.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:49:18 +00:00
ClaudeandClaude Sonnet 5.5 f6fc291dfe Kundenansicht neu E-Fix M9: featureView nennt den Löschtag auch für News, Webshop und Buchungskalender im Abo-Modell
featureView lieferte loeschungAm nur für den Terminplaner und Öffnungszeiten live. Im Abo-Modell werden aber auch News (30 Tage), Webshop und
Buchungskalender (90 Tage) nach dem Ende des Abos tatsächlich gelöscht (K39, Löschjobs); "Meine" und WOMS zeigten kein Datum, der Kommentar
"bei News verspräche das Datum eine Löschung, die nie kommt" stimmte nicht mehr. Jetzt: abo_ende bei einem Dokument des Abo-Modells (istAboModell)
von news, shop und unterkunft ergibt loeschTagNach(disabledAt, loeschFristTage(key)), derselbe Berliner Tag wie in K39. Team-Sperre, Bestand ohne
Abo-Modell und der Terminplaner bleiben unverändert. Test in knLoeschfristAnzeige.test.js. (Der Text in WOMS gehört zum WOMS-Teil W2.)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:49:18 +00:00
ClaudeandClaude Sonnet 5.5 3e32ddfe86 Kundenansicht neu E-Fix M8: Wechselangebot beim Einschalten nur, wenn der Wechsel möglich ist
aboDeckt nennt falsches_abo vor gekuendigt, beginnt_spaeter und nicht_aktiv (die Reihenfolge bleibt gepinnt). Die 409-Antwort setzte wechsel: true
allein aus grund === 'falsches_abo': ein Hosting-Abo in der Probezeit, mit offener Zahlung oder vorgemerkter Kündigung bekam das Angebot, landete auf
#wechsel bei "Ein Wechsel ist derzeit nicht möglich" und verlor den eigentlichen Grund. Jetzt prüft pruefeAboDeckung bei falsches_abo zusätzlich
wechselMoeglich (aboFunktionen.js, dieselben Bedingungen wie canSwitch und assertCanSwitch: Hosting, aktiv, nicht gekündigt, kein offener Wechsel);
ein Lesefehler dabei nimmt nur das Angebot weg. Test in funktionEinschalten.test.js.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:47:36 +00:00
ClaudeandClaude Sonnet 5.5 29ca12a409 Kundenansicht neu E-Fix M7: Bewegung reduziert ohne Rest-Fahrt der Leisten-Zeilen, #fortschritt unter dem haftenden Kopf
- Die Regel der zwei Fahrstuhl-Zeilen (#kn-zeile-haupt, #kn-zeile-etage, Spezifität 1-2-0) schlug das .kn-rw { transition: none } der Reduced-Motion-
  Regel: mit "Bewegung reduzieren" glitt die alte Zeile noch 550 ms durch die neue (beide Texte überlagert). Jetzt steht im Reduced-Motion-Block
  dieselbe Selektorliste mit transition: none.
- #fortschritt (scrollIntoView block start) ließ den Abschnitt "Was wir zuletzt erledigt haben" hinter dem haftenden Kopf beginnen: scroll-margin-top
  mit Kopfhöhe, Bannerhöhe, Safe-Area und 8 px (Desktop 16 px). Die Gutschrift-Etage scrollt im Blatt, dessen Oberkante schon unter dem Kopf liegt: dort
  ist nichts nötig.
Prüfung: Werkzeug-Ansichten kn-huelle-reduziert (jetzt auch die Zeilen bei offener Etage) und kn-p2-website-fortschritt, Pins in test/knEfixCss.test.js.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:46:15 +00:00
ClaudeandClaude Sonnet 5.5 140275022d Kundenansicht neu E-Fix M6: Zahlungsknöpfe sind nach der Rückkehr von Stripe wieder bedienbar
Nach "Zahlungspflichtig bestellen" setzt die Seite den Knopf auf "Weiter zur Zahlung…" (gesperrt) und leitet zu Stripe. Bricht der Kunde per
Zurück-Wischgeste ab, stellt Safari die Seite aus dem Bfcache her: der Knopf blieb gesperrt, ein neuer Versuch ging nur nach Neuladen. Nur
ansicht-wechsel.js hatte einen pageshow-Handler. Neuer Helfer beiRueckkehr(el, fn) in ansicht-hilfen.js (pageshow mit persisted, entfernt sich,
sobald das Element nicht mehr im Dokument steht); genutzt von der Bestellseite und von "Zahlungsart aktualisieren" (Verträge).
Speicher und Gutschrift leiten nicht zu Stripe, dort ist nichts zu tun.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:45:15 +00:00
ClaudeandClaude Sonnet 5.5 8bb02f3949 Kundenansicht neu E-Fix M5: Chat hält die Eingabe über der Tastatur (revealChatComposer aus app.js)
Der Chat der neuen Ansicht übernahm nur das Ausblenden der Leiste, nicht das Nachschieben der Eingabe: auf kleinen Handys (iPhone SE, Android mit
hoher Tastatur) konnte die Eingabezeile unter der Tastatur stehen bleiben, und die letzte Nachricht verschwand beim Schrumpfen des Verlaufs.
Jetzt wie in app.js: 350 ms nach dem Fokus im Composer und bei jedem visualViewport-Resize wird die Eingabe in den sichtbaren Bereich geschoben
(composerVersatz, nur wenn verdeckt, 8 px Luft, nur am Handy), und ein ResizeObserver hält die neueste Nachricht sichtbar, wenn man unten war.
Hörer und Beobachter werden beim Neuaufbau abgebaut. Tests für die Rechnung und die Verdrahtung in knAnsichtChat.test.js.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:44:20 +00:00
ClaudeandClaude Sonnet 5.5 f8962ffa50 Kundenansicht neu E-Fix M4: Mitarbeiter-Banner verschiebt Blatt und Menü nicht mehr unter den Kopf
Der Banner stand im Fluss (relative) und schob den Kopf nach unten, das am Handy feste Blatt begann aber fest bei der Kopfhöhe: es deckte den
unteren Teil des Kopfs samt Profilbild ab (Konto und Abmelden nicht erreichbar); am Desktop blieb das Menü bei fest 36 px stehen, der Banner
ist aber 40 px hoch und beim Scrollen blieb eine Lücke. Jetzt bleibt der Banner oben stehen (sticky), huelle.js setzt --kn-banner-h auf seine
Höhe (auch bei Umbruch am Handy), und Kopf, Menü, Blatt und Konto-Popover beginnen darunter. Werkzeug-Ansicht kn-huelle-banner-blatt.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:43:20 +00:00
ClaudeandClaude Sonnet 5.5 4c64edc057 Kundenansicht neu E-Fix M3: Fehlermeldungen der Bestellseiten und Formulare als Live-Region (role="alert")
abo-error, switch-error (und der Fehler der Wechsel-Vorschau), storage-error, tk-error samt Fehlerzeile der Rückfrage-Antwort, der Fehler von
"Zahlungsart aktualisieren" sowie login-error und form-error auf Login, Kündigen und Widerrufen waren einfache Absätze: Screenreader lasen
sie nicht vor, obwohl der Knopf wieder frei wurde. Jetzt role="alert", gepinnt in test/knEfixCss.test.js.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:42:07 +00:00
ClaudeandClaude Sonnet 5.5 ddaf2879c4 Kundenansicht neu E-Fix M2: aboModellFuerKunde liest bei Modus off kein Kundendokument
aboModellFuerKunde las das Kundendokument auch bei KUNDENANSICHT_NEU=off, obwohl neueAnsichtFuerKunde dort sofort false liefert. Alt-Kunden
bekamen so eine zusätzliche Lesung und einen neuen Fehlerpfad (ein 503 beim Widerruf des Wechsels ließ die Beenden-Mail der Analyse
entfallen, die Team-Aktivierung und GET /api/admin/analyse/:id antworteten mit 500). Jetzt sofort false im Modus off; Pilot und on
lesen wie bisher (kein Dokument: false). Test in analyseAboModell.test.js (eingeplanter 503 wird im Modus off nicht verbraucht).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:41:28 +00:00
ClaudeandClaude Sonnet 5.5 306c53871d Kundenansicht neu E-Fix M1: Widerrufsstreifen und Pille nach Kauf, Wechsel und Speicher-Plus neu laden
ladeFristen lief nur einmal beim Start; eine neue 14-Tage-Frist nach einem Wechsel oder Speicher-Plus erschien erst nach Neuladen, ein
abgelaufener Streifen blieb in einem langen PWA-Tab stehen. Die Hülle lädt die Fristen jetzt bei kn:zaehler-neu (vertraege, alle) und beim
Zurückkehren in den Tab neu (ladeFristenNeu: ein Fehler lässt den Stand stehen, die jüngste Antwort gilt); das Speicher-Plus meldet
kn:zaehler-neu vertraege nach Erfolg, der Wechsel tat es schon.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:40:29 +00:00
ClaudeandClaude Sonnet 5.5 4cac03a2bd Kundenansicht neu E-Fix F6: Pin auf build-legal-pdfs.sh kennt die Modi abo6 und funktionen6
P8 hat dem Skript die Fassungsfunktion funktionen6 und die Entwurfsmodi abo6 und funktionen6 ergänzt; der Pin zählte noch fünf Node-Aufrufe.
Jetzt sechs Aufrufe (60 s, 256 MB, je Fassungsfunktion einer), die zwei Entwurfsmodi sind gepinnt und `all` druckt sie weiterhin nicht mit;
bestehende Modi, Chrome-Grenze (120 s), Usage-Zeile und das Verbot paralleler Läufe bleiben unverändert geprüft.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:39:17 +00:00
ClaudeandClaude Sonnet 5.5 72b5ac5265 Kundenansicht neu E-Fix F5: Fassung 6 ohne PDF behauptet keinen Anhang, das Team bekommt den Hinweis
Die PDFs der Fassung 6 entstehen erst nach der Freigabe. Bis dahin gingen K01, K12 und K13 ohne Anhang raus, sagten aber "im Anhang" bzw.
"beigefügt", und das Team erfuhr es nicht (nur K38/T24 hatten dafür den Block anhang_fehlt).

- Neuer Helfer vertragsAnhang.js: lädt das PDF der zugestimmten Fassung und meldet fehlt nur bei Fassung 6 ohne lesbare Datei.
- K01, K12, K13: Block anhang (Anhangsaussagen) und ohne_anhang (Satz, dass die Unterlagen gesondert kommen).
- T02, T12, T13: Block anhang_fehlt "Unterlagen von Hand nachsenden" (T12 auch beim späteren Wirksamwerden, T13 auch ohne Aufgabe).
- Alte Fassungen unverändert: Hash-Pins auf die Wiedergabe der Vorlagen vor der Änderung, Anhang und Texte wie bisher.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:38:54 +00:00
ClaudeandClaude Sonnet 5.5 9c04f806eb Kundenansicht neu E-Fix F4: Team-Wiedereinschalten ohne gesendete Abrechnung nimmt die gespeicherte (Aufbauphase)
WOMS fragt nach einer Team-Sperre die Abrechnung nicht mehr ab und sendet keine; der Server setzte dann pauschal 'abo', pruefeAboDeckung warf
409 abo_fehlt, und eine in der Aufbauphase ('projekt', noch kein Abo) eingebaute Funktion liess sich nicht wieder einschalten.
Jetzt gilt bei disabled/team die am Dokument gespeicherte einrichtungAbrechnung, sonst wie bisher 'abo'; eine ausdrücklich gesendete
Abrechnung geht vor. Test in funktionenAdminAbo.test.js (Aufbauphase: ein, aus, ohne Angabe wieder ein = 200 mit warnung; mit abo hart).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:32:28 +00:00
ClaudeandClaude Sonnet 5.5 3962074af6 Kundenansicht neu E-Fix F3b: Safe-Area links und rechts in Hülle, öffentlichen Seiten und Chat-Vollbild
dashboard.html und die öffentlichen Seiten setzen viewport-fit=cover, aber kein Rand nutzte env(safe-area-inset-left|right): Im Querformat
am iPhone rutschten die ersten Zeichen unter Notch und Insel. Kopf, Hauptseite, Blatt, Streifen, Konto-Sheet, Fehlerzeile, Leiste, Banner und die
Sperre sowie Login, Rechtsseiten, Auftrag, Offline-Seite und die Kopfzeile des Chat-Vollbilds bauen ihre Ränder jetzt mit
max(Wert, env(safe-area-inset-left|right)). Am Desktop und ohne Notch ändert sich nichts (Inset 0). Pins in test/knEfixCss.test.js.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:31:33 +00:00
ClaudeandClaude Sonnet 5.5 8e349f6eca Kundenansicht neu E-Fix F3c: 44-px-Tippfläche für Kündigen, Widerrufen, Vertragsbedingungen und Zahlen am Handy
"Verträge hier kündigen", "Vertrag widerrufen", die Vertragsbedingungen und das "Zahlen" der Zeile "Rechnung offen" waren 14-px-Textlinks
von etwa 16 px Höhe. Am Handy jetzt inline-flex mit 44 px Mindesthöhe; das "Zahlen" steht rechts in der Zeile (der Trenner davor ist ein
eigenes Element und am Handy ausgeblendet, textContent unverändert). Pin in test/knEfixCss.test.js, Tippflächen-Prüfung in den
Werkzeug-Ansichten kn-p2-abo-privat und kn-p2-website-rechnung.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:30:45 +00:00
ClaudeandClaude Sonnet 5.5 92d7d247e9 Kundenansicht neu E-Fix F3a: Fokusring der Shell-Reiter liegt innen und ist sichtbar
.kn [tabindex]:focus-visible (module.css, 0-3-0, später geladen) und .kn button:focus-visible setzten outline-offset +2px und schlugen
.kn-t:focus-visible und .kn-reiter-t:focus-visible (0-2-0). Der Ring lag außerhalb und wurde von Balken und Reiterzeile abgeschnitten.
Jetzt .kn .kn-t[role="tab"] und .kn .kn-reiter-t[role="tab"] (0-4-0) mit Versatz -2px; der aktive Reiter auf dem schwarzen Gleiter
bekommt vom Gleiter einen hellen Innenring (Rückfall ohne Overlay: weißer Ring am Reiter). Pin in test/knEfixCss.test.js.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:30:06 +00:00
ClaudeandClaude Sonnet 5.5 112475e873 Kundenansicht neu E-Fix F2: Sperre der Mitarbeiter-Kundenansicht als echte Sperre in der neuen Seite
public/kundenansicht.js hängt bei 409 view_replaced und 401 view_expired .kb-view-overlay an den Body; die Regeln dafür standen nur in der
alten kundenbereich.css. In der neuen Seite entstand ein unformatierter Absatz mit nacktem Knopf unter der Leiste. Jetzt: fixiert, deckend weiß,
über Konto und Leiste, Text und Knopf "Tab schließen" (44 px, schwarz). Pin in test/knEfixCss.test.js, Ansicht kn-huelle-sperre im Werkzeug.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:27:07 +00:00
ClaudeandClaude Sonnet 5.5 d4050f870a Kundenansicht neu E-Fix F1: Kerninfo und Knopf der Bestellseiten passen in den nutzbaren Bereich (390x844, 375x667)
Die Messung im Werkzeug nahm innerHeight; nutzbar ist am Handy nur der Bereich zwischen Kopf und fester Leiste (735 px bei 844, 558 px bei 667).
Gemessen vor der Korrektur (Blockanfang bis Knopfunterkante, 375x667): Abo bestellen privat 799 px, Wechsel privat 888 px, Zusatzspeicher 567 px.

- Block knapp: Leistung als eine Zeile mit Link "Leistungen ansehen" auf die Leistungsbeschreibung, Preis, Fällig, Beginn, Laufzeit und Zahlung in je ein bis zwei Zeilen.
- Die ausführlichen Sätze stehen unter dem Knopf und der Kundenart-Zeile (baueEinzelheiten), ohne Klappe.
- Handy: Beschriftung läuft in der Zeile mit, kleinere Überschrift, knappere Abstände.
- Die Wortlaute des Blocks sind gekürzt und gehören in die Rechtsrunde.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:25:48 +00:00
ClaudeandClaude Sonnet 5.5 779320c080 Kundenansicht neu P7: Block der Bestellseiten kompakter, damit Kerninfo und Knopf auf ein Handy-Fenster passen
Die Wechselseite brauchte bei 390 x 844 bis zu 952 px vom Anfang des Blocks bis zum Knopf. Auf dem Handy stehen die Beschriftungen jetzt klein über dem Wert (weniger Abstand), und die Zeile Wartungs-Abo wiederholt die Leistungen nicht mehr, die schon in der Zeile Leistung stehen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:33:59 +00:00
ClaudeandClaude Sonnet 5.5 036bf4b0b9 Kundenansicht neu P7: Kündigen und Widerrufen in du (Fassung 6, C14)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:24:31 +00:00
ClaudeandClaude Sonnet 5.5 9166e023c5 Kundenansicht neu P7: bekannte Fehlertexte der Dienste in der neuen Ansicht in du
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:22:24 +00:00
ClaudeandClaude Sonnet 5.5 d17520c4f5 Kundenansicht neu P7: Gutschrift-Antrag schlank, Bedingungen als eigene Ansicht statt Klappe
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:20:57 +00:00
ClaudeandClaude Sonnet 5.5 9b00771d11 Kundenansicht neu P7: E-Mail-Speicher schlank, Plus mit Linkzeile, Warnung als eine Zeile
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:15:36 +00:00
ClaudeandClaude Sonnet 5.5 e7ac625f26 Kundenansicht neu P7: Wechselseite schlank, Sofortbeginn-Häkchen nur privat
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:13:49 +00:00
ClaudeandClaude Sonnet 5.5 c6ecb5242c Kundenansicht neu P7: Bestellseite schlank (Block, Linkzeile, Häkchen nur privat)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:11:44 +00:00
ClaudeandClaude Sonnet 5.5 fcf357c0b7 Kundenansicht neu P7: gemeinsame Bausteine der Bestellseiten (Block, Linkzeile, Kundenart-Zeile)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:09:04 +00:00
ClaudeandClaude Sonnet 5.5 841bc76bbb Kundenansicht neu P7: Gutschrift-Antrag in Fassung 6 mit Nachweis, Pin gegen Kundenart aus dem Body
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:07:05 +00:00
ClaudeandClaude Sonnet 5.5 da2fe83d47 Kundenansicht neu P7: Zusatzspeicher-Plus in Fassung 6 mit Wortlaut-Nachweis
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:06:04 +00:00
ClaudeandClaude Sonnet 5.5 528ca0875e Kundenansicht neu P7: K01 und K12 behaupten für Betriebe kein Sofortbeginn-Verlangen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:05:06 +00:00
ClaudeandClaude Sonnet 5.5 4e0e42f633 Kundenansicht neu P7: Wechsel Fassung 6, Betrieb ohne Sofortbeginn-Zeitstempel
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:03:46 +00:00
ClaudeandClaude Sonnet 5.5 99e1987bc4 Kundenansicht neu P7: Bestellung Fassung 6 mit Kundenart vom Server und Wortlaut-Nachweis
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:02:04 +00:00
ClaudeandClaude Sonnet 5.5 bf1e968a83 Kundenansicht neu P7: GET /api/abo liefert Fassung 6 und Wortlaut nur der neuen Ansicht
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 19:59:43 +00:00
ClaudeandClaude Sonnet 5.5 4a3b0030d2 Kundenansicht neu P7: Fassungsweiche und Vertragsnachweis (V12)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 19:59:06 +00:00
ClaudeandClaude Sonnet 5.5 71b2543e95 Kundenansicht neu P7: Wortlaut-Entwurf und Nachweistexte der Bestellseiten
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 19:58:05 +00:00
ClaudeandClaude Sonnet 5.5 bfe3895a50 Kundenansicht neu P7: Einschalten zeigt Linkzeile, Häkchen und Unterlagen der Fassung 6 aus P8
Verdrahtet P5 Task 15 und P7 Task 0 (N33): consentAbo liest Linkzeile und Hinweis aus WORTLAUT_V6, unterlagenFuer die fünf Funktionen aus FUNKTIONEN_UNTERLAGEN_V6 (Analyse bleibt bei den Alt-Unterlagen bis zur Freigabe), config.aboFunktionen.texte.hinweis entfällt, pdfAnhaenge liest PDFs der Fassung 6, sichereAdresse lässt /neu/rechtliches/ zu. Der Analyse-Kundenweg setzt wie der Team-Weg den Abo-Marker (unlockedBy und einrichtungAbrechnung abo).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 19:56:40 +00:00
ClaudeandClaude Sonnet 5.5 447129d4e2 zeitenBestellen: Pin auf die Listen in app.js an den Buchungskalender angepasst
VIEW_HASHES, APP_PARENT und FEATURE_LABELS tragen seit dem Buchungskalender
einen Eintrag zwischen Webshop und Zeiten. Die Regexe folgen den echten Listen;
app.js bleibt unverändert.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 19:30:54 +00:00
ClaudeandClaude Sonnet 5.5 524ac64d20 Kundenansicht der Mitarbeiter: Einschalten gesperrt statt 403-Text nach dem Tipp
Die Hülle reicht viewAs der Sitzung an die Einschalt-Option der Module durch
(News, Terminplaner, Webshop, Buchungskalender, Zeiten), auch wenn ein Modul es
nicht nennt. In Entdecken ist der Einschalten-Knopf gesperrt, daneben steht
"In der Kundenansicht nicht möglich." Meine bleibt bei gesperrtem Knopf auf der
Bestätigungsseite (jetzt gepinnt).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 19:30:26 +00:00
ClaudeandClaude Sonnet 5.5 0df96611dc module.css: Korrekturen aus der Sichtprüfung, Bild der Analyse in neuer Optik
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 19:23:05 +00:00
ClaudeandClaude Sonnet 5.5 f76749133f module.css: Analyse hell (an-, hs-)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:58:57 +00:00
ClaudeandClaude Sonnet 5.5 a9b50a5542 module.css: Öffnungszeiten und News (zt-, fn-, nw-)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:57:29 +00:00
ClaudeandClaude Sonnet 5.5 b20ac71750 module.css: Webshop und Buchungskalender (sh-, bk-)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:56:07 +00:00
ClaudeandClaude Sonnet 5.5 cbd36c0ae6 module.css: Terminplaner und gemeinsame Bestellbausteine (tm-)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:54:49 +00:00
ClaudeandClaude Sonnet 5.5 a6a656f18a module.css: Gerüst, Prüftest und gemeinsame Bausteine unter .kn
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:53:14 +00:00
ClaudeandClaude Sonnet 5.5 5534655671 Analyse: im Abo-Modell Linkzeile und Zielgruppen-Häkchen, Hosting berechtigt, Reiter in der Etage
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:50:49 +00:00
ClaudeandClaude Sonnet 5.5 c37faabcd3 Öffnungszeiten live: im Abo-Modell Einschalten und kein Behalten-Weg
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:49:08 +00:00
ClaudeandClaude Sonnet 5.5 5f3d8f3e96 Buchungskalender: im Abo-Modell Anfrage ohne Preis und Einschalten nach der Zusage
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:46:42 +00:00
ClaudeandClaude Sonnet 5.5 28da4180bd Webshop: im Abo-Modell Anfrage ohne Unternehmer-Haken, Privat-Hinweis, Einschalten
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:45:17 +00:00
ClaudeandClaude Sonnet 5.5 1cabab2826 News: im Abo-Modell Einschalten statt Bestellen, Projektwahl über dataset
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:40:59 +00:00
ClaudeandClaude Sonnet 5.5 b6f70a502a Terminplaner: im Abo-Modell Einschalten statt Bestellen, ohne Häkchen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:39:54 +00:00
ClaudeandClaude Sonnet 5.5 364ded1662 Funktionsmodule: gemeinsame Einschalt-Seite für das Abo-Modell
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:38:51 +00:00
ClaudeandClaude Sonnet 5.5 a50c5ef6c9 Module: im Abo-Modell Reiter mit Kurzlabels an die Etage melden (C2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:37:12 +00:00
ClaudeandClaude Sonnet 5.5 b91ba1d5cb Funktionen neu: Korrekturen aus den Gates (Hover des Knopfes bleibt #111, Ticket auch am Feature)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:31:08 +00:00
ClaudeandClaude Sonnet 5.5 c87c87e76b Funktionen neu: Bilder für Entdecken aus Testdaten (Einbettungen auf Beispiel-Website)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:25:45 +00:00
ClaudeandClaude Sonnet 5.5 9ca7900213 Funktionen neu: Aussehen (weiß, Pillen, 44-px-Zeilen) mit Quelltext-Prüfung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:14:08 +00:00
ClaudeandClaude Sonnet 5.5 36890c6734 Funktionen neu: Bestätigung als Overlay der Zurück-Geste, Erfolgsmeldung, Rückkehr nach dem Wechsel
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:11:36 +00:00
ClaudeandClaude Sonnet 5.5 f30e72ebbb Funktionen neu: Ansicht mit Meine und Entdecken, Website-Auswahl, Hinweise je Abo-Lage, Rechnungszeile
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:10:02 +00:00
ClaudeandClaude Sonnet 5.5 57112cc51f Funktionen neu: Einschalten senden, 409-Folgen, Wechsel mit gemerkter Funktion, zeigeEinschalten für die Module
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:07:30 +00:00
ClaudeandClaude Sonnet 5.5 906c9ec1f0 Funktionen neu: Bestätigungsseite zum Einschalten (Linkzeile, Häkchen nur für privat in der Frist)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:05:18 +00:00
ClaudeandClaude Sonnet 5.5 c72c9d9f5a Funktionen neu: Modell für Meine und Entdecken (mehrere Websites, Anfragestatus)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:03:22 +00:00
ClaudeandClaude Sonnet 5.5 437947cdf6 Funktionen neu: Einschalt-Logik (Vorprüfung, Fristen, 409-Codes) und Testdaten
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:01:27 +00:00
ClaudeandClaude Opus 5.5 1cd16ff293 Merge feature/kundenansicht-neu (P5, P6, P8, P6b) in kn/ui vor P3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:55:13 +00:00
ClaudeandClaude Sonnet 5.5 01c4f22a84 Kundenansicht neu P2: Test der Dashboard-Weiche läuft den Fall ohne Datei gegen einen leeren Nachbau
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:53:36 +00:00
ClaudeandClaude Sonnet 5.5 04a81a1d46 Kundenansicht neu P2: Feinschliff nach der Sichtprüfung (Kontrast der Menüzahl, Links, Anmelde-Hinweise)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:43:21 +00:00
ClaudeandClaude Sonnet 5.5 eb7c44ac11 Kundenansicht neu P2: Adressen auf derselben Herkunft zulassen (Vorschau, Zahlungsseiten)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:24:27 +00:00
ClaudeandClaude Sonnet 5.5 2ac7d3f64c Kundenansicht neu P2: Aussehen der Ansichten
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:21:31 +00:00
ClaudeandClaude Sonnet 5.5 3c2450dd2e Kundenansicht neu P2: öffentliche Seiten und Auftragsbestätigung im neuen Stil, Text unverändert
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:17:45 +00:00
ClaudeandClaude Sonnet 5.5 ade7e872d9 Kundenansicht neu P2: Verträge mit Reitern
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:14:28 +00:00
ClaudeandClaude Sonnet 5.5 861b74f70e Kundenansicht neu P2: Passwort ändern als Etage
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:12:32 +00:00
ClaudeandClaude Sonnet 5.5 d99c29b6c6 Kundenansicht neu P2: Übernahmegutschrift im Wortlaut
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:11:15 +00:00
ClaudeandClaude Sonnet 5.5 1b66f6aa81 Kundenansicht neu P2: Wechsel im Wortlaut der Fassung 5 mit gemerkter Funktion
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:09:08 +00:00
ClaudeandClaude Sonnet 5.5 27e6c8fe4f Kundenansicht neu P2: Bestellseite im Wortlaut der Fassung 5
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:07:22 +00:00
ClaudeandClaude Sonnet 5.5 4473a746ee Kundenansicht P6b: Team-Weg der Analyse im Abo-Modell (WOMS)
POST/GET /api/admin/analyse/:projectId kennt jetzt das Abo-Modell:
- Gilt aboModellFuerKunde, prüft das Einschalten das Abo über aboDeckt
  (Hosting und Wartung decken die Analyse); fehlt es, 409 mit code
  abo_fehlt, grund, wechsel (und ab) wie bei der Team-Aktivierung der
  anderen Funktionen, bevor etwas geschrieben wird. Lesefehler: 503.
- Das neu eingeschaltete Dokument trägt den Abo-Marker (unlockedBy und
  einrichtungAbrechnung 'abo', istAboModell); activateAnalyse nimmt dazu
  den Parameter abrechnung, setzt ihn im selben Schritt wie den Zustand
  und stellt ihn bei der Rücknahme wieder her.
- GET: Eignung und Text folgen dem Abo-Modell (Hosting-oder-Wartungs-Text).
- Alle anderen Kunden und das Ausschalten laufen unverändert.

Tests: test/adminAnalyseAboModell.test.js (9 Fälle).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:06:33 +00:00
ClaudeandClaude Sonnet 5.5 f9c4fbe9bb Kundenansicht neu P2: E-Mail-Speicher im Wortlaut der Fassung 5
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:05:45 +00:00
ClaudeandClaude Sonnet 5.5 8a23652e07 Kundenansicht neu P2: Stundenkonto mit Vorschuss-Anzeige
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:02:59 +00:00
ClaudeandClaude Sonnet 5.5 989e7be640 Kundenansicht neu P2: Reiter Abo und Abo-Auswahl
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:02:10 +00:00
ClaudeandClaude Sonnet 5.5 cf391eb117 Kundenansicht neu P2: Reiter Rechnungen mit Aufträgen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:00:28 +00:00
ClaudeandClaude Sonnet 5.5 8c8d29657d Kundenansicht neu P2: Ansicht Chat mit dem Composer und lilafreier CSS-Kopie
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:59:28 +00:00
ClaudeandClaude Opus 5.5 72c5fa9fea Merge kn/recht (P8 Rechtsrunde Fassung 6) in feature/kundenansicht-neu
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 16:59:27 +00:00
ClaudeandClaude Sonnet 5.5 9a3ce7c8bf Kundenansicht P6: Zusage-Mails K42 und K43 für Webshop- und Buchungskalender-Anfrage ohne Preis
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:56:52 +00:00
ClaudeandClaude Sonnet 5.5 a3831040ba Kundenansicht neu P2: Ansicht Website mit Projektdetails als Etage
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:55:16 +00:00
ClaudeandClaude Sonnet 5.5 bb1efa5c9e Kundenansicht P6: Ticket-Texte zum Stundenkonto je Abrechnung (Abo, Projekt)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:54:52 +00:00
ClaudeandClaude Sonnet 5.5 e036d24ae5 Kundenansicht neu P2: Bausteine für Projektdetails und Aktivität
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:53:22 +00:00
ClaudeandClaude Sonnet 5.5 df53cd039b Kundenansicht P6: Widerruf des Wechsels beendet Wartungs-Funktionen, die Analyse bleibt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:52:18 +00:00
ClaudeandClaude Sonnet 5.5 166188c069 Kundenansicht neu P2: Installationshinweis
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:51:44 +00:00
ClaudeandClaude Sonnet 5.5 3c44aaea77 Kundenansicht neu P2: Zeile Rechnung offen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:51:10 +00:00
ClaudeandClaude Sonnet 5.5 b77c3291c5 Kundenansicht neu P2: Wortlaut-Orakel für die Texte der Fassung 5
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:50:49 +00:00
ClaudeandClaude Sonnet 5.5 d5a7312896 Kundenansicht P6: Wechsel merkt sich die gewünschte Funktion (details.funktion, wechselFunktion)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:50:24 +00:00
ClaudeandClaude Sonnet 5.5 91e5bca1c5 Kundenansicht neu P2: Hilfen, Testdaten und Quelltext-Regeln
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:49:18 +00:00
ClaudeandClaude Sonnet 5.5 db47f13510 Kundenansicht P6: Abo-Ende-Abgleich im Tagesjob
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:47:12 +00:00
ClaudeandClaude Sonnet 5.5 7a03f073bc Kundenansicht P6: alte Abgleiche lassen Abo-Modell-Dokumente dem neuen Abgleich, Zeiten ohne Behalten
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:46:15 +00:00
ClaudeandClaude Sonnet 5.5 d8c952ac2a Kundenansicht P6: News-Löschschritt und Zurücksetzen nach der Löschung im Abo-Ende-Abgleich
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:44:08 +00:00
ClaudeandClaude Sonnet 5.5 385844192d Kundenansicht neu P1: Vollständigkeitstest der Dateiübersicht
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:43:58 +00:00
ClaudeandClaude Sonnet 5.5 5297a05125 Kundenansicht neu P1: Korrekturen aus der Sichtprüfung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:43:19 +00:00
ClaudeandClaude Sonnet 5.5 92ac61dffd Kundenansicht P6: Abo-Ende-Abgleich für News, Terminplaner, Webshop und Buchungskalender
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:42:06 +00:00
ClaudeandClaude Sonnet 5.5 de8b0c0fca Kundenansicht P6: Team-Aktivierung mit Abo-Prüfung und Abrechnung (V10), Projektstart ohne Abo nur mit Warnung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:39:07 +00:00
ClaudeandClaude Sonnet 5.5 408f7d0cc2 Kundenansicht P6: K38 an den Kunden und T24 ans Team beim Einschalten
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:34:22 +00:00
ClaudeandClaude Sonnet 5.5 cdc8f5c424 Kundenansicht P6: K02 und K03 nennen das Ende der Abo-Funktionen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:31:06 +00:00
ClaudeandClaude Sonnet 5.5 9489a70f0c Kundenansicht P6: Mail-Builder für K38, K39, T24 und die Zeiten-Varianten K40, K41
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:29:34 +00:00
ClaudeandClaude Sonnet 5.5 664043e8e9 Kundenansicht P6: Mailvorlagen K38 bis K41 und T24 für das Abo-Modell
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:28:15 +00:00
ClaudeandClaude Sonnet 5.5 142aa6e9d6 Kundenansicht neu P1: weiße Seite, Manifest und leere Ansichten für P2 und P3
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:26:23 +00:00
ClaudeandClaude Sonnet 5.5 3d4d49de82 Kundenansicht P6: Team-Endpunkt Stundenkonto mit Vorschuss
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:24:31 +00:00
ClaudeandClaude Sonnet 5.5 7595844170 Kundenansicht neu P1: Hülle mit Start über /api/auth/me und Modul-Lader
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:24:16 +00:00
ClaudeandClaude Sonnet 5.5 f016d7ae6c Kundenansicht P6: Vorschuss im Stundenkonto (Einrichtung statt Mehraufwand)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:23:39 +00:00
ClaudeandClaude Sonnet 5.5 710c57cf87 Kundenansicht neu P1: Fahrstuhl-Zeile, Desktop-Menü und Tastaturzustand
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:14:55 +00:00
ClaudeandClaude Sonnet 5.5 57d5db2344 Kundenansicht P5: Analyse im Abo-Modell (Hosting, Zielgruppe erzwungen, Nachweis, Tagesprüfung)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:14:35 +00:00
ClaudeandClaude Sonnet 5.5 0432b7e01d Rechtstexte: Hochladen-Skript für WOMS Rechtliches (ein Dokument je Aufruf, nur mit Freigabe)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:13:20 +00:00
ClaudeandClaude Sonnet 5.5 23252df359 Kundenansicht P5: Shop-Anfrage im Abo-Modell (Unternehmer aus Kundenart)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:11:13 +00:00
ClaudeandClaude Sonnet 5.5 99d7e2bdca Rechtstexte: PDF-Bau für Fassung 6 (Modi abo6 und funktionen6, nach Freigabe)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:10:31 +00:00
ClaudeandClaude Sonnet 5.5 b6f2df0d4a Kundenansicht P5: Katalog, Abo-Stand und Anfragestatus in GET /api/funktionen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:09:51 +00:00
ClaudeandClaude Sonnet 5.5 4953c8fd0f Kundenansicht neu P1: Konto mit Avatar, Sheet und Popover
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:09:40 +00:00
ClaudeandClaude Sonnet 5.5 b4b18792c3 Rechtsrunde Fassung 6: Entscheidungsliste, Kanzlei-Fragen, Datenschutz-Prüfpunkte, Ablauf
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:09:34 +00:00
ClaudeandClaude Sonnet 5.5 e30318a470 Kundenansicht P5: POST /api/funktionen/:key/einschalten (nur neue Ansicht)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:07:30 +00:00
ClaudeandClaude Sonnet 5.5 3241aedc3c Kundenansicht neu P1: Widerrufsstreifen mit GET /api/fristen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:06:51 +00:00
ClaudeandClaude Sonnet 5.5 a8b8838d9e AVV Fassung 6: Terminplaner, Webshop, Buchungskalender mit Klick auf die Linkzeile (Entwurf)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:06:02 +00:00
ClaudeandClaude Sonnet 5.5 e144a9611d Kundenansicht neu P1: Zähler für Chat und Verträge nach V3
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:05:51 +00:00
ClaudeandClaude Sonnet 5.5 f25b7608b4 Website-Analyse Fassung 6: Hosting- und Wartungs-Abo, Leistungsbeschreibung und AVV (Entwurf)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:04:58 +00:00
ClaudeandClaude Sonnet 5.5 748226d631 Kundenansicht neu P1: Etage 2 mit Zurück, Reiterzeile und Brotkrume
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:04:46 +00:00
ClaudeandClaude Sonnet 5.5 9e7db59326 Leistungsbeschreibungen Fassung 6: Webshop und Buchungskalender (Entwurf)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:03:46 +00:00
ClaudeandClaude Sonnet 5.5 f5b43c55a9 Kundenansicht P5: generisches Einschalten im Abo (einschalten, Heilung, Ticket-Marker)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:03:17 +00:00
ClaudeandClaude Sonnet 5.5 d112fe2326 Leistungsbeschreibungen Fassung 6: Terminplaner, News-Pinnwand, Öffnungszeiten live (Entwurf)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:00:55 +00:00
ClaudeandClaude Sonnet 5.5 a51643c4a5 Kundenansicht neu P1: gemeinsame Reiter mit Balken, Overlay-Rückfall und Zählern
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:00:39 +00:00
ClaudeandClaude Sonnet 5.5 1d149f50b1 Kundenansicht P5: Einrichtungs-Ticket mit Abrechnung, unlockedBy für alle Funktionen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:00:00 +00:00
ClaudeandClaude Sonnet 5.5 819725a3c1 Kundenansicht neu P1: Router mit Anker-Kette, Overlay und Verlaufsstempel
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:56:10 +00:00
ClaudeandClaude Sonnet 5.5 b000dba698 Kundenansicht P5: Kundenart im Zustimmungsnachweis
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:56:00 +00:00
ClaudeandClaude Sonnet 5.5 33b7a4ce0f Rechtstexte: Generator-Satz funktionen6 im neuen Stil
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:55:41 +00:00
ClaudeandClaude Sonnet 5.5 4bf1b67e67 Kundenansicht P5: Katalogfelder, consentAbo mit Hinweis und Unterlagen-Quelle im Funktionen-Register
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:55:19 +00:00
ClaudeandClaude Sonnet 5.5 90b7f10f1a Abo-Bedingungen Fassung 6: Seite im neuen Stil (Entwurf)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:53:49 +00:00
ClaudeandClaude Sonnet 5.5 8e0d2b2736 Kundenansicht P5: gesammelte Widerrufsfristen (sammleFristen, GET /api/fristen)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:53:29 +00:00
ClaudeandClaude Sonnet 5.5 9b177a15a3 Kundenansicht neu P1: Routentabelle mit allen alten Hashes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:51:43 +00:00
ClaudeandClaude Sonnet 5.5 882438df38 Kundenansicht P5: Abo-Zuordnung der Funktionen (aboProdukt, aboDeckt)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:51:38 +00:00
ClaudeandClaude Sonnet 5.5 7f4e84e0b1 Abo-Bedingungen Fassung 6: Entwurf der Änderungen im Wortlaut
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:51:35 +00:00
ClaudeandClaude Sonnet 5.5 6f0808647f Kundenansicht neu P1: h(), api() mit ApiFehler und Quelltext-Regel
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:50:12 +00:00
ClaudeandClaude Sonnet 5.5 5c21023e28 Kundenansicht P5: Kundenart beim Anlegen Pflicht (ab Schalter pilot), beim Ändern optional
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:49:25 +00:00
ClaudeandClaude Sonnet 5.5 46edb7c418 Kundenansicht neu P1: Tokens und Basis-CSS im weißen Stil
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:48:54 +00:00
ClaudeandClaude Sonnet 5.5 214d698d6f Abo-Fassung 6: Wortlaute für Linkzeile und Häkchen, Unterlagen-Tabelle der Funktionen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:48:24 +00:00
ClaudeandClaude Sonnet 5.5 8d3eb73db7 Kundenansicht P5: Kundenart-Helfer und Felder in /api/auth/me
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:48:07 +00:00
ClaudeandClaude Sonnet 5.5 2a8db582cb Abo-Bedingungen Fassung 5: Archiv vorbereitet (erst mit P10 aktiv)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:47:37 +00:00
ClaudeandClaude Sonnet 5.5 65b1389b55 Abo-Fassung 6: Fassungstabelle, aktuelleAboFassung, termsVersionNeu
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:47:05 +00:00
ClaudeandClaude Sonnet 5.5 c5b1333953 Kundenansicht P5: Setup-Skript für Kundenart und Einrichtungs-Marker
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:47:04 +00:00
ClaudeandClaude Sonnet 5.5 c2b7073d99 Kundenansicht neu: Einstieg für Sichtprüfungen über das Werkzeug
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 14:24:06 +00:00
ClaudeandClaude Sonnet 5.5 c6a9a2c4a6 Kundenansicht neu: /api/auth/me meldet ansicht neu oder alt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 13:01:40 +00:00
ClaudeandClaude Sonnet 5.5 f2c9eb107a Kundenansicht neu: Dashboard-Weiche vor den statischen Dateien, Platzhalter-Seite
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 12:59:48 +00:00
ClaudeandClaude Sonnet 5.5 7c4a4a567d Kundenansicht neu: Pilot-Schalter (off/pilot/on) mit Randfall-Tests
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 12:56:13 +00:00
ClaudeandClaude Opus 5.5 f573d49f0a Plan: Kundenansicht neu – elf Paket-Pläne P0–P10 und Nachträge der Planrunde
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 12:21:04 +00:00
ClaudeandClaude Opus 5.5 08dd766e9c Plan: Kundenansicht neu – Übersicht und Verträge V1–V13
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 09:48:32 +00:00
ClaudeandClaude Opus 5.5 d3d28492c0 Spec: Kundenansicht neu (weiß, Fahrstuhl-Zeile, Funktionen im Abo, schlanke Bestätigungen)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 08:52:02 +00:00
knsoandClaude Opus 5.5 d33a483fd8 Buchungskalender: Freigeben zeigt bei 409 die richtige Meldung
Bisher stand bei jedem 409 „Der Zeitraum ist inzwischen belegt.“, auch wenn der Rechnungsauftrag fehlte (zustimmung_fehlt) oder sich der Stand geändert hatte (status_ungueltig). Jetzt nur bei zeitraum_belegt; sonst der Text aus buchungen-fehler.js. Aufgefallen beim ersten Test mit Donau Home.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 18:35:40 +00:00
knsoandClaude Opus 5.5 a45f436124 Buchungskalender: Zustimmung per WhatsApp zusammenführen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 15:13:33 +00:00
knsoandClaude Opus 5.5 1e0eb9f569 Buchungskalender: Preise je Personenzahl (Portal-Editor)
Neuer Haken "Preis hängt von der Personenzahl ab" im Preis-Editor (nur bei Höchstzahl ab 2 Personen). Angeschaltet zeigt jede Staffel k Euro-Felder (1 Person bis k Personen, k = min(Höchstzahl, 10)) statt eines Preises; "Personen im Preis inklusive" und "Aufpreis je weiterer Person" sind dann ausgeblendet, ihre Werte gehen unverändert mit. Speichern sendet personenCent und preisProNachtCent = personenCent[0]. Beim Laden ist der Haken an, wenn personenCent gespeichert sind. Kundenansicht (nur lesen) sperrt Haken und Felder. Felder brechen auf dem Handy um. Tests im Fake-DOM.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 15:06:04 +00:00
knsoandClaude Opus 5.5 02a3732e4a Funktionen: Zustimmung in Textform auch per WhatsApp
Terminplaner, Webshop und Buchungskalender nehmen beim Aktivieren durch das Team den Weg whatsapp an (Nachweis: „per WhatsApp in Textform“). Die Analyse bleibt bei Chat oder E-Mail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 15:02:49 +00:00
knsoandClaude Opus 5.5 a4ade5f649 Buchungskalender: Preise je Personenzahl (Server)
Jede Staffel darf personenCent tragen (Preis pro Nacht für 1 bis k Personen, k = 2 bis 10), ohne Schemaänderung im JSON-Feld staffeln. pruefePreise prüft Länge, Werte und Gleichheit über alle Staffeln und setzt preisProNachtCent = personenCent[0]. berechnePreis wählt die Spalte der Gästezahl (Posten mit Personenzahl), bei mehr Gästen als Spalten gilt die letzte plus Zusatzperson-Aufpreis. preiseVollstaendig prüft die Konsistenz. Ohne personenCent rechnet alles wie bisher. Tests mit Mimis Preismatrix, Doku in unterkunft-api.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 15:02:45 +00:00
knsoandClaude Opus 5.5 b264d7f734 Buchungskalender: große Felder als TEXT (Zeilenbreite in Appwrite)
ukAnfragen überschritt mit gast/posten/stornobedingungenText als VARCHAR die MariaDB-Zeilenbreite von 65535 Byte; das Setup brach in Produktion nach 22 Attributen ab. Test sichert die Breite aller Collections ab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:57:10 +00:00
knsoandClaude Opus 5.5 69df9e7046 Tests: Aufträge-Rechtstexttests kennen den Satz unterkunft (Merge-Folge)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:49:42 +00:00
knsoandClaude Opus 5.5 a1400ac315 Datenschutz: Abschnitt 6f Buchungskalender
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:47:59 +00:00
knsoandClaude Opus 5.5 3526909d61 Buchungskalender: Mail-Nummern K36, K37, T23 (K31/K32/T22 gehören den Aufträgen)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:47:29 +00:00
knsoandClaude Opus 5.5 fd971309a8 Buchungskalender: origin/main (Aufträge) zusammenführen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:46:31 +00:00
knsoandClaude Opus 5.5 59783ce685 Buchungskalender: Livegang – für Mimi kostenlos
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:41:48 +00:00
knsoandClaude Opus 5.5 1c6b6bf8d6 Buchungskalender: Abschluss-Fixwelle B zusammenführen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:34:30 +00:00
knsoandClaude Opus 5.5 a6c6dabaa3 Buchungskalender: Abschluss – Storno-Muster-Zitat bleibt bis zur Angleichung mit dem Portal-Mustertext (Fixwelle B)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:33:49 +00:00
knsoandClaude Opus 5.5 3f5b030a50 Buchungskalender: Abschluss – L5 Livegang-Notiz (Reihenfolge Datenbank/Deploy/Pilot, Indizes, Textform, WOMS-Push, Paragraf 6f)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:32:26 +00:00
knsoandClaude Opus 5.5 592ac20503 Buchungskalender: Abschluss – Mail-Texte G16 und K33
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:32:02 +00:00
knsoandClaude Opus 5.5 f4bc752553 Buchungskalender: Abschluss – L2-L4 Rechtstexte (Hinweis-Mail mit Loeschdatum, Linkgueltigkeit, Kontoloeschung), HTML/PDF/sha256 neu
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:32:02 +00:00
knsoandClaude Opus 5.5 1d9515823c Buchungskalender: Abschluss – Kleinigkeiten Server (Betrag/Waehrung, Erstattungs-Webhook, Obergrenze, Zaehler, Cache, Kommentare, Setup-Check)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:29:55 +00:00
knsoandClaude Opus 5.5 6d35674c1e Buchungskalender: Abschluss – L1 Textform-Zustimmung fuer die Aktivierung ohne Zahlung (Server, GET-Feld unterkunft)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:27:50 +00:00
knsoandClaude Opus 5.5 4f9fb6b109 Buchungskalender: Abschluss – S7 Buchungslink-Gueltigkeit (90 Tage nach Abreise, 30 Tage nach Absage), tokenHash beim Anonymisieren leeren
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:26:12 +00:00
knsoandClaude Opus 5.5 17400cac1f Buchungskalender: Abschluss – S6 Belegung laedt nur Relevantes, bis-Indizes, 10-s-Speicher
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:25:17 +00:00
ClaudeandClaude Sonnet 5.5 1cbec5b32d Kontrolle: Betreff-Zusaetze Erinnerung und Kopie, Mail-Protokolle ohne Meldung und Adresse
Die Erinnerung beginnt mit 'Erinnerung: ', die Teamkopie mit 'Kopie: ' (der Mailer
nimmt dafuer ein optionales subjectPrefix). Die Protokollzeilen in mails.js nennen
nur Nummer und Fehlerkennung.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 15:24:59 +00:00
knsoandClaude Opus 5.5 ab838d07bc Buchungskalender: Abschluss – S5 Hinweis-Mail K35 vor der Loeschung, Rechnungen im Zustand abgeschaltet lesbar
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:23:56 +00:00
knsoandClaude Opus 5.5 1fe1ae31bf Buchungskalender: Abschluss – Preis-Editor löscht Feldfehler beim Tippen, Kalenderraster volle Breite, Export-Link-Feld
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:22:00 +00:00
knsoandClaude Opus 5.5 e2fc4eb676 Buchungskalender: Abschluss – S4 CSS-Variablen im Ticket-Text
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:21:43 +00:00
knsoandClaude Opus 5.5 5bf1504ce8 Buchungskalender: Abschluss – Portal-Fehlertexte für alle Servercodes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:21:33 +00:00
knsoandClaude Opus 5.5 54310d9bae Buchungskalender: Abschluss – S2 Setup-Test kennt den unique-Index
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:21:29 +00:00
knsoandClaude Opus 5.5 727613d0e2 Buchungskalender: Abschluss – S2 Rechnungsnummer unter einer Sperre, unique-Index; S3 Storno mit Ursprungs-Aussteller
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:21:19 +00:00
knsoandClaude Opus 5.5 392b85c1f0 Buchungskalender: Abschluss – Buchungsseite zeigt 409-Codes, nur https-Zahlung, Feldfehler barrierefrei
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:21:13 +00:00
knsoandClaude Opus 5.5 a071e57fb7 Buchungskalender: Abschluss – Widget-CSS ohne @layer, alles unter .wku-root, Reset, Fenster 600 Tage, Serverfehlertexte
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:20:27 +00:00
ClaudeandClaude Sonnet 5.5 b6c1d81342 Kontrolle: Bestaetigen per Link prueft Token und Ablauf in der Sperre erneut
Ein Token-Tausch durch die Erinnerung zwischen Pruefung und Sperre ergibt 404 wie bei
einem ungueltigen Link; das Portal ist nicht betroffen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 15:20:24 +00:00
knsoandClaude Opus 5.5 435dd0b746 Buchungskalender: Abschluss – S1 keine doppelte Erstattung nach Heilung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:20:23 +00:00
ClaudeandClaude Sonnet 5.5 c2245b6905 Kontrolle: Projektkonto und Rechnungsbau waehlen dieselbe Aufwands-AB
Gemeinsame Auswahl juengsteAufwandAb (gueltig, jueengste nach gesendetAm) im AB-Speicher.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 15:19:48 +00:00
ClaudeandClaude Sonnet 5.5 4177b76d82 Kontrolle: Schlussrechnung setzt die AB aus dem Projektdatensatz auf abgerechnet
Nach dem Umhaengen gilt die neue AB, nicht die aus den Stripe-Metadaten.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 15:19:04 +00:00
ClaudeandClaude Sonnet 5.5 1bb172ad91 Kontrolle: Storno einer Anzahlung bei bestehendem Schluss-Entwurf der AB abgewiesen
409 'Erst den Entwurf der Schlussrechnung löschen.'; das Storno einer Anzahlung laeuft
unter der Kundensperre.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 15:18:46 +00:00
ClaudeandClaude Sonnet 5.5 e3ae152f12 Kontrolle: Senden eines Anzahlungs- oder Schluss-Entwurfs prueft die AB unter der Kundensperre erneut
Unter sperreRechnung(kundenId): AB noch versendet oder bestaetigt, bei Schluss keine
andere festgeschriebene Schlussrechnung, bei Anzahlung Summe aller festgeschriebenen
Rechnungen plus diese nicht ueber der AB-Summe, sonst 409 und nichts wird finalisiert.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 15:18:22 +00:00
ClaudeandClaude Sonnet 5.5 57d1ab82c6 Kontrolle: Anzahlung und Schlussrechnung nehmen das Zahlungsziel der AB
Ohne daysUntilDue gilt bei Anzahlung und Schluss auftrag.zahlungszielTage (Route
und createProjectInvoice), sonst weiterhin 14 Tage; eine Angabe gilt vor der AB.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 15:17:06 +00:00
ClaudeandClaude Sonnet 5.5 d96d09f589 Kontrolle: Steuernummer steht im Rechnungsfuss der Projektrechnungen
Die Steuernummer aus den Finanz-Einstellungen kommt als Zeile direkt nach dem
§ 19-Satz in den Fuss; ohne Wert bleibt der Fuss unveraendert.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 15:15:56 +00:00
knsoandClaude Opus 5.5 2df56eccc5 Buchungskalender: UI-Durchlauf – Screenshots wie im Webshop, Sichtbarkeitsprüfung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:10:00 +00:00
knsoandClaude Opus 5.5 838bc96d1b Buchungskalender: UI-Durchlauf – Text „null“ in der Summe, Knopf- und Feldgestaltung im Portal
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:09:57 +00:00
knsoandClaude Opus 5.5 f5e304ad7a Buchungskalender: UI-Durchlauf – Platzhalter in Stornobedingungen blockieren das Speichern
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:09:57 +00:00
knsoandClaude Opus 5.5 196921fdf0 Buchungskalender: UI-Durchlauf – Feldfehler der Buchungsseite verschwinden beim Ausfüllen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:09:57 +00:00
knsoandClaude Opus 5.5 03ae8c77cd Buchungskalender: UI-Durchlauf (Playwright)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:06:33 +00:00
knsoandClaude Opus 5.5 cb0799a35c Buchungskalender: UI-Durchlauf – Zustimmungs-Hinweis verschwindet nicht
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:05:45 +00:00
knsoandClaude Opus 5.5 e5793fd151 Buchungskalender: Abgleich – G22/K33-Platzhalter
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:58:50 +00:00
knsoandClaude Opus 5.5 74543b75c3 Buchungskalender: Abgleich – API-Doku mit den bindenden Formen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:56:03 +00:00
knsoandClaude Opus 5.5 c3eef4747d Buchungskalender: Abgleich – T22/K34 passen zu den Vorlagen, Config-Werte geprüft
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:55:32 +00:00
knsoandClaude Opus 5.5 fbd07b3d8c Buchungskalender: Abgleich – iCal-Export (Escape, aktive Unterkünfte, ab gestern, Rate-Limit)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:54:16 +00:00
knsoandClaude Opus 5.5 95cbd473d0 Buchungskalender: Abgleich – gleichzeitige Zweitzahlung wird erstattet
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:53:36 +00:00
knsoandClaude Opus 5.5 f01e6def59 Buchungskalender: Abgleich – art monteurzimmer, Zurückziehen-409 mit Meldung, Tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:53:13 +00:00
knsoandClaude Opus 5.5 54442a4682 Buchungskalender: Abgleich – deutsche Fehlertexte der Fachcodes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:53:13 +00:00
knsoandClaude Opus 5.5 b992dd2388 Buchungskalender: Abgleich – Stornomuster wortgleich zur Doku
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:52:42 +00:00
knsoandClaude Opus 5.5 2eb38032d3 Buchungskalender: Abgleich – Rechnungs-PDF 200-Pfad getestet, 501 nur bei fehlendem Modul
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:52:13 +00:00
knsoandClaude Opus 5.5 d27f6d5ddb Buchungskalender: Abgleich – Testprotokoll der Mails und Config-Vorgaben
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:51:44 +00:00
knsoandClaude Opus 5.5 83cdab405b Buchungskalender: Strang ui zusammenführen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:50:03 +00:00
knsoandClaude Opus 5.5 cebde0e070 Buchungskalender: Strang widget zusammenführen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:50:03 +00:00
knsoandClaude Opus 5.5 97dd84fbb8 Buchungskalender: Strang mail zusammenführen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:49:33 +00:00
knsoandClaude Opus 5.5 7b978ad2e4 Buchungskalender: Strang server zusammenführen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:49:27 +00:00
knsoandClaude Opus 5.5 35b0438d00 Buchungskalender: Team-API wie Webshop (anfrage null, Stripe-Status)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:46:30 +00:00
knsoandClaude Opus 5.5 9d01d38a45 Buchungskalender: Bestellung auf Anfrage, Freischaltung und Ticket
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:44:21 +00:00
knsoandClaude Opus 5.5 827ee54875 Buchungskalender: Aufbewahrung – Sperre wie Ablauf, Restpunkte
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:43:44 +00:00
knsoandClaude Opus 5.5 c019d3c3b0 Buchungskalender: Aufbewahrung und Löschfristen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:38:35 +00:00
knsoandClaude Opus 5.5 f57a3d477e Buchungskalender: iCal-Import und -Export
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:32:34 +00:00
knsoandClaude Opus 5.5 e3b399e003 Buchungskalender: Zustimmungs-Test streng, Online-Zahlung nur beim Einschalten prüfen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:32:20 +00:00
knsoandClaude Opus 5.5 32cffb3518 Buchungskalender: Verwaltungs-API – Einstellungen, Zustimmung, Online-Zahlung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:30:20 +00:00
knsoandClaude Opus 5.5 5541e0cb60 Buchungskalender: Rechtstexte – automatische Erstattung, Stripe-Rücksprung, Kartenzahlung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:27:19 +00:00
knsoandClaude Opus 5.5 d0ab92658a Buchungskalender: Verwaltungs-API für Vermieter
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:26:46 +00:00
knsoandClaude Opus 5.5 05006f1480 Buchungskalender: Stornobedingungen je Unterkunft, Zustimmung und Bestellstatus
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:26:22 +00:00
ClaudeandClaude Sonnet 5.5 aad0a8b807 Tests: Wächter der Kundenansicht sperrt auch die Routen der Auftragsbestätigung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:24:19 +00:00
ClaudeandClaude Sonnet 5.5 d9058ef1a6 Aufträge: Durchlauf-Test über alle Endpunkte (Festbetrag, Aufwand, AB bis Schlussrechnung, Privatperson, Kundenansicht, Schalter)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:23:52 +00:00
knsoandClaude Opus 5.5 b25689a015 Buchungskalender: Rechtstexte, Einbau- und Livegang-Notiz
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:23:21 +00:00
knsoandClaude Opus 5.5 efeb99e214 Buchungskalender: Rechnungen, Einstellungen und Funktionen-Kachel
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:23:19 +00:00
knsoandClaude Opus 5.5 a94b50fbc1 Buchungskalender: Zahlung – Sitzung wiederverwenden, Zahlung nach Storno erstatten
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:23:06 +00:00
ClaudeandClaude Sonnet 5.5 f3b6e00bf7 Merge auftraege/l6-kundenportal: Kundenportal (Strang L6)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:21:18 +00:00
ClaudeandClaude Sonnet 5.5 5d5f6333a4 Merge auftraege/l5-bestaetigung: Bestätigung und Mails (Strang L5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:21:11 +00:00
ClaudeandClaude Sonnet 5.5 8f53b560d5 Merge auftraege/l7-rechtstexte (ohne Datenschutz-Commit): Rechtstexte (Strang L7)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:20:51 +00:00
ClaudeandClaude Sonnet 5.5 bd12892ed2 Merge auftraege/l4-ab-pdf: Auftragsbestätigung PDF (Strang L4)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:20:45 +00:00
ClaudeandClaude Sonnet 5.5 095694bc44 Merge auftraege/l3-ab-kern: Auftragsbestätigung Kern (Strang L3)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:20:43 +00:00
ClaudeandClaude Sonnet 5.5 e5fc5e2c74 Merge auftraege/l2-rechnungen: Rechnungen (Strang L2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:20:36 +00:00
ClaudeandClaude Sonnet 5.5 743af802c1 Merge auftraege/l1-projektkonto: Projektkonto (Strang L1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:20:30 +00:00
ClaudeandClaude Sonnet 5.5 d2c48ab99b Rechnungen: Routen für Projektrechnungen und Vorschau
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:18:36 +00:00
ClaudeandClaude Sonnet 5.5 d990155df9 Kundenportal Aufträge: Playwright-Prüfung mit Bildschirmfotos (Handy 390 px, Desktop 1440 px, dunkel)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:18:23 +00:00
ClaudeandClaude Sonnet 5.5 88a7305e41 Rechnungen: Rechnungsnummer buchen, AB abgerechnet, Storno und Löschen geben frei
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:17:58 +00:00
ClaudeandClaude Sonnet 5.5 ab3e6938da Rechnungen: Sperre je Kunde gegen doppelte Abrechnung (Aufwand, Anzahlung, Schluss)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:17:11 +00:00
ClaudeandClaude Sonnet 5.5 d264f2af5a Rechnungen: Projektrechnungen bei Stripe (Zusatzfelder, A4, Minuszeilen, Projektdatensatz) und Vorschau
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:16:48 +00:00
ClaudeandClaude Sonnet 5.5 3beba876d6 Aufträge L5: Widerruf eines Projektauftrags auf der Widerrufsseite (nur Privatpersonen in der Frist, Nummer optional)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:16:45 +00:00
ClaudeandClaude Sonnet 5.5 60a51c0f68 Widerrufsseite: Vertragsart Projektauftrag mit freiwilliger Nummer der Auftragsbestätigung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:16:27 +00:00
knsoandClaude Opus 5.5 10fe9a2ae0 Buchungskalender: Portal-Tests mit fester Uhr
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:16:24 +00:00
knsoandClaude Opus 5.5 ca0f762272 Buchungskalender: Buchungsseite, Stripe-Zahlung und Webhook
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:16:17 +00:00
ClaudeandClaude Sonnet 5.5 e680ea3266 AB-Kern: Admin-Routen zum Senden, Erinnern, für neue Fassungen, zum Stornieren und zum Zuordnen eines Projekts
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:16:09 +00:00
knsoandClaude Opus 5.5 4553bc8e92 Buchungskalender: Mails – Link-Pflicht und Testprotokoll nur im Test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:16:04 +00:00
ClaudeandClaude Sonnet 5.5 7d3114d7a3 AB-Kern: Admin-Routen zum Lesen, Anlegen, Ändern, Löschen und für das PDF
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:16:02 +00:00
ClaudeandClaude Sonnet 5.5 c72b252e63 Aufträge L5: K04 und T09 mit Block projekt für den Widerruf eines Projektauftrags
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:16:01 +00:00
ClaudeandClaude Sonnet 5.5 73c5446cf8 Kundenportal Aufträge: Abschnitt im Reiter Rechnungen, Projekt unter der Rechnungsnummer
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:15:56 +00:00
ClaudeandClaude Sonnet 5.5 c116c3cb4f Rechnungen: mapInvoice liefert Projekt, Art und Auftrag
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:15:44 +00:00
ClaudeandClaude Sonnet 5.5 66ec8e0a44 AB-Kern: Projekt nachträglich einer Auftragsbestätigung zuordnen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:15:37 +00:00
ClaudeandClaude Sonnet 5.5 22a7c8123d Aufträge L5: Test – in der Kundenansicht ist das Bestätigen von Aufträgen gesperrt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:15:33 +00:00
ClaudeandClaude Sonnet 5.5 b53f9f67ed Kundenportal Aufträge: Abschnitt „Aufträge“ im Portal (Karten, Bestätigen, Ansichtsmodus gesperrt)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:15:29 +00:00
ClaudeandClaude Sonnet 5.5 4f8e956574 Rechnungen: Anzahlung und Schlussrechnung bauen (Minuszeilen, Entwurfsschutz, Selbstheilung)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:15:25 +00:00
ClaudeandClaude Sonnet 5.5 503337d7ae Aufträge L5: Portal-Routen für Aufträge (eigene gesendete ABs, PDF, Bestätigen)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:15:22 +00:00
ClaudeandClaude Sonnet 5.5 c791146af4 AB-Kern: Stornieren versendeter und bestätigter Aufträge
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:15:21 +00:00
ClaudeandClaude Sonnet 5.5 cf87a7e1e0 AB-Kern: Neue Fassung und Ersetzen der alten AB beim Senden
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:15:11 +00:00
ClaudeandClaude Sonnet 5.5 6cb5bfa6c7 Aufträge L5: öffentliche Bestätigung per Link (idempotent, Frühbeginn nur bei Privatpersonen)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:14:59 +00:00
ClaudeandClaude Sonnet 5.5 11531cacb6 Rechnungen: Aufwandsrechnung bauen (Auswahl, Satz aus der AB, Doppelabrechnung, 20 Zeilen)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:14:48 +00:00
ClaudeandClaude Sonnet 5.5 0da5b30119 AB-Kern: Erinnerung mit neuem Token und geprüfter PDF-Datei
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:14:46 +00:00
ClaudeandClaude Sonnet 5.5 aca393d564 Aufträge L5: öffentliche Link-Routen (ansehen, PDF) mit Ratenbegrenzung, Schalter und Statusprüfung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:14:46 +00:00
ClaudeandClaude Sonnet 5.5 cad9ba7e40 Kundenportal Aufträge: auftrag.html und auftrag.css (noindex, Mail-Stil, Fußzeile, Standalone)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:14:45 +00:00
knsoandClaude Opus 5.5 8c8fa05591 Buchungskalender: Preis-Editor prüft Grenzen wie der Server
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:14:38 +00:00
ClaudeandClaude Sonnet 5.5 c74f7eeea7 AB-Kern: Senden unter Sperre, Fehlerpfade, Dateirest und Mail-Fehler
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:14:29 +00:00
ClaudeandClaude Sonnet 5.5 576a17bfa4 Aufträge L5: Auftrag widerrufen (nur Privatpersonen innerhalb der Frist, unter Sperre)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:14:22 +00:00
ClaudeandClaude Sonnet 5.5 335ac60e54 Aufträge L5: Auftrag bestätigen (idempotent, unter Sperre, Frühbeginn nur bei Privatpersonen)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:14:08 +00:00
ClaudeandClaude Sonnet 5.5 ed7567397d AB-Kern: Senden mit Prüfungen vor der Nummer, Nummer, PDF, Ablage, Token und Mail
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:14:02 +00:00
ClaudeandClaude Sonnet 5.5 38734e99aa Kundenportal Aufträge: Seitenlogik der öffentlichen Bestätigungsseite
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:13:59 +00:00
ClaudeandClaude Sonnet 5.5 b78ca971af Rechnungen: baueRechnung für freie Rechnung und Festbetrag, Testhilfe
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:13:52 +00:00
ClaudeandClaude Sonnet 5.5 3464808a40 AB-PDF: Entwurfs-Kennzeichnung für die Vorschau und Hinweis auf die ersetzte AB
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:13:51 +00:00
ClaudeandClaude Sonnet 5.5 19abac0373 Aufträge L5: Mails zur Auftragsbestätigung (K31 ohne BCC mit Teamkopie ohne Link, K32, T22)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:13:46 +00:00
ClaudeandClaude Sonnet 5.5 3a46210e9c AB-Kern: Vorschau-PDF und austauschbare Abhängigkeiten (deps)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:13:45 +00:00
knsoandClaude Opus 5.5 51a36be36a Buchungskalender: Buchungsseite – Ablauf, bezahlt und Wiederholung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:13:40 +00:00
ClaudeandClaude Sonnet 5.5 6f213dca25 AB-PDF: erzeugeAuftragPdf zeichnet Briefkopf, Tabelle über mehrere Seiten, Belehrung und Fuß mit pdfkit
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:13:34 +00:00
ClaudeandClaude Sonnet 5.5 0fc2eb4516 AB-Kern: Entwürfe anlegen, ändern und löschen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:13:26 +00:00
ClaudeandClaude Sonnet 5.5 bd87bb115c Kundenportal Aufträge: DOM-Bausteine (Zusammenfassung, Bestätigen mit Häkchen, Sperre)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:13:24 +00:00
ClaudeandClaude Sonnet 5.5 fd189b085d Stripe-Nachbau: Zusatzfelder, Seitengröße und Minuszeilen wie bei Stripe
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:13:17 +00:00
ClaudeandClaude Sonnet 5.5 01406c82e5 AB-PDF: Textleser für PDF-Tests (ToUnicode, nur Node-Bordmittel)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:13:14 +00:00
ClaudeandClaude Sonnet 5.5 f0402a7ea9 Aufträge L5: Mailvorlagen K31 Auftragsbestätigung (mit Teamkopie), K32 Auftrag bestätigt, T22 Hinweis ans Team
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:13:10 +00:00
ClaudeandClaude Sonnet 5.5 f71e3e8345 AB-PDF: Bausteine der Auftragsbestätigung mit Höhen und Zusammenhalt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:13:02 +00:00
ClaudeandClaude Sonnet 5.5 8e643160ec build-legal-pdfs: Modus auftraege, PDFs und Prüfsummen der Widerrufsbelehrung der Projektaufträge und der AGB
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:12:56 +00:00
ClaudeandClaude Sonnet 5.5 2d91a54e4a Rechnungen: Anzahlung und Schlussrechnung mit Minuszeilen (Positionen)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:12:52 +00:00
ClaudeandClaude Sonnet 5.5 4706f232b5 AB-PDF: Maße und Seitenaufteilung mit wiederholtem Tabellenkopf
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:12:48 +00:00
knsoandClaude Opus 5.5 e15486263f Buchungskalender: Mails an Gäste, Vermieter und Team
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:12:47 +00:00
ClaudeandClaude Sonnet 5.5 ef646f12de AB-Kern: Eingabeprüfung für Auftragsbestätigungen (pruefeEingabe)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:12:47 +00:00
ClaudeandClaude Sonnet 5.5 9fa965521a Projektkonto: Team-Route für das Abzeichen abgerechnet am Arbeitsblatt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:12:45 +00:00
ClaudeandClaude Sonnet 5.5 846059b455 Projektkonto: Admin-Routen für Konten je Kunde und Detail je Projekt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:12:40 +00:00
knsoandClaude Opus 5.5 37cd312549 Buchungskalender: Anfragen-Reiter – Erstattung und Fehlermeldungen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:12:37 +00:00
ClaudeandClaude Sonnet 5.5 e6a0fc9acf Kundenportal Aufträge: reine Ansichtslogik (Token, Beträge, Zustände, Status, AB-Nummer)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:12:34 +00:00
ClaudeandClaude Sonnet 5.5 731bc8c05e AB-PDF: Zahlungsplan, Termine, AGB-Satz, Privatperson-Seiten, Fuß und Vorschau in den PDF-Daten
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:12:33 +00:00
ClaudeandClaude Sonnet 5.5 6c1eaf697b Rechnungen: Aufwandszeilen je Tag, Kalenderwochen-Zusammenfassung ab 20 Zeilen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:12:28 +00:00
ClaudeandClaude Sonnet 5.5 32726fa1c2 Projektkonto: Abzeichen abgerechnet je Ticket (Rechnungsnummer oder Entwurf)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:12:25 +00:00
ClaudeandClaude Sonnet 5.5 aeae17b491 build-legal-html: Satz auftraege für die Widerrufsbelehrung der Projektaufträge und die AGB, beide Seiten erzeugt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:12:23 +00:00
ClaudeandClaude Sonnet 5.5 7cec85f888 Projektkonto: Detail mit ABs, Rechnungen und offenen Arbeitsblättern
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:12:19 +00:00
ClaudeandClaude Sonnet 5.5 7ad9c12bca AB-PDF: auftragPdfDaten mit Kopf, Anschrift, Positionen und Prüfung der Pflichtangaben
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:12:18 +00:00
ClaudeandClaude Sonnet 5.5 7553253746 Rechnungen: Positionen-Grundlagen (Stundentext, Kalenderwoche, feste Positionen, Prüfung)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:12:07 +00:00
ClaudeandClaude Sonnet 5.5 8032aa67c6 Projektkonto: Konten je Kunde (Projektwahl, Zeitquelle, Zeitwert je Tag, Ohne Projekt, Schalter, Stripe-Ausfall)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:12:01 +00:00
ClaudeandClaude Sonnet 5.5 edccd4d8af AGB-Kopie: Quelle docs/rechtliches/agb.md wortgleich mit der Website (Fassung 2026-09-30), Tests gegen AGB.tsx
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:11:57 +00:00
ClaudeandClaude Sonnet 5.5 b097fedf5a AB-PDF: Zeichen ohne Glyph werden ersetzt statt als Kästchen gedruckt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:11:54 +00:00
ClaudeandClaude Sonnet 5.5 e3df5383d1 AB-Kern: Nummernvergabe AB-JJJJ-NNNN nach Berliner Jahr
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:11:52 +00:00
knsoandClaude Opus 5.5 db9fc61894 Buchungskalender: Kalender und Unterkünfte im Portal
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:11:51 +00:00
ClaudeandClaude Sonnet 5.5 ae6d84b2a4 AB-PDF: Figtree Regular und SemiBold (OFL) mit Schriftlader und Glyphenprüfung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:11:48 +00:00
ClaudeandClaude Sonnet 5.5 445d234695 Rechtstexte der Aufträge: Endfassung der Textbausteine, Bestätigung, Frühbeginn-Erklärung und Widerrufsbelehrung (Fassung 2026-10-06)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:11:47 +00:00
ClaudeandClaude Sonnet 5.5 cf3e0fd52a Aufträge L5: Bestätigungs-Token (32 Zufallsbytes, nur als Hash gespeichert, 60 Tage gültig)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:11:43 +00:00
ClaudeandClaude Sonnet 5.5 10679ae726 Projektkonto: Stripe-Rechnungen lesen (Liste, Einzellesen fehlender, Ausfall)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:11:41 +00:00
ClaudeandClaude Sonnet 5.5 68ffac24c4 AB-Kern: Testhilfe und Vertragsprüfung der L0-Module
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:11:34 +00:00
ClaudeandClaude Sonnet 5.5 3305371111 Projektkonto: reine Rechenregeln (vereinbart, berechnet, offen, Zeitwert je Tag gerundet, Ohne Projekt)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:11:32 +00:00
ClaudeandClaude Sonnet 5.5 987268e2f0 Kundenportal: Rechnungsliste kennt das Projekt (Feld projekt aus den Stripe-Metadaten)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:11:30 +00:00
knsoandClaude Opus 5.5 0b0d8da335 Buchungskalender: Buchungsseite für Gäste
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:10:50 +00:00
knsoandClaude Opus 5.5 40a366b8ae Buchungskalender: ID-Prüfung erlaubt Bindestrich (UUIDs)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:10:46 +00:00
knsoandClaude Opus 5.5 bc9b4d9576 Buchungskalender: Widget-Monatsnavigation und Tastaturfokus
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:10:34 +00:00
knsoandClaude Opus 5.5 0c5ca76a4d Buchungskalender: öffentliche API gegen manipulierte IDs und Lookups absichern
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:10:30 +00:00
ClaudeandClaude Opus 5.5 8627ed1124 Aufträge: Router-Gerüste und Einbindung in server/index.js und Team-Router
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:08:37 +00:00
ClaudeandClaude Opus 5.5 4f467fa9c5 Finanzen: Einstellung Steuernummer für die Auftragsbestätigung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:08:06 +00:00
ClaudeandClaude Opus 5.5 e9e198e53c Aufträge: PDF-Ablage mit Pfadprüfung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:07:32 +00:00
ClaudeandClaude Opus 5.5 98eceaf14c Aufträge: Sichten auf die Auftragsbestätigung (Team, öffentlich, Frühbeginn)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:07:15 +00:00
knsoandClaude Opus 5.5 9b509fefbc Buchungskalender: Website-Widget mit Kalender und Anfrageformular
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:06:53 +00:00
ClaudeandClaude Opus 5.5 47a92811a3 Aufträge: Arbeitsblätter laden, normieren und als abgerechnet erkennen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:06:50 +00:00
knsoandClaude Opus 5.5 3ecea05b52 Buchungskalender: Portal-Bereich und Anfragen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:06:48 +00:00
knsoandClaude Opus 5.5 035967f8eb Buchungskalender: öffentliche API für Kalender und Anfrage
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:06:30 +00:00
ClaudeandClaude Opus 5.5 9cd5ea3038 Aufträge: Speicher der Projektrechnungen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:06:19 +00:00
knsoandClaude Opus 5.5 118be44847 Buchungskalender: PDF-Writer und Rechnungs-PDF
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:06:11 +00:00
ClaudeandClaude Opus 5.5 a3db7f310a Aufträge: Speicher der Auftragsbestätigungen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:05:49 +00:00
ClaudeandClaude Opus 5.5 4e4feba4c9 Aufträge: vorläufige Textbausteine der Auftragsbestätigung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:05:02 +00:00
ClaudeandClaude Opus 5.5 004e564479 Aufträge: Setup-Skript für Collections und Zusatzfelder
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:04:51 +00:00
ClaudeandClaude Opus 5.5 ddffc7a92d Aufträge: Konfiguration und Pilot-Schalter AUFTRAEGE_ENABLED
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:04:48 +00:00
ClaudeandClaude Opus 5.5 0ab3cbbaa3 Aufträge: Appwrite-Schema der Collections auftraege und projektRechnungen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:04:01 +00:00
ClaudeandClaude Opus 5.5 16f08b5ac3 Aufträge: Konstanten und Fehlerklasse (L0)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:03:25 +00:00
ClaudeandClaude Opus 5.5 1b5b5b7094 Aufträge: pdfkit und fontkit als Abhängigkeiten, node_modules auch als Symlink ignorieren
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:02:50 +00:00
knsoandClaude Opus 5.5 d7b5b98187 Buchungskalender: Verdrahtung absichern (Outbox, Shop-Webhook, Session)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:02:41 +00:00
knsoandClaude Opus 5.5 a68baf10d9 Buchungskalender: Verdrahtung, Router-Gerüste und API-Vertrag
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:59:38 +00:00
knsoandClaude Opus 5.5 1d2ca2791d Buchungskalender: Storno und Erstattung getrennt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:58:17 +00:00
knsoandClaude Opus 5.5 f7650fe883 Buchungskalender: Storno und Buchung idempotent bei Teilfehlern
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:55:56 +00:00
knsoandClaude Opus 5.5 947e9da2e3 Buchungskalender: Statusmaschine, Token und Rechnungsnummern
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:52:52 +00:00
knsoandClaude Opus 5.5 802b5d3ad4 Buchungskalender: Mengen-Obergrenze für Posten auf 20000
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:50:22 +00:00
knsoandClaude Opus 5.5 bdf191d945 Buchungskalender: Speicher und Belegung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:49:29 +00:00
knsoandClaude Opus 5.5 c38c8e7c13 Buchungskalender: Preisrechnung nachgeschärft (Summe, Kleinunternehmer, Vollständigkeit)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:49:20 +00:00
knsoandClaude Opus 5.5 84e7a99f41 Buchungskalender: Preisrechnung und Datumshilfen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:47:41 +00:00
knsoandClaude Opus 5.5 5717a5cdd0 Buchungskalender: Konfiguration, Schalter, Schema und Setup-Skript
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:45:57 +00:00
knsoandClaude Opus 5.5 18369a348c Plan: Buchungskalender (23 Tasks, 4 parallele Stränge)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:41:26 +00:00
knsoandClaude Opus 5.5 668bc62c55 Spec Buchungskalender: Nachträge aus der Code-Prüfung (N1–N6)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:36:12 +00:00
knsoandClaude Opus 5.5 5a05bd27e9 Spec: Buchungskalender für Unterkünfte (Funktion unterkunft)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:22:02 +00:00
ClaudeandClaude Opus 5.5 9d82fc579f Umsetzungspläne Aufträge und Rechnungen: Übersicht und 12 Strang-Pläne
Parallele Stränge L0–L7 (Portal), W0–W2 (WOMS), Z (Integration und
Kontrolle); Vertragsfragen der Planrunde entschieden (Abschnitt
„Nachträge nach der Planrunde“ in der Übersicht).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 12:49:15 +00:00
ClaudeandClaude Opus 5.5 0bb463eb7d Spezifikation nachgeschärft, Plan-Übersicht mit Verträgen der Stränge
Nachträge aus der Planrunde (Fragment-Link, Teamkopie ohne Link, Schalter
AUFTRAEGE_ENABLED, Projekt später zuordnen, Schlussrechnung bis 40 Zeilen,
zusammengesetzte Sperrschlüssel, Zuständigkeiten).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 12:06:09 +00:00
ClaudeandClaude Opus 5.5 e00b4ae572 Spezifikation: Auftragsbestätigung und projektbezogene Rechnungen
Entwurf aus dem Brainstorming vom 06.10.2026 (Abschnitte im Chat freigegeben):
Projektkonto, Rechnungsarten Anzahlung/Schluss/Aufwand/Festbetrag über Stripe,
Auftragsbestätigung als eigenes PDF mit Bestätigungsklick und Verbraucher-Variante,
zwei Pläne.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 10:03:25 +00:00
ClaudeandClaude Opus 5.5 8814c4aef1 Webshop: fehlendes Stripe-Plattformprofil klar melden statt 'Stripe nicht erreichbar'
Lehnt Stripe die Kontoanlage ab, weil das Connect-Plattformprofil von WEBklar fehlt (oder Connect nicht aktiv ist),
bekommt der Kunde 503 'plattform_nicht_bereit' mit eigenem Du-Text, und das Log nennt die Ursache (ohne Stripe-Text).
Gefunden im Pilot 2026-10-06.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 08:58:20 +00:00
ClaudeandClaude Opus 5.5 50f854ef1c Öffnungszeiten: Datenschutz-Abschnitt heißt 6e (6d ist seit dem Webshop vergeben); Härtungspunkt N13
Entwurf umbenannt (zeiten-datenschutz-6e-entwurf.md), Leistungsbeschreibung, Kanzlei-Fragen,
Livegang-Notiz und Test-Pins ziehen mit. N13: Rückgabe-Marker nach dem Löschen fehlt (vor on).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 06:30:03 +00:00
ClaudeandClaude Opus 5.5 d0691236d5 Öffnungszeiten: Test hält fest, dass ein gescheitertes Löschen der Zeiten den Kunden bis zum nächsten Lauf hält
Seit TP2 bleiben die Abo-Dokumente stehen (held), wenn das Löschen abhängiger Daten scheitert. Der Test für die
Aufbewahrungs-Löschung prüft jetzt auch für die Zeiten: held nennt den Kunden, das Abo-Dokument bleibt, der
nächste Lauf findet den Kunden wieder und erst nach dem Löschen der Zeiten gehen die Abo-Dokumente.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 06:15:58 +00:00
ClaudeandClaude Opus 5.5 3984d468bf Öffnungszeiten: Datenschutz-Entwurf an den Stand von main angepasst (§ 7-Satz, Nummer 6d)
Der Wächter in zeitenRecht.test.js hat den Drift gefunden: Abschnitt 7 in public/datenschutz.html enthält seit TP2
den Satz zu Bestellungen ohne Abo. Das wörtliche Zitat im Entwurf ist nachgeführt, der Vorschlag ändert wie
bisher nur den Anfang des Satzes. Dazu ein Nachtrag im Entwurf: Abschnitt 6d ist seit dem Webshop-Livegang
vergeben, der Abschnitt der Öffnungszeiten muss beim Übernehmen als 6e eingebaut werden (Anker id="zeiten"
bleibt); die Umnummerierung entscheidet der Inhaber.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 06:15:58 +00:00
ClaudeandClaude Opus 5.5 3e66dfa91c Tests nach dem Zusammenführen mit main: Reihenfolge der Funktionsschlüssel und Importe von funktionen.js
Die Öffnungszeiten stehen jetzt hinter dem Webshop (news, termine, shop, zeiten): Register, FEATURE_KEYS und
ticketFreischaltungKeys in zeitenRegister, die drei Listen in app.js (VIEW_HASHES, APP_PARENT, FEATURE_LABELS)
in zeitenBestellen und shopWiring (APP_PARENT endet mit webshop, zeiten). funktionenZeitenTile prüft die Importe
von funktionen.js gegen den Stand von main (fn-util.js und analyse-view.js); die Kachel der Öffnungszeiten
bringt weiterhin keinen neuen Import mit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 06:15:58 +00:00
ClaudeandClaude Opus 5.5 e87140e5ba Merge remote-tracking branch 'origin/main' into feature/oeffnungszeiten
Zusammenführung von TP2-Abschluss und Webshop (origin/main cab9c33) mit Öffnungszeiten live. Beide
Funktionen bleiben vollständig: jeder Konflikt ist als Vereinigung gelöst, Reihenfolge der Funktionsschlüssel
überall news, termine, shop, zeiten.

Konfliktdateien (28): public/app.js, dashboard.html, funktionen.js, widerrufen.html; scripts/stripe-products.mjs;
server/config.js, index.js, middleware/kundenansichtGuard.js, routes/abo.js, routes/admin/funktionenAdmin.js,
routes/funktionen.js, services/customerAdmin.js, customerViewLog.js, featureRegistry.js, retention.js,
siteFeatures.js, stripeBilling.js; test/customerViewLog, featureFreischaltung, featureRegistry, funktionenRoutes,
funktionenTermine, kundenansichtGuard, mailTemplates, publicForms, siteFeatures, stripeProductsScript (je
.test.js) und test/support/schema.js.

Entscheidungen: Mailvorlagen G01–G15, K01–K30, T01–T21 lückenlos (K24–K26 Webshop, K27–K30 Öffnungszeiten);
Stripe-Katalog mit shopSetup und zeitenSetup; Widerruf kennt shop und zeiten; ABO_FUNKTIONEN bleibt analyse
und termine (kein zeiten); activate() schreibt die Zeiten-Felder nur über extra; kein Halter von
feature-activate:<id> ruft disable() auf; die Aufbewahrungs-Löschung löscht Zeiten-Daten vor dem Abschalten und
hält den Kunden bei einem Fehler (held); die Admin-Antwort trägt ersatzTicket und warnung; Hash-Routen webshop
und zeiten, Ansichten view-webshop und view-zeiten; assertOwnProject behält die Meldung von main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 06:12:23 +00:00
ClaudeandClaude Opus 5.5 c7cd436ec2 Öffnungszeiten: Mails K24–K27 heißen jetzt K27–K30, Feld k24At heißt einschaltMailAt
K24–K26 sind seit dem Webshop-Livegang (main) vergeben; der Mail-Lader sucht die Vorlage über den
Dateianfang, doppelte Nummern hätten die falsche Mail geschickt. Das Feld ist noch nicht in der
Produktion angelegt (zeiten-setup.mjs lief noch nicht), daher jetzt ein sprechender Name.
Spec und Plan behalten die alten Nummern (Hinweis oben im Dokument).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 06:00:17 +00:00
ClaudeandClaude Opus 5.5 cab9c33db6 Webshop nach TP2-Merge: Shop-Mails in der Klasse normal (kein zusätzliches notEqual in der Outbox-Abfrage)
Seit 2c7cd26 kostet jede Art in VORRANG/NACHRANGIG ein notEqual in der Abfrage der Klasse normal; live stößt die URL
bei nginx an (414). G09–G15 laufen deshalb in der Klasse normal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 14:59:12 +00:00
Claude a4a18a1c89 Merge branch 'feature/terminplaner-tp2' into feature/webshop 2026-10-05 14:56:43 +00:00
ClaudeandClaude Opus 5.5 348ddc927c Analyse-Tests: feste Zeiten vor NOW statt echter Uhr (Reihenfolge kippte ab 2026-10-05 10:00 UTC)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 14:55:54 +00:00
ClaudeandClaude Sonnet 5.5 9350fccd77 K02/K03: Hinweis auf das Ende von Analyse und Terminplaner nur für Kunden, die eine von beiden haben
Der Absatz "Die Website-Analyse und der Terminplaner gehören zum Wartungs-Abo und enden mit ihm."
stand bei jeder Kündigung eines Wartungs-Abos. Nach der Entscheidung vom 05.10.2026 steht er nur
noch, wenn der Kunde ein Funktionsdokument (portalFeatures, featureKey analyse oder termine) in
einem Zustand ausser off hat.

- abo.js: hatAboFunktionen(sub) liest über listCustomerFeatureDocs (wie die übrigen Funktions-
  Abfragen) die Dokumente des Kunden des Abos; ein Lesefehler zählt als ja (eine Logzeile, die Mail
  geht in jedem Fall raus). Der Block funktionen in K02 (Kündigung im Stripe-Kundenportal) und K03
  (Kündigungsseite, weiterhin nur beim Abo) ist jetzt isWartung(sub) && hatAboFunktionen(sub);
  beim Hosting-Abo wird gar nicht erst gelesen. Die Vorlagentexte bleiben unverändert.
- Tests: K02 und K03 ohne Dokument, mit aktiver Analyse, Terminplaner in setup/ordered/disabled,
  nur off (und ohne Zustand), News oder Dokument eines anderen Kunden, Lesefehler (Hinweis bleibt,
  Mail geht raus, eine Logzeile) und Hosting-Abo (kein Hinweis, kein Lesen).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 14:50:03 +00:00
ClaudeandClaude Sonnet 5.5 2c7cd26532 Termine-Outbox: notEqual mit genau einem Wert (Appwrite 1.8.1), nachrangige Mails kommen wieder dran
Auf der echten Appwrite 1.8.1 lehnt notEqual mit Liste die Abfrage ab (HTTP 400
general_query_invalid, "NotEqual queries require exactly one value"). Die Klasse normal der
Mail-Warteschlange schickte immer eine Liste, jeder Lauf warf dort, und die nachrangige Klasse
(G05, K20, K21) kam nie dran.

- appwriteAdmin: Query.notEqual nimmt genau einen Wert (eine Liste wirft einen TypeError, der
  keinerVon nennt); neu keinerVon(attribute, werte) = je Wert ein notEqual (Appwrite schneidet
  sie); dokumenteUrl() baut die Listen-URL ohne Netz, mit derselben Serialisierung wie die Anfrage.
- outbox: klassenFilter liefert fuer jede Klasse eine Liste von Abfragen (normal = sechs notEqual
  auf art), die Ausschlussliste ist keinerVon('featureId', ...). AUSSCHLUSS_MAX 90 -> 30 (der nginx
  antwortet in dieser Abfrageform ab 46 Funktions-IDs mit 414); der Kommentar "100 Werte je Abfrage"
  entfaellt.
- Nachbau (appwriteMock): notEqual mit Liste oder ohne Wert => 400 wie Appwrite; ein Dokument mit
  Feld null oder ohne Feld passt nicht zu notEqual (SQL-Semantik).
- Tests: Query-Helfer, keinerVon, dokumenteUrl, Nachbau (400, null), ein Lauf bedient alle drei
  Klassen, Ausschlussliste hoechstens 30 und URL unter 6000 Zeichen im schlimmsten Fall.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 14:47:29 +00:00
ClaudeandClaude Opus 5.5 ce84fc849a Webshop-Test: Reiter-Panels abwarten (Node 20 lädt import() langsamer)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 10:30:02 +00:00
ClaudeandClaude Opus 5.5 bf308a75ae Webshop-Rechtstexte: IP-Hash mit Zahlung oder Storno gelöscht, keine Telefonnummer des Käufers
AVV, Leistungsbeschreibung, Datenschutzerklärung (6d), Datenschutz-Absatz und Kanzlei-Paket an das Verhalten
angepasst; HTML und PDF neu erzeugt (Fassung 2026-10-05 unverändert, nie ausgeliefert).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 10:26:56 +00:00
ClaudeandClaude Opus 5.5 43473c0879 Webshop: IP-Hash entfällt mit Zahlung oder Storno, keine Käufer-Telefonnummer mehr gespeichert
schliesseAb und verwirf entfernen ipHash aus dem Bestell-JSON, sobald die Bestellung nicht mehr offen ist
(Begrenzung offener Bestellungen je IP bleibt). customer_details.phone wird nicht mehr in kaeufer.telefon
übernommen; Kundenansicht und Bestelldetail geben auch in alten Datensätzen keine Telefonnummer aus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 10:26:56 +00:00
ClaudeandClaude Opus 5.5 4ab84e31ac Webshop: Playwright-Durchlauf (Anfrage bis Widerruf) mit Nachbauten von Appwrite, Stripe Connect und Mail
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 10:23:45 +00:00
ClaudeandClaude Opus 5.5 72d79ddba7 Webshop-Rechtstexte an den Code angepasst: Archiv, IP-Hash, keine Telefonnummer, Eingangsmail
AVV (Vorbemerkung Abs. 2, Nr. 2, 3, 8, 9, Anlage), Leistungsbeschreibung (Nr. 2, 5, 6, 10),
Datenschutzerklaerung Abschnitt 6d, Datenschutz-Absatz fuer Haendler und Kanzlei-Paket (F12, F14, F16, F18, F19,
Fundstellen) beschreiben jetzt genau, was gebaut ist: jede bestaetigte Fassung der Rechtstexte liegt im Archiv und wird
mit den Shop-Daten geloescht, G09 zeigt die archivierte Fassung zum Hash der Bestellung; Eingangsmail G15 bei SEPA; die
Stripe-Kasse fragt keine Telefonnummer ab; die Bestellung speichert einen gesalzenen IP-Hash (Missbrauchsschutz,
Pseudonym), der bei Loeschung oder Anonymisierung entfaellt; Freischaltung durch das Team nur mit Zustimmung in Textform
samt Nachweis. Fassung 2026-10-05 bleibt, HTML und PDFs neu erzeugt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 10:07:36 +00:00
ClaudeandClaude Opus 5.5 52543a99b1 Webshop: Livegang-Anleitung (Stripe Connect, Webhook, Schalter, Schema, Pilot)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 10:03:55 +00:00
ClaudeandClaude Opus 5.5 578a64782b Shop Review B M-6: Stripe-Kasse ohne Telefonnummer, Rechnungsanschrift nur bei Bedarf
phone_number_collection ist aus, billing_address_collection steht auf auto (Datenminimierung).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 10:01:05 +00:00
ClaudeandClaude Opus 5.5 c36cb9b4a3 Shop Review B M-3: Versandhinweis bei jedem Preis, nicht bei reinen Abholshops
"zzgl. Versand" steht jetzt unter dem Preis jeder Artikelkarte und im Detail, nur wenn der
Shop versendet. Unter dem Raster bleibt der Steuerhinweis.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 10:00:42 +00:00
ClaudeandClaude Opus 5.5 b2072e4e7c Shop Review B M-2: Widerrufsfunktion laeuft auch bei SHOP_ENABLED=off und ausserhalb des Pilotkreises
GET /:siteKey liefert dann die Widerrufsseite (nurWiderruf), POST /:siteKey/widerruf samt
Preflight geht durch, solange die Funktion active oder disabled ist. Bestellen, Pruefen, Status
und Bilder bleiben gesperrt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:59:58 +00:00
ClaudeandClaude Opus 5.5 08d837d690 Shop Review B I-6, I-7: Archiv der bestaetigten Rechtstexte, G09 ohne Link-Ersatz
I-6: Neue Collection shopRechtstexteArchiv (ID r + 35 Hex aus sha256(shopId + hash)); das
Bestaetigen legt die Fassung samt Versand-/Zahlungstext und Gewaehrleistungstext ab, G09 zeigt
die Fassung zum rechtstexteHash der Bestellung (Rueckfall mit Warnung auf die aktuell
bestaetigte), loescheShopDaten loescht das Archiv mit.

I-7: Grenzen AGB 20000, Belehrung 12000, Formular 3000 plus Pruefung der maskierten Laenge beim
Speichern; G09 ersetzt die AGB nie mehr durch einen Link, sondern scheitert (Marker bleibt
offen, wird wiederholt). Test mit Texten in Hoechstlaenge gegen die Grenzen der Warteschlange.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:58:31 +00:00
ClaudeandClaude Opus 5.5 2f4f360a21 Shop Review B I-5: Bestaetigen nur ohne Platzhalter, Musterkopf und mit Adresse der Widerrufsfunktion
pruefeBestaetigbar listet die Probleme in Du-Form; POST /api/shop/rechtstexte/bestaetigen
antwortet 400 nicht_bestaetigbar mit der Liste, GET und PUT liefern sie als probleme mit.
Der Reiter Rechtliches zeigt die Punkte im Bestaetigungsblock und sperrt den Knopf.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:53:17 +00:00
ClaudeandClaude Opus 5.5 1f56b86791 Shop Review B I-4: Steuerart und Lieferzeit ohne Vorbelegung
kleinunternehmer ist ab jetzt null (nicht gewaehlt), lieferzeit leer. istVollstaendig verlangt eine
bewusste Wahl (true oder false), der Shop kann sonst nicht geoeffnet werden. Im Reiter
Einstellungen gibt es zwei Pflicht-Radiobuttons ohne Vorauswahl; Preise, Artikel und Muster
rechnen bei null wie bei Regelbesteuerung. Tests und Fixtures angepasst.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:50:54 +00:00
ClaudeandClaude Opus 5.5 7104f97feb Shop Review B I-2, I-3: Gewaehrleistungskasten vor der Zusammenfassung, AGB-Hinweis ueber dem Bestellknopf
Die amtliche Gewaehrleistungsgrafik steht in hervorgehobenem Kasten mit Ueberschrift vor der
Zusammenfassung der Kasse (hoechstens 420 px breit, sofort geladen, Vergroessern oeffnet sie in
einem mit Esc und Knopf schliessbaren Dialog). Direkt ueber "Zahlungspflichtig bestellen" steht
"Es gelten unsere AGB. Informationen zu Ihrem Widerrufsrecht finden Sie in der
Widerrufsbelehrung." mit Links auf beide Texte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:47:28 +00:00
ClaudeandClaude Opus 5.5 72fb37edcc Shop Review B I-1: Muster-AGB § 2 und § 9 passen zum Ablauf, Angaben nach Art. 246c EGBGB
§ 2 beschreibt Bestaetigungsseite, sofortige Bestellbestaetigung bei Karte/Klarna und
Eingangsmail mit spaeterem Vertragsschluss bei SEPA. § 9 nennt technische Schritte,
Berichtigung von Eingabefehlern, Speicherung, Sprache und Verhaltenskodizes. Markdown-Muster
und Test sind angeglichen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:46:19 +00:00
ClaudeandClaude Opus 5.5 77fb8c8f44 Shop Review B I-1: Eingangsmail G15 bei SEPA, G09 nimmt die Bestellung ausdruecklich an
G15 "Wir haben Ihre Bestellung erhalten" geht einmal raus, sobald Stripe die Sitzung
abgeschlossen, die Zahlung aber noch nicht gemeldet hat (meldeEingang, Marker mailsEingangAt,
aus dem Webhook und aus pruefeOffene). G09 sagt ausdruecklich, dass der Kaufvertrag mit der
Mail geschlossen ist, trennt Bestell- und Zahlungsdatum und nennt die Telefonnummer des
Anbieters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:44:48 +00:00
ClaudeandClaude Opus 5.5 1b7aa9ebe3 Shop Review B I-8: Aktivierung des Webshops durch das Team nur mit Zustimmung in Textform
AVV Webshop, Vorbemerkung Abs. 2: Das Team darf den Webshop ohne Online-Bestellung
nur einschalten, wenn der Kunde vorher in Textform zugestimmt hat. Die Textform-Aktivierung
gilt jetzt wie beim Terminplaner auch fuer 'shop' (Modus on, Pilotadressen ausgenommen,
shopTextformNoetig): Nachweis in featureConsents mit Fassung (config.shop.fassung), Datum,
Weg, Mitarbeiter und dem Wortlaut der Erklaerungen der Bestelltexte, K16 mit
Leistungsbeschreibung und AVV als PDF. GET /api/team/funktionen/:kunde liefert zusaetzlich
shop { textformNoetig, avvVersion, links }.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:44:11 +00:00
ClaudeandClaude Opus 5.5 a129bdb0ec Webshop: Test der Shop-Konfiguration kennt formMinSekunden und minAntwortMs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:40:10 +00:00
ClaudeandClaude Opus 5.5 309c98d531 Shop Review A M10: Kontext-Cache des Shops beim Abschalten leeren
disable (und damit disableCustomerFeatures bei Kundenloeschung) leert fuer die Funktion shop den Kontext-Cache dieses siteKeys, per dynamischem Import und best effort.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:38:58 +00:00
ClaudeandClaude Opus 5.5 45487d6c7e Shop Review A M3: Eingangsbestaetigung G12 je Empfaengeradresse begrenzen (3 je Stunde)
Ueber die gemeinsame Empfaenger-Kappe (darfEmpfaenger) geht an dieselbe Adresse hoechstens dreimal je Stunde ein G12. Darueber hinaus wird der Widerruf trotzdem gespeichert und mit eingang beantwortet, es wird aber keine weitere Mail eingereiht (bestaetigungAt bleibt leer).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:38:16 +00:00
ClaudeandClaude Opus 5.5 c6706f88ff Shop Review A M2: Bestellaktionen, Erstattung und Widerruf erledigt auch bei abgeschaltetem Shop
Die Routen aktion, erstatten und widerrufe/erledigt akzeptieren jetzt auch den Zustand disabled; Einstellungen, Artikel und Oeffnen bleiben gesperrt. Die Oberflaeche laesst Bestellungen und Widerrufe bei abgeschaltetem Shop bearbeitbar (ctx.abwickeln), der Hinweis nennt es.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:37:29 +00:00
ClaudeandClaude Opus 5.5 21805c9a93 Shop Review A M6: Stripe-Status auf 4000 Zeichen begrenzen (30 Eintraege zu 100 Zeichen, notfalls weiter kuerzen)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:35:40 +00:00
ClaudeandClaude Opus 5.5 d4b9e6a5b9 Shop Review A M9: Mindestbetrag 0,50 EUR (Stripe) und Sitzungsende direkt vor dem Stripe-Aufruf nachfuehren
berechne lehnt Gesamtbetraege unter 0,50 EUR mit dem Code mindestbetrag und einem Kaeufertext ab. legeBestellungAn schiebt expires_at um die seit Beginn vergangenen ganzen Sekunden (Warten auf Sperren) weiter und fuehrt ablaufAt auf denselben Wert nach, damit Stripes 30-Minuten-Minimum sicher bleibt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:35:17 +00:00
ClaudeandClaude Opus 5.5 cb6291410d Shop Review A I4: Shop-Limit 300 je Stunde, hoechstens 3 offene Bestellungen je IP und Shop in 30 Minuten
Die IP wird nur als gesalzener Kurz-Hash (sha256, 16 Hex-Zeichen) im Bestell-JSON abgelegt; der Tagesjob entfernt ihn bei der Anonymisierung mit. Die Pruefung laeuft unter der Bestandssperre, Honigtopf und Fehlversuche legen nichts an und zaehlen nicht. Antwort 429 zu_viele_offen mit Retry-After 1800.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:34:03 +00:00
ClaudeandClaude Opus 5.5 55fe1c567f Shop Review A I3: Artikel-Editor ueberschreibt den Bestand nicht mehr (bestandGeladen, Bestand nur bei echter Aenderung schreiben)
Der Editor sendet neben bestand den beim Laden gesehenen Wert (bestandGeladen, je Variantenwert mit id). speichereProdukt behaelt unter der Artikelsperre den aktuell gespeicherten Stand, wenn gesendet gleich geladen ist, und schreibt nur vom Haendler geaenderte Bestaende. bestandGeladen wird nie gespeichert; ohne den Wert (alter Browser) gilt der gesendete Bestand wie bisher.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:32:31 +00:00
ClaudeandClaude Opus 5.5 9c2897a0b0 Shop Review A I1/I2: Statuswechsel und Bestandsabzug unter einer Sperre, fehlende Schritte (Bestand, G09, K25) per Marker nachholen
Der Bestandsabzug laeuft jetzt im selben Block unter shop-bestand und shop-produkte wie der Statuswechsel auf bezahlt, zieheBestandAb haelt keine eigene Sperre mehr. Im Bestell-JSON stehen bestandAbgezogenAt, mailsKaeuferAt und mailsHaendlerAt als Marker; schliesseAb (Webhook-Wiederholung, Statusabruf, Minutenjob) holt fehlende Schritte genau einmal nach, Mails hoechstens fuenfmal versucht. Bezahlte Bestellungen ohne Bestandsabzug zaehlen weiter als reserviert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:30:36 +00:00
ClaudeandClaude Opus 5.5 42a6bb5a4a Webshop-Spec: Warenkorb liegt im sessionStorage (wie gebaut)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:27:40 +00:00
ClaudeandClaude Opus 5.5 329b8cc949 Shop J2: Kanzlei-Paket an die freiwillige Bestellnummer im Widerruf angepasst
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:27:13 +00:00
ClaudeandClaude Opus 5.5 c259e45412 Shop J2: Einbauanleitung, Datenschutz-Absatz für die Händler-Website, Kanzlei-Paket Webshop (F1 bis F29) mit Test
Die Fragenliste F1 bis F18 ist ins Paket übernommen und um F19 bis F29 ergänzt; die alte Datei verweist nur noch auf das Paket.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:27:01 +00:00
ClaudeandClaude Opus 5.5 dbb2042983 Shop J2: Widerruf der Bestellung Webshop (Einrichtung) im öffentlichen Widerrufsformular und in abo.js, Tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:20:39 +00:00
ClaudeandClaude Opus 5.5 9283ff42c1 Shop J2: Datenschutzerklärung Abschnitt 6d Webshop (Auftragsverarbeitung, Stripe Connect, Fristen), PDF neu, Test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:19:31 +00:00
ClaudeandClaude Opus 5.5 172d9f1778 Shop P1b: Bestellnummer im Widerruf freiwillig, ohne Nummer Bestelldatum und Artikel in der Erklärung (§ 356a BGB)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:18:14 +00:00
ClaudeandClaude Opus 5.5 5693f37d5f Shop J1: Leistungsbeschreibung und AVV Webshop an Entscheidung W5 angepasst (Webshop an den Vertrag über den Betrieb der Website gekoppelt, nicht an ein Abo)
Nr. 1, 5, 9 Abs. 4 und Nr. 10 der Leistungsbeschreibung sowie AVV Nr. 2 und 9 nennen den Vertrag über den Betrieb der Website statt Hosting- oder Wartungs-Abo; HTML, PDF, Prüfsummen und Kanzlei-Frage F9 nachgezogen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:17:51 +00:00
ClaudeandClaude Opus 5.5 16a7f05aa2 Shop P1a: Du-Form im Portal klein (du, dich, dein) wie im übrigen Kundenportal
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:17:09 +00:00
ClaudeandClaude Opus 5.5 ea4457e6a4 Öffnungszeiten: Fixrunde Doku (Datenschutz-Entwurf, Einbau-Anleitung, Kanzlei-Fragen, Livegang-Notiz)
- 6d-Entwurf § 7: Stripe-Sitzung nur bei Bestellung, IP-Adresse und Browserkennung bei jeder Zustimmung
  (auch Gratisweg); Offener Punkt 4 beantwortet
- Einbau-Anleitung: CSP braucht auch style-src, Portal mit docker compose up -d neu erstellen, Satz zu
  Einzelseiten-Apps, data-api nur am Script-Tag
- Kanzlei-Fragen: ZQ11 (Team-Aktivierung ohne Zustimmung), ZQ12 (Pflichtinformationen als Haken und in K15);
  ZQ3 und Schlussnotiz an den Stand von Wertersatz-Satz und Widerruf-Seite angepasst
- Livegang-Notiz: Zusammenführung über origin/main mit erwarteten Konflikten, genaue Push-Refs und
  Webhook-Warnung, Sicherungen und Prüfungen vor der Compose-Änderung, Schema-Check nach dem Anlegen,
  Runner-Neustart prüfen, setup-legal als ungetestet, Live-Checks und Rückweg, Handrezept für Testdaten,
  Test-Anpassung bei bald:false, Checkliste nachgeführt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:16:42 +00:00
ClaudeandClaude Opus 5.5 17ad5cbcdc Shop J1: Leistungsbeschreibung und AVV Webshop (Fassung 2026-10-05) als Markdown, HTML, PDF und Prüfsumme, Satz/Modus shop in den Build-Skripten, Kanzlei-Fragenliste
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:15:35 +00:00
ClaudeandClaude Opus 5.5 b8055b336c Shop J3: Tagesjob und Stundenjob, Löschfristen, Kundenlöschung
- shop/jobs.js: taeglicherJob (Käuferdaten bezahlter Bestellungen und Widerrufe 3 Jahre nach Ende des Bestelljahres anonymisieren, Grenze 1. Januar Berlin; unbezahlte/stornierte Bestellungen nach 30 Tagen löschen; Shops seit 90 Tagen disabled samt Artikeln, Bestellungen, Widerrufen, Bildern und shopConfig löschen, Funktionsdokument bleibt disabled mit Vermerk), seitenweise mit Deckel je Lauf, Schritte und Dokumente einzeln abgesichert; stuendlicherJob räumt nie beanspruchte Bilder auf
- shop/loeschen.js: gemeinsamer Baustein zum Löschen aller Daten eines Shops (Tagesjob und Löschfristen)
- shop/kundenLoeschung.js + customerAdmin.js: Kundenlöschung schaltet den Webshop ab (kunde_geloescht) und schließt die Shops
- retention.js: fällige Kunden verlieren auch ihre Shopdaten (vor den Abo-Dokumenten, bei Fehler held)
- server/index.js: Minuten- (75 s), Stunden- (130 s) und Tagesjob (200 s) registriert
- Tests: shopJobs.test.js, retention.test.js erweitert

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:15:20 +00:00
ClaudeandClaude Opus 5.5 73820fb1e6 Shop G5: Reiter Bestellungen (Liste mit Filter, Suche, Blättern, Detail, Aktionen, Erstatten mit Rückfrage, Widerrufe, CSV-Export) und Test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:15:19 +00:00
ClaudeandClaude Opus 5.5 cc668d3765 Shop G4: Reiter Artikel mit Liste (Hoch/Runter, Aktiv-Schalter, Löschen mit Rückfrage), Editor (Preis, Grundpreis, Varianten, Bilder mit Upload, Produktsicherheit, Lebensmittel) und Feldfehlern
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:14:05 +00:00
ClaudeandClaude Opus 5.5 648d0e07fe Shop G6: Reiter Einstellungen und Rechtliches (Felder, Versand, Zahlungsarten, Muster einsetzen, Entwurf, Bestätigung mit Hash)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:12:46 +00:00
ClaudeandClaude Opus 5.5 3ed96a5838 Shop G3: Reiter Übersicht mit Checkliste und Öffnen-Schalter, Stripe-Konto, Einbau-Code mit Vorschau und Kennzahlen
Die Platzhalter-Prüfungen in shopView.test.js hängen nicht mehr am Text „Kommt gleich …“.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:10:30 +00:00
ClaudeandClaude Opus 5.5 aa9ee35ab6 Öffnungszeiten: Fixrunde Website-Skript und Editor (JSON-LD, Kopf beim Scrollen, späte Platzhalter, Links, Bestätigung)
- JSON-LD wird ohne "<" geschrieben (<), ein </script> im Firmennamen bricht keinen Block mehr
- verschiebe(): ist die Hinweis-Leiste weggescrollt, bekommen feste Elemente ihren eigenen Inline-top zurück
  (Köpfe, die per Klasse beim Scrollen wegfahren, funktionieren wieder)
- Späte Platzhalter (Einzelseiten-Apps): entprellter MutationObserver auf body zeichnet neue oder wieder
  eingehängte Platzhalter nach
- sichererLink lehnt Zugangsdaten in der Adresse ab; die Adresse der Schnittstelle bestimmt nur das eigene
  Script-Tag, kein Platzhalter mehr (der siteKey am Platzhalter bleibt laut Spec 4.2)
- Editor: die Bestätigung klebt unter der Kopfleiste und bleibt nach dem Speichern eines unteren Abschnitts sichtbar

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:10:02 +00:00
ClaudeandClaude Opus 5.5 6d8487633b Shop E3: Händler-Aktionen (versendet, abholbereit, abgeschlossen, storniert), Erstattung unter Sperre, Bestellliste, Detail, CSV und Kundenrouten
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:08:55 +00:00
ClaudeandClaude Opus 5.5 eb616ddbe8 Shop H2: Standalone-Seiten GET /shop/:siteKey und /shop/:siteKey/widerruf mit neutraler Seite, Schutz-Headern und Route-Test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:07:48 +00:00
ClaudeandClaude Opus 5.5 583a2d33c2 Shop F2: POST /pruefen, POST /bestellung und GET /bestellung/:id (Herkunft, Honigtopf, Formular-Token, Grenzen, Stripe-Abgleich, maskierte E-Mail)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:06:29 +00:00
ClaudeandClaude Opus 5.5 c13bc94a0f Shop H1: Einbau-Widget public/embed/shop.js (Raster, Detail, Warenkorb, Kasse mit Server-Zusammenfassung, Rückkehr von Stripe, Widerrufsfunktion) und statische Tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:05:08 +00:00
ClaudeandClaude Opus 5.5 738afa2d0e Shop G2: Hülle #webshop mit Anfrage, Bestellseite, Statuskarte und fünf Reitern
Eigene Helfer in shop-dom.js (inkl. euroEingabe, datumText), Anfrageformular und -karten
in shop-anfrage.js, Reiter als lazy geladene Platzhalter-Module für G3 bis G6.
Die Bestellseite zeigt den Hinweis zu „Ändern“ nur noch, wenn es Schritte gibt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:05:01 +00:00
ClaudeandClaude Opus 5.5 7464a4ef55 Shop D3: Connect-Webhook (account.updated, Checkout-Abschluss/Verwerfen, charge.refunded nachtragen)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:04:47 +00:00
ClaudeandClaude Opus 5.5 ecf67d6978 Öffnungszeiten: Fixrunde Server (Schema-Kopplung, K27, Wertersatz, Widerruf, Ticket nachlegen)
- activate() schreibt die Zeiten-Felder (aboEndeAt, behaltenSessionId) nur noch über extra
  (nur Abo-Rückkehr und Team-Aktivierung der Zeiten); News und Terminplaner laufen auch vor zeiten-setup.mjs
- Abo-Abgleich: K27 gilt als offen, solange der Marker vor disabledAt liegt (kein Zurücksetzen nach disable() nötig);
  K27 geht erst nach dem Ersatz-Ticket raus (scheitert es, wiederholt der nächste Lauf beides);
  Löschung setzt auch activatedAt zurück
- Zustimmung "beginn" (bezahlt) mit Wertersatz-Satz (§ 357a Abs. 2 BGB) wie bei News und Terminplaner, Fassung 2026-10-05
- Widerruf: Bestellung "zeiten" in ORDERS, FEATURE_OF_ORDER, orderLabel und widerrufen.html
- Team "Ticket nachlegen" der Zeiten ohne Kauf geht über holeZeitenNach statt über den Zahlungsweg
- Einschalten-Route nimmt nur { zustimmung }, nicht die Altform { consent: true }
- Team-Aktivierung behält kauf/unlockedByPurchase bei einem bisher gekauften Dokument

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:04:05 +00:00
ClaudeandClaude Opus 5.5 987b184aa1 Shop F3: POST /:siteKey/widerruf (Herkunft, Honigtopf, Formular-Token, Mindestantwortzeit), Preflight für POST-Routen, Konfig formMinSekunden und minAntwortMs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:03:06 +00:00
ClaudeandClaude Opus 5.5 bf99f659eb Shop E2: Abschluss (schliesseAb), verwirf, Abgleich offener Bestellungen, Minutenjob
schliesseAb ist die einzige Stelle fuer den Status bezahlt: Sperre je Bestellung, Konto muss zum Shop passen,
idempotent, spaete Zahlung nach Ablauf wird angenommen, SEPA erst bei bezahlt, Bestandsabzug mit Ueberverkauft-Hinweis,
G09 an den Kaeufer und K25 an den Haendler (Mailfehler machen nichts rueckgaengig). pruefeOffene und jobs.js minutenJob
gleichen offene Bestellungen mit Stripe ab. fakeStripe: completeSession.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:01:04 +00:00
ClaudeandClaude Opus 5.5 9de23c7fe2 Webshop E5: Widerrufe (§ 356a BGB) erfassen, G12 sofort, K26 an Händler, Liste und Bearbeitet-Markierung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:00:29 +00:00
ClaudeandClaude Opus 5.5 809da80c9b Shop G1: Verdrahtung #webshop, Webshop-Kachel, Bestellseite mit key und optionaler Zusammenfassung
Dashboard-Abschnitt view-webshop, shop.css und Platzhalter shop.js, app.js (Hash, Refresher, Label, Elternansicht), Kachel in funktionen.js nach shop.verfuegbar, GET /api/funktionen liefert shop.verfuegbar, funktion-bestellen.js nimmt key/name/zusammenfassung (Standard: Terminplaner unverändert).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:57:02 +00:00
ClaudeandClaude Opus 5.5 0ebd2e6663 Webshop E4: Mails (G09-G14 an Käufer, K25/K26 an Händler), shop/mails.js, Mailversand der Warteschlange auch bei Terminplaner off
Käufermails laufen über die Warteschlange des Terminplaners (G09/G12 mit Vorrang),
G09 wird bei Überlänge um den AGB-Text gekürzt (Link statt Text). Der Mailteil des
Minutenjobs und die Aufräumschritte des Tagesjobs laufen auch bei TERMINE_ENABLED=off,
solange SHOP_ENABLED nicht off ist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:56:40 +00:00
ClaudeandClaude Opus 5.5 38bd56fe8b Shop D2: Kundenrouten Stripe-Konto (einrichten, Link, aktualisieren) mit Sperre gegen Doppelklick
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:56:04 +00:00
ClaudeandClaude Opus 5.5 98d1e6613d Shop F1: öffentliche Schnittstelle zum Lesen (Konfiguration mit Artikeln, Artikelbilder, Grenzen, Formular-Token)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:54:06 +00:00
ClaudeandClaude Opus 5.5 9722f0a938 Shop C7: Kundenrouten der Verwaltung (Status, Einstellungen, Artikel, Rechtstexte, Oeffnen, Bilder)
- routes/shop.js: gemeinsame Pruefung mitShop (Schalter, eigenes Projekt, Funktion setup|active, lesend auch disabled, shopConfig
  bei Bedarf anlegen); GET /status erweitert (Bereitschaft, geoeffnet, Snippet, Vorschau, Stripe ohne Konto-ID, Zahlen);
  Einstellungen, Artikel (mit Bild-Pruefung, Beanspruchen und Aufraeumen), Sortierung, Rechtstexte (Entwurf, Bestaetigen mit Hash),
  Oeffnen/Schliessen, Bild-Upload (octet-stream, 8 MB) und Vorschau; Schreib-Limit 300 je Stunde und Kunde
- services/shop/bilder.js: Artikelbilder nach dem Muster von newsImages.js (1600/480 px WebP, Sidecar, 24-h-Aufraeumen, 40 Uploads/h)
- Kundenansicht: sieben Verwaltungs-Pfade erlaubt und als shop_aenderung protokolliert; Anfrage, Oeffnen, Bestaetigen, Stripe bleiben gesperrt
- Tests: shopRoutes, shopBilder; Erwartungen von kundenansichtGuard, customerViewLog und shopAnfrage angepasst

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:52:53 +00:00
ClaudeandClaude Opus 5.5 9d19a2cbad Shop E1: Bestellung anlegen (Reservierung unter Sperre, Stripe-Kasse auf dem Händlerkonto, Rück-URL-Prüfung)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:48:56 +00:00
ClaudeandClaude Opus 5.5 da43ee2b41 Shop D1: Connect-Dienst (Konto, Onboarding, Checkout, Erstattung, Webhook-Prüfung) und Fake-Stripe für Connect
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:44:10 +00:00
ClaudeandClaude Opus 5.5 03a7f1d877 Webshop: amtliche Gewährleistungsmitteilung (VO (EU) 2025/1960 Anhang I) als Grafik und wörtlicher Text
Grafik aus dem Amtsblatt (ABl. L vom 2.10.2025, S. 5) ausgeschnitten; Text der Grafik wörtlich für Alternativtext
und Bestellbestätigung.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:43:43 +00:00
ClaudeandClaude Opus 5.5 0fb84021f8 Webshop B2: Team-Entscheidung über die Anfrage, Übersicht /api/team/shop und Kundenmail K24
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:43:38 +00:00
ClaudeandClaude Opus 5.5 608ff61d3d Shop C6: Kontext per Projekt und siteKey, Bereitschaft und Oeffnungs-Regel
Laden von shopConfig (Einstellungen, Rechtstexte als Entwurf/bestaetigt, Stripe-Status) mit sicherem Parsen,
30-s-Speicher je siteKey mit generation-Zaehler (active und disabled), bereitschaft() mit den fuenf Punkten,
istGeoeffnet() und oeffentlicheRechtstexte().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:43:37 +00:00
ClaudeandClaude Opus 5.5 1febdfaf53 Webshop C4: Warenkorb-Berechnung (preise.js) mit Tests
Reine Funktion berechne(): Positionen zusammenfassen, Bestand und Reservierung,
Lieferart und Land, Mindestbestellwert, Versandkosten, enthaltene USt je Satz
(Versand anteilig) und Kleinunternehmer-Hinweis.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:41:57 +00:00
ClaudeandClaude Opus 5.5 83605975b0 Webshop B1: Anfrage-Dienst, Kundenroute /api/shop und Team-Mail T21
Anfrage stellen, zurückziehen und Stand lesen (ensureShopConfig, normalisiereAnfrage,
stelleAnfrage, zieheZurueck, anfrageAnsicht), Router /api/shop mit Rate-Limit 20/h und
Sperre in der Kundenansicht, Vorlage T21 Neue Webshop-Anfrage mit Tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:40:24 +00:00
ClaudeandClaude Opus 5.5 0063c04d8a Shop C5: Muster-Rechtstexte (Widerrufsbelehrung, Formular, AGB), Versand/Zahlung, Gewaehrleistungshinweis, Normalisierung und Hash
Widerrufsbelehrung nach Anlage 1 (Stand 2026, mit Hinweis auf die Widerrufsfunktion), Formular nach Anlage 2,
Muster-AGB ohne OS-Plattform, erzeugter Versand-/Zahlungstext, Gewaehrleistungshinweis (Wortlaut zu pruefen),
Muster-Markdown fuer die Kanzlei und Tests inkl. Abgleich Markdown/Code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:40:23 +00:00
ClaudeandClaude Opus 5.5 bea9f3cfba Shop C3: Produkte normalisieren, Käufer-Sicht und Speichern (shopProducts)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:39:33 +00:00
ClaudeandClaude Opus 5.5 129f8de278 Shop C2: Einstellungen normalisieren, laden und speichern
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:35:37 +00:00
ClaudeandClaude Opus 5.5 0e74f1669c Webshop A3: Register-Eintrag shop, Haken, Rechtstext-Satz und Ticket-Details
Registereintrag config.features.shop (Bestelltexte mit Fassung config.shop.fassung, Rechtsunterlagen-Getter,
Mailtexte, Ticket), STANDARD_HAKEN.shop (Schalter, Bestellpruefung ueber shop/anfrage.js, Unterlagen, Belehrung),
legalDocs-Satz shop, shopTicketDetails sowie Rumpf von anfrageOffenePunkte. Tests angepasst und um Shop-Faelle erweitert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:35:28 +00:00
ClaudeandClaude Opus 5.5 c10e50d0b3 Öffnungszeiten: Livegang-Notiz
Reihenfolge nach Spec 13.6 mit allen LIVEGANG-Hinweisen, Probelauf, Rechtsrunde
und Checkliste der offenen Code-Punkte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:32:59 +00:00
ClaudeandClaude Opus 5.5 6782d99960 Shop C1: Geld- und Grundpreis-Funktionen (euro, parseEuro, grundpreisText)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:32:38 +00:00
ClaudeandClaude Opus 5.5 308268e901 Webshop: Recherche eingearbeitet (kein PayPal bei Connect, Gewährleistungshinweis)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:30:56 +00:00
ClaudeandClaude Opus 5.5 e9ab15bdb5 Webshop A1/A2: Konfiguration, Schalter, Katalog shopSetup, Schema und Setup-Skript
config.shop (SHOP_ENABLED, SHOP_PILOT_EMAILS, SHOP_UPLOAD_DIR, STRIPE_CONNECT_WEBHOOK_SECRET), config.shopSetup (299 € einmalig),
Collection-IDs shopConfig/shopProducts/shopOrders/shopWiderrufe, services/shop/schalter.js, Katalogschluessel shopSetup in
stripeBilling und stripe-products.mjs, services/shop/schema.js, scripts/shop-setup.mjs (--check), schemaKeys fuer die Test-Mocks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:30:21 +00:00
ClaudeandClaude Opus 5.5 b168b10485 Webshop: Design und Umsetzungsplan
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:26:40 +00:00
ClaudeandClaude Sonnet 5.5 90e8607e26 Öffnungszeiten: Rechtstext-Entwürfe und Einbau-Anleitung
Leistungsbeschreibung „Öffnungszeiten live“ als statische Seite unter
/vertragsunterlagen/zeiten-leistungsbeschreibung.html (Register-Adresse, nur HTML-Kommentar
ENTWURF), Entwürfe für Datenschutz § 6d mit Ergänzung § 6, den Block funktionen in K02/K03
und die Kanzlei-Fragen (⚖ AVV Funktionen, Behalten-Zustimmung), Absatz „Öffnungszeiten auf
dieser Website“ für Variante A und B, Einbau-Anleitung für das Team. Keine Codeänderung,
datenschutz.html und die Mailvorlagen bleiben unverändert. test/zeitenRecht.test.js.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 07:34:56 +00:00
ClaudeandClaude Opus 5.5 af63b69ec1 Merge origin/main in feature/terminplaner-tp2 (Analyse-Cockpit, Subdomain-Sperre, Projekte)
Konflikte: funktionen.js folgt der Aufteilung aus main (fn-util.js, analyse.js); der Link
'Zu Verträge' der Analyse-Seite kommt jetzt auch in analyse.js aus vertraege-link.js.
Checkout und Team-Ansicht übernehmen assertOwnProject(..., { forNewOrder: true }) und
includeArchived aus main. Test-Pins der absoluten Importe nachgezogen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 07:11:58 +00:00
ClaudeandClaude Opus 5.5 be007af7d0 K22: Abo-Ende nur bei beendetem Wartungs-Abo (nicht das Datum eines Hosting-Abos)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 07:04:52 +00:00
ClaudeandClaude Sonnet 5.5 bea07e73b1 TP2 Abschlussprüfung: Rechtstexte (AVV, 6c, News-LB-Archiv, Livegang-Checkliste)
- AVV Vorbemerkung (2): K16 der Textform-Aktivierung trägt nur Leistungsbeschreibung und AVV als PDF
- AVV Nr. 9 Abs. 6, Anlage (Löschung), LB Nr. 8 Abs. 3 und Datenschutz 6c: das Einrichtungs-Ticket bleibt mit einer Kurzfassung der Einstellungen als Arbeitsnachweis (keine Daten von Gästen)
- AVV Nr. 8 Abs. 2: "Gastdaten löschen" löscht nur wartende E-Mails; fehlgeschlagene behalten Adresse und Inhalt bis Nr. 9 Abs. 4
- Datenschutz § 7: Frist für Bestellungen ohne Abo (drei Jahre nach Ablauf des Jahres der Bestellung)
- News-Leistungsbeschreibung vom 30.09.2026 unverändert archiviert (SHA-256-Pin), die geltende Datei verweist darauf
- Kanzlei-Paket: Zitate, Q4/Q9/Q23, Livegang-Checkliste (Abschnitt 7.6/7.7), K23-Hinweis
- build-legal-html: Rückwärtsschrägstrich in Navigationsadressen abgelehnt; TP1-Spezifikation E2 (ohne USt)
- HTML, PDFs und Prüfsummen neu erzeugt (Terminplaner-Unterlagen und Datenschutzerklärung)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 06:54:55 +00:00
ClaudeandClaude Opus 5.5 6a9daa0583 Öffnungszeiten: Urlaub-Frage lässt offene Hinweis-Formulare stehen
„Ja, anlegen“ zeichnet nur noch die Hinweis-Liste neu (offene, ungespeicherte
Formulare und Zeilen bleiben), statt den ganzen Abschnitt zu ersetzen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 06:54:45 +00:00
ClaudeandClaude Opus 5.5 b3e96d4a56 Öffnungszeiten: Editor für Hinweis- und Kontakt-Leiste, Google und Vorschau
Hinweis-Leiste (Liste, „Hinweis-Leiste anlegen“, Art, Knopf, Zeitraum, Preis-Hinweis),
Kontakt-Leiste (Telefon, Adresse, WhatsApp mit Datenschutz-Haken, R12 in der Kundenansicht),
Google (Für Google kopieren, Profil-Link), Live-Vorschau mit Regler „Uhrzeit testen“
und UI-Prüfung test/ui/zeiten-portal.ui.mjs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 06:43:22 +00:00
ClaudeandClaude Sonnet 5.5 5f99f08a39 TP2 Abschlussprüfung: Fragebogen und Bestellseite (veralteter Tab, Abo-Hinweis, Zurück aus Stripe)
- PUT /fragebogen: ein Tab mit veralteter Branche bekommt 409 EINSTELLUNG_VERALTET (die Branche wechselt nur die Vorlage), bestaetigt wird mit
  dem gespeicherten Stand vereinigt; die ungeprüften Vorlagenpreise können nicht mehr als geprüft gelten
- GET /einstellungen liefert in off aboHinweis (aboMeldung, auch bei nicht lesbarem Abo); die Hinweiskarte zeigt ihn immer und hat „Neu laden“
- Bestellseite und Statuskarte laden bei pageshow (persisted) neu, wenn der Browser sie nach der Zahlungsseite aus dem bfcache holt
- renderBestellen zeichnet „Ändern“ und „Zum Schritt“ nur mit zumSchritt; nach einem 409 bekommt die Meldung den Fokus
- Zusammenfassung nennt „Kleinunternehmer: ja/nein“

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 06:35:42 +00:00
ClaudeandClaude Sonnet 5.5 2fc48df915 TP2 Abschlussprüfung: Löschung (IP/Browser auf portalFeatures, Erstattungsspur, K22-Datum)
- markOrdered speichert IP und Browser der Bestellung nicht mehr am Funktionsdokument (nur im Nachweis featureConsents, dort mit
  Löschfrist); Altwerte werden bei einer neuen Bestellung geleert
- Aufbewahrungslauf (retention): consentIp, consentUserAgent, consentText, consentAt werden an allen Funktionsdokumenten eines fälligen
  Kunden geleert, auch an abgeschalteten; Fehlschlag hält den Kunden wie die übrigen abhängigen Daten
- Einmal-Skript scripts/feature-consent-felder-leeren.mjs (Standard Probelauf, --apply leert nur, wo featureConsents die Angabe trägt)
- 30-Tage-Reset (jobs.js): auch eine bezahlte Einrichtung mit Grund abo_ende_vor_einrichtung und noch nicht gemeldetem T19 wird nicht
  zurückgesetzt, solange zahlungMeldung nicht gemeldet ist (die Spur der Erstattung bleibt)
- K22: enddatum ist das Abo-Ende (lage.endedAt), sonst der Tag des Abschaltens; loeschdatum bleibt 30 Tage nach dem Abschalten

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 06:26:33 +00:00
ClaudeandClaude Opus 5.5 242fbb01e5 Öffnungszeiten: Plan-löschen-Rückfrage zählt die Ausnahmen beim Öffnen
Der Hinweis „Die N Ausnahmen, die nur für diesen Plan gelten, werden mit gelöscht“
wurde beim Bauen der Karte berechnet und war nach dem Speichern einer Ausnahme
veraltet. Er wird jetzt beim Öffnen der Rückfrage aus dem aktuellen Stand gelesen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 06:23:56 +00:00
ClaudeandClaude Sonnet 5.5 b974b3a3ec TP2 Abschlussprüfung: Zahlung/Team (Wiederzustellung, K16-Warnung, Sperrschlüssel)
- adoptPaidSession übernimmt ein erneut zugestelltes checkout.session.completed nicht mehr, wenn das Dokument nach 30 Tagen zurückgesetzt wurde
  (off, purchaseStatus zurueckgesetzt, gleiche Sitzung): kein Setup, kein Ticket, keine K15/T16
- meldeFreigeschaltet liefert true/false; die Staff-Aktivierung in Textform vermerkt eine nicht gesendete K16 mit Unterlagen in adminNote und
  antwortet mit warnung: true
- disable() nutzt die Sperre feature-activate:<id> statt feature-disable:<id> (Freischaltung und Abschalten überholen sich nicht)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 06:18:34 +00:00
ClaudeandClaude Opus 5.5 f01ed15247 Öffnungszeiten: Speichern zeichnet nur neu, was sich geändert hat
Ungespeicherte Eingaben in anderen Formularen bleiben stehen: nach dem Speichern
wird nur die Plankarte, die Planliste (mit den Eingaben der übrigen Karten),
Feiertage oder Ausnahmen neu gezeichnet, „Nein, danke“ entfernt nur die Frage.
Während des Speicherns sind auch die Eingabefelder gesperrt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 06:18:21 +00:00
ClaudeandClaude Opus 5.5 04a3525f79 Öffnungszeiten: Editor für Pläne, Feiertage und Ausnahmen
Speichern über PUT /api/zeiten (Version, 409, Feldfehler per ID-Pfad), Zeitpläne mit
Wochentagen, Hauptplan wechseln und Löschen (R10, R33), Feiertage je Bundesland,
Betriebsurlaub und Sondertage mit Hinweis-Leisten-Frage. Die Hülle behält den Editor
bei einem neuen Stand und zeichnet nur den Kopf neu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 06:02:31 +00:00
ClaudeandClaude Sonnet 5.5 4dc447ed33 Kanzlei-Paket: Prüfbefunde (genauere Aussagen, Zitate zu Q4/Q8/Q21, Fundstellen)
Genauere Aussagen: Widerrufsfrist je Bestellung mit spätestem Fristende für
"rechtzeitig", Textform beim Wiedereinschalten nur ohne Bestellung, Storno
und kein_widerruf in G02 getrennt beschrieben, keine Ablehnung nach dem
Klick außer aus den Gründen der Leistungsbeschreibung Nr. 4 Abs. 3, Preis
nur mit echten Wortlauten.

Zitate: Q4 mit dem früheren News-Haken und der früheren Leistungsbeschreibung
(Stand 3a0b008, per SHA-256 gepinnt und mit Git abgeglichen), Q8 mit
Leistungsbeschreibung Nr. 9 Abs. 2 und 3, Q19 mit Nr. 5 Abs. 2 und der Preiszeile
von K15, Q21 mit dem gespeicherten Nachweis und der Chat-Vorlage des
Ticketsystems. Begriffe (Betriebsart, Pilot, Kulanz) erklärt, interne
Entscheidungsnummern entfernt, Anlage C (gerenderte Mails K15 und K16) angekündigt.

Tests: Zitate unter 25 Zeichen zählen nicht mehr als geprüft (benannte Liste,
Belehrung und Knopftexte werden streng geprüft), schärfere Fundstellen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 05:53:24 +00:00
ClaudeandClaude Opus 5.5 cee44d6852 Öffnungszeiten: Behalten nur ohne Abo anbieten (kein_abo)
Behalten (30 €) gab es bei jedem inklusive false. nicht_aktiv und gekuendigt heißen aber,
dass das Wartungs-Abo läuft (Server: 409 Abo läuft wieder). Jetzt gilt an allen fünf Stellen
(Kachel, Karte, Behalten-Seite, Weg aus der Kachel, Abgeschaltet-Karte) dasselbe Prädikat:
inklusive false UND grund kein_abo; sonst „Abo läuft wieder“ ohne Bezahlknopf.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 05:41:24 +00:00
ClaudeandClaude Sonnet 5.5 7050b63a37 Kanzlei-Paket Terminplaner: Fragen Q1 bis Q26
Abschnitt 4 mit den 26 Fragen (Frage, Fundstelle im Text, unsere Annahme,
Alternative). Jede Kennung der früheren Listen "Für die Rechtsprüfung offen"
(H1 bis C8) steht in der Fundstelle genau einer Frage oder in Abschnitt 6
mit Begründung; die Hinweise aus den Reviews (Pilot/Kulanz, Rückkehr nur
nach Abo-Ende, Erstattung bei Ablehnung, Löschung auf Weisung, vereinbarte
Fassung in K15, G02 bei kostenlosen Buchungen) sind in den passenden Fragen.

Tests: Fragen lückenlos Q1 bis Q26 mit vier Teilen, jede Frage verwiesen,
Q4 nennt News, § 356 Abs. 3 und Altkunden, Abdeckung der entfernten Listen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 05:31:57 +00:00
ClaudeandClaude Opus 5.5 7bb918390a Öffnungszeiten: Kachel und Bestell-/Einschaltweg im Portal
Kachel „Öffnungszeiten live“ inline in funktionen.js (keine neuen Importe, keine Zusatzabfrage),
Ansicht #zeiten (zeiten.js, zeiten-bestellen.js, zeiten.css) mit Einschalten im Wartungs-Abo,
Bestellen für 30 € (Widerrufsbelehrung vor dem Knopf, Ruling R32), Behalten nach dem Abo-Ende,
Statuskarte je Zustand, Rückkehr von Stripe und Platz für den Editor (Aufgabe 29).
GET /api/funktionen liefert dafür zeiten.belehrung (Haken belehrung der Funktion).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 05:30:23 +00:00
ClaudeandClaude Sonnet 5.5 6765b14feb Kanzlei-Paket Terminplaner: Aufbau, Texte, Ablauf, Fundstellen
Sieben Abschnitte nach D5: Zweck und Umfang, Produktablauf, Texte in
Endfassung (wörtliche Zitate mit Marker auf die Fragen), Fundstellen im Code,
Nicht Teil der Prüfung (mit Zuordnung der früheren offenen Punkte) und
Checkliste für den Inhaber. Abschnitt 4 (Fragen Q1 bis Q26) folgt.

Der Test prüft Aufbau, jedes Zitat gegen Programm und Dokumente, die
Fundstellen, die Zahlen gegen den Code und die Konsistenz der Abo-Ende-Texte.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 05:24:11 +00:00
ClaudeandClaude Opus 5.5 f20f056c60 Öffnungszeiten: Helfer für den Editor
Reine Client-Helfer (public/zeiten-view.js): Grenzen, neue IDs, Kurznamen, Fensterregeln und
Wochenfehler wie auf dem Server, Hauptplan wechseln, Löschgründe, Hinweis-Vorschlag zum
Betriebsurlaub, Preis-Hinweis, Link-Prüfung. Die Tests halten jede Regel gegen
server/services/zeiten/pruefen.js (zufällige Eingaben, gleiche Ergebnisse).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 05:05:01 +00:00
ClaudeandClaude Sonnet 5.5 ec320853ce Gaestetexte/Datenschutz: Storno-Regel im Archiv, PDF-Pruefsumme, PDF-Kopfpruefung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 05:03:54 +00:00
ClaudeandClaude Sonnet 5.5 9a2a70b491 Retention: abhaengige Daten vor dem Abo loeschen, Fehlschlag haelt den Kunden fuer den naechsten Lauf
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 05:03:54 +00:00
ClaudeandClaude Opus 5.5 811e054ca4 Öffnungszeiten: Ticket nachlegen lehnt eine Behalten-Zahlung ab (Stillstand an feature-admin)
bezahlteSitzung weist Sitzungen mit metadata.zweck behalten mit 409 ab. Sonst ginge die Sitzung über onFeaturePaid in bucheBehalten, das
feature-admin:<projekt> nimmt, während die Team-Route sie schon hält: Stillstand und dauerhaft belegter Schlüssel. Test in
test/zeitenTeamBehalten.test.js: 409 binnen Sekunden, Sperre frei, folgende Team-Aktion nicht blockiert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 04:58:25 +00:00
ClaudeandClaude Sonnet 5.5 bade4edb74 Terminplaner: Staff-Aktivierung nur mit Zustimmung in Textform (Modus on), Portal
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 04:44:44 +00:00
ClaudeandClaude Sonnet 5.5 019833d385 Öffnungszeiten: Tests und Belehrung nach dem Merge mit TP2
Zeiten nutzen die echte Widerrufsbelehrung wie der Terminplaner (der Platzhalter BELEHRUNG_PLATZHALTER ist
mit TP2 F7 entfallen). Gepinnte Listen als Vereinigung beider Seiten: K15-Blöcke, T16-Blöcke, legal-Schlüssel der
Projektliste, hindernis in der Team-Ansicht für alle Funktionen. Neue Tests: Abnahme (Freischalten) der Zeiten über den
Haken freischaltbar und mit Ersatz-Hash, abnahme gibt für Zeiten nie ab.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 04:43:20 +00:00
ClaudeandClaude Opus 5.5 1d5a565e9f Merge feature/terminplaner-tp2 (a629b25) in feature/oeffnungszeiten
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 04:42:31 +00:00
ClaudeandClaude Sonnet 5.5 5b5b4b90eb Retention: Zustimmungsnachweise der Funktionsbestellung drei Jahre nach Abo-Ende loeschen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 04:38:00 +00:00
ClaudeandClaude Sonnet 5.5 a629b2519b Datenschutz: Abschnitt 6c Terminplaner, Kundenansicht und Nachweise bei Funktionsbestellung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 04:34:16 +00:00
ClaudeandClaude Sonnet 5.5 6ec8885bfd Terminplaner: Hinweistexte nach Recherche, Storno nur bei Preis
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 04:29:32 +00:00
ClaudeandClaude Opus 5.5 ed99234ae4 Öffnungszeiten: Hinweis-Leiste, Kontakt-Leiste und JSON-LD im Website-Skript
zeiten.js zeichnet mit denselben Daten wie die Platzhalter eine Hinweis-Leiste
(Shadow DOM, erstes Kind von body, rückt feste und klebende Köpfe, scroll-padding-top),
eine Kontakt-Leiste nur auf schmalen Schirmen (Abstandhalter, scroll-padding-bottom,
entfällt bei eigener Anruf-Leiste) und ergänzt das JSON-LD der Seite (nur die beiden
Zeiten-Felder, sonst eigener Block). Alle Adressen aus der Schnittstelle werden im
Browser erneut geprüft. Ruling R29: tick mit try/finally, sofort werfender fetch,
erst zeichnen dann laden beim Zurückkehren in den Tab.

Tests: statische Funktionstests der reinen Funktionen und eine Playwright-Prüfung
gegen den UI-Testserver (echte öffentliche Schnittstelle, Nachbau von Appwrite).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 04:27:04 +00:00
ClaudeandClaude Sonnet 5.5 722c686690 Gästetexte: Hinweis-Dokument ohne Entwurf-Wort, Archiv-Schutz
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 04:21:17 +00:00
ClaudeandClaude Sonnet 5.5 495d5ad9ca Gästetexte des Terminplaners: Entwurf-Kennzeichnung entfernt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 04:08:46 +00:00
ClaudeandClaude Sonnet 5.5 c1266fcce7 Gästetexte des Terminplaners: Fassung 2026-10-05, Erinnerungs- und Abo-Ende-Satz, Archiv je Fassung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 04:01:49 +00:00
ClaudeandClaude Opus 5.5 a6a4eae294 Öffnungszeiten: Website-Skript mit Status und Tabelle
public/embed/zeiten.js ruft die öffentliche Zeiten-API einmal je Seite ab und füllt
die Platzhalter [data-webklar-zeiten] (tabelle, status, badge; data-plan, Hauptplan
ohne Angabe). Reine Funktionen (waehleWechsel, naechsterWechsel, waehleHinweis,
jetztMitServer, tabelleDaten) stehen zwischen den Marken @rein-anfang/@rein-ende.
Timer auf den nächsten Wechsel (jede Wartezeit auf 10 Minuten begrenzt), Neuladen
höchstens alle 10 Minuten, Daten älter als 24 Stunden, Fehler oder aktiv:false
lassen das Ersatz-HTML stehen bzw. stellen es wieder her. Testseite zeiten-demo.html.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 04:01:31 +00:00
ClaudeandClaude Sonnet 5.5 1a91a331b4 Gaestetexte: Archiv-Skript und eingefrorene Fassung 2026-10-02
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 03:54:15 +00:00
ClaudeandClaude Sonnet 5.5 01bf05cc35 Widerruf: Terminplaner als eigene Auswahl, Frist und Mails je Funktion
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 03:50:26 +00:00
ClaudeandClaude Opus 5.5 a2d11ad6c4 Runner: Zeiten-Skript beim Veröffentlichen einsetzen
injectIntoHtml nimmt eine Marke je Tag (geprüft gegen data-wk-[a-z]+),
injectZeiten setzt den data-wk-zeiten-Tag ohne Datenschutzseite ein,
validateJob lässt nur den siteKey durch, ZEITEN_PUBLIC_BASE in der Konfiguration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:46:06 +00:00
ClaudeandClaude Opus 5.5 34384c7a39 AVV Terminplaner: Kleinschreibung nach Semikolon (TOM Trennung)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:43:39 +00:00
ClaudeandClaude Sonnet 5.5 90098726b9 Rechtstexte und K15: Prüfbefunde F5–F8 (AVV genauer, PDF-Prüfsumme, Unterlagen der vereinbarten Fassung)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 03:42:27 +00:00
ClaudeandClaude Opus 5.5 a198c472bd Öffnungszeiten: Feiertags-Erinnerung und Jobs
erinnereFeiertage(now): stündlich, nur bei Schalter pilot/on (pilot: nur
freigegebene Kunden) und nur 08:00 bis 20:00 Berliner Zeit; je Funktion in
setup/active zuerst der Merker sha1(featureId|datum) je Tag (409 = schon
erinnert), danach eine Mail K25 mit allen neuen Tagen ohne BCC ans Team;
Merker älter als 30 Tage werden gelöscht. zeitenAboJob (täglich) und
zeitenErinnerungsJob (stündlich) mit eigener Überlappsperre, ohne Wurf;
in server/index.js nach 240 s bzw. 300 s gestartet, mit unref.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:40:07 +00:00
ClaudeandClaude Opus 5.5 9a14e874c0 Öffnungszeiten: Daten bei Kundenlöschung entfernen
Kunde löschen schaltet die Funktion zeiten wie den Terminplaner ab (Grund
kunde_geloescht) und löscht Zeiten, Erinnerungsmerker und Antwortspeicher
sofort; Fehler halten die Kundenlöschung nicht auf. Die Löschfristen löschen
die Zeiten vor dem Abschalten und zählen sie im Bericht.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:30:00 +00:00
ClaudeandClaude Sonnet 5.5 2c059cb3d6 Funktionen: Vertragsunterlagen als K15-Anhang, Belehrung vor dem Bestellknopf
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 03:20:56 +00:00
ClaudeandClaude Opus 5.5 efc6dcf332 Öffnungszeiten: Abo-Abgleich mit 30 Tagen Frist
gleicheZeitenAboAb(now) koppelt die im Wartungs-Abo enthaltenen Zeiten
(unlockedBy abo) an das Abo (Spec 5, Z11): Abo-Ende erkennen (aboEndeAt, K26),
nach 30 Berliner Kalendertagen abschalten (abo_ende, K27, Ersatz-Ticket),
bei Rückkehr des Abos wieder einschalten (Neubau nach dem Abschalten, K24
wieder), 30 Tage nach dem Abschalten Daten löschen und state off.
Lesefehler beim Abo: Kunde überspringen, nie abschalten oder löschen.

R25: activate() leert aboEndeAt und behaltenSessionId; starteBehalten lehnt
bei laufendem Wartungs-Abo mit 409 ab (Lesefehler 503).
R17: k24Mail mit einbau null und wieder schaltet beide Einbau-Blöcke ab,
K24 bekommt dafür den Block weiter um den Kasten "Wie geht es weiter?".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:16:56 +00:00
ClaudeandClaude Sonnet 5.5 03bdeb4461 Bestelltexte des Terminplaners (vier Zustimmungen, Belehrung im Nachweis, Fassung)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 03:10:29 +00:00
ClaudeandClaude Sonnet 5.5 49acd13f84 Rechtstexte: HTML-Generator und PDFs des Terminplaners
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 03:03:40 +00:00
ClaudeandClaude Sonnet 5.5 24cb549e78 Rechtstexte: AVV Terminplaner
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 02:57:05 +00:00
ClaudeandClaude Opus 5.5 53253f0451 Öffnungszeiten: nach Abo-Ende für 30 € behalten
Neuer Modus neben Bestellen und Einschalten (Spec 3.3): POST /api/funktionen/zeiten/behalten
startet einen Checkout mit zweck 'behalten', ohne den Zustand zu ändern; der Webhook-Zweig
bucht vor markPaid/adoptPaidSession unter feature-admin zum Kauf (Frist weg, stripeSessionId =
Sitzung), schaltet eine abgeschaltete Funktion wieder ein (Neubau nach der Sperre), ohne neues
Ticket, mit K15 im Behalten-Wortlaut. Passt die Sitzung nicht, geht T19 nach der Sperre raus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:54:39 +00:00
ClaudeandClaude Opus 5.5 97e6971276 Öffnungszeiten: Team-Aktionen und Ersatz-Ticket beim Abschalten
Team-Route für zeiten: Aktivieren läuft über den Gratis-Pfad (einschaltenZeiten, setup,
unlockedBy team, Neubau, Einbau-Ticket, keine K24), Abschalten setzt disabled mit Grund team
ohne Löschfrist und legt über nachAbschaltung bei geändertem Wochenplan das Ticket
"Ersatz-Tabelle aktualisieren" an. PATCH /:customerId/zeiten/team speichert die
Team-Einstellungen mit teamVersion. Die Team-Ansicht liefert für zeiten zusätzlich plaene,
team, teamVersion und hindernis.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:39:43 +00:00
ClaudeandClaude Sonnet 5.5 6c474c46c3 Rechtstexte: Leistungsbeschreibung Terminplaner
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 02:39:28 +00:00
ClaudeandClaude Sonnet 5.5 6b4cbb3204 Rechtstexte: Widerrufsbelehrung der Funktionen aus dem Gesetzesmuster
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 02:35:04 +00:00
ClaudeandClaude Sonnet 5.5 3073fffcf5 News: Zitat § 356 Abs. 5 Nr. 2, Wertersatz-Satz und § 19 UStG statt brutto
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 02:30:38 +00:00
ClaudeandClaude Opus 5.5 11dc4b2c9f Öffnungszeiten: Freischalten erst mit eingetragenen Zeiten
Der 5-Minuten-Job setzt Öffnungszeiten live nach dem Schließen des Tickets
nur noch auf active, wenn der Kunde Zeiten eingetragen hat (Haken
freischaltbar: Hauptplan gespeichert, mindestens ein Tag nicht "zu";
Lesefehler ergeben false, das Dokument bleibt in setup). Neuer Register-Haken
nachFreischaltung: runActivation ruft ihn je freigeschaltetem Dokument vor der
K16 auf, Fehler werden nur geloggt; für die Zeiten merkt er den Ersatz-Hash
(wochenplanHash). Selbstheilung (Spec 3.2): im Lauf des Jobs (nicht in der
Projektliste mit customerId) holt holeZeitenNach für Zeiten in setup fehlendes
Ticket und K24 nach, je Dokument abgesichert und ohne Kundendaten im Log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:28:52 +00:00
ClaudeandClaude Sonnet 5.5 b20d0464c4 Rechtstexte: Dokument-Registry und Fassungskonstante des Terminplaners
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 02:24:42 +00:00
ClaudeandClaude Opus 5.5 47ae572fff Öffnungszeiten: Bezahlen gesperrt, wenn im Abo enthalten
Checkout: neue Stufe 7b pruefeInklusive (Funktionen mit aboInklusive):
laeuft das Wartungs-Abo (auch nicht aktiv), 409 inklusive mit Hinweis aufs
Einschalten; kein Abo, Hosting und gekuendigt zahlen weiter 30 Euro.

Zahlung (markPaid): setup mit unlockedBy kauf, dann Neubau (stosseNeubauAn,
einbau am Dokument), erst danach das Einbau-Ticket mit dem Ergebnis; die
Webhook-Wiederholung baut nicht noch einmal. Spaete Zahlungen aendern ein
vorhandenes unlockedBy abo/team nicht. News und Terminplaner unveraendert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:18:30 +00:00
ClaudeandClaude Opus 5.5 66793d06d5 Öffnungszeiten: Einschalten im Wartungs-Abo
POST /api/funktionen/zeiten/einschalten (Gratisweg, Spec 3.2): Abo frisch
prüfen, Zustimmung (consentFrei) und Nachweis ohne Stripe-Sitzung, Zustand
setup mit unlockedBy abo, dann Neubau, Einbau-Ticket und K24; ein zweiter
Klick holt Fehlendes nach (holeZeitenNach, eigene Sperre). GET /api/funktionen
liefert das Angebot (zeiten), features.zeiten mit unlockedBy, aboEndeAt, einbau,
loeschungAm sowie zeitenStatus und zeitenLeer je Projekt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:03:49 +00:00
ClaudeandClaude Opus 5.5 8d670ca5dc Öffnungszeiten: Einbau-Ticket und Ersatz-HTML für das Team
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 01:48:20 +00:00
ClaudeandClaude Opus 5.5 302b58cf72 Öffnungszeiten: Neubau anstoßen und Feld im Runner-Auftrag
buildZeiten (einbau.js) liefert dem Runner-Auftrag { siteKey } bei Funktion in setup/active,
null bei Sperrliste und Fehlern. stosseNeubauAn (neubau.js) stößt den Neubau unter der
Sperre zeiten-neubau an und meldet angestossen oder manuell.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 01:41:10 +00:00
ClaudeandClaude Opus 5.5 0042d8dd8f Öffnungszeiten: Mails K24 bis K27
Neue Kundenmails (Du-Form, Skelett von K23):
- K24 eingeschaltet (Blöcke angestossen/manuell nach einbau, wieder nach der Abo-Rückkehr)
- K25 Feiertage prüfen (Blöcke pruefen/mehrere, ohne BCC)
- K26 Abo beendet (abo_ende/hosting/wechsel_widerrufen, Frist, einmalig 30 € behalten)
- K27 abgeschaltet (feste Zeiten, Eingaben bis zum Löschdatum, Wiedereinschalten)

server/services/zeiten/mails.js: Builder k24Mail bis k27Mail und sendeZeitenMail
(wirft nie, ohne Adresse { sent:false, reason:'keine_adresse' }, Log ohne Adresse und Namen).

K15 bekommt den Block behalten (Gegenstück neu für den Einrichtungstext; bestellMail
setzt neu = nicht behalten, Standard unverändert), T16 die Blöcke abo und team
(sendT16 führt sie als Vorgabe aus). Pins in mailTemplates- und featureMails-Tests
angepasst; K26 darf „Wartungs-Abo“ im Betreff führen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 01:33:04 +00:00
ClaudeandClaude Opus 5.5 d1f9176c99 Öffnungszeiten: öffentliche Route
GET /api/public/zeiten/v1/:siteKey mit CORS *, OPTIONS 204, 405, weichem ETag
ohne stand, Cache-Control max-age=60, Expose-Headers Date, Grenze 120 je Minute
und IP (zeiten/limits.js), 503 ohne Details und 404 für unbekannte Pfade.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 01:19:43 +00:00
ClaudeandClaude Sonnet 5.5 938b7099dd Funktionen: Team-Route fuer die Terminplaner-Einbauanleitung
GET /api/team/funktionen/:customerId/termine/anleitung?projectId= liefert die volle
Einbauanleitung (ohne kurz) fuer das WOMS-Panel: stand, warnungen mit Drift-Warnung
gegen die Version im Ticket, abschnitte, text, state, ticketWoid, ticketVersion und
hindernis (nur in setup). Nur lesend, in jedem Zustand und Modus, ohne customerViewLog.
Die Teamansicht GET /:customerId traegt hindernis je Terminplaner in setup, mit einem
Abo-Lauf je Anfrage und ohne portalFeatures erneut zu lesen. Die Einbau-Doku nennt die
Anleitung je Kunde im WOMS-Panel und die Kurzfassung im Ticket.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 01:18:30 +00:00
ClaudeandClaude Opus 5.5 d4c1be91c7 Öffnungszeiten: wechsel der öffentlichen Antwort trägt wieder den Berliner Tag
Das Website-Skript rechnet keine Zeitzonen und braucht tag (Berliner Kalendertag
bei ab) für „heute“ und die nächsten 14 Tage; wechsel-Einträge sind wieder
{ ab, tag, offen, text, kurz }.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 01:15:42 +00:00
ClaudeandClaude Opus 5.5 1c9f773e97 Öffnungszeiten: öffentliche Antwort mit Cache
oeffentlich.js baut die Antwort für das Website-Skript (Pläne mit Woche, 60 Tagen
und Statuswechseln, Hinweise, Kontakt-Leiste, JSON-LD, Firma, Team) und merkt sie
30 Sekunden je siteKey. Ungültige Schlüssel und ausgeschalteter Schalter lösen
weder eine Abfrage noch einen Cache-Eintrag aus; die Funktion antwortet schon in
setup. findActiveBySiteKey nimmt dafür { states } entgegen (Vorgabe wie bisher
nur active).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 01:12:16 +00:00
ClaudeandClaude Sonnet 5.5 f830698dc3 Funktionen: Freischalten nach Abnahme und Ticket nachlegen (Team)
Neue Team-Aktionen abnahme (Freischalten ohne Ticket-Schliessung, K16
einmal, unter feature-activate:<id> mit Haken freischaltHindernis) und
ticket (Ticket nachlegen: Zahlung bei Stripe pruefen und Webhook-Weg,
geloeschtes Ticket ersetzen, vorhandenes unberuehrt). Ticketanlage aus
markPaid in legeTicketAn herausgeloest.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 01:08:03 +00:00
ClaudeandClaude Opus 5.5 7bf2ad2665 Öffnungszeiten: Kundenschnittstelle zum Bearbeiten
GET/PUT /api/zeiten/:projectId und GET .../vorschau für den Editor im Portal.
Zugang nach Funktionszustand (ordered/setup/active lesen und speichern,
disabled mit Daten nur lesen), Version 0 vor dem ersten Speichern, 409
ZEITEN_VERALTET, 60 Speichervorgänge je Kunde und Stunde. Die Kundenansicht darf
speichern (Protokoll zeiten_gespeichert), den WhatsApp-Haken setzt nur der
Kunde selbst: bestaetigtAt kommt vom Server (R12), in der Kundenansicht 403.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 01:01:24 +00:00
ClaudeandClaude Sonnet 5.5 8fe14c2fe6 Funktionen: Freischalt-Hindernisse und Freigabe-Mail ausgelagert
freischaltHindernis (Abo zuerst, dann Einstellung; Abo-Lesefehler = abo_unbekannt)
und meldeFreigeschaltet (K16, Fehler geloggt, mit attachments) in
termine/freischaltung.js. Der Standardhaken freischaltbar fuer termine
delegiert an freischaltHindernis, runActivation an meldeFreigeschaltet.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 00:56:35 +00:00
ClaudeandClaude Opus 5.5 097036c9a5 Öffnungszeiten: Speicher mit Versionen und Cache
LRU-Speicher mit Ablaufzeit (lru.js), Cache-Instanz der öffentlichen Antwort
(cache.js: 500 Einträge, 30 s) und Appwrite-Speicher mit getrennten Versionen
für Kundendaten und Team-Einstellungen (speicher.js), alle Schreibzugriffe
unter der Sperre zeiten:<projectId>.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 00:50:41 +00:00
ClaudeandClaude Sonnet 5.5 2db3eca357 Terminplaner: Kurzfassung der Einbauanleitung fuer das Einrichtungs-Ticket, T16-Gesundheitsblock
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 00:45:56 +00:00
ClaudeandClaude Opus 5.5 88fb9263f9 Öffnungszeiten: Prüfregeln für Zeiten und Team-Einstellungen
normalisiereZeiten/normalisiereTeam prüfen und normalisieren alles, was Kunde und
Team speichern: Fensterregeln, Ausnahmen, Hinweise, Kontakt, Telefon, Links, Größe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 00:35:10 +00:00
ClaudeandClaude Sonnet 5.5 90692b646b Terminplaner: Einbauanleitung als reiner Generator (Einbau bis Abrechnung, Warnungen)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 00:22:04 +00:00
ClaudeandClaude Opus 5.5 5f52ef54e8 Öffnungszeiten: Ersatz-Tabelle und Wochenplan-Hash
ersatzHtml liefert die schlichte Wochentabelle eines Plans als reines ASCII
(Entities, maskierter Planname), wochenplanHash den 16-stelligen sha1 der
Tage des Hauptplans. Dafuer wird modusUndFenster aus der Zeitlogik exportiert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 00:19:22 +00:00
ClaudeandClaude Opus 5.5 61c2f72631 Öffnungszeiten: Hinweise, JSON-LD, Google-Text und Erinnerungstage
Reine Funktionen in zeitlogik.js: hinweisZeitraum/gueltigeHinweise (Berliner Kalendertage,
Rang, Beginn in 8 Tagen), jsonLdFuer (Mitternachtsfenster, rund 00:00–23:59, Sonderzeiten
der nächsten 60 Tage, neuerBlock mit @context), googleText, erinnerungsTage und
imErinnerungsfenster (08:00–20:00 Berlin).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 00:12:20 +00:00
ClaudeandClaude Sonnet 5.5 13bbadc729 Terminplaner: Einbauanleitung als reiner Generator (Modell bis Benachrichtigungen)
einbauanleitung({ feature, project, einstellung, fragebogen, version, now, updatedAt, kunde,
entwurf }) setzt die ersten sechs Abschnitte der Team-Anleitung zusammen (modell, leistungen,
ressourcen, regeln, gastfelder, benachrichtigungen, feste Reihenfolge, nicht kopierbar) plus
stand { version, updatedAt } und den Volltext. Rein: kein I/O, config nur beim Aufruf
(Portal-Adresse, Haltezeit); gleiche Eingabe und gleiches now ergeben gleiche Ausgabe, die
Eingabe bleibt unveraendert.

Feldweissliste statt Objektkopie: nie feedToken, keine Betriebs-E-Mail, keine
Leistungsbeschreibung, keine unbekannten Zusatzfelder. Kundentexte stehen unmaskiert als Text,
einzeilige Werte werden auf eine Zeile gebracht (keine vorgetaeuschte Ueberschrift).
Abwesenheiten und Sonderoeffnungen nur ab heute (Berliner Kalendertag), Listen ueber 20
Eintraege enden mit "und N weitere". Hilfen wochenplanText und kuerzeListe sind exportiert.

Warnungen und Kurzfassung bleiben leer (kommen mit D3b und D4); ohne Einstellung entstehen
keine Abschnitte.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 00:07:38 +00:00
ClaudeandClaude Opus 5.5 59b0ea799d Öffnungszeiten: Live-Status, Texte und Wechselzeitpunkte
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:59:20 +00:00
ClaudeandClaude Sonnet 5.5 ee454017b0 Terminplaner: Einbau-Snippet mit Knopf und Farbvorschlag nach WCAG verschoben
einbauSnippet und esc wandern aus routes/termine.js nach services/termine/einbauanleitung.js
(Parameter statt ctx/config). Neu: Modus knopf mit data-knopf (nur bei gesetztem Text, hoechstens
40 Zeichen, Zeilenumbrueche zu Leerzeichen, maskiert; unbekannter Modus gilt als inline), WCAG-
Helfer luminanz/kontrast/textFarbeAuf 1:1 aus dem Widget und cssVorschlag(farbe) mit Warnung,
wenn der Akzent gegen Weiss unter 3 : 1 liegt. VORGABE_FARBE wird exportiert. Die Kundenansicht
bleibt unveraendert (Modus inline). Farbparitaet Widget/Server ueber alle 4096 Farben im
17er-Raster getestet.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 23:55:42 +00:00
ClaudeandClaude Sonnet 5.5 06ebcd1b0a Terminplaner: Datenschutz-Absatz als Rechtstext-Baustein
datenschutzAbsatz(einstellung, anrede) liefert den Absatz "Online-Terminbuchung" fuer die
Datenschutzerklaerung der Betriebs-Website als reinen Text (Sie- und Du-Fassung, fuenf bedingte
Saetze: Telefon-Feld, Nachricht/Zusatzfelder, Erinnerung, Widerruf ueber zeigtWiderruf, Telefon des
Betriebs). RECHTSTEXTE_ENTWURF = true. Test pinnt den Wortlaut gegen die Markdown-Quelle in allen
32 Bedingungskombinationen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 23:47:58 +00:00
ClaudeandClaude Opus 5.5 0f25f7cb52 Öffnungszeiten: Tagesbild und Öffnungsintervalle
Reine Zeitlogik in Europe/Berlin: tagesbild (Sondertag > Urlaub > Feiertag > Wochenplan,
planbezogen vor global, später angelegt gewinnt), oeffnungsIntervalle (zusammengefügt,
Fenster über Mitternacht gehören zum Starttag), zuUtcOderDanach (März-Lücke auf 03:00).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:44:28 +00:00
ClaudeandClaude Opus 5.5 2636ce60be Feiertage: Namen und Prüftage im Zeitraum
Neue Exporte feiertagName und feiertageImZeitraum (feiertagejs getHolidays je Jahr,
Namen per translate('de')). Heiligabend, Silvester, Mariä Himmelfahrt in BY und
Fronleichnam in SN/TH (Datum aus RP) sind 'pruefen'; istFeiertag bleibt unverändert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:36:44 +00:00
ClaudeandClaude Sonnet 5.5 1ac142b92f Terminplaner TP2: UI-Testserver Entwurf und Playwright Fragebogen bis Bestellklick
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 23:34:38 +00:00
ClaudeandClaude Opus 5.5 db9f7df747 Öffnungszeiten: Register-Eintrag und Stripe-Katalog
Funktion zeiten in config.features (30 €, aboInklusive, consentFrei, teilweiseLive),
freiweg() und Standard-Haken in featureRegistry.js, Stripe-Katalog zeitenSetup
(Metadaten zeiten-einrichtung) in stripeBilling.js und scripts/stripe-products.mjs,
vorläufige zeitenTicketDetails; Schlüssellisten-Pins um zeiten ergänzt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:31:36 +00:00
ClaudeandClaude Opus 5.5 2314f7d940 Öffnungszeiten: Index idx_feature für die Erinnerungsmerker
Task 9 löscht die Merker einer Funktion per featureId; Appwrite verlangt dafür einen Index.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:26:30 +00:00
ClaudeandClaude Opus 5.5 4b4cd1058f Öffnungszeiten: Schalter, Collections und Felder
ZEITEN_ENABLED/ZEITEN_PILOT_EMAILS/ZEITEN_KEIN_NEUBAU als config.zeiten (neben config.termine),
zeitenSetup (30 € einmalig), Collection-IDs oeffnungszeiten und zeitenErinnerungen, Schema der beiden
Collections plus fünf neue portalFeatures-Felder, scripts/zeiten-setup.mjs (idempotent, --check),
schemaKeys kennt die neuen Collections und Felder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:25:37 +00:00
ClaudeandClaude Opus 5.5 385f768d76 Öffnungszeiten live: Umsetzungsplan (37 Tasks in Blöcken A bis M)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:19:34 +00:00
ClaudeandClaude Sonnet 5.5 87d267e51a Terminplaner TP2: Ansicht nach Zustand und Rueckkehr von Stripe
Die Ansicht #termine folgt dem Zustand aus GET /api/termine/einstellungen:
off mit laufendem Abo zeigt den Fragebogen samt Bestellseite, ohne Abo den
Hinweis des Servers (termine.hinweis, R111) und bei kein_abo den Link zu
Vertraege aus einer gemeinsamen Quelle (vertraege-link.js); ordered und setup
zeigen die Statuskarte und die Reiter Leistungen, Team & Plaetze und
Einstellungen nur zum Lesen (ohne Reiter Termine); active und disabled
bleiben wie bisher. Rueckkehr von Stripe: #termine?bestellt=1&projekt=<id>
und #termine?projekt=<id> waehlen das Projekt, "Danke!" nur mit bestellt=1,
die Adresse wird einmal gelesen und bereinigt.

R110: GET /einstellungen liefert bestellung und offen auch in ordered (ohne
Abo-Punkt, abo bleibt null); scheitert "Zahlung fortsetzen", zeichnet die
Huelle Liste und Daten neu (bei 409 zustimmung_fehlt mit dem
Bestellformular, sonst nach dem neuen Zustand, die Meldung bleibt am Knopf,
solange die Bestellung offen ist).

Dazu: "null" als Text im lesbaren Reiter Leistungen entfernt, Export und
Status der Einstellungen erst ab der Freischaltung.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 23:10:54 +00:00
ClaudeandClaude Opus 5.5 360d218d4e Öffnungszeiten live: Design (Entscheidungen Z1–Z23 des Inhabers, Prüfung gegen den Code eingearbeitet)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 22:52:38 +00:00
ClaudeandClaude Sonnet 5.5 52456dac76 Terminplaner TP2 C10a: Nachbesserung 1 (Abo-Hinweis vom Server, R111)
GET /api/funktionen liefert bei termine.verfuegbar ein fuenftes Feld hinweis
(aboMeldung mit dem Register des Terminplaners, null bei bestellbar true).
Die Kachel zeigt diesen Text maskiert an; die Browser-Kopie der Abo-Texte und
der Gleichheitstest entfallen. Der Link "Zu Verträge" ist eine gemeinsame
Konstante fuer Analyse und Terminplaner und erscheint bei kein_abo.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 22:43:17 +00:00
ClaudeandClaude Sonnet 5.5 6ae0f67457 Terminplaner TP2: Kachel mit Bestellweg, Abo-Felder in der Funktionen-Liste
GET /api/funktionen liefert bei termine.verfuegbar: true zusaetzlich bestellbar
(true | false | null bei Lesefehler im Abo), grund (kein_abo | gekuendigt |
nicht_aktiv | null), preis und einrichtung aus dem Register; nicht verfuegbar
bleibt exakt { verfuegbar: false }. Die Kachel zeigt je Zustand Pille und Knopf:
Aus mit Abo "Terminplaner einrichten · 149 €" bzw. "Fragebogen fortsetzen",
Aus ohne Abo keinen Knopf, sondern den Hinweis zum Abo, Zahlung offen und In
Einrichtung "Status ansehen", Aktiv "Termine oeffnen", Abgeschaltet "Termine
ansehen". Zielprojekt: aktiv, ordered/setup, abgeschaltet, Entwurf, erstes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 22:34:50 +00:00
ClaudeandClaude Sonnet 5.5 8d628ef906 Terminplaner TP2: Fragebogen-Schritte 4 bis 8 und Bestellseite
- public/termine-fragebogen.js: Schritt 4 Zeiten (je Person/Platz wochenplanFeld,
  Bundesland ohne Vorauswahl mit leerer erster Option, "An Feiertagen geschlossen" im
  PATCH, danach PUT mit bundeslandGewaehlt; darunter abwesenheiten und sonderoeffnungen
  mit eigenem Container und eigenem zeichne, damit ungespeicherte Zeiten bleiben),
  Schritt 5 Regeln (Sammelknopf Alle sofort buchbar / Alle nur als Anfrage mit
  buchungsartFuerAlle und buchungsartLage, regeln, storno mit Muster nach Absagefrist,
  rechtliches, zahlung), Schritt 6 Gastfelder, Schritt 7 mit gemeinsamem
  leseAbschnitte, Schritt 8 Website (betrieb nur Telefon und vier Links, Einbauwuensche
  Modus/Knopftext/Stelle/Gestaltung/Bemerkung im PUT, Hinweis auf Mehrwuensche nach
  Stunden); danach renderBestellen. Platzhalter-Schritt entfernt.
- termine.css: Klassen der Schritte 4 bis 8
- Tests: termineView (Importe, PATCH/PUT nur an erlaubten Stellen, Bundesland ohne
  Vorauswahl im Quelltext), termineFragebogenView (Verhalten mit Fake-DOM fuer die
  Schritte 4 bis 8, Schluesselmengen, Bestellseite nach Schritt 8)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 22:18:44 +00:00
ClaudeandClaude Sonnet 5.5 9bf5ec09f8 Terminplaner TP2: Fragebogen-Geruest und Schritte 1 bis 3
- public/termine-fragebogen.js: renderFragebogen(root, ctx) mit Fortschrittsleiste
  (ol, aria-current, Handy: Schritt n von 8), Start beim ersten offenen Schritt
  (alles bestaetigt: Bestellseite), freier Navigation, einem Formular je Schritt,
  Weiter = ein PATCH ueber neuerSpeicher, danach PUT /fragebogen; Fokus auf die
  Ueberschrift; Kundenansicht ohne PUT
- Schritt 1 Branche und Betrieb (Gesundheitsbereich: Erklaertext statt Weiter, keine
  Anfrage; Branche wechseln mit Rueckfrage), Schritt 2 Wer wird gebucht,
  Schritt 3 Leistungen mit Haken Preise geprueft; Schritt 7 als Beispiel eines
  Schritts mit Feldern, Schritte 4, 5, 6 und 8 folgen in C9b
- termine.css: Fortschrittsleiste und Handy-Ansicht
- Tests: termineView (Dateiliste, PUT nur im Fragebogen, nur ueber termine.js geladen),
  termineFragebogenView (Verhalten mit Fake-DOM)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 21:55:51 +00:00
ClaudeandClaude Opus 5.5 f1b43ca2bf Spec: Nachtrag abgeschlossene Tage und 'Heute bisher' (Final-Review F1/F2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:34:43 +00:00
ClaudeandClaude Opus 5.5 fea58fc621 Analyse: Mini-Verlauf zeichnet einen einzelnen Messtag zwischen Lücken als Punkt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:34:43 +00:00
ClaudeandClaude Opus 5.5 0db59d38e9 Analyse: Zeitraum = abgeschlossene Tage, "Heute bisher" getrennt, Feinschliff
Final-Review-Fixwelle der Cockpit-Übersicht:

- F1 Collector: Der Zeitraum sind die range Kalendertage bis gestern (Berliner Tag),
  der Vorzeitraum die range Tage davor; heute steht getrennt in today { day, pv, visitors,
  contacts }. Kein Einbruch mehr am rechten Rand von Diagramm und Mini-Verläufen,
  kein falsches "x % weniger" tagsüber. Portal: weekdayStats bekommt die ganze Reihe
  (completedDays entfällt), "Vergleich ab" = firstDay + 2 x range, Kopfzeile
  "Analyse aktiv · Heute bisher: N Aufrufe · Stand: HH:MM Uhr" (alter Collector ohne today
  bleibt ohne "Heute bisher").
- F2 "direkt kontaktiert" zählt nur Telefon, E-Mail und WhatsApp (auch im Vorzeitraum),
  Links nach außen nicht.
- F3 Mini-Verlauf "Ø Zeit pro Seite": Tage ohne Messung sind Lücken statt 0-Tälern.
- F4 Fehlerfarbe beim Ausschalten rot, --an-faint auf 55 %, Achsenschrift per
  Container-Abfragen auf 11-12 px (y-Rand wächst mit der Stellenzahl, x-Beschriftungen mit
  Mindestabstand), Ranglisten-Anteile an allen Seitenaufrufen, Tippflächen bis 880 px
  mindestens 40 px hoch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:34:43 +00:00
ClaudeandClaude Opus 5.5 eadb768d1a Analyse: verspätete Statistik-Antwort überschreibt den Stand nicht mehr
loadStats schrieb state.stats/range/stamp schon beim Eintreffen der Antwort,
vor der current()-Prüfung der Aufrufer. Eine späte 30-Tage-Antwort überschrieb
so den 7-Tage-Stand, und "Alle anzeigen" oder der Ranglisten-Umschalter malte
danach mit 30-Tage-Zahlen. Jetzt liefert fetchStats nur die Antwort, adopt()
übernimmt sie erst nach current(). Übersicht und Heatmap teilen sich eine
laufende Anfrage je Zeitraum.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:34:43 +00:00
ClaudeandClaude Opus 5.5 a9f0711e97 Analyse: Cockpit-Übersicht mit Reitern Übersicht, Heatmap, Einstellungen
analyse.js setzt die Bausteine aus analyse-cards.js zusammen: Kopfleiste mit
Stand und Zeitraum, Reiter (Pfeiltasten), Übersicht mit Ladegerüst und
Fehlerkarte, Diagramm-Blase, Ranglisten-Umschalter, Heatmap erst beim Öffnen
des Reiters, Einstellungen. Einschalten, gesperrt, nicht verfügbar und
unbekannt erscheinen im selben Rahmen, Texte unverändert.

analyse.css (neu) trägt alle Analyse-Regeln unter .an mit eigenem türkisen
Akzent; der Analyse-Abschnitt in funktionen.css entfällt. Der Client-Test des
Banners liest die Banner-Regeln jetzt aus analyse.css.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:34:43 +00:00
ClaudeandClaude Opus 5.5 1964e5fcca Analyse: Farbfeld der Einstellungen zeigt die tatsächliche Banner-Farbe
Das Farbfeld nutzt bannerColor() wie die Website: eine für weiße Schrift
zu helle Akzentfarbe wird zu #111111 statt der gespeicherten Farbe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:34:43 +00:00
ClaudeandClaude Opus 5.5 55e98b038c Analyse: HTML-Bausteine der Cockpit-Übersicht
Reine String-Builder (Kopfleiste, Reiter, Das Wichtigste, Kacheln mit
Mini-Verlauf und Chips, Diagramm, Kontakt-Aktionen, Ranglisten, Datenbasis,
Heatmap-Karte, Laden, Fehler, Einstellungen) für die neue Analyse-Ansicht.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:34:43 +00:00
ClaudeandClaude Opus 5.5 f119a0cacc Analyse: Wochentag-Hinweis zählt den heutigen, unvollständigen Tag nicht mit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:34:43 +00:00
ClaudeandClaude Opus 5.5 69c94d83d7 Analyse: Vergleich zum Vorzeitraum und Klartext-Regeln als reine Funktionen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:34:43 +00:00
ClaudeandClaude Opus 5.5 9d1691cf6b Analyse: Oberfläche in eigenes Modul analyse.js, gemeinsame Helfer in fn-util.js
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:34:43 +00:00
ClaudeandClaude Opus 5.5 97bff91a99 Analyse: Zeitumstellungs-Test mit Abenduhr und Ereignissen, erkennt naives Vorzeitraum-Fenster
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:34:43 +00:00
ClaudeandClaude Opus 5.5 37bec17d73 Analyse: Vorzeitraum, firstDay, Tageswerte und Durchschnitte in der Statistik; activatedAt in der Ansicht
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:34:43 +00:00
ClaudeandClaude Opus 5.5 c1dada3189 Plan: Analyse-Übersicht neu (Cockpit)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:34:43 +00:00
ClaudeandClaude Opus 5.5 2f9dfd8ad5 Spec: Analyse-Übersicht neu (Cockpit)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:34:43 +00:00
ClaudeandClaude Sonnet 5.5 20583424a3 Funktionen: Bestellseite und Statuskarten
Terminplaner TP2 C8: public/funktion-bestellen.js mit sicherLink, bestellKoerper,
fortsetzenKoerper, renderBestellen (Zusammenfassung, offene Punkte, Preis,
vollstaendige Widerrufsbelehrung als scrollbarer Text, Unterlagen, Zustimmungs-Haken,
Knopf mit bestellung.consent.knopf) und statusKarte (ordered/setup, Danke).
request() in termine-dom.js behaelt den ganzen Body der Fehlerantwort (err.body),
damit 409 bestellung_offen und fassung_veraltet offen/consent zeigen koennen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 21:27:36 +00:00
ClaudeandClaude Sonnet 5.5 747be432c5 Terminplaner TP2: Reiter-Exporte und wochenplanFeld fuer den Fragebogen
- termine-tab-einstellungen.js: die acht Abschnitte (betrieb, darstellung, regeln,
  gastFelder, erinnerung, storno, rechtliches, zahlung) sind Exporte; betrieb nimmt
  { nur, titel, hinweis } (nur die gewaehlten Felder, lese schreibt nur diese)
- termine-tab-team.js: wochenplanFeld(r, pfad) => { el, lese() => { plan } | { fehler } },
  der Editor einer Person benutzt es selbst
- termine-speichern.js: fehlt kommt aus der Antwort des Servers
- termine.js: baueCtx aus zeigeReiter herausgeloest (Verhalten gleich)
- test/support/fakeDom.js: Minimal-DOM und Fake-fetch fuer die Client-Tests

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 21:06:42 +00:00
ClaudeandClaude Sonnet 5.5 0090332c70 Terminplaner TP2 C7a: Nachbesserung 1
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 20:53:48 +00:00
ClaudeandClaude Sonnet 5.5 edaefd4237 Terminplaner TP2: Fragebogen-Helfer im Client
Reine Helfer in public/termine-view.js: FRAGEBOGEN_SCHRITTE (Spiegel von
SCHRITTE und SCHRITT_TITEL), schrittStatus, ersterOffenerSchritt,
ressourcenAusNamen, buchungsartFuerAlle, buchungsartLage und
zusammenfassung (acht Gruppen, nur Text, Bundesland nur bei ausdruecklicher
Wahl, nie feedToken). Tests in test/termineFragebogenView.test.js, Spiegel-Test
in test/termineView.test.js.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 20:48:10 +00:00
ClaudeandClaude Sonnet 5.5 91b1a3bcd8 Terminplaner TP2: Entwuerfe nach 90 Tagen und bei Kundenloeschung entfernen
Neu: server/services/termine/entwuerfe.js. Ein Terminplaner im Zustand off ist ein
Entwurf (Fragebogen, Einstellung). loescheEntwuerfe(now) im Tagesjob (nach den
abgeschalteten Funktionen, nur im vollen Lauf): letzte Aenderung = juengerer Zeitpunkt
aus $updatedAt des Funktionsdokuments und termineConfig.updatedAt, aelter als 90 Tage
=> Einstellung weg, das Funktionsdokument nur, wenn es nie in Betrieb oder bezahlt war
(sonst bleibt es wegen des siteKeys; ein von E8 zurueckgesetztes Dokument nie).
Vor jedem Loeschen unter feature-order, feature-admin und termine-cfg frisch lesen
(noch off, Frist, keine Buchung). Seiten zu 100 per cursorAfter, hoechstens 2000
geprueft und 100 geloescht je Lauf, der Rest folgt an Folgetagen; Einzelfehler zaehlen
in fehler. Tagesbericht: Segment "Entwuerfe n geloescht (m offen)".
loescheEntwuerfeVonKunde: Kundenloeschung entfernt alle off-Entwuerfe vollstaendig,
Fehler werden nur protokolliert.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 20:33:33 +00:00
ClaudeandClaude Sonnet 5.5 29a38a9592 Terminplaner TP2: Branche und Vorlage im Entwurf, PUT /fragebogen
POST /api/termine/einstellungen/vorlage nimmt jetzt eine Branche (BRANCHEN), optionale
Betriebsfelder (nichtleere Werte ueberschreiben das Konto) und schreibt den Fragebogen mit
der Einstellung in einem Vorgang. Der Gesundheitsbereich ist ausgeschlossen (400
gesundheitsbereich, nichts wird angelegt). Eine bestehende Einstellung ersetzt nur
ersetzen:true mit passender version im Zustand off (neue IDs und Kalender-Token, Fragebogen
neu, Einbauwuensche bleiben). Die Route arbeitet im Entwurf (off, active, ordered/setup nur
in der Kundenansicht, disabled nie).

PUT /api/termine/fragebogen speichert nur den Fragebogen (Version der Einstellung bleibt),
ist in der Kundenansicht gesperrt (Waechter und Router), zaehlt gegen das Schreibbudget und
antwortet mit { fragebogen, offen }.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 20:12:00 +00:00
ClaudeandClaude Sonnet 5.5 1cd74226cb Terminplaner TP2: Entwurf schreiben, PATCH mit fehlt, nie oeffentlich
PATCH /api/termine/einstellungen schreibt den Entwurf nach eigenen Regeln
(withTermine({ schreiben, entwurf }) in routes/termine.js): off und active
ja, ordered und setup nur in der Kundenansicht (sonst 409 gesperrt), disabled
409 abgeschaltet wie bisher. In off zusaetzlich laeuft(customerId): ohne
Wartungs-Abo 409 kein_abo (Text aus aboMeldung), bei Lesefehler 503
abo_pruefung; eine Kuendigung oder offene Zahlung sperrt nur die Bestellung,
nicht das Speichern des Entwurfs. Das Funktionsdokument legt erst der Handler
an (ensureFeatureDoc), nach der Eingabepruefung. PATCH normalisiert mit
entwurf: state !== 'active' und antwortet mit fehlt. Tests: Zustandsmatrix,
Mandanten, Pflichtangaben, Abo, und die Gegenprobe an der oeffentlichen
Schnittstelle (Entwurf und Einbauwuensche nie oeffentlich).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 19:48:57 +00:00
ClaudeandClaude Opus 5.5 75794f14d5 Preview: Deploy auf fremde Subdomain sperren
Ein kopiertes Repo (git clone + push, Vorlage) bringt die
.webklar-preview.json des Originals mit. Sein erster Push ueberschrieb
die Preview des Originals, danach scheiterte der Webhook mit 500
(Unique-Index auf subdomain). Fast passiert bei Olzinger -> HSK-Vorlage,
04.10.2026.

- foreignSubdomainOwner: Subdomain gehoert einem anderen Repo oder einem
  Projekt ohne Repo; ein in Gitea umgezogenes Repo (Weiterleitung) zaehlt
  nicht als fremd
- Webhook: fremde Subdomain -> kein Deploy, Projekt ohne Preview (neu:
  synced, Repo-Name statt fremdem Anzeigenamen); eine schon vergebene
  eigene Preview bleibt, WOMS zeigt den Grund als Preview-Fehler
- runDeploy prueft fuer alle anderen Wege (WOMS, Reaktivieren), vor dem
  Runner, damit ein gesperrter Aufruf keinen laufenden Bau verwaist;
  Appwrite-Fehler bei der Pruefung blockieren dort nicht (Webhook streng)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 19:46:27 +00:00
ClaudeandClaude Sonnet 5.5 42a3befc5f Terminplaner TP2 C4a: Nachbesserung 1
Die Wahl des Live-Hosts steht jetzt nur noch in liveHost() (kontext.js);
websiteUrl() und der Betriebsvorschlag des Fragebogens (routes/termine.js)
nutzen dieselbe Funktion.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 19:34:37 +00:00
ClaudeandClaude Sonnet 5.5 250fc0c4df Terminplaner TP2: Entwurfszugriff lesen, GET ohne Dokument, GET-Felder
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 19:29:12 +00:00
ClaudeandClaude Sonnet 5.5 31aa80affd Terminplaner TP2: Fragebogen im Kontext und Speicher
kontext.js: ctx.fragebogen (normalisiert; fehlt er oder ist er kaputt, der leere,
kaputt mit einer Logzeile und ohne Wurf), ladeEinstellung liefert zusaetzlich
fragebogen, neuer kontextFuerPortal(feature) fuer das Portal in jedem Zustand.
speichereFragebogen schreibt unter termine-cfg:<id> nur fragebogen, updatedAt und
updatedBy (ohne version; fehlt das Dokument: 409 keine_einstellung). Neue Option
fragebogen bei speichereEinstellung schreibt Einstellung und Fragebogen in
derselben Sperre. kontextPerSiteKey bleibt unveraendert (Entwurf nie oeffentlich),
Tests dazu fuer off, ordered, setup, disabled und nach activate. Der Test
ladeEntwurf in funktionenCheckout.test.js schaltet die neue Logzeile stumm.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 19:14:22 +00:00
ClaudeandClaude Sonnet 5.5 008ad4fb1e Terminplaner TP2: Bestell-Gate und Gesundheits-Stichwortpruefung
fragebogen.js ersetzt den Stub aus Block B: bestellpruefung (Kriterien (a) bis (h),
je Punkt ein Eintrag, nach Schrittfolge sortiert, Abo-Punkt nur bei mitAbo !== false),
SCHRITT_TITEL, gesundheitsWarnungen (Stichworte nur am Wortanfang, hoechstens 10
Meldungen) und bestellungAnsicht fuer die Bestellseite. einstellungen.js exportiert
sichtbareLeistungen und MELDUNG_PFLICHT. Testhelfer bestellbarerStand und entwurfDoc;
der Stub-Test entfaellt, die zwei B-Tests mit dem Stub laufen gegen das echte Gate.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 19:00:12 +00:00
ClaudeandClaude Sonnet 5.5 ab47fabcb2 Terminplaner TP2: Fragebogen-Attribut, Branchen, Schritte, Normalisierung
termineConfig bekommt das Attribut fragebogen (Text 20000, Inhalt bis
FRAGEBOGEN_MAX 16000). fragebogen.js: SCHRITTE, BRANCHEN (Vorlagen plus
Gesundheitsbereich), leerFragebogen und normalisiereFragebogen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 18:49:06 +00:00
ClaudeandClaude Sonnet 5.5 36b36bcc4d Terminplaner-Banner und Meldung nach Abschaltgrund mit Löschdatum
GET /api/termine/einstellungen liefert bei abgeschalteter Funktion
abgeschaltet { grund, seit, loeschungAm }. Die 409-Meldung bei
Schreibversuchen (Code abgeschaltet) und der Banner im Portal
unterscheiden Wartungs-Abo beendet, Abo vor der Einrichtung beendet und
die übrigen Gründe und nennen das Löschdatum, wenn es eines gibt.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 18:41:03 +00:00
ClaudeandClaude Sonnet 5.5 d66dd60a76 Zahlung nach Abo-Ende: Zusatztests Webhook und Abgleich (T19 nie doppelt)
Nur Tests in test/termineAboAbgleich.test.js, kein Produktionscode: handleFeatureCheckoutEvent
und gleicheAboAb gegeneinander (Review Focus 1, nie Geld ohne Spur, nie doppelt).
- Zahlung nach dem Abschalten: genau ein T19 (zahlung_nach_ende, Sitzung), kein Ticket,
  kein K15/T16; Wiederholung und async_payment_succeeded stumm
- T19-Versand scheitert: Webhook wirft, Zahlung gebucht, Fall offen; erste Wiederholung
  mit funktionierendem Versand sendet genau ein T19
- Folgetag-Abgleich: K22 vor_einrichtung, Marker aboEndeMailAt, kein zweites T19
- Abgleich vor der Webhook-Wiederholung und parallel (Promise.all): insgesamt ein T19
- Grund team: T16 abgeschaltet, kein T19; Abo laeuft (ordered/setup): Ticket, K15, T16
- Rennen Zahlung vor Abgleich: Ticket, T16 und ein T19; Stripe-Retry stumm

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 18:29:10 +00:00
ClaudeandClaude Sonnet 5.5 c5009da570 Abo-Ende: Sofortpfad beim Widerruf des Wechsels, Team-Aktivierung nur mit laufendem Abo
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 18:19:48 +00:00
ClaudeandClaude Sonnet 5.5 6bd6aed7d5 Terminplaner TP2 E8: Nachbesserung 1 (offener Zahlungsfall bleibt sichtbar, R108)
setzeFunktionZurueck setzt ein Funktionsdokument mit offenem Zahlungsfall
(zahlungMeldung nicht leer und nicht 'gemeldet') nicht auf off zurueck: Warnung
ohne Kennungen, +1 in lauf.fehler, kein Wurf. Das Dokument bleibt disabled mit
seinen Markern, der Abo-Abgleich holt T19 weiter nach, nach 'gemeldet' setzt der
naechste Lauf zurueck. Buchungen und Einstellung werden wie bisher geloescht.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 18:08:54 +00:00
ClaudeandClaude Sonnet 5.5 30701ba277 Löschung nach 30 Tagen: Abo-Vorprüfung, Funktionsdokument danach auf off
Bei Grund abo_ende prüft die Löschung vor jedem unumkehrbaren Schritt frisch,
ob das Wartungs-Abo wieder läuft (Lesefehler: nichts löschen). Sind alle
Buchungen und die Einstellung weg, wird das Funktionsdokument unter
feature-admin und termine-cfg auf off zurückgesetzt (siteKey bleibt).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 17:57:41 +00:00
ClaudeandClaude Sonnet 5.5 91e7f7d8a3 Tagesjob: Abo-Abgleich vor der Löschung, auch bei TERMINE_ENABLED=off mit bezahlten Dokumenten
taeglicherJob ruft gleicheAboAb(now) nach den Löschfristen und vor der Löschung
abgeschalteter Funktionen auf (Schritt „Abo-Abgleich“, ein Fehler dort stoppt weder
Löschung noch Rest; die Fehler des Abgleichs zählen in fehler). Das Ergebnis trägt
aboAbgleich, tagesbericht hängt „Abo-Abgleich N geprüft / N abgeschaltet / N wieder ein /
N Mails / N ausgenommen“ vor „N Fehler“ an.

fuehreAus nimmt trotzAus: Im Modus off läuft der Tagesjob nur, wenn es ein vom Kauf
freigeschaltetes Termine-Dokument gibt (eine Abfrage, limit 1; bei einem Lesefehler
Warnung und { aus: true }). Dann läuft ein eingeschränkter Lauf (eingeschraenkt: true,
gleiche Überlappungssperre): nur Abo-Abgleich und Löschung abgeschalteter Funktionen,
mit eigener Berichtszeile und den Feldern, die meldeTagesjob und die Statusroute lesen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 17:45:48 +00:00
ClaudeandClaude Sonnet 5.5 1c74c89feb Terminplaner: Erstattungsmeldung unabhaengig vom Abo nachholen (R107)
Der taegliche Abo-Abgleich holt T19 fuer jedes bezahlte, abgeschaltete Dokument mit
Grund abo_ende_vor_einrichtung und zahlungMeldung ungleich gemeldet nach, auch wenn das
Wartungs-Abo wieder laeuft oder nicht lesbar ist (der Nachlauf liest das Abo nie).
Reihenfolge wie bisher: T19, dann K22 (nur wenn aboEndeMailAt leer), dann Marker.
meldeAboEnde sendet bei gesetztem K22-Marker und offener Erstattung nur T19.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 17:31:54 +00:00
ClaudeandClaude Opus 5.5 4fc4c70b7d Archiv/Rechte: Review-Punkte
- archiviertes Projekt bleibt unter Funktionen sichtbar, solange dort eine
  Funktion bestellt/eingerichtet/aktiv ist (z. B. Terminbuchungen)
- auf archivierten Projekten nichts neu bestellen oder einschalten
- Projekttyp Vorlage nur fuer Admins; repo-readme nur fuer bekannte Projekte

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:11:54 +00:00
ClaudeandClaude Opus 5.5 3156bb790a Projekte: alle Mitarbeiter, private Repos, Archiv fuer Kunden ausgeblendet
Entscheidungen vom 04.10.2026:
- Projekt-Routen (website-projects, preview, gitea, services, analyse)
  fuer jeden Mitarbeiter (Label mitarbeiter oder admin); Kunden-,
  Mitarbeiter- und Finanz-Verwaltung bleiben nur fuer Admins
- Neue Repos aus der Vorlage werden privat angelegt
- Archivierte Projekte sieht der Kunde nicht mehr: Projektliste,
  Projektdetails, Preview-Zugang, Funktionen (Mitarbeiter-Ansicht schon)
- Projektliste des Kunden mit Limit (Appwrite liefert sonst nur 25)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:02:02 +00:00
ClaudeandClaude Sonnet 5.5 13523ca5e2 Abo-Abgleich: Rückkehr innerhalb der Frist schaltet wieder ein (K23)
Schritt 3 von gleicheAboAb: Ein abgeschaltetes Dokument mit Grund abo_ende,
dessen Wartungs-Abo wieder läuft, wird unter feature-admin:<projectId> nach
frischem Lesen von Dokument und Abo und mit vorhandener Einstellung über
activate wieder eingeschaltet (Daten und Einstellungen bleiben). K23 geht
nur, wenn K22 ging (aboEndeMailAt vor dem activate gelesen); scheitert K23,
bleibt die Reaktivierung bestehen. Nicht automatisch: off nach der Löschung,
abo_ende_vor_einrichtung, team, kunde_geloescht, Altbestand ohne Grund.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 14:44:47 +00:00
ClaudeandClaude Sonnet 5.5 addb88d5fd Abo-Abgleich: Abo endet vor der Einrichtung ⇒ abgeschaltet, T19 und K22
Terminplaner TP2 E5. Schritt 1b in gleicheAboAb: Dokumente im Zustand ordered
oder setup werden bei fehlendem Wartungs-Abo unter feature-admin:<projectId> nach
frischem Lesen von Dokument und Abo mit Grund abo_ende_vor_einrichtung abgeschaltet.
Die Meldelogik liegt in meldeAboEnde (auch für den Sofortpfad E9): bei bezahlter
Einrichtung zuerst T19 über meldeErstattung (außerhalb jeder feature-admin-Sperre),
dann K22 mit Block vor_einrichtung, dann der Marker aboEndeMailAt; scheitert T19,
folgen weder K22 noch Marker und der nächste Tag wiederholt beides. Ein unbezahltes
ordered-Dokument wird nur abgeschaltet. Schritt 2 holt auch abo_ende_vor_einrichtung
nach. Die Stripe-Sitzung und das Ticket bleiben unangetastet.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 14:32:14 +00:00
ClaudeandClaude Sonnet 5.5 17f933dbc7 Abo-Abgleich: aktive Terminplaner ohne Wartungs-Abo abschalten, K22 nachholen
gleicheAboAb(now) liest alle nicht abgeschalteten Terminplaner-Funktionen einmal,
bewertet je Kunde die Abo-Lage (aboMemo) und schaltet aktive, bezahlte Funktionen
ohne laufendes Wartungs-Abo ab (disabledReason abo_ende). Geschrieben wird nur
unter feature-admin:<projectId> nach frischem Lesen von Dokument und Abo; bei
Lesefehlern bleibt alles aktiv. Pilot- und Kulanz-Dokumente zaehlen als
ausgenommen. meldeAboEnde sendet K22 und setzt aboEndeMailAt erst nach Erfolg.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 14:20:57 +00:00
ClaudeandClaude Sonnet 5.5 973135d2de Mails K22, K23 und Hinweis auf das Ende der Funktionen in K02/K03
Terminplaner TP2 E3. K22 "Terminplaner beendet" (Blöcke abo_ende, hosting,
vor_einrichtung, daten) und K23 "Terminplaner läuft wieder" im Layout von K19,
Du-Form, Texte als Entwurf für die Mail-Überarbeitung. K02 und K03 bekommen den
Block funktionen (in K03 innerhalb von abo); routes/abo.js setzt ihn nur beim
Wartungs-Abo und nie bei der Speicher-Kündigung.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 14:09:05 +00:00
ClaudeandClaude Sonnet 5.5 eae6eee03c Terminplaner TP2 E2: Nachbesserung 1 (Löschdatum nur für den Terminplaner)
Ruling R105: featureView setzt loeschungAm nur für featureKey 'termine' (der
30-Tage-Löschlauf löscht nur Terminplaner-Daten); disabledReason und disabledAt
bleiben für alle Funktionen. Test: News ohne Löschdatum, Zeitfenster-Prüfung
entfernt, Teamroute-Ansicht pinnt loeschungAm für den Terminplaner.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 14:02:57 +00:00
ClaudeandClaude Sonnet 5.5 35c9800856 Abschaltgrund und Loeschfrist in der Funktionsansicht (featureView, frist.js)
featureView liefert zusaetzlich disabledReason, disabledAt und loeschungAm (Berliner
Tag, 30 Tage nach disabledAt); alle drei nur im Zustand disabled, sonst null.
Neu termine/frist.js mit LOESCHFRIST_TAGE, loeschzeit und loeschTag (nur zeit.js/util.js).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 13:58:06 +00:00
ClaudeandClaude Sonnet 5.5 144b053851 Abo-Baustein aboStatus.js: Wechsel-Regel in laeuft/bestellbar, aboLage; Analyse nutzt ihn
aboStatus.js (Kernversion aus B5 ausgebaut): ein privater Lader lage(customerId, { altbestand })
ueber getCurrentSubscription und listContractEvents, alle Praedikate bauen darauf. laeuft kennt
altbestand (Vorgabe true: productKey || 'wartung' plus Wechsel-Regel; false: die heutige Regel der
Analyse, keine Ereignisabfrage). Wechsel-Regel: sagt der Spiegel "Wartung laeuft" und ist das
juengste wechsel-Ereignis des Abos widerrufen, laeuft das Wartungs-Abo nicht (kein_abo beim
Bestell-Gate, Freischaltung bleibt in setup). Neu aboLage ({ laeuft, art, endedAt }) und
aboMemo().lage; der Memo merkt eine Ablehnung fuer den Lauf. analyse.js nutzt
laeuft(id, { altbestand: false }); Verhalten der Analyse unveraendert (+1 Pin-Test).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 13:50:51 +00:00
ClaudeandClaude Sonnet 5.5 cf9f9b30a0 Funktionen-Seite: Preis und Bestelltexte aus dem Register
GET /api/funktionen liefert features.news und features.termine (oeffentlicheAnsicht,
auch wenn der Terminplaner noch nicht freigegeben ist) und ustHinweis; setupEstimate und
consentText bleiben als News-Alias. Die News-Kachel und die Buchungsseite lesen Preis,
Knopftext und Zustimmungstext aus features.news und fallen ohne features auf die bisherigen
Werte zurueck. News sendet weiter consent: true an /api/funktionen/news/checkout.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 13:42:20 +00:00
ClaudeandClaude Opus 5.5 800630ea4b Projekt anlegen/archivieren: Review-Punkte
- Anlegen uebernimmt keinen alten Eintrag gleichen Namens mehr (Repo in
  Gitea geloescht): Platzhalter werden entfernt, ein verknuepftes altes
  Projekt blockiert den Namen mit klarer Meldung, bevor ein Repo entsteht;
  neue Projekte starten mit archived=false
- archiveRepo aendert Gitea und Dokument unter der Repo-Sperre; der Import
  liest den Archiv-Stand vor dem Spiegeln frisch aus Gitea
- Reaktivieren: Preview aus -> synced, Deploy-Fehler -> deploy_failed
  statt dauerhaft 'pending'

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:36:26 +00:00
ClaudeandClaude Sonnet 5.5 f57dad73ca Freischaltung ueber das Register mit Pruefung von Einstellung und Abo
activateClosedTickets oeffnet jetzt alle Funktionen mit ticketFreischaltung im Register
(ticketFreischaltungKeys) statt fest news und fragt unter der Sperre feature-activate:<id>
nach dem frischen Lesen den Haken freischaltbar(doc). Der Standard fuer termine verlangt eine
gespeicherte Einstellung und ein laufendes Wartungs-Abo. false oder Fehler (z. B. Abo nicht
lesbar) lassen das Dokument in setup: Log und genau ein adminNote-Eintrag "Freischaltung
wartet: <Grund>", keine Mail. News bleibt unveraendert.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 13:35:57 +00:00
ClaudeandClaude Opus 5.5 017a9c22f2 Webhook: System-Commits legen kein Auto-Ticket an
Beim Anlegen mit Kunde (ohne Ticket) loeste der Commit der
.webklar-preview.json durch webklar-system ein zusaetzliches Ticket
"Website <name>" aus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:32:51 +00:00
ClaudeandClaude Sonnet 5.5 f4754f573a Zahlungseingang in jedem Zustand: nie Geld ohne Spur, nie doppelt (T16/T19)
onFeaturePaid wertet den Fall aus markPaid aus. Trifft die Zahlung eine Funktion, die
schon freigeschaltet, in Einrichtung oder abgeschaltet ist, geht nur eine Team-Meldung
raus (T16 Block bereits_aktiv, bei Abo-Ende vor der Einrichtung T19), kein Ticket und
keine K15. Beide Meldungen laufen genau einmal je Dokument: unter feature-meldung:<id>
frisch lesen, Marker zahlungMeldung gemeldet erst nach gesendeter Mail. Scheitert der
Versand, wirft onFeaturePaid (Webhook 500), Stripe wiederholt und der naechste Versuch
sendet genau einmal.

meldeErstattung ist die einzige Stelle fuer T19 (Zahlung nach dem Abo-Ende und spaeter
die Meldung des Abo-Abgleichs), wirft nie. Bei Funktionen mit Wartungs-Abo-Pflicht
zeigt T16 nach der Verbuchung den Block kein_abo, wenn das Abo nicht mehr laeuft; ein
Lesefehler laesst den Block aus und blockiert nichts. customerOf und projectName sind
exportiert.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 13:25:26 +00:00
ClaudeandClaude Opus 5.5 d878df96e0 Zuordnungs-Lauf: vom System-Konto angelegte Projekte nicht alle 10 Minuten abfragen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:24:06 +00:00
ClaudeandClaude Opus 5.5 e43cdb667b Projekt-Ablauf im Ticketsystem: Anlegen, Archivieren, Import repariert
- Neues Projekt ohne Subdomain: subdomain null statt '' (Unique-Index
  subdomain_unique, jedes weitere scheiterte mit "Document with the
  requested ID already exists"); ebenso Typ "Internes Projekt"
- Anlegen unter derselben Repo-Sperre wie der Webhook repository/created;
  upsertWebsiteProjectByRepo macht aus einem 409 ein Update
- Gitea-Beschreibung = Anzeigename; Art (kind) beim Anlegen aus dem Ticket
- Reaktivieren setzt status zurueck (vorher blieb 'archived' stehen)
- Import: archivierte Repos als archiviert, Archiv-Stand aus Gitea wird
  gespiegelt, ein fehlerhaftes Repo bricht den Lauf nicht mehr ab
- Abgleich ueberschreibt Projektnamen nicht mehr
- CORS erlaubt PUT (Preview-Variablen speichern)
- Vorlage zieht beim Zuweisen nie in die Org um
- Erinnerungs-Lauf ueberspringt archivierte Projekte
- kaputte Umlaute (U+FFFD) in drei Fehlermeldungen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:22:53 +00:00
ClaudeandClaude Sonnet 5.5 468df49812 Zahlungseingang: markPaid meldet den Fall und merkt ihn im Dokument
markPaid liefert jetzt { doc, created, fall }. Trifft eine Zahlung ein Dokument, das nicht
mehr auf die Bezahlung wartet (active, disabled, setup mit Ticket), wird sie einmalig
verbucht (purchaseStatus paid, unlockedByPurchase, Rechnung, Notiz) und der noch zu
meldende Fall im eigenen Feld zahlungMeldung vermerkt (bereits_aktiv | abgeschaltet |
abo_ende), in einem einzigen Schreibzugriff. Ein Folgeereignis bucht nichts doppelt und
liefert den Fall erneut, bis zahlungMeldung auf gemeldet steht. ordered und fortsetzbares
setup laufen wie bisher, off bleibt unveraendert (Uebernahme ueber adoptPaidSession).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 13:13:42 +00:00
ClaudeandClaude Sonnet 5.5 992d7358fa Mail T16 je Funktion, neue Vorlage T19 Erstattung faellig
T16 (Funktion bestellt) nennt die Funktion aus dem Register statt fest
"News-Pinnwand": neue Platzhalter funktion, zustand, anleitung_url und
gesundheit_text, neue Bloecke kein_abo, bereits_aktiv, anleitung (Knopf
"Im Ticketsystem oeffnen" zur Einbauanleitung) und gesundheit. teamNotice
und unmatchedNotice fuellen funktion aus featureConfig (ohne bekannten
Schluessel "Funktion"); die neuen Bloecke kein_abo, bereits_aktiv und
gesundheit bleiben aus, bis B10a und D4 sie fuellen. Die News-Mails
bleiben woertlich (HTML, Text und Betreff byte-gleich).

T19 (Erstattung faellig, Team-Hinweis) ist neu: volle Erstattung von
Hand in Stripe, Bloecke zahlung_nach_ende, ticket_offen und rechnung.
Gesendet wird sie spaeter ueber meldeErstattung (B10b).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 13:06:39 +00:00
ClaudeandClaude Sonnet 5.5 e200efb7e5 Mails K15/K16 je Funktion (News-Wortlaut eingefroren)
K15 (Bestellung eingegangen) und K16 (Funktion bereit) tragen den Wortlaut der
Funktion als Platzhalter (funktion, funktion_gen/akk/nom/pron/dein, preis_hinweis,
danach, bereit, knopf, portal_url) statt fest News-Pinnwand. siteFeatures baut die
Werte mit bestellMail/bereitMail aus dem Register-Eintrag (mails.*, setupEstimate,
returnHash) und dem Katalogpreis je feature.product. K15 geht ohne Anhaenge raus.
Die News-Mails bleiben Zeichen fuer Zeichen gleich (Test featureMails friert
Betreff und Text ein).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 12:56:45 +00:00
ClaudeandClaude Sonnet 5.5 9dd0244537 Funktions-Ticket: Text aus dem Register, Laengendeckel, Notfall-Details und gezielte Wiederholung
markPaid holt Betreff und Details des Einrichtungs-Tickets aus dem Register (ticketTexte), createFeatureTicket
bleibt die einzige Stelle, die das Ticket anlegt. Betreff auf 128, Details auf 7000 Zeichen gekuerzt (surrogatsicher,
Schnitt an der Zeilengrenze, einheitliche Schlusszeile). Wirft die Details-Funktion oder liefert sie keinen Text,
gelten die Notfall-Details (Kopfzeilen und Verweis ins WOMS). Nach einem 400 bei langen Details gibt es genau einen
zweiten Versuch mit den Notfall-Details und derselben woid; 5xx, Netzfehler und kurze Details werden ohne
Wiederholung weitergereicht. Stub termine/ticketDetails.js bis Task D4.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 12:48:02 +00:00
ClaudeandClaude Sonnet 5.5 7fcb268644 Checkout je Funktion: Zustimmungsnachweis vor der Weiterleitung, Zustaende aus dem Register
Rest der Route POST /api/funktionen/:key/checkout unter der Sperre feature-order:<Kunde>:<Projekt>:
Zustand aus dem Register (setup/active, disabled, ordered mit offener Sitzung, "Zahlung fortsetzen"),
Stripe-Sitzung mit der Zeit des Klicks, Zustimmungsnachweis (Serverfassungen und -texte) vor markOrdered
und vor der URL. Standard-Haken belehrung mit BELEHRUNG_PLATZHALTER fuer den Terminplaner.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 12:38:50 +00:00
ClaudeandClaude Sonnet 5.5 1f6ae0f7e0 Terminplaner TP2 B6: Nachbesserung 1
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 12:30:44 +00:00
ClaudeandClaude Sonnet 5.5 ded6bb2a77 Checkout je Funktion: Vorpruefungen in der Reihenfolge der Spezifikation
POST /api/funktionen/:key/checkout ersetzt /news/checkout (der News-Pfad bleibt als
key = news erhalten). Stufen ohne Seiteneffekt: 1 Kundenansicht (403), 2 Schluessel
gegen das Register (404), 3 Zustimmung (400 zustimmung_fehlt, 409 fassung_veraltet mit
der Serverfassung; News weiter mit consent: true), 4 Projekt (400/403; bei fortsetzen
muss das Dokument ordered sein), 5 Schalter (409 nicht_verfuegbar), 5b Unterlagen
(503 unterlagen_fehlen), 6 Bestellpruefung (409 bestellung_offen mit offen), 7 Wartungs-Abo
(409 kein_abo | gekuendigt | nicht_aktiv, Lesefehler 500). fail() gibt err.body weiter.

featureOrder.js: pruefeZustimmung, istFortsetzen, aboMeldung (einzige Quelle der
Abo-Hinweistexte), ladeEntwurf, pruefeAbo, orderDeps. featureRegistry.js: featureSperre
und die Standard-Haken verfuegbar, bestellpruefung (mitAbo: false, ohne abo) und
unterlagenBereit des Terminplaners. Der Rest der Route (Sperre, Zustand, Sitzung,
Nachweis) bleibt bis Task B7 nur fuer News; andere Funktionen enden mit 503.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 12:25:09 +00:00
ClaudeandClaude Sonnet 5.5 d568a3798d Abo-Baustein (Kernversion) und Platzhalter fuer die Bestellpruefung
aboStatus.js: laeuft(customerId) (Wartungs-Abo, productKey || 'wartung', Status in
ACTIVE_STATUSES), bestellbar(customerId) mit den Gruenden kein_abo, gekuendigt,
nicht_aktiv in dieser Pruefreihenfolge und aboMemo() (je Aufruf und Kunde nur eine
Lesung). Fail-closed: ein Lesefehler wirft, nie false oder ok:false.

termine/fragebogen.js: Platzhalter bestellpruefung() mit der endgueltigen Signatur,
liefert []; Block C ersetzt ihn.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 12:12:09 +00:00
ClaudeandClaude Sonnet 5.5 786df1230e Zustimmungsnachweis je Funktion (featureConsents) und Fristanker aus dem Klick
server/services/featureConsents.js speichert und liest den Nachweis zur Bestellung
einer Funktion (nur anfuegen, sieben Exporte): speichereZustimmung (Texte und Fassungen
als JSON, SHA-256 ueber die Texte, vollstaendig oder gar nicht), letzteZustimmung,
zustimmungFuerSitzung (leere Sitzung fragt nicht ab), leseZustimmung, texteHash,
zustimmungsfassungen und nachweisTexte (Haken, Belehrung, Knopf aus dem Register).
paidFeatureOrders liefert featureKey und vertragAt (Klick der bezahlten Sitzung, sonst
consentAt, sonst orderedAt; Lesefehler faellt mit Log auf orderedAt zurueck); der Widerruf
einer Funktion rechnet die Frist aus vertragAt. Eine spaetere, unbezahlte Zustimmung
verschiebt den Anker nicht.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 12:05:12 +00:00
ClaudeandClaude Sonnet 5.5 d8e4d79891 Schema: Zustimmungsnachweis featureConsents im Skript feature-setup
scripts/feature-setup.mjs legt neben den portalFeatures-Attributen die Collection
featureConsents (nur Server-Key, ohne Rechte) mit 11 Attributen und den Indizes
idx_session und idx_project_feature an; --check meldet je fehlende Collection,
Attribut und Index eine Zeile "- <id>[.<key>] FEHLT", schreibt nichts und liefert
Exit-Code 1 bei Luecken. config.collections.featureConsents (Env
APPWRITE_COLLECTION_FEATURE_CONSENTS) und schemaKeys('featureConsents') in
test/support/schema.js ziehen mit. setup-abo-collections.mjs bleibt unveraendert.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 11:57:16 +00:00
ClaudeandClaude Sonnet 5.5 98fa03aad7 Stripe: Katalog Terminplaner-Einrichtung (149 €) und Checkout je Funktion aus dem Register
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 11:53:06 +00:00
ClaudeandClaude Sonnet 5.5 c584d220c8 Funktionen: Register config.features mit News- und Termine-Eintrag
config.features (news, termine) als reine Daten in config.js, config.ustHinweis,
config.funktionen als Alias mit Gettern auf features.news. Neu featureRegistry.js
(featureConfig, featureKeys, featureHaken mit Ueberschreib-Regel, oeffentlicheAnsicht);
FEATURE_KEYS, FEATURE_LABELS und DISABLED_MESSAGE in siteFeatures.js kommen aus dem
Register, Werte unveraendert.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 11:48:06 +00:00
ClaudeandClaude Sonnet 5.5 e2f50407a0 Terminplaner: Minutenjob in Verfall und Mailversand getrennt geschuetzt
fuehreAus nimmt { teil } und sperrt je job:teil; minutenJob startet Verfall
und Mailversand (Warteschlange plus Betriebspruefung) gemeinsam mit
Promise.all. Haengt ein Teil, laeuft der andere im naechsten Takt weiter
(verfallUebersprungen bzw. mailUebersprungen); uebersprungen gilt nur, wenn
beide Teile uebersprungen werden. Die Ueberlappungswarnung nennt den Teil,
das Fehlerlog behaelt minutenJob.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 11:41:00 +00:00
ClaudeandClaude Sonnet 5.5 aaed273b62 Funktionen: disable() idempotent, disabledReason, Schema-Skript feature-setup
disable() laeuft unter einer Sperre je Dokument und aendert ein schon
abgeschaltetes Dokument nicht mehr (disabledAt als Beginn der 30-Tage-Frist,
Grund und Notiz bleiben); nur Altbestand ohne disabledAt bekommt dieses Feld.
Neu: DISABLED_REASONS und der Parameter reason (Vorgabe team, unbekannt = 400),
disableCustomerFeatures reicht ihn durch, die Kundenloeschung nutzt
kunde_geloescht. activate() leert zusaetzlich disabledReason und aboEndeMailAt
und laesst zahlungMeldung unberuehrt.

scripts/feature-setup.mjs legt portalFeatures.disabledReason, aboEndeMailAt und
zahlungMeldung idempotent an (--check meldet Luecken mit Exit 1);
schemaKeys('portalFeatures') zieht die Schluessel mit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 11:33:17 +00:00
ClaudeandClaude Sonnet 5.5 39225594e5 Terminplaner: Status-Route fuer das Team
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 11:24:02 +00:00
ClaudeandClaude Sonnet 5.5 4601c5ef3b Terminplaner: Tagesjob-Meldung, Betriebe-Zaehlung und Bericht
meldeTagesjob meldet am Ende des Tagesjobs ueber den Melder aus A10a2 die Vorfaelle
tagesjob (Fehler oder Abbruch), deckel (Loeschdeckel an 3 Tagesjobs in Folge) und
betriebe (ab 20 aktiven Betrieben, D8) und merkt letzterTagesjob (nur Zaehlungen).
taeglicherJob liefert geloescht und aktiveBetriebe, tagesbericht haengt
"n aktive Betriebe" an.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 11:17:07 +00:00
ClaudeandClaude Sonnet 5.5 689a21f9ac Terminplaner: Betriebspruefung mit Abkuehlzeit fuer die Team-Mail
pruefeBetrieb (neu: termine/betrieb.js) prueft am Ende jedes Minutenjobs
Mailbudget (ab 70 %), Warteschlange (aeltester faelliger Eintrag > 10 Min
ueberfaellig, neu outbox.warteschlangeAlter) und Anmeldepause
(pausiertBis). Der gemeinsame Melder meldeVorfall schreibt je Schluessel
hoechstens stuendlich ein Warnlog und schickt die Team-Mail T20 beim
Beginn und danach fruehestens nach 6 Stunden; Lesefehler und Mailfehler
stehen nur im Log. T20 steht jetzt auch im Test der Team-Fusszeile.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 11:01:16 +00:00
ClaudeandClaude Sonnet 5.5 3a3319ec14 Terminplaner: Team-Mail-Vorlage T20 Betriebsmeldung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 10:26:08 +00:00
ClaudeandClaude Sonnet 5.5 43b3caddde Terminplaner: veraltete G01 verworfen, Index bookingId
Eine G01, die erst nach holdMinutes + 1 Minute faellig wird, ist mit verfallenem Link
nutzlos: sie wird nicht mehr gesendet, sondern verworfen (kein Transportaufruf, kein
Budgetplatz, Felder geleert, kein Zustellfehler, raeumeAuf loescht sie nach 48 Stunden).
Neuer Index idx_booking auf termineOutbox fuer zustellFehler und verwerfeWartende;
verworfeneG01() zaehlt seit Prozessstart.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 10:21:25 +00:00
ClaudeandClaude Sonnet 5.5 36b94c3035 Terminplaner: Mail-Warteschlange begrenzt und nach Vorrang geordnet
verarbeite liest nicht mehr alle wartenden Eintraege, sondern je Klasse
(vorrang, normal, nachrangig) eine begrenzte Liste (config.termine.mailListe,
Standard 100) mit select und orderAsc(naechsterVersuch). Bei Pause oder
leerem Gesamtbudget endet der Lauf ohne Abfrage, bei erreichter Reserve
entfaellt die nachrangige Klasse. Funktionen mit ausgeschoepftem Kontingent
kommen in eine Ausschlussliste (hoechstens 90 Werte); hoechstens vier weitere
Runden je Klasse, damit keine Funktion hinter einer vollen verhungert.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 10:13:26 +00:00
ClaudeandClaude Sonnet 5.5 4d96565240 Terminplaner: Empfaenger-Kappe ueber Betriebe (G01, Freitext in G04/G06)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 10:03:39 +00:00
ClaudeandClaude Sonnet 5.5 c35711caad Terminplaner: Mailkontingent je Funktion, Vorrang-Reserve und kleinere Haltungsgrenze
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 09:54:11 +00:00
ClaudeandClaude Sonnet 5.5 978722023f Terminplaner: Zellen-Sweep seitenweise ueber idx_bis mit Deckel
raeumeVerwaisteZellen(now, { deckel = config.termine.zellenDeckel }) liest
Zellen mit bis < jetzt - 1 Tag in Seiten zu 100 ueber idx_bis (aelteste
zuerst, ohne Cursor neu gelesen), gruppiert je Ressourcensperre mit Map und
loescht unter der Sperre nur, was dort noch dieselbe Buchung hat und noch alt
ist. Die Buchungen werden nicht gelesen. Der Lauf endet am Deckel (5000,
gedeckelt wird im Tagesjob gezaehlt und im Log gewarnt), bei einer leeren
Seite oder einer Seite ohne Erfolg; der erste Fehler wird am Ende geworfen.
ungueltigeZellen prueft zuerst das Zeitfenster (auch fuer belegeZellen).
verwaisteZellen, alleZellen und ALLE_ZELLEN_MAX entfallen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 09:44:47 +00:00
ClaudeandClaude Sonnet 5.5 d040a55f89 Terminplaner: CSV-Export seitenweise gestreamt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 09:32:14 +00:00
ClaudeandClaude Sonnet 5.5 a1732613c9 Terminplaner: Loeschung abgeschalteter Funktionen seitenweise, Rest an Folgetagen
loescheAbgeschaltete liest je abgeschalteter Funktion (disabledAt vor 30 Tagen) Seiten zu 100
(neu buchungen.ersteBuchungen: nur $id, limit, nie Gastfelder) und loescht sie ueber arbeiteSeiten
samt Zellen, bis die Seite leer ist. Die Funktion wird nach jedem Lesen einer Seite und alle 50
Buchungen frisch geprueft (nochAbgeschaltet); wird sie wieder eingeschaltet, endet die Loeschung
fuer sie sofort. Deckel config.termine.loeschDeckel je Funktion: am Deckel bleibt die Funktion
disabled mit altem disabledAt, eine Warnung ohne Kennungen steht im Log, der naechste Tag macht
weiter, die uebrigen Funktionen kommen im selben Lauf dran. Die Einstellung (termineConfig) wird
nur nach leerer Seite ohne Fehler und mit letzter Pruefung unter termine-cfg:<id> geloescht.
loescheAbgeschalteteFunktionen liefert { funktionen, buchungen, gedeckelt }; taeglicherJob traegt
gedeckelt.abgeschaltet getrennt vom Loeschzaehler ein (Tagesbericht: Loeschlaeufe am Deckel).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 09:24:33 +00:00
ClaudeandClaude Sonnet 5.5 9b4305e5de Terminplaner: Aufbewahrungs-Loeschung je Funktion mit Deckel, Sicherheitsnetz und Bericht
erledigt/nichterschienen werden jetzt je Funktion (portalFeatures termine, active|disabled)
nach deren aufbewahrungTage seitenweise geloescht; beide Status teilen sich den Deckel
loeschDeckel der Funktion, alle Schritte eines Laufs zusammen loeschDeckelGesamt (neu,
Standard 20000). Ein Sicherheitsnetz loescht erledigt/nichterschienen ohne Funktionsfilter
nach 365 Tagen (Buchungen ohne Funktionsdokument, unlesbare Einstellung). Der Tagesbericht
nennt "Loeschlaeufe am Deckel: n, Rest folgt an den nächsten Tagen" nur bei gedeckelt > 0.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 09:09:48 +00:00
ClaudeandClaude Sonnet 5.5 5c95dfc322 Terminplaner: Aufbewahrungs-Loeschung seitenweise (Grundlagen, verfallen/abgesagt/abgelehnt)
loeschKandidaten liest eine Seite (Standard 100, aelteste Bezugszeit zuerst, optional je Funktion)
ohne Gastfelder; die Frist steht in der Abfrage. arbeiteSeiten (Seite lesen, bearbeiten, neu lesen)
endet bei leerer Seite, am Deckel (config.termine.loeschDeckel, 2000 je Status und Lauf, Rest an den
Folgetagen, gezaehlt als gedeckelt) oder bei einer Seite ohne Fortschritt. verfallen, abgesagt und
abgelehnt laufen darueber, unter der Buchungssperre werden Status und Bezugszeit frisch geprueft.
taeglicherJob liefert gedeckelt getrennt vom Loeschzaehler. Neue Indizes idx_status_ends und
idx_status_updated auf termineBookings (scripts/termine-setup.mjs legt sie an).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 08:55:32 +00:00
ClaudeandClaude Sonnet 5.5 2dd9c0dec7 Appwrite-Abfragen: select, notEqual, countDocuments und Nachbau dazu
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 08:38:21 +00:00
ClaudeandClaude Sonnet 5.5 8c1055e8a0 Kundenansicht: Terminplaner-POSTs nur noch ueber eine explizite Liste
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 08:34:29 +00:00
ClaudeandClaude Opus 5.5 a72380cc8b Terminplaner TP2 Verkauf: Umsetzungsplan (Bloecke A, B, E, C, D, F und Livegang-Checkliste G)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 20:04:49 +00:00
ClaudeandClaude Opus 5.5 6ed0e3bcaf Terminplaner TP2 Verkauf: Design mit den Entscheidungen D1–D8 des Inhabers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 18:17:11 +00:00
ClaudeandClaude Opus 5.5 3a0b0089dd Terminplaner: eigener Betreff fuer die Ablehnung einer Anfrage (G04)
Eingang (G03) und Ablehnung (G04) hatten denselben Betreff "Ihre Terminanfrage bei {betrieb}".
G04 heisst jetzt "Ihre/Deine Terminanfrage bei {betrieb} – leider abgelehnt".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 17:40:52 +00:00
ClaudeandClaude Opus 5.5 5077dd5a20 Mitarbeiter-Zugang: --ohne-buckets (Server-Key ohne buckets-Scope, Buckets dann in der Konsole)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 11:10:09 +00:00
ClaudeandClaude Opus 5.5 10b393687c Mitarbeiter-Zugang: Review-Punkte (fail closed, Kunden-Konto bleibt, Probelauf robuster)
- removeEmployeeEntry: nur 404 heisst "kein Konto", jeder andere Fehler bricht ab
- Mitarbeiter loeschen (Admin-Panel): Konto ohne Mitarbeiter-Label (Kunde mit
  altem employees-Eintrag) bleibt bestehen, nur der Eintrag geht
- Mitarbeiter bearbeiten gibt nie automatisch das Label (der Eintrag kann von
  einem Kunden stammen); nur Neuanlage bekommt "mitarbeiter"
- --rechte: --offen-lassen <ids>, unlesbare Listen als Fehler statt Abbruch
  (--anwenden verweigert dann), --sicherung <datei> + docker-cp-Hinweis,
  Bucket-Einstellung transformations wird mitgeschickt
- getEmployeeByEmail entfernt (unbenutzt)
- Tests: write/users-unverified, zweiter Lauf idempotent, >100 Dokumente,
  Loeschen mit Kunden- und Mitarbeiter-Konto

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 11:00:48 +00:00
ClaudeandClaude Opus 5.5 2659385205 Mitarbeiter nur noch per Label: Kunden-Konten bekommen keinen Team-Zugang
Kunden haben Konten im selben Appwrite-Projekt wie das Ticketsystem. Bisher
galt als Mitarbeiter, wer einen employees-Eintrag hatte - den legte das
Ticketsystem beim Login jedes Kontos automatisch an, und jedes angemeldete
Konto konnte ihn auch selbst anlegen. Damit kam ein Kunde ueber /api/team an
Chats, Kundenansicht, Angebote und Finanzen.

- staffLabels.js: Mitarbeiter = Label "mitarbeiter" oder "admin" (setzt nur
  der Server-Key)
- Team-Eingang (requireStaffJwt) und Portal-Login-Sperre nur noch per Label;
  kein Abgleich ueber employees-Eintrag oder Mail mehr
- Portal-Login: Konto-Abruf gestoert -> 503 fuer alle (fail closed)
- Mitarbeiter anlegen/bearbeiten setzt das Label "mitarbeiter"
- scripts/mitarbeiter-zugang.mjs: --status (markiert employees-Eintraege von
  Kunden), --label, --eintrag-loeschen (nur Eintrag, nie das Konto), --rechte
  (Probelauf, --anwenden mit Sicherung, --zurueck): any/guests/users ->
  label:mitarbeiter + label:admin in Collections, Buckets und bei Dokument-/
  Datei-Sicherheit auch einzelnen Dokumenten und Dateien

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 10:54:09 +00:00
ClaudeandClaude Opus 5.5 be7f9b1aa3 Terminplaner: Test an zweiten absoluten Import von funktionen.js (/geraete.js) angepasst
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 08:18:21 +00:00
ClaudeandClaude Opus 5.5 5bd8ee8457 Terminplaner: Abschluss – kein "null" in abgeschalteten Einstellungen, Kommentar zu disabledAt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 08:17:44 +00:00
ClaudeandClaude Sonnet 5.5 f134ed0032 Terminplaner: Abschluss – Spezifikation und Rechtshinweise an den Stand des Codes angeglichen
- Spezifikation: Abweichungen aus den Abschlussreviews nachgezogen (kontakt_url, kein ETag und formMinSekunden,
  Token-Endpunkte unter /termin/*, Zellen bis blockEndsAt, Outbox-Fristen, Reihenfolge Buchung vor Zellen,
  Widerrufsfrist nach §§ 187/188/193 BGB, Abwesenheit schlägt Sonderöffnung, Kunden-API vollständig,
  Impressum-Adresse) sowie R53 (Wartezeit vor dem Absenden), R54 (Widerruf als Knopf, im Knopfmodus
  dauerhaft) und R55 (Widerruf bei jeder Verbraucher-Buchung, auch Anfragen).
- Neuer Abschnitt "Betrieb und Livegang – Hinweise" in 14 (SESSION_SECRET nie rotieren, Modus off stoppt die
  Löschjobs, Zellbereinigung vor TP2 begrenzen, Warteschlange listet jede Minute alles Fällige).
- Plan: Impressum-Link auf https://webklar.com/impressum.
- Rechtshinweise: "offen 1" durch R14 erledigt, Haken für vorzeitigen Beginn nach R29/R31, Widerrufshinweis
  nach R55 und G02; offene Punkte zu Abo-Ende und Knopfmodus in den Datenschutz-Entwürfen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:44 +00:00
ClaudeandClaude Sonnet 5.5 794a63c0d7 Terminplaner: Abschluss – Client (Formular-Token, Widerruf prominent, Telefonlink, Einbau-Code, G02)
- R53: Das Widget wartet vor jedem Absenden, bis das Formular-Token formMinSekunden plus 0,5 s alt
  ist ("Einen Moment …", Knopf gesperrt); gilt nach dem Neuladen der Konfiguration und beim Wiederholen
  nach "neu_laden". Kein stilles Erfolgs-201 ohne Buchung mehr.
- R54: "Vertrag widerrufen" im Fuß ist ein zweiter Knopf (44 px). Im Knopfmodus lädt das Widget die
  Konfiguration beim Laden der Seite (ein Abruf) und zeigt bei Widerrufsrecht dauerhaft einen zweiten
  Knopf "Vertrag widerrufen" neben dem Buchungsknopf, der den Dialog beim Widerrufsformular öffnet.
- Telefonlink im Widget und auf der Gästeseite: Text mit Buchstaben oder Mehrfachangaben wird nie zum
  tel:-Link, "(0)" hinter der Landesvorwahl entfällt.
- Portal, Einstellungen: Kasten "Einbau auf deiner Website" mit Einbau-Code und Kopierknopf; Hinweistexte
  zum Widerruf passen zu R55 (auch Anfragen).
- G02: Widerrufssatz nennt, dass der Knopf bis zum Beginn des Termins gilt und danach das Formular auf der
  Website oder die E-Mail an den Betrieb.
- Einbau-Anleitung angepasst (siteKey und Code im Portal, Knopfmodus, Checkliste); Tests und Playwright.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:44 +00:00
ClaudeandClaude Sonnet 5.5 bd65c9dc58 Terminplaner: Abschluss – Server (Widerruf auch bei Anfragen, Gastdaten löschen vollständig, formMinSekunden)
- R55: Widerrufsbelehrung, Anschrift-Pflicht und Knopf "Vertrag widerrufen" gelten für jede
  Verbraucher-Buchung mit sichtbarer Leistung, auch Anfragen (zeigtWiderruf); der Nachweis
  hält die Belehrung auch bei Anfragen fest. Damit bleibt die Annahme einer Anfrage nie mehr
  still ohne G02.
- "Gastdaten löschen" leert auch absageGrund und löscht noch nicht versandte Mails der Buchung
  (neu: verwerfeWartende in der Warteschlange).
- R53: GET /:siteKey/config nennt formMinSekunden.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:44 +00:00
ClaudeandClaude Sonnet 5.5 4be886a4de Terminplaner: Portal-Ansicht – Fokus und aria-busy
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:44 +00:00
ClaudeandClaude Sonnet 5.5 3ad056f261 Terminplaner: Portal-Ansicht Einstellungen – Nachbesserungen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:44 +00:00
ClaudeandClaude Sonnet 5.5 0b75b31009 Terminplaner: Portal-Ansicht Leistungen, Team, Einstellungen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:44 +00:00
ClaudeandClaude Sonnet 5.5 650919b022 Terminplaner: Portal-Ansicht – Nachbesserungen (2/2)
- Annehmen mit Rueckfrage in der Seite (schliesst den Vertrag, schickt G02)
- Offene Anfragen: alle bis Ende des Buchungszeitraums (Fenster zu je 62 Tagen,
  hoechstens 6 Aufrufe), passend zur Zahl auf Kachel und Banner
- Meldung ins Bild geholt, Fehler mit Fokus; einzige Live-Region
- Netzwerkfehler auf Deutsch; Antwort einer frueheren Website erscheint nie
  unter der neuen (Zaehler je Aufruf); Datumssprung nur mit echtem Tag
- Ende um Mitternacht als 24:00, Statusfilter nach Eintragen auf Alle,
  abgeschaltet ohne Einstellung mit Export-Link, dunkles Datumsfeld
- Playwright: Zeitzone New York/Berlin, ferne Anfragen, Projektwechsel,
  Meldung im Bild, Datumssprung, abgeschaltet ohne Einstellung

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:44 +00:00
ClaudeandClaude Sonnet 5.5 9c07f6a8c5 Terminplaner: Portal-Ansicht – Nachbesserungen (1/2: Module getrennt)
public/termine.js in Huelle und Module zerlegt, Verhalten unveraendert (R48):
termine-dom.js (Helfer ohne Zustand), termine-tab-termine.js (Liste, Kopf, Zeilen),
termine-aktionen.js (Aktionen mit Rueckfrage), termine-formulare.js (Uhrzeiten,
Termin eintragen, Zeit sperren). Reiter bekommen Zustand und aktuell() nur ueber
ctx; die Huelle haengt sie ueber die Registry REITER ein. Statiktest prueft alle
public/termine*.js.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:44 +00:00
ClaudeandClaude Sonnet 5.5 43e4826ae3 Terminplaner: Portal-Ansicht, Kachel, Termine
Kachel in Funktionen (Kommt bald / Aus / Aktiv mit Anfragen-Zahl / Abgeschaltet),
Ansicht #termine als Unterseite von Funktionen: Projektauswahl, Vorlagenauswahl
beim ersten Oeffnen, Reiter-Geruest (Platzhalter fuer Leistungen, Team & Plaetze,
Einstellungen) und der Reiter Termine mit Tag/Woche/Monat, Filtern, Aktionen mit
Rueckfragen in der Seite, Termin eintragen und Zeit sperren.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:44 +00:00
ClaudeandClaude Sonnet 5.5 70940e02c8 Terminplaner: Gaesteseiten – Nachbesserungen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:44 +00:00
ClaudeandClaude Sonnet 5.5 734e397e73 Terminplaner: Gaesteseiten Bestaetigen, Verwalten, Widerrufen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:44 +00:00
ClaudeandClaude Sonnet 5.5 781eccb2d6 Terminplaner: Einbau-Skript – veraltetes Angebot
Fehler an leistung/ressource (beim Absenden und bei Tage/Zeiten) laden zuerst die Konfiguration
neu (ladeNeu): gilt die Auswahl nicht mehr, beginnt die Strecke von vorn mit Hinweis; sonst
steht die Meldung des Servers. Knopfmodus: Aufbaufehler zeigt den Ausweichtext statt "Wird
geladen".

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:44 +00:00
ClaudeandClaude Sonnet 5.5 4465ac7b94 Terminplaner: Einbau-Skript – Nachbesserungen
429 ohne Schleife (Meldung und Erneut-versuchen-Knopf, Formulare bleiben stehen), Schriftfarbe
auf der Betriebsfarbe mit mindestens 4,5 : 1, AGB-Link aus der oeffentlichen Konfiguration
(betrieb.agbUrl), Haken starten bei geaenderter Auswahl ohne Haken, Fehler ohne Feld nicht
verschluckt, Konfiguration fail closed, ladeNeu fuer Neuladen und neu_laden, Tastatur und
Fokus (Modifikatoren, Monatspfeile, Belehrungstext), Aufraeumen. UI-Testserver: Timer,
offene Verbindungen, Hinweis auf sessionCookie.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:44 +00:00
ClaudeandClaude Sonnet 5.5 5fef7e47f0 Terminplaner: Einbau-Skript, Demo und Einbauanleitung
public/embed/termine.js: Buchungs-Widget fuer Kundenwebsites (inline und Knopf mit dialog,
Monatsraster nach APG, Haken-Texte und Knopf aus der Konfiguration, Honigtopf kontakt_url,
Widerrufsfunktion, Konfiguration nach 100 min neu, Ausweichtext bei Fehlern). Rechnet nie mit
Datum oder Zeitzone des Browsers. Dazu Demo-Seite, Einbauanleitung fuer Mitarbeiter, statischer
Test und Playwright-Skript mit wiederverwendbarem UI-Testserver (test/ui/termineTestServer.mjs).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Sonnet 5.5 bb45d35ec7 Terminplaner: Kunden-Schnittstelle – Nachbesserungen
- GET /zeiten?ohne=<Buchung>: Leistung, Dauer und Person der Buchung wie beim
  Verschieben (verschiebeZeiten exportiert), unbekannte/fremde/Sperre 404 (R42)
- Schreib-Begrenzung als gemeinsamer Baustein (writeLimiter.js) fuer News und
  Terminplaner statt kopierter Funktion (R43)
- Unlesbares Einstellungs-JSON: Kontext traegt die echte Version, Speichern
  ueberschreibt den Schaden statt dauerhaft 409
- U+FEFF als Escape statt als rohes Zeichen
- Tests: Mandantentrennung mit echtem zweitem Kunden, R42, Limiter, kaputtes JSON

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Sonnet 5.5 e3e198fb1a Terminplaner: Kunden-Schnittstelle
/api/termine: Einstellungen (Vorlage, PATCH mit Version, Feed-Link), Buchungen
(Liste, Zeiten, von Hand, Sperre, Aktionen), CSV-Export, Protokoll der
Kundenansicht. Pflichtangaben fuer Gaeste (Telefon immer, Anschrift bei
Widerrufsbelehrung) in normalisiereEinstellung, Entwurf aus Vorlagen,
kontextPerSiteKey ohne Pflichtangaben nicht buchbar; speichereEinstellung
nimmt eine Funktion unter der Sperre.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Sonnet 5.5 9e8fd33713 Funktionen: Terminplaner als zweite Funktion (Team-Route, Kundenansicht)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Sonnet 5.5 ae1886c030 Terminplaner: Zaehler je IP getrennt und Abgelaufene zuerst, Notantwort beim Schreiben, Pruefung aller Antworten auf interne IDs
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Sonnet 5.5 2b0c05b8e9 Terminplaner: oeffentliche Schnittstelle v1 mit Missbrauchsschutz
Router /api/public/termine/v1 (config, tage, zeiten, buchungen, widerruf, Token-Seiten /termin/*, Kalender-Abo) mit
eigenem 16-KB-Parser und Fehlermiddleware, CORS nach Herkunft, Honeypot und Formular-Token, Grenzen je IP (IPv6 je /64),
siteKey und Adresse in limits.js, Mindestantwortzeit (TERMINE_MIN_ANTWORT_MS) und 10-Sekunden-Cache fuer tage/zeiten.
Vor dem globalen express.json eingehaengt; /api/public/ ist vom Kundenansicht-Waechter ausgenommen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Sonnet 5.5 43ac05aa06 Terminplaner: Jobs lesen abgeschaltete Funktionen vor dem Löschen neu, Tagesbericht im Log
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Sonnet 5.5 485ff3bc15 Terminplaner: Hintergrund-Jobs
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Opus 5.5 efe17551c2 Spec/Plan: Zustimmung vorzeitiger Beginn und Widerrufsfrist nach §§ 187/188/193 BGB (Rulings R29, R31)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Sonnet 5.5 589f5088a6 Terminplaner: Handeintraege verlangen nur den Namen, Bestaetigung braucht eine Adresse
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Sonnet 5.5 be49d38e0c Terminplaner: Aktionen des Betriebs, manuelle Termine, Sperren
Neues Modul ablaufBetrieb.js: aktion (annehmen, ablehnen, absagen, verschieben,
nichterschienen, anonymisieren), manuell, sperre, entsperre. ablauf.js exportiert
die gemeinsamen Bausteine (Pruefungen, Meldungen, mailSicher, pruefeSlot,
verschiebeKern). R31: brauchtZustimmungVorzeitig folgt der Berliner Widerrufsfrist
(withdrawalDeadline ab now + Haltezeit); bei einer Anfrage mit Preis immer.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Sonnet 5.5 c4ec24448e Terminplaner: Fix Widerruf nach Terminbeginn, Zustimmung beim Verschieben, Berliner Widerrufsfrist, Grenzen nur ohne Treffer
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Sonnet 5.5 2d9b75b3bf Terminplaner: Absagen, Verschieben, Widerruf durch Gaeste
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Sonnet 5.5 1289b5697b Terminplaner: Bestaetigung prueft vorher, dass die Zellen der Haltung ihr noch gehoeren
bestaetige zieht unter der Buchungssperre fehlende eigene Zellen nach (belegeZellen mit ausser). Hat ein anderer
Gast sie uebernommen, wird die Buchung verfallen und es kommt dieselbe 410-Antwort wie bei abgelaufener Haltung,
ohne Mails.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Sonnet 5.5 d1470b894a Terminplaner: Buchung halten und bestaetigen
Gast-Ablauf in ablauf.js: halte (Eingabe pruefen, Slot neu berechnen, Buchung gehalten vor den Zellen,
Grenze offener Termine mit G01 zu_viele), vorschau (Bestaetigungs- und Verwaltungstoken) und bestaetige
(gehalten -> bestaetigt/angefragt, G02/G03 und K20). einstellungen.js exportiert text und emailAdresse,
mails.js dauerText zur Wiederverwendung.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Sonnet 5.5 8e25cd2d13 Terminplaner: websiteUrl verweist auf die Live-Domain statt auf die Vorschau
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Sonnet 5.5 30d40ad6ce Terminplaner: Kontext, Einstellungs-Speicher, Mail-Werte
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Sonnet 5.5 af4029af81 Terminplaner: Mail-Warteschlange mit Stundenbudget
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Sonnet 5.5 70b904260a Terminplaner: G08 Satz ohne Widerspruch zu zugeordnet, Fristhinweis 'Nach unseren Unterlagen'
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Sonnet 5.5 6c25a76e28 Terminplaner: G01 Block bestaetigen, G02 kein_widerruf, G06 grund/nachricht, Hinweise zu Blockpaaren
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Sonnet 5.5 274192e88e Terminplaner: Mailvorlagen G01–G08, K20, K21
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Sonnet 5.5 171b5086b1 Terminplaner: ICS-Faltung prueft Surrogatpaare statt wirkungslosem UTF-8-Decode
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Sonnet 5.5 181445a69d Terminplaner: Kalenderdatei, Gaesteversand, Vorlagen-Engine fuer G-Vorlagen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Sonnet 5.5 0c033ab1d2 Terminplaner: Widerrufsfunktion im Satzrahmen des Musters, Belehrung nur mit Anschrift und Telefon
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Sonnet 5.5 459efff10a Terminplaner: Rechtstext-Bausteine fuer Gaeste (Entwurf)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Sonnet 5.5 9ad8515d60 Terminplaner: Zellen nur unter der Ressourcensperre freigeben, gemeinsame Hilfsfunktionen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Sonnet 5.5 e281db1b2b Terminplaner: Zellen gegen Doppelbuchung, Tokens, Buchungsspeicher
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Sonnet 5.5 4def5c735d Terminplaner: Verfuegbarkeit, Abwesenheit schlaegt Sonderoeffnung, waehleRessource ignoriert abgelaufene Haltungen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Sonnet 5.5 cce6e64dd1 Terminplaner: Verfuegbarkeit
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Sonnet 5.5 759be69250 Terminplaner: Einstellungen, URL-Host einschraenken und unsichtbare Formatzeichen entfernen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Sonnet 5.5 9cacd5bd3b Terminplaner: Einstellungen und Branchen-Vorlagen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Sonnet 5.5 310f8434d6 Terminplaner: Zeit (Berlin, Umstellung) und Feiertage
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Sonnet 5.5 a5e12d7f34 Appwrite: Bulk-Create, Bereichsabfragen, seitenweises Laden, genauerer Nachbau
appwriteAdmin: adminFetch kennt die Option raw (Body unverpackt), createDocuments
(Bulk-Create, alles oder nichts, 409 bei vorhandener ID), listAllDocuments (seitenweise
per cursorAfter, sortiert nach $id, Fehler ueber max) und Query.between/greaterThanEqual/
lessThanEqual/cursorAfter.

Nachbau: Bulk-Create mit Alles-oder-nichts (409/400, hoechstens 100), die neuen Abfragen
samt cursorAfter, Zeitpunktvergleich fuer ISO-Datumswerte, Optionen datetimes (Normalisierung
auf +00:00, leeres Datum => 400) und defaultLimit (Standard-Limit wie Appwrite). Ohne die
Optionen unveraendert. testApp exportiert send(method, path, opts).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Opus 5.5 334dc1e669 Terminplaner: Schalter, Konfiguration, Schema
Schalter TERMINE_ENABLED (off|pilot|on) mit TERMINE_PILOT_EMAILS, config.termine
und die vier Collection-IDs, Schema der Termine-Collections als reine Daten
(server/services/termine/schema.js), Setup-Skript scripts/termine-setup.mjs mit
--check und schemaKeys() fuer den Appwrite-Nachbau der Tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Opus 5.5 f63165c158 Rechtliches: Anlage 1 und 2 zu Art. 246a EGBGB als Quelle fuer die Terminplaner-Widerrufsbelehrung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Opus 5.5 f28060198c Plan TP1: Funde der Planpruefung eingearbeitet (Einstellungsfeld, Verwaltungstoken, Verschieben, Testbarkeit, Missbrauchsschutz)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Opus 5.5 e3ff05c804 Plan: Terminplaner TP1 Kern (17 Aufgaben + Livegang), Spec-Feinschliff
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Opus 5.5 275dbcd690 Spec: Terminplaner (TP1 Kern, TP2 Verkauf, TP3 Online-Zahlung)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00
ClaudeandClaude Opus 5.5 d8328722b3 Zusatzseite Website-Analyse: Keks-Symbol bei Widerruf und Widerspruch genannt
Der Knopf zum Wiederoeffnen des Banners ist jetzt ein Keks-Symbol. Die Zusatzseite
(runner/src/templates/datenschutz-analyse.html) sagt deshalb "Klicke auf
„Datenschutz-Einstellungen“ (das Keks-Symbol, in der Regel unten rechts auf jeder Seite)"
und im Widerspruchsrecht "über „Datenschutz-Einstellungen“ (das Keks-Symbol)".

Fassung bleibt 2026-10-02 (Banner-Text unveraendert). Archiv: neue Seite unter
datenschutz-analyse-2026-10-02.html, die vorige Formulierung (nur auf der WEBklar-Testseite
ausgeliefert) als datenschutz-analyse-2026-10-02-erstfassung.html; README und Banner-Archiv
beschreiben die Aenderung. Die Leistungsbeschreibung bleibt unveraendert ("Schaltflaeche
„Datenschutz-Einstellungen“ unten rechts" stimmt weiter).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:24:07 +00:00
ClaudeandClaude Opus 5.5 0a113c2983 Heatmap in Geraeterahmen (Computer/Tablet + Handy) und Keks statt "Datenschutz-Einstellungen"
Heatmap im Portal (Funktionen -> Analyse): die echte Website laeuft jetzt in realistischen
Geraeterahmen, gross MacBook oder iPad (Umschalter oben), daneben das iPhone, jeweils in echter
Breite (1512 / 820 / 402 px), verkleinert und im Geraet scrollbar, mit den Klicks der
passenden Geraeteklasse als Overlay. MacBook und iPhone stammen aus den Rahmen der neuen
Website (device-mockups), das iPad ist im gleichen Stil nachgebaut (public/geraete.css/.js).

- heatmap-link: embed: true haengt __wk_embed=1 an; w.js reicht das als data-embed an h.js.
- h.js eingebettet (nur in einem Rahmen): keine Leiste, Links und Formulare gesperrt (kein
  Seitenwechsel, also keine Zaehlung und kein Banner), Scrollleisten aus, Klickzahl per
  postMessage nur an die Herkunft von h.js (project.webklar.com).
- iframe mit Sandbox allow-scripts allow-same-origin (keine Formulare, Pop-ups, keine
  Navigation des Portals); Laden erst, wenn die Heatmap in Sicht kommt. Laesst sich eine
  Website nicht einbetten, kommt nach 20 s ein Hinweis mit "In neuem Tab oeffnen" (alter Weg).

Banner-Skript: der Einstellungsknopf nach der Wahl ist jetzt ein angeknabberter Keks unten
rechts (Inline-SVG, 44 px). Er dreht sich langsam, haelt beim Darueberfahren/Fokus an und
zeigt dann "Datenschutz-Einstellungen" (auch sein Name fuer Screenreader, passend zum
Bannertext); bei reduzierter Bewegung dreht er sich nicht.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:09:17 +00:00
ClaudeandClaude Opus 5.5 ff7fe35c06 Rechnungen: Stripe-Rechnungen im Kundenportal anzeigen (Abo, Funktionen, Projektrechnungen)
Der Tab "Rechnungen" las nur InvoiceNinja. Seit dem Finanzen-Livegang laufen Rechnungen
aber ueber Stripe; die bezahlte Abo-Rechnung eines Kunden fehlte deshalb im Portal.

- /api/invoices fuehrt Stripe (alle Stripe-Kunden des Kunden: Abo-Spiegel und Suche nach
  metadata.webklarCustomerId) und InvoiceNinja zusammen, neueste zuerst; Entwuerfe und
  0-EUR-Rechnungen bleiben verborgen. Faellt eine Quelle aus, kommt der Rest mit incomplete.
- /api/invoices/:id/pdf laedt Stripe-PDFs frisch ueber den Server, nur fuer eigene Rechnungen
  (sonst 404) und nur von *.stripe.com.
- Seite: Status-Farben fuer Bezahlt/Offen/Ueberfaellig/Storniert, "Ansehen" statt
  "Ansehen / Zahlen" bei bezahlten Rechnungen, Hinweis bei unvollstaendiger Liste; der
  Abo-Hinweis verweist nur noch fuer die Zahlungsart auf "Abo verwalten & Rechnungen".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 09:33:22 +00:00
ClaudeandClaude Opus 5.5 df8b952b81 Alte Mail-Links aufs Admin-Panel leiten ins Ticketsystem (Antrag direkt geoeffnet)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 04:05:17 +00:00
ClaudeandClaude Opus 5.5 95e2807dc7 Team-Mails: Links ins Ticketsystem statt Admin-Bereich
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 03:52:54 +00:00
ClaudeandClaude Opus 5.5 7c00633db0 Portal: Admin-Panel, Rechnungsanalyse und alte Admin-Schnittstellen entfernt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 03:50:14 +00:00
ClaudeandClaude Opus 5.5 3a4fa581c0 Portal: Mitarbeiter-Login gesperrt (Hinweis aufs Ticketsystem), alte Mitarbeiter-Sitzungen enden
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 03:44:10 +00:00
ClaudeandClaude Opus 5.5 d0ca3c41cd Plan: Admin-Umzug Etappe 4 (Abschalten)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 03:32:42 +00:00
ClaudeandClaude Opus 5.5 40f0a04908 Gutschriften: Entscheidung und Verrechnung je Antrag gesperrt (gleichzeitige Klicks zweier Mitarbeiter)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 03:15:25 +00:00
ClaudeandClaude Opus 5.5 45203284ee Abo-Status eines Kunden fuer alle Mitarbeiter im Team-Eingang
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 02:52:38 +00:00
ClaudeandClaude Opus 5.5 0fdd13246c Gutschriften: signierte Links fuer Nachweise im Ticketsystem
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 02:51:49 +00:00
ClaudeandClaude Opus 5.5 7d93ae0cd9 Team-Eingang: Gutschriften, Angebote, E-Mail-Speicher und Funktionen fuer alle Mitarbeiter, Kundenfilter, Zaehlroute
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 02:50:55 +00:00
ClaudeandClaude Opus 5.5 a06c3bc8ef Plan: Admin-Umzug Etappe 3 (Gutschriften, Abo & Angebote, E-Mail-Speicher, Funktionen)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 02:28:18 +00:00
ClaudeandClaude Opus 5.5 a6ac4510cd Chat-Baustein: Option requestHeaders fuer Hochladen und Entfernen (Ticketsystem mit JWT)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 23:30:29 +00:00
ClaudeandClaude Opus 5.5 48dd19b75a Chat ueber den Team-Eingang: gemeinsamer Router fuer Admin-Panel und Ticketsystem, signierte Anhang-Links
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 23:29:05 +00:00
ClaudeandClaude Opus 5.5 5f5df7ba1a Signierte Datei-Links (15 min) fuer Anhaenge im Ticketsystem
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 23:27:38 +00:00
ClaudeandClaude Opus 5.5 acf6ff9ee1 Plan: Admin-Umzug Etappe 2 (Chat)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 23:10:48 +00:00
ClaudeandClaude Opus 5.5 6718018211 Verwaiste Repo-Platzhalter blockieren keine Zuordnung mehr
Ein in Gitea geloeschtes Org-Repo liess sein unverknuepftes Projekt-Dokument
liegen. Der Unique-Index auf repoFullName ignoriert Gross-/Kleinschreibung,
darum scheiterte beim Zuweisen eines gleich benannten neuen Repos das
Umbenennen des Projekts; alle Pushes landeten im Platzhalter ohne Ticket,
also ohne Arbeitsblatt und ohne Push-Zeitmail (WEBklar/Schmacko-napf).

- repoMove: vor dem Umbenennen ein unverknuepftes fremdes Dokument mit dem
  Zielnamen entfernen (verknuepfte bleiben unangetastet)
- Webhook repository/deleted: unverknuepfte Platzhalter des geloeschten
  Repos entfernen, nur wenn Gitea das Repo wirklich nicht mehr kennt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 21:58:11 +00:00
ClaudeandClaude Opus 5.5 a64a996291 Kundenansicht: Waechter-Korrekturen aus dem Abschlussreview
- Pfade ohne Ruecksicht auf Gross-/Kleinschreibung pruefen (/API/news umging die Sperre)
- Tab mit Kennung, Cookie ist keine Kundenansicht mehr -> 409 statt fremder Daten
- Cookie lebt 1 h laenger als die Ansicht, damit 'abgelaufen' gemeldet und protokolliert wird

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 19:54:50 +00:00
ClaudeandClaude Opus 5.5 0ccdde1c28 Datenschutz Abschnitt 6: Kundenansicht mit News, Sperren und Protokoll (12 Monate)
Stand 01.10.2026. Der Test koppelte den Stand bisher an termsVersion; jetzt
genuegt 'nicht vor termsVersion', weil die Erklaerung unabhaengig geaendert wird.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 19:38:14 +00:00
ClaudeandClaude Opus 5.5 c1fda8a717 Kundenansicht im Dashboard: Kennung bei jeder Anfrage, Banner mit Beenden, Hinweis bei ersetzter oder abgelaufener Ansicht
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 19:36:55 +00:00
ClaudeandClaude Opus 5.5 c5afd42a5f Kundenansicht: Beenden mit Rueckweg ins Ticketsystem, /me mit Ansichts-Kennung, Team-Routen Portal-Status, Protokoll, Preview-Link
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 19:32:59 +00:00
ClaudeandClaude Opus 5.5 7d8ba56088 Kundenansicht: News-Aenderungen mit Mitarbeiter und Titel protokollieren
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 19:30:47 +00:00
ClaudeandClaude Opus 5.5 25f3c1df4d Kundenansicht: zentraler Waechter (2 h, Verwechslungsschutz, Schreibsperre ausser News)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 19:29:47 +00:00
ClaudeandClaude Opus 5.5 1cd776a2ee Kundenansicht: Einmal-Link aus dem Ticketsystem, Sitzung und Preview-Zugang fuer 2 Stunden, Protokoll
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 19:27:21 +00:00
ClaudeandClaude Opus 5.5 fdf594706d Kundenansicht: Protokoll-Collection customerViewLog mit Schreiben, Lesen und Loeschen nach 12 Monaten
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 19:25:26 +00:00
ClaudeandClaude Opus 5.5 772095ab19 Team-Eingang: /api/team mit Appwrite-JWT fuer alle Mitarbeiter, CORS-Header fuer Uploads
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 19:23:57 +00:00
ClaudeandClaude Opus 5.5 be65baff01 Plan: Admin-Umzug Etappe 1 (Fundament + Kundenansicht)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 19:19:57 +00:00
ClaudeandClaude Opus 5.5 5fb39f1a1b Spec: Admin-Panel ins Ticketsystem (Kundenansicht, Protokoll, Team-Schnittstelle, 4 Etappen)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 19:11:32 +00:00
ClaudeandClaude Opus 5.5 546d3c84d0 Export: Formelschutz nur fuer Text (inkl. Tab/CR), CSV mit Rechten 0600, Runbook
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 18:50:36 +00:00
ClaudeandClaude Opus 5.5 495c3a2c40 Analyse: Abgleich schaltet neu aktivierte Analysen nicht ab (Sperre + Nachlesen), weitere Abgleich-Tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 18:50:36 +00:00
ClaudeandClaude Opus 5.5 78170567b4 Analyse: alle Beendigungswege schalten den Messdienst ab (Kunde/Projekt weg, Domain, Löschfrist, Widerruf Wechsel)
Messdienst: POST /a/admin/sites/:key/off setzt active=0 und offSince einmalig; Portal-Client stopSite.
analyse.js: endAnalyse als gemeinsamer Kern, endAnalyseForCustomer, analyseOnProjectChanged,
analyseDisabled (Sperre/Löschfrist) und täglicher Abgleich runAnalyseReconcile (Projekt/Kunde weg,
Domains nachziehen, K18-Wiederholung, Messdienst für off/disabled, fehlende Entfernen-Tickets nachholen).
Entfernen-Ticket je Abschaltung gekennzeichnet (kein Doppelticket, auch wenn das Speichern der ID scheiterte).
Hooks: Kunde löschen, updateWebsiteProject, withdrawOrder (Wechsel), siteFeatures.disable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 18:50:36 +00:00
ClaudeandClaude Opus 5.5 fbc40252a3 Analyse: K18 mit PDFs und Nachweis, K19 neu, Tickets für Einschalten, Ausschalten und Veröffentlichen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 18:50:36 +00:00
ClaudeandClaude Opus 5.5 3bac4093af Analyse: Portal-Texte, Verantwortlichen-Vorschau und Bestätigungen nach geprüftem Wortlaut
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 18:50:36 +00:00
ClaudeandClaude Opus 5.5 ddb91a9950 Analyse: geprüfte Rechtstexte Fassung 2026-10-02 veröffentlichen (HTML, PDF, Zusatzseiten-Vorlage, Archiv)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 18:50:36 +00:00
ClaudeandClaude Opus 5.5 f401dc14d7 Analyse: Zielgruppen-Bestätigung in Historie, Rücknahme bei Fehler nach Dokument-Update, handelnde Person mit Name und E-Mail
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 18:50:36 +00:00
ClaudeandClaude Opus 5.5 3d05472b25 Analyse: Einwilligungs-Historie, Nachweis beim Einschalten, Sperre ohne Verantwortlichen-Angaben, Textform für Mitarbeiter
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 18:50:36 +00:00
ClaudeandClaude Opus 5.5 c6824f83a6 Analyse: Skript nur für aktive Websites, leeres Skript sonst, siteKey in der Skript-URL
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 18:50:26 +00:00
ClaudeandClaude Opus 5.5 97890a8eee Analyse: Tageswerte nach 25 Monaten löschen, secure_delete, CSV-Export
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 18:50:26 +00:00
ClaudeandClaude Opus 5.5 a73e0be952 Analyse: Messdienst: Heatmap-Pfad wie gespeicherter Pfad, Tests für Kodierung und Stunde
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 18:50:26 +00:00
ClaudeandClaude Opus 5.5 96409c6306 Analyse: Messdienst: Ping ohne Einwilligung, Zeitstempel auf Stunde, Pfade ohne Kennungen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 18:50:26 +00:00
ClaudeandClaude Opus 5.5 8d64a18d35 Analyse: Client prüft Einwilligung vor dem Senden, Fassung der Banner-Wahl, fetch ohne Cookies
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 18:50:26 +00:00
Claude c2cbc4156f Plan: Website-Analyse Pflicht-Aenderungen aus der Rechtspruefung 2026-10-01 18:50:26 +00:00
ClaudeandClaude Opus 5.5 9449542686 Rechtliches: Entwurf AVV Website-Analyse aus public/ entfernen (bis zur geprueften Fassung)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 15:50:40 +00:00
ClaudeandClaude Sonnet 5.5 c0c1a3d7a9 Finanzen: Laendersuche ohne Prototypschluessel, beim Anlegen des Stripe-Kunden keine leeren Adressfelder
- stripeCountry: Object.hasOwn statt Zugriff ueber die Prototypkette ('constructor', '__proto__' ergaben ein Objekt als Land)
- stripeCustomerFields(customer, { forCreate }): beim Anlegen (getOrCreateStripeCustomer) fehlen leere PLZ/Ort ganz;
  das Loeschen per '' bleibt beim Abgleich (Aktualisieren)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 15:42:06 +00:00
ClaudeandClaude Sonnet 5.5 f0b57ebe41 Finanzen: Nachtrag vor Livegang (Kundenabgleich beim Senden, Adress-Sperre, Land und Anschrift sauber an Stripe)
- sendInvoiceNow: Entwurf mit webklarCustomerId laedt den WOMS-Kunden und bringt den Stripe-Kunden vor dem
  Finalisieren auf die aktuellen Daten (Fehler: 502, nichts finalisiert); ohne Kennung kein Abgleich
- Adress-Sperre (§ 14 Abs. 4 Nr. 1 UStG): Senden ueber 250 € ohne Strasse/PLZ/Ort -> 409 vor jedem Stripe-Schreibzugriff
  (Anlegen mit senden=true und Entwurf senden; Text und Grenze wie im WOMS)
- stripeCustomerFields: Land aus deutschen Namen (unbekannt -> weglassen), PLZ/Ort mit Strasse immer mitsenden ('' loescht)
- Stripe-Nachbau: Kunden-Update fuehrt die Anschrift feldweise zusammen, '' loescht ein Feld

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 15:42:06 +00:00
ClaudeandClaude Sonnet 5.5 73811e0300 Finanzen: Stripe-Kunde vor der Rechnung auf die aktuellen Kundendaten bringen
Ein wiederverwendeter Stripe-Kunde (ueber das Abo oder die Metadaten gefunden) trug
noch die alte E-Mail und Anschrift, die Stripe auf die Rechnung schreibt
(Empfaengeranschrift, § 14 Abs. 4 Nr. 1 UStG). createProjectInvoice aktualisiert ihn
jetzt nach getOrCreateStripeCustomer und vor dem ersten Rechnungsaufruf mit E-Mail,
Name (Firma vor Name) und Anschrift (Land DE, wenn keine zweistellige Angabe).
Scheitert das, entsteht keine Rechnung: 502 "Kundendaten konnten nicht an Stripe
uebertragen werden." ohne invoiceId. Die Felder baut stripeCustomerFields fuer das
Anlegen und das Aktualisieren gemeinsam. Der Stripe-Nachbau kennt POST /v1/customers/:id.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 15:42:06 +00:00
ClaudeandClaude Sonnet 5.5 5ed4fbd188 Finanzen: Setup-Skript mit --iban und strenger Pruefung von --valid-until, Enable-Banking-Variablen in .env.example
M5: --iban speichert iban und ibanMasked (erste und letzte vier Zeichen); --valid-until nur als Datum
oder voller ISO-Zeitpunkt mit Zeitzone, sonst Abbruch.
M16: ENABLE_BANKING_APP_ID und ENABLE_BANKING_KEY_PATH auskommentiert dokumentiert.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 15:42:06 +00:00
ClaudeandClaude Sonnet 5.5 039fdcaf27 Finanzen: Vorschau, Steuer und Aufgaben genauer (Fixkosten ohne ESt, verspaetete Auszahlungen, Abos nicht ladbar)
M6: fixkosten30Cents zaehlt nur Fixkosten, die ESt-Vorauszahlung bleibt sicherer Posten.
M7: verspaetete Auszahlung (Ankunftstag vorbei, noch nicht bezahlt) kommt heute.
M8: ohne Stripe sind Kleinunternehmer-Zaehler und Anteil unbekannt (null), keine Warnung daraus.
M12: Verbindungsdokument ohne Sitzung zaehlt als keine Verbindung.
M13: Buchungen privat/fixkosten gehen nicht in die Fixkosten-Vorschlaege.
M14: Vorjahresumsatz ueber 25.000 EUR ist eine hohe Aufgabe.
M15: scheitert das Laden der Abos, meldet abos.fehler statt die ganze Uebersicht zu kippen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 15:42:06 +00:00
ClaudeandClaude Sonnet 5.5 83ee9d5a25 Finanzen: Zuordnung zu Rechnungen robuster (Speicherfehler nach pay, Doppelzuordnung, Protokoll)
M1: scheitert Appwrite nach dem Bezahlen, kommt 502 mit klarer Meldung statt eines blanken 500.
M2: Wiederaufnahme nur bei passendem bezahltem Betrag; eine Buchung, die schon an einer anderen
bezahlten Rechnung haengt, wird nicht ein zweites Mal vermerkt und bezahlt (409).
M3: Protokollzeile je erfolgreicher, nicht umkehrbarer Zuordnung (Buchung, Rechnung, Nutzer).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 15:42:06 +00:00
ClaudeandClaude Sonnet 5.5 daa451094a Finanzen: Zeitgrenzen fuer Stripe-Lesen, Schutz vor doppelt angelegten Rechnungen
I2: Leseanfragen an Stripe mit timeout 15 s und einer Wiederholung (auch Folgeseiten), die Uebersicht
wartet hoechstens 20 s auf Stripe und rechnet sonst ohne Stripe weiter.
I3: idempotencyKey vom Aufrufer an stripe.invoices.create, Schutz im Prozess gegen gleichzeitiges und
30 s spaeteres Doppelanlegen (409), 502 mit invoiceId/Nummer wenn die fertige Rechnung nicht lesbar ist.
M9: Warnung, wenn die Rechnungsliste bei 1000 abgeschnitten wird.
M11: Ursache eines 502 an einer bestehenden Rechnung (Art, Status, Code) im Protokoll.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 15:42:06 +00:00
ClaudeandClaude Sonnet 5.5 ef2ce49669 Finanzen: Start-Abruf nur bei altem Stand, unlesbare Buchungen ueberspringen, Mail bei Widerruf des Bankzugangs
I1: shouldRunStartupSync; der Abruf 2 Minuten nach dem Start entfaellt, wenn der letzte erfolgreiche
Abruf weniger als 5,5 Stunden her ist (Deployments verbrauchten das Tageslimit der Bank).
M4: eine unlesbare Buchung wird uebersprungen und gezaehlt, der Abruf endet ok.
M10: 401/403 beim Abruf sendet T18 einmal (Stufe 0 in warnedThresholds).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 15:42:06 +00:00
ClaudeandClaude Sonnet 5.5 3c5e06723f Finanzen: Uebersicht und Rechnungsrouten fuer WOMS
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 15:42:06 +00:00
ClaudeandClaude Sonnet 5.5 7dbf2ce75f Finanzen: Steuerruecklage, Kleinunternehmer-Zaehler und Aufgabenliste
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 15:42:06 +00:00
ClaudeandClaude Sonnet 5.5 34dfe181e4 Finanzen: Liquiditaetsvorschau, Abo-Kuendigung: cancelAt allein massgeblich, cancelAtPeriodEnd ohne cancelAt keine Verlaengerung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 15:42:06 +00:00
ClaudeandClaude Sonnet 5.5 9361000a70 Finanzen: Liquiditaetsvorschau 30 Tage (sicher/erwartet)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 15:42:06 +00:00
ClaudeandClaude Sonnet 5.5 34ff97e924 Finanzen: Einstellungen und Fixkostenliste
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 15:42:06 +00:00
ClaudeandClaude Sonnet 5.5 6e43d54fe8 Finanzen: Zuordnung zu Rechnungen erst vermerken, dann bezahlen; Vormerkungen nicht zuordnen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 15:42:06 +00:00
ClaudeandClaude Sonnet 5.5 2bf67cb48d Finanzen: Bankbuchungen Rechnungen, Auszahlungen, Fixkosten und Kategorien zuordnen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 15:42:06 +00:00
ClaudeandClaude Sonnet 5.5 f14a6b7b57 Finanzen: Zuordnungsvorschlaege und Erkennung wiederkehrender Ausgaben
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 15:42:06 +00:00
ClaudeandClaude Sonnet 5.5 3cedb5feef Finanzen: Rechnung senden ohne Doppelanlage (E-Mail vorab, erneut senden, Summengrenze)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 15:42:06 +00:00
ClaudeandClaude Sonnet 5.5 05e3478229 Finanzen: Projektrechnungen in Stripe anlegen, senden, stornieren, Entwurf loeschen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 15:42:06 +00:00
ClaudeandClaude Sonnet 5.5 db243f50bb Finanzen: Stripe-Rechnungen und Auszahlungen lesen (deutsch gemappt, 60-s-Cache)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 15:42:06 +00:00
ClaudeandClaude Sonnet 5.5 6af9834293 Finanzen: Admin-Routen fuer Bankstatus, Abruf, Neuverbindung und Buchungen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 15:42:06 +00:00
ClaudeandClaude Sonnet 5.5 7b35ed67a5 Finanzen: Warnung vor Ablauf des Bankzugangs (T18) und Abruf-Jobs
checkBankConsent mailt bei 30, 14, 3 Tagen und am Ablauftag (Berliner Kalendertage),
merkt sich die erreichten Stufen in warnedThresholds und holt eine nicht versandte
Mail beim naechsten Lauf nach. Jobs nur mit Enable Banking: Abruf nach 2 Minuten und
alle 6 Stunden, Pruefung nach 3 Minuten und taeglich.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 15:42:01 +00:00
ClaudeandClaude Sonnet 5.5 cba14ed0f0 Finanzen: Bankzugang aus WOMS neu verbinden (Freigabe, Rueckkehr, Kontowahl)
startBankAuth/completeBankAuth in bankSync.js. Die Rueckkehr prueft und speichert die
Sitzung unter der Sperre der Abrufe (ein laufender Abruf kann den neuen Zugang nicht
mehr ueberschreiben) und ruft danach einmal ab. Nachbau: holdNext haelt einen Aufruf an.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 15:41:54 +00:00
ClaudeandClaude Sonnet 5.5 75ca8b78c0 Finanzen: Index auf bankTransactions.status, Vormerkungssuche mit Rueckfall
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 15:41:54 +00:00
ClaudeandClaude Sonnet 5.5 588cc95f5d Finanzen: Bankabruf speichert Buchungen idempotent, Vormerkungen, Ablauf
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 15:41:54 +00:00
ClaudeandClaude Sonnet 5.5 3dd2fac50b Finanzen: Collections und Setup-Skript mit Sitzungsuebernahme
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 15:41:54 +00:00
ClaudeandClaude Sonnet 5.5 9329318df5 Finanzen: Enable-Banking-Client (JWT, Freigabe, Sitzung, Salden, Umsaetze)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 15:41:54 +00:00
ClaudeandClaude Opus 5.5 53b50011e8 Finanzen: Spec und Plaene 1-3 (Bank, Stripe/Auswertung, WOMS/Livegang)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 15:41:54 +00:00
ClaudeandClaude Sonnet 5.5 30412cb0ea Analyse: Verweildauer bei internem Seitenwechsel sofort senden
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 13:32:21 +00:00
ClaudeandClaude Sonnet 5.5 ab4c53d6df Analyse: Statistik-Abruf auf richtigen Pfad + Vertragstest
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 13:32:21 +00:00
ClaudeandClaude Sonnet 5.5 b193b327ab Preview: Verfolger-Wettlauf bei neuen Projekten behoben
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 11:10:54 +00:00
ClaudeandClaude Sonnet 5.5 d416a7aa09 Analyse: Dev-Hinweis in der richtigen Kachel + Render-Test
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 11:07:08 +00:00
ClaudeandClaude Opus 5.5 4bf3726c00 Aufraeumen: versehentlich eingecheckte Arbeitsdatei entfernt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 11:05:25 +00:00
ClaudeandClaude Sonnet 5.5 466fcffa21 Analyse: geo-update holt Neustart nach (Marker), Runner-Test für build-Injektion
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 11:04:50 +00:00
ClaudeandClaude Sonnet 5.5 714687fe3b Website-Analyse: AVV als Seite, www/Apex-Domains, Abo-Check unabhängig vom Schalter, Dev-Hinweis
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 11:04:50 +00:00
ClaudeandClaude Sonnet 5.5 7ead59ff73 Analyse: Banner-Knoepfe gleich breit
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:56:51 +00:00
ClaudeandClaude Sonnet 5.5 2ca18f9c65 Analyse: Services-Route faellt bei Fehler der Analyse-Abfrage nicht aus
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:39:56 +00:00
ClaudeandClaude Sonnet 5.5 0b59dac2be Analyse: Lesezugriff fuer WOMS
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:37:25 +00:00
ClaudeandClaude Sonnet 5.5 821e947275 Analyse: Rechtstexte – letzte Präzisierungen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:35:59 +00:00
ClaudeandClaude Sonnet 5.5 e3fed71c76 Analyse: Rechtstexte Fix 1 (Kommentare nicht veroeffentlichen, Abschalt-Wortlaut, TOM)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:34:20 +00:00
ClaudeandClaude Sonnet 5.5 e33e1cf5ec Analyse: Rechtstexte (Entwuerfe, anwaltlich zu pruefen)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:30:30 +00:00
ClaudeandClaude Sonnet 5.5 7fc5ba8528 Analyse: Heatmap-Link, Sperre, leere Domains, Tab-Wettlauf
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:27:20 +00:00
ClaudeandClaude Sonnet 5.5 5a6fbc7328 Analyse: Portal-Kachel und Auswertung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:24:48 +00:00
ClaudeandClaude Sonnet 5.5 fc937b0d1e Analyse: Eingaben und Liste robuster
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:22:19 +00:00
ClaudeandClaude Sonnet 5.5 f4c8b291d7 Analyse: Routen fuer Portal und WOMS
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:19:37 +00:00
ClaudeandClaude Sonnet 5.5 696f17b470 Analyse: Runner-Auftrag begrenzt Datenschutz-Angaben
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:17:10 +00:00
ClaudeandClaude Sonnet 5.5 62f196f705 Analyse: Runner-Auftrag
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:15:46 +00:00
ClaudeandClaude Sonnet 5.5 813c56e6a4 Analyse: Setup-Test um activatedMailAt ergaenzt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:13:43 +00:00
ClaudeandClaude Sonnet 5.5 37203f00c4 Analyse: Freischalten selbstheilend, Sperre, Retention-Abschaltung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:13:28 +00:00
ClaudeandClaude Sonnet 5.5 f0eee196ed Analyse: Freischalten, Abschalten, Abo-Pruefung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:10:39 +00:00
ClaudeandClaude Sonnet 5.5 9e66f55c2c Analyse: Schema, Konfiguration, Client
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:07:05 +00:00
ClaudeandClaude Sonnet 5.5 9fa05e23d1 Runner: Analyse-Einsetzen ignoriert Kommentare/Skripte (Fix Runde 1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:05:08 +00:00
ClaudeandClaude Sonnet 5.5 18d37d1e01 Runner: Analyse null erlaubt, Adresse optional (R10)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:03:24 +00:00
ClaudeandClaude Sonnet 5.5 7de9f1aaa3 Runner: Analyse-Skript und Datenschutz-Zusatzseite einsetzen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:02:52 +00:00
ClaudeandClaude Sonnet 5.5 add7fa4add Analytics: Heatmap-Endpunkt nachgeschaerft
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 09:59:48 +00:00
ClaudeandClaude Sonnet 5.5 28c7a90d8b Analytics: Skripte ausliefern
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 09:58:10 +00:00
ClaudeandClaude Sonnet 5.5 da8bba2a1c Analytics: Banner-Skript Review-Fixes (R8/R9, Tab-Widerruf)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 09:53:59 +00:00
ClaudeandClaude Sonnet 5.5 5930f052d7 Analytics: Banner-Skript und Heatmap-Overlay
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 09:50:20 +00:00
ClaudeandClaude Sonnet 5.5 305939fdbd Analytics: Verdichtung, Statistik, Heatmap-Daten
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 09:45:14 +00:00
ClaudeandClaude Sonnet 5.5 6233850bb3 Analytics: Annahme gehaertet
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 09:41:25 +00:00
ClaudeandClaude Sonnet 5.5 8b107019c3 Analytics: Laenderzuordnung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 09:30:58 +00:00
ClaudeandClaude Sonnet 5.5 3a48e6e677 Analytics: Ereignisse annehmen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 09:27:22 +00:00
ClaudeandClaude Sonnet 5.5 cd9aebe4b1 Analytics: Admin-Routen nur auf internem Listener (R5), Transaktion, Duplikat-Check
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 09:24:57 +00:00
ClaudeandClaude Sonnet 5.5 f7da79e4b3 Analytics: Geruest, Datenbank, Website-Register
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 09:23:27 +00:00
ClaudeandClaude Opus 5.5 a1ce46672a Plan: Website-Analyse
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 09:18:23 +00:00
ClaudeandClaude Opus 5.5 29986cf697 Spec: Website-Analyse (Banner, eigener Sammeldienst, Heatmap)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 09:16:16 +00:00
ClaudeandClaude Sonnet 5.5 c37dd49b05 Preview: Bereitschaft aus echtem Zustand statt hasPreview (Standardwert false sperrte Previews)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 23:01:17 +00:00
ClaudeandClaude Sonnet 5.5 04b45e4207 Preview: Dev-Modus nur mit dev-Skript waehlbar
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 22:47:54 +00:00
ClaudeandClaude Sonnet 5.5 a5cb668853 Preview: isRunnerDev einheitlich (Reconcile, Dev-Start), 137-Fehlertext
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 22:32:09 +00:00
ClaudeandClaude Sonnet 5.5 56637f2005 Preview: Build 2 GB, kein Dev-Rueckfall bei bestehender Preview, oeffentliche Seiten nie automatisch auf Dev
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 22:29:26 +00:00
ClaudeandClaude Sonnet 5.5 c256b78dae Runner: json-Typ vor eigener Erkennung (R21), Erstdeploy-Rueckfall, json-index beim Veroeffentlichen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 22:18:41 +00:00
ClaudeandClaude Sonnet 5.5 8a028ce75a Runner: index.html-Rueckfall wie bisheriger Deploy (Kandidat/Platzhalter/Unterordner)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 22:15:27 +00:00
ClaudeandClaude Opus 5.5 54f307d12a Runner: DEPLOY.md auf umgesetzten Livegang-Stand gebracht
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 21:54:01 +00:00
ClaudeandClaude Sonnet 5.5 9488efc97a Preview: Dev-Anmeldung immer mit mode dev (R19)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 21:54:01 +00:00
ClaudeandClaude Sonnet 5.5 126d556bd7 docs(runner): Livegang-Anleitung (Netz, Compose, Firewall, deploy.sh-Vorschlag)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 21:54:01 +00:00
ClaudeandClaude Sonnet 5.5 b7cbe3aa38 fix(preview): Status bleibt bei Runner-Builds, dev-Label, Abgleich mit dem Runner, Längenkürzung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 21:54:01 +00:00
ClaudeandClaude Sonnet 5.5 19cd8f90ce feat(runner): /status meldet bekannte Dev-Spezifikationen (known)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 21:54:01 +00:00
ClaudeandClaude Sonnet 5.5 96c26fb9b8 fix(runner): Dev-Neustart bei geänderten Variablen, Liveness-Prüfung, npm-ci-Fallback
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 21:54:01 +00:00
ClaudeandClaude Sonnet 5.5 cd9a6e0cf1 Preview: Runner-Schutzschalter 503 als Server ausgelastet durchreichen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 21:54:01 +00:00
ClaudeandClaude Sonnet 5.5 b3cf78c82a Preview: Admin-API Nachbesserung (off verdraengt Bau, env-Sperre)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 21:54:01 +00:00
ClaudeandClaude Sonnet 5.5 07bef6971c Preview: Admin-API und Modus-Vorgabe
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 21:54:01 +00:00
ClaudeandClaude Sonnet 5.5 3316b562af Preview: Dev-Registrierung nutzt gespeicherten Branch und Stand
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 21:54:01 +00:00
ClaudeandClaude Sonnet 5.5 cf2841adf5 Preview: Router fuer Dev-Server
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 21:54:01 +00:00
ClaudeandClaude Sonnet 5.5 d788dba937 Preview: Runner-Verfolger verdraengt sauber, R15-Grenzen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 21:54:01 +00:00
ClaudeandClaude Sonnet 5.5 4258b5b9f1 Preview: Builds ueber den Runner
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 21:54:01 +00:00
ClaudeandClaude Sonnet 5.5 40a8aad3f9 Preview: Schema-Skript und Runner-Konfiguration
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 21:53:41 +00:00
ClaudeandClaude Sonnet 5.5 def490ceca Preview: GCM-Tag-Laenge fest, Inhalt validiert
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 21:53:41 +00:00
ClaudeandClaude Sonnet 5.5 20ca1de694 Preview: Variablen verschluesselt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 21:53:41 +00:00
ClaudeandClaude Sonnet 5.5 e358514240 Runner: Proxy härten (R13 Listen-Adressen, Hop-by-hop, Upgrade-Race, Touch, Timeout)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 21:53:41 +00:00
ClaudeandClaude Sonnet 5.5 4afe7eb5cd Runner: Proxy und Startseite, register/wake (R12)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 21:53:41 +00:00
ClaudeandClaude Sonnet 5.5 6cd50bba9f Runner: Dev-Server Start/Abbruch-Korrekturen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 21:53:41 +00:00
ClaudeandClaude Sonnet 5.5 71f9243bf2 Runner: instabile Build-Tests behoben
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 21:53:41 +00:00
ClaudeandClaude Sonnet 5.5 5b902519a9 Runner: Dev-Server bei Bedarf
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 21:53:41 +00:00
ClaudeandClaude Sonnet 5.5 4fffd8b218 Runner: Build-Auftraege Review-Korrekturen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 21:53:41 +00:00
ClaudeandClaude Sonnet 5.5 4ed7e5d7a5 Runner: Build-Auftraege mit Warteschlange
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 21:53:41 +00:00
ClaudeandClaude Sonnet 5.5 12513b73c3 Runner: Docker-Zeitlimit liefert sicher 124 nach rm -f
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 21:53:41 +00:00
ClaudeandClaude Sonnet 5.5 bcc0e42374 Runner: Docker-Adapter gehärtet (R10)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 21:53:41 +00:00
ClaudeandClaude Sonnet 5.5 10ef2c8085 Runner: Docker-Adapter mit Sicherheitsprofil
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 21:53:41 +00:00
ClaudeandClaude Sonnet 5.5 b8174e296a Runner: Erkennung pnpm/yarn-Details
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 21:53:41 +00:00
ClaudeandClaude Sonnet 5.5 e2c517a015 Runner: Projekterkennung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 21:53:41 +00:00
ClaudeandClaude Sonnet 5.5 bc5fddf13f Runner: Test-Lauf-ID korrigiert
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 21:53:41 +00:00
ClaudeandClaude Sonnet 5.5 8588e63124 Runner: Eingabepruefung gehaertet (R8)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 21:53:41 +00:00
ClaudeandClaude Sonnet 5.5 aeb08580bb Runner: Geruest, Eingabepruefung, Auth
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 21:53:41 +00:00
ClaudeandClaude Opus 5.5 c402837df6 Plan: Preview 2.0 mit Preview-Runner
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 21:53:41 +00:00
ClaudeandClaude Opus 5.5 651745eccc Spec: Preview 2.0 mit Preview-Runner, Modi und Variablen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 21:53:41 +00:00
ClaudeandClaude Opus 5.5 edef85c7b3 Preview-Deploy: gleichzeitige Deploys desselben Repos nacheinander
Projektaenderung und Gitea-Webhook deployten Kirschner Gebaeudeservice
am 30.09.2026 gleichzeitig; beide git clone liefen ins selbe
preview-data-Verzeichnis und brachen ab. runDeploy sperrt jetzt je
Arbeitsverzeichnis und Subdomain (keyedLock), andere Repos laufen
weiter parallel. Test stellt drei gleichzeitige Deploys nach.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:02:42 +00:00
ClaudeandClaude Opus 5.5 d4241d20c4 Datenschutz Abschnitt 6: Mail-Hinweis an Kunden bei Chatnachrichten
Satz zur K17-Benachrichtigung (hoechstens eine alle 15 Minuten).
PDF-Fassung neu erzeugt; sie enthaelt damit auch Abschnitt 6a
(News-Pinnwand), der seit 0dc57fe nur im HTML stand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:45:14 +00:00
ClaudeandClaude Opus 5.5 00ea4baf65 Chat: Kunde bekommt Mail, wenn das Team schreibt (K17)
Neue Vorlage K17 im Stil von K16 mit Nachricht, Absender und Link
auf den Portal-Chat. Hoechstens eine Mail je Kunde im Zeitfenster
CHAT_NOTIFY_THROTTLE_MIN (15 Min.), ohne BCC ans Team, nicht ohne
E-Mail-Adresse oder Portalzugang. Das Admin-Panel zeigt unter dem
Kundennamen, ob der Hinweis rausging.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:00:33 +00:00
ClaudeandClaude Opus 5.5 37489aa866 Admin-Chat: neue Unterhaltung mit jedem Kunden starten
Ueber der Chat-Liste sucht ein Feld alle Kunden (Name, E-Mail,
Kd.-Nr.) und oeffnet einen leeren Chat, auch wenn der Kunde noch
nie geschrieben hat. Der Server konnte das schon.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:51:57 +00:00
ClaudeandClaude Opus 5.5 4e8b8f1698 Projektarten: Schema-Skript liest alle Attribute (limit 200)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:33:21 +00:00
ClaudeandClaude Opus 5.5 43c168ab54 Projektarten: Zuordnungs-Mail heisst T17 (T16 ist Funktion bestellt)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:32:12 +00:00
ClaudeandClaude Opus 5.5 c1f8ace348 Test: Arbeitsblatt-Reihenfolge unabhaengig von der Uhrzeit
Die 55 Fuellblaetter hatten keinen festen Zeitstempel und waren ab
30.09.2026 18:00 UTC neuer als ws1 - der Test schlug ab dann fehl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:32:12 +00:00
ClaudeandClaude Sonnet 5.5 d3c6858ecc Projektarten: Zuordnungs-Mail auch ohne vorherigen Versuch nachholen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 18:31:20 +00:00
ClaudeandClaude Sonnet 5.5 1f1ffb5275 Projektarten: neue Repos erkennen und Zuordnungs-Mail senden
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 18:31:20 +00:00
ClaudeandClaude Sonnet 5.5 de4b6f3a35 Projektarten: T16 Versuchszähler je Mail (R7)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 18:31:11 +00:00
ClaudeandClaude Sonnet 5.5 b8a530958f Projektarten: Mail T16 Repo zuordnen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 18:31:07 +00:00
ClaudeandClaude Sonnet 5.5 08958dee77 Projektarten: Services-Uebersicht fuer WOMS
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 18:30:53 +00:00
ClaudeandClaude Sonnet 5.5 3e6f35f836 Projektarten: Zuordnen gibt Warnungen von assignProject weiter
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 18:30:53 +00:00
ClaudeandClaude Sonnet 5.5 36c7822783 Projektarten: Zuordnen-Ablauf serverseitig
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 18:30:53 +00:00
ClaudeandClaude Sonnet 5.5 df0c7a05e8 Tickets: Startdatum als TT.MM.JJJJ, Frist gleich (R5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 18:30:53 +00:00
ClaudeandClaude Sonnet 5.5 c93cb93a36 Tickets: createTicketForProject und deleteTicket
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 18:30:53 +00:00
ClaudeandClaude Sonnet 5.5 85b972f9dc Projektarten: Schema-Skript fuer websiteProjects
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 18:25:28 +00:00
ClaudeandClaude Sonnet 5.5 a2e308711e Projektarten: Katalog
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 18:25:27 +00:00
ClaudeandClaude Opus 5.5 b98562bb4f Plan: Projektarten, Zuordnungs-Ablauf und Services
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:25:27 +00:00
ClaudeandClaude Opus 5.5 7a4af8e3c8 Spec: Projektarten, Zuordnungs-Ablauf und Bereich Services
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:25:27 +00:00
knsoandClaude Opus 5.5 b45108c1de Funktionen: Stripe-Sitzungs-ID bis 128 Zeichen, keine Rohmeldungen beim Bestellen
Live-Fehler: cs_live-Sitzungs-IDs (~66 Zeichen) passten nicht in das Feld
portalFeatures.stripeSessionId (64), die Bestellung brach nach dem Anlegen
der Stripe-Sitzung ab und der Kunde sah die Appwrite-Meldung.
- Setup-Skript: Feld 128 Zeichen, bestehendes Feld wird per ensureStringSize vergroessert
- Fehler von Appwrite oder unerwartete Fehler: Log + verstaendlicher Hinweis

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:58:44 +00:00
knsoandClaude Opus 5.5 4b7f73abba Funktionen/News: Schlussdurchgang (Webhook-Retry, Checkout-Sitzung, Rechnungslink, Widerruf Funktion, UI- und Rechtstext-Korrekturen)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:38:15 +00:00
knsoandClaude Opus 5.5 0dc57fe5e8 News: Aufbewahrung, Datenschutz-Abschnitt und Leistungsbeschreibung (Entwurf)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:23:41 +00:00
knsoandClaude Opus 5.5 0e0776b406 news.js: Variablen-Defaults ohne Spezifitaet, Combo-Umbruch, Bildmaße absichern
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:19:40 +00:00
knsoandClaude Opus 5.5 035d1d5517 Einbau-Skript news.js für Kundenwebsites
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:16:37 +00:00
knsoandClaude Opus 5.5 5a09d060a1 Portal: Fortschritt unter Projekte, neuer Tab Funktionen mit News-Editor
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:06:02 +00:00
knsoandClaude Opus 5.5 6dac6aa073 Admin: News je Projekt aktivieren und abschalten
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:56:41 +00:00
knsoandClaude Opus 5.5 a421308019 News: Cache-Buster für Bild-URLs, HEAD, Limiter-Feinschliff
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:52:47 +00:00
knsoandClaude Opus 5.5 319f8937aa News: Kunden-API und öffentliche Lese-API
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:49:39 +00:00
knsoandClaude Opus 5.5 d320730251 News: Beiträge – Titel optional, Datumsprüfung, Bildregeln
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:45:42 +00:00
knsoandClaude Opus 5.5 434bab0b7a News: Beiträge, Anzeigedauer und öffentliche Seitenansicht
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:43:24 +00:00
knsoandClaude Opus 5.5 ad0b08f32a News: Fotos mit Pixelgrenze, sauberer Fehlerzuordnung und mehr Tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:40:24 +00:00
knsoandClaude Opus 5.5 ac7375e889 News: Fotos verkleinern und Metadaten entfernen (sharp)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:38:33 +00:00
knsoandClaude Opus 5.5 8374a570aa Funktionen: Bestellung serialisiert, Sitzung nachtragen, Freischaltung einmalig
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:35:59 +00:00
knsoandClaude Opus 5.5 0cc35bf1a5 Funktionen: Buchung per Stripe, Webhook, Mails und Freischalt-Job
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:33:48 +00:00
knsoandClaude Opus 5.5 770cbe32b7 Funktionen: Bestellschutz, serialisiertes markPaid, idx_session
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:29:12 +00:00
knsoandClaude Opus 5.5 4b1df8fbfa Funktionen: Zustände, Ticket beim Kauf, Freischaltung bei geschlossenem Ticket
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:27:27 +00:00
knsoandClaude Opus 5.5 16fbb03c13 Stripe: Einmalpreis News-Einrichtung 30 €
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:24:21 +00:00
knsoandClaude Opus 5.5 85bee3190c Schema: portalFeatures-Zustände und newsPosts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:24:21 +00:00
knsoandClaude Opus 5.5 c55e6f2dfe Plan: Funktionen-Tab und News-Pinnwand
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:21:21 +00:00
knsoandClaude Opus 5.5 586e3cc85d Spec: Funktionen-Tab und News-Pinnwand
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:17:25 +00:00
ClaudeandClaude Opus 5.5 7ae0ffdd3a Abo-Bedingungen Fassung 5: Unterstützung ans Ende von § 4 Abs. 4, Verweis in Abs. 7 korrigiert
"Dazu zählen" bezieht sich wieder direkt auf die aufgewendete Zeit; das Hosting-Abo
übernimmt Rundung, Abstimmung und Unterstützung (Satz 1, 2 und 4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:31:16 +00:00
ClaudeandClaude Opus 5.5 8acc743cf7 Abo-Bedingungen Fassung 5: Unterstützung in § 4, Fassung 4 archiviert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:26:39 +00:00
ClaudeandClaude Opus 5.5 88854aa2b4 Push-Zeitmail: Zeit nur in 15-Minuten-Schritten
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:09:12 +00:00
ClaudeandClaude Opus 5.5 7da4452bbd Unterstützung: eigene Zeilen und Gesamtzeit in den Projektdetails
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:08:16 +00:00
ClaudeandClaude Opus 5.5 7c6b0c906b Unterstützung: Rechenregel und Abzug im Abo-Stundenkonto
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:01:57 +00:00
ClaudeandClaude Opus 5.5 c4fab31603 Push-Zeitmail: Zeit-Knöpfe gleich breit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:47:11 +00:00
ClaudeandClaude Opus 5.5 6643283c6f Push-Zeitmail: Review-Fixes (Rate-Limit begrenzt, kein NaN-Datum, Störung ≠ gelöscht)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:46:04 +00:00
ClaudeandClaude Opus 5.5 7d4767a137 Push-Zeitmail: Webhook schickt Mail nach neuem Git-Arbeitsblatt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:43:00 +00:00
ClaudeandClaude Opus 5.5 a28ae21389 Push-Zeitmail: Bestätigungsseite /zeit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:41:57 +00:00
ClaudeandClaude Opus 5.5 1a1005d099 Push-Zeitmail: Vorlage T15, Zuordnung Pusher → Mitarbeiter, Versand
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:39:12 +00:00
ClaudeandClaude Opus 5.5 ec3f22d98e Push-Zeitmail: Zeit für Git-Arbeitsblatt eintragen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:34:17 +00:00
ClaudeandClaude Opus 5.5 5b7365b99e Push-Zeitmail: signierter Token für Zeit-Links
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:33:39 +00:00
ClaudeandClaude Opus 5.5 cdc814ae09 Plan: Push-Zeitmail (5 Tasks); Spec: Betreff [Zeit], Commits als Text
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:26:59 +00:00
ClaudeandClaude Opus 5.5 af7ecce78d Spec: Rate-Limit präzisiert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:20:58 +00:00
ClaudeandClaude Opus 5.5 6f4be30c18 Spec: Push-Zeitmail (Zeit für Git-Pushes per E-Mail eintragen)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:20:52 +00:00
ClaudeandClaude Opus 5.5 d7c2027d3f 2FA-Login: Prüfung schlägt nicht offen fehl, Fehlversuche vor dem Aufruf zählen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:39:24 +00:00
ClaudeandClaude Opus 5.5 acc86c267b Skript: 2FA für Mitarbeiter anzeigen, einschalten, abschalten
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:32:34 +00:00
ClaudeandClaude Opus 5.5 0555a2274d Wechsel Ticketsystem → Projekt-Admin per Einmal-Link, Rückweg-Button
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:31:10 +00:00
ClaudeandClaude Opus 5.5 9b14a74fcc Portal-Login: E-Mail-Code für Mitarbeiter mit 2FA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:29:36 +00:00
ClaudeandClaude Opus 5.5 25f15c0ecb Portal: offene Mitarbeiter-Code-Anmeldungen (staffMfa)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:27:38 +00:00
ClaudeandClaude Opus 5.5 23d78815f9 Portal-Login: Appwrite-Sitzung merken, MFA-Aufrufe im Namen der Sitzung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:27:05 +00:00
ClaudeandClaude Opus 5.5 d5c70a5bd6 Gesamtprüfung: Zeitlimit für Gitea, Aufräumen ohne Weiterleitung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:21:34 +00:00
ClaudeandClaude Opus 5.5 febac3c2a3 Skripte: Organisation einrichten, Kundenprojekte umziehen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:21:34 +00:00
ClaudeandClaude Opus 5.5 ed8c0f82d5 Webhook: nach Umzug Repo-Adresse aus dem aktuellen Namen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:21:34 +00:00
ClaudeandClaude Opus 5.5 a5da4849e5 Schnittstellen für Import, Zuweisen, Nachtragen; Webhook wartet auf Umzug
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:21:34 +00:00
ClaudeandClaude Opus 5.5 5c39640224 Neue Repos importieren, Abgleich ohne Subdomains, Selbstheilung nur für gehostete Projekte
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:21:34 +00:00
ClaudeandClaude Opus 5.5 5ce8aa6fbb Zuweisen: Warnungen statt Abbruch, Zuweisung unter der Projekt-Sperre
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:21:34 +00:00
ClaudeandClaude Opus 5.5 a960701d48 Zuweisen mit Umzug und Nachtragen der Pushes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:21:34 +00:00
ClaudeandClaude Opus 5.5 405b2a0485 Umzug: Webhook-Adresse aus config.url, Aufräumen auch nach Selbstheilung
Fix round 1 (Review C1/I1/I2/I3, Controller-Ruling R11):
- C1 (kritisch): Hooks haben in echtem Gitea kein Top-Level-url, nur config.url -
  isInternalRepo/deletePortalHooks lasen bisher ein Feld, das es in Produktion nicht gibt.
- I1: Schritt-2-Namensvergleich jetzt ohne Ruecksicht auf Gross-/Kleinschreibung.
- I2: fs.rm in Schritt 9 gegen EACCES/EBUSY/ENOTEMPTY & Co. abgesichert.
- I3: Aufraeumen (Cache, alter Einzel-Webhook, Preview-Verzeichnis) laeuft jetzt ueber den
  neuen idempotenten finishMove()-Baustein in jedem Fall, in dem das Repo tatsaechlich in
  WEBklar liegt - auch nach einem Appwrite-Fehler, bei Selbstheilung und bei einem schon
  dort liegenden Repo (WARN.hookNotRemoved bei fehlgeschlagenem Loeschen).
- Fake Gitea: Hook-Form ohne Top-Level-url, 301 mit relativer Location (auch fuer
  Unterpfade) wie im echten Gitea 1.25.2, failNext() fuer gezielte Fehler in Tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:21:34 +00:00
ClaudeandClaude Opus 5.5 6e6c9a4c24 Umzug zugewiesener Repos nach WEBklar
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:21:34 +00:00
ClaudeandClaude Opus 5.5 51c4852488 Push-Historie: Obergrenze beim Blättern, Test für HeadCommit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:21:34 +00:00
ClaudeandClaude Opus 5.5 e409c11d5c Push-Historie aus den Gitea-Aktivitäten lesen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:21:34 +00:00
ClaudeandClaude Opus 5.5 8d5de87ead Push-Arbeitsblätter: deutsche Zeit, keine System-Pushes, keine Doppelten
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:21:34 +00:00
ClaudeandClaude Opus 5.5 531ee2a02b Gitea-Client herausgelöst, Konfiguration für Organisation und System-Konto, Test-Nachbauten
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:21:34 +00:00
ClaudeandClaude Opus 5.5 ea71b3f1c3 Sperren: Timeout eines Wartenden hebt die Sperre nicht auf
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:21:34 +00:00
ClaudeandClaude Opus 5.5 679f996597 Bausteine: slugify mit Umlauten, deutsche Zeit, Sperren
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:21:34 +00:00
ClaudeandClaude Opus 5.5 a3faa1aff1 Umsetzungspläne: Kundenportal, Ticketsystem, Lead-Automatik
Plan 1 Kundenportal (Gitea-Grundlage, Umzug, Nachtragen, Livegang),
Plan 2 Ticketsystem (Dialog, Knöpfe), Plan 3 Lead-Automatik
(neue Lead-Repos in WEBklar über das System-Konto).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:21:34 +00:00
ClaudeandClaude Opus 5.5 3edb5f8bb2 Konzept: gemeinsame Gitea-Organisation für alle Mitarbeiter
Neutrales System-Konto statt knso, Organisation WEBklar mit einem
Webhook, automatischer Umzug beim Zuweisen, Pushes der letzten 30 Tage
nachtragen, Knopf "Preview einschalten", deutsche Uhrzeit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:21:34 +00:00
ClaudeandClaude Opus 5.5 8c9d28c30f Profilbild: zufälliger DiceBear-Avatar (Big Smile) pro Kunde
Der Server erzeugt das SVG lokal unter /api/avatar.svg (Seed = Kunden-ID),
es geht also keine Anfrage an dicebear.com. Die Initialen bleiben als
Fallback, bis das Bild geladen ist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:36:49 +00:00
ClaudeandClaude Opus 5.5 e4efdac226 Hosting-Abo: Preis 15 € -> 20 € pro Monat (Vertragsbedingungen v4, Katalog, Leitfaden)
- server/config.js: amountCents 2000, lookup_key webklar_hosting_monatlich_2000
- ensurePrice haengt einen neuen Preis an das vorhandene Stripe-Produkt (metadata.webklar),
  statt ein zweites "WEBklar Hosting-Abo" anzulegen
- abo-bedingungen.html (Uebersicht, Preise, Leistungsbeschreibung) + PDF, Leitfaden + PDF
- Tests auf 20 € umgestellt (399/399)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:29:01 +00:00
ClaudeandClaude Opus 5.5 78b4d7b7fe Livegang: Vertragsbedingungen Version 4 und Datenschutzerklärung Stand 30.09.2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:03:39 +00:00
ClaudeandClaude Opus 5.5 46f229804d Plan 2: Livegang-Schritte nach der Gesamtprüfung korrigiert und ergänzt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 15:46:55 +00:00
ClaudeandClaude Sonnet 5.5 09fc940619 Oberfläche: Speicher-Zusammenfassung nach Backend-Modell (Reduzierung aufheben per Plus)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-29 15:42:19 +00:00
ClaudeandClaude Sonnet 5.5 05a59c4d61 Oberfläche: Speicher-Stepper bei geplanter Reduzierung, vollständige Bestellübersicht, Hinweis bei gesperrten Änderungen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-29 15:37:03 +00:00
ClaudeandClaude Sonnet 5.5 f7eeda8c19 Wechsel ins Wartungs-Abo: Übersicht, Zustimmung, Bestellung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-29 15:24:46 +00:00
ClaudeandClaude Sonnet 5.5 c95695c175 Zusatzspeicher: Plus und Minus mit Bestellübersicht
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-29 15:17:54 +00:00
ClaudeandClaude Sonnet 5.5 90a3c57aa3 Bestellseiten für Hosting- und Wartungs-Abo
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-29 15:13:22 +00:00
ClaudeandClaude Sonnet 5.5 19af93338b Verträge: Mein Vertrag, Vergleich der Abos, Wechsel-Karte, Unterlagen in der zugestimmten Fassung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-29 15:09:36 +00:00
ClaudeandClaude Sonnet 5.5 0129ac48e7 Kundenbereich: „Wartung“ heißt jetzt „Verträge“, auch am Desktop in der Navigation
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-29 15:01:09 +00:00
ClaudeandClaude Sonnet 5.5 36b54f6dab Löschfristen: bei abgeschnittenen Listen abbrechen, Teilfehler melden, Randfälle
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-29 14:54:43 +00:00
ClaudeandClaude Opus 5.5 6c8e7703ae Vertragsbedingungen Version 4: Datenverlust-Klausel § 13 Abs. 3 mit Ausnahme für Absatz 1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 14:52:38 +00:00
ClaudeandClaude Sonnet 5.5 9342ba10c8 Löschfristen: Probelauf, Bericht ans Team, Löschen nach drei Jahren
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-29 14:51:50 +00:00
ClaudeandClaude Sonnet 5.5 cc42ab16c1 Admin E-Mail-Speicher: belegter Speicher wie in konsoleH, klare Vorgabe je Kunde
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-29 14:48:49 +00:00
ClaudeandClaude Fable 5.1 12a24514a4 Mails: Vertragsbestätigung je Fassung, Zustimmung zum AVV, kleinere Textkorrekturen
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-29 14:46:41 +00:00
ClaudeandClaude Fable 5.1 63d3bb11bf Admin: Reiter E-Mail-Speicher, Abo mit Produkt; Schnittstelle fürs Ticketsystem
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-29 14:37:42 +00:00
ClaudeandClaude Fable 5.1 19f9c747a2 Mails: Produktname statt fest Wartungs-Abo, neue Mails für Wechsel und Zusatzspeicher, UptimeRobot-Hinweis
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-29 10:38:43 +00:00
ClaudeandClaude Fable 5.1 0d2254ca20 Kündigung und Widerruf: Paketzahl in jeder Bestätigung, klare Hinweise fürs Team, Tests
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-29 10:15:51 +00:00
ClaudeandClaude Sonnet 5 e5cc2f3c82 Abo-Angebote: für Hosting- oder Wartungs-Abo
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 10:08:59 +00:00
ClaudeandClaude Fable 5.1 7ffe95f24a Kündigung und Widerruf: Vertrag bzw. Bestellung auswählbar (§ 312k, § 356a BGB)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-29 09:52:37 +00:00
ClaudeandClaude Fable 5.1 f66a713531 Zusatzspeicher: Plan nach Stripe-Abgleich erneut prüfen, Aufgabenliste unter Sperre, K13 ohne Kosten
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-29 09:31:28 +00:00
ClaudeandClaude Opus 5.5 662f8d8144 Wechsel: Lastschrift-Fehler nur für den echten Wechsel, Abgleich ohne Webhook, Gutschrift netto, Skript prüft Stripe
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 09:24:02 +00:00
ClaudeandClaude Fable 5.1 bc20ce0673 Zusatzspeicher: Vorschau im flexiblen Abrechnungsmodus, Aufgaben nach Zeitpunkt, keine stillen Abbrüche
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-29 09:16:07 +00:00
ClaudeandClaude Opus 5.5 188db4b3c1 Wechsel Hosting → Wartung: neuer Abbuchungstag, Gutschrift, 12 neue Monate
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 11:30:21 +00:00
ClaudeandClaude Fable 5.1 379cc26dcd Zusatzspeicher: Plus sofort anteilig, Minus zum Monatsende, Aufgaben für konsoleH
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 10:48:29 +00:00
ClaudeandClaude Fable 5.1 887e1e54a0 Abo-API: Nachweis-ID nach Appwrite-Regeln, Webhook bei dauerhaften Fehlern, Tests
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 10:19:05 +00:00
ClaudeandClaude Opus 5.5 7df0b72019 Stripe: Testrabatt-Namen, Produktbild, Hosting-Leistungen, Tests für Bestandskunden
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 10:02:03 +00:00
ClaudeandClaude Fable 5.1 c4607e9c43 Abo-API: Bestellung je Produkt, Katalog, Speicherstand, Wechsel-Möglichkeit
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 09:43:09 +00:00
ClaudeandClaude Opus 5.5 05363d06ad Vertragsereignisse: Speicher-Aufgaben mit gelesenen Details
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 09:25:16 +00:00
ClaudeandClaude Opus 5.5 e5cc57cefd Stripe: Preise je Produkt, Testrabatt Hosting, Kundenportal „Abo verwalten“, Mindestlaufzeit ab termStartAt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 09:21:25 +00:00
ClaudeandClaude Opus 5.5 7799d7c4de Vertragsereignisse: Nachweise, Widerrufsfristen, Speicher-Aufgaben
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 08:59:30 +00:00
ClaudeandClaude Opus 5.5 7e869280f3 Rechtsrunde: Wartung ab wirksamem Wechsel, Wortlaut § 9, Aufbewahrung bis Jahresende
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 08:45:40 +00:00
ClaudeandClaude Opus 5.5 f856869747 Rechtsrunde: Wartung ab wirksamem Wechsel, Wortlaut § 9, Aufbewahrung bis Jahresende
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 08:43:17 +00:00
ClaudeandClaude Opus 5.5 71fa0fc8e2 Spec und Plan 2: Aufbewahrung bis Jahresende + 3 Jahre, Hinweise zum Wechsel nach der Textprüfung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 08:39:27 +00:00
ClaudeandClaude Opus 5.5 478fbae3aa Rechtsrunde: Wechsel erst mit Zahlung, Zusatzspeicher teilweise kündbar, Aufbewahrung nach dem letzten Abo
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 08:19:29 +00:00
ClaudeandClaude Opus 5.5 91d9360dd2 Spec und Plan 2: Entscheidungen aus der Rechtsrunde (Wechsel erst mit Zahlung, Speicher teilweise kündigen, Löschfristen, AGB)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 08:12:54 +00:00
ClaudeandClaude Fable 5.1 e456c3de69 Übernahmegutschrift: Verrechnung nach Wechsel, Mail K09, Leitfaden, Admin zeigt das Abo, Tests
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 07:49:51 +00:00
ClaudeandClaude Opus 5.5 0c2e50fc20 Vertragsbedingungen Version 4: Webmail, Übersicht, Löschung bei Vertragsende, Hinweise zum Widerruf, Tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 20:45:29 +00:00
ClaudeandClaude Fable 5.1 6f7daf757f Datenschutz (Entwurf): Übermittlung an UptimeRobot, Bestellungen im Portal, Formulare, Tests
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-27 20:37:06 +00:00
ClaudeandClaude Fable 5.1 a2c1f0d81c Übernahmegutschrift und Leitfaden: für beide Abos (Entwurf)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-27 20:23:44 +00:00
ClaudeandClaude Fable 5.1 0098899ac6 Datenschutz (Entwurf): unsere Abos, Überwachung mit UptimeRobot
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-27 20:05:34 +00:00
ClaudeandClaude Opus 5.5 fead8fdd5a Vertragsbedingungen Version 4: Korrekturen aus der Prüfung (Widerrufsfrist, Kündigung, Bestellübersicht, Postfächer, AVV, § 19 UStG)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 19:41:22 +00:00
ClaudeandClaude Opus 5.5 f19187cc16 Vertragsbedingungen Version 4 (Entwurf): Hosting- und Wartungs-Abo, Postfächer, Zusatzspeicher, Wechsel; Fassung 3 archiviert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:17:31 +00:00
ClaudeandClaude Opus 5.5 f016ad0326 Pläne: Hinweise aus dem Review von Plan 1 (Livegang-Prüfung, Rechnungszeilen, lookup_keys)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:47:02 +00:00
ClaudeandClaude Opus 5.5 47c1af7015 Stripe: Testadresse STRIPE_API_BASE nur für 127.0.0.1/localhost
Ein verirrter Wert in Produktion kann den Live-Schlüssel so nie an einen
fremden Server schicken; getStripe() bricht mit klarer Meldung ab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:46:26 +00:00
ClaudeandClaude Opus 5.5 2694ef18d6 Abo-Spiegel: fehlt ein neues Feld in Appwrite, wird ohne dieses Feld gespiegelt
Schützt Webhooks und den öffentlichen Kündigungs-/Widerrufsknopf, falls das
Setup-Skript erst nach dem Deploy läuft; der Fehler steht laut im Log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:45:54 +00:00
ClaudeandClaude Opus 5.5 dc61649228 Stundenkonto: Abos ohne bekannten Preis bekommen als Altbestand weiter ihre Stunde
Wie im Abo-Spiegel gelten Rechnungen ohne jeden bekannten Abo-Preis als
Wartung. Ein unbekannter Preis neben einem bekannten (z. B. Hosting) bringt
weiterhin keine Stunde.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:45:09 +00:00
ClaudeandClaude Opus 5.5 51fc2434e5 Stundenkonto: Arbeitszeit nur für bezahlte Wartungs-Monate, nicht für andere Abo-Rechnungen
Die Rechnung wird frisch geladen; eine Stunde bringt nur eine reguläre
(nicht anteilige) Wartungs-Zeile über 0 €. Preis-IDs werden einmal bei
Stripe nachgeschlagen und zwischengespeichert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:33:33 +00:00
ClaudeandClaude Opus 5.5 d097704d49 Abo-Spiegel: Produkt und tatsächlicher Preis aus den Stripe-Positionen
Neues Appwrite-Feld subscriptions.productKey (Setup-Skript). Ohne bekannten
Abo-Preis gilt ein Abo weiter als Wartungs-Abo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:31:56 +00:00
ClaudeandClaude Opus 5.5 54defb75c4 Tests: Stripe- und Appwrite-Nachbau im Testprozess
getStripe() nutzt in Tests über STRIPE_API_BASE den Nachbau auf 127.0.0.1;
in Produktion ist der Wert leer und nichts ändert sich.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:30:56 +00:00
ClaudeandClaude Opus 5.5 877fde7a6f Abo-Katalog: Wartung, Hosting und Zusatzspeicher als Produkte (noch ohne Verwendung)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:29:16 +00:00
ClaudeandClaude Opus 5.5 afec0b82fd Plan 2: letzter Test ohne Platzhalter
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:04:56 +00:00
ClaudeandClaude Opus 5.5 405408d076 Umsetzungspläne: Plan 1 (unsichtbare Vorarbeiten) und Plan 2 (sichtbarer Teil mit Version 4)
Plan 1: Produktkatalog, Abo-Spiegel mit productKey, Arbeitszeit nur für
Wartungs-Rechnungen, Stripe- und Appwrite-Nachbau für Tests. Kann vor den
neuen Vertragsbedingungen live gehen.
Plan 2: Rechtstexte (Gate Rechtsrunde), Vertragsereignisse, Zusatzspeicher,
Wechsel, öffentliche Formulare, Angebote je Produkt, Mails, Admin,
Ansicht "Verträge", Website, Ticketsystem, Livegang.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:04:44 +00:00
ClaudeandClaude Opus 5.5 c76ea0bf9e Konzept: Mailvorlagen sind in main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 14:54:19 +00:00
ClaudeandClaude Opus 5.5 09870a3b83 Konzept: Hetzner-Paket M, UptimeRobot und neue Mailvorlagen eingetragen
Postfächer liegen im Hetzner-Paket M (50 GB), Wechsel auf L ab 40 GB
belegt. Die Überwachung läuft mit UptimeRobot: Eintrag in der
Datenschutzerklärung, kein Unterauftragsverarbeiter nach Anlage 3,
Monitore beim Abo-Start anlegen und beim Ende entfernen. Die
HTML-Mailvorlagen auf feature/mail-vorlagen enthalten Produktangaben
fest und bekommen Platzhalter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 14:54:10 +00:00
ClaudeandClaude Opus 5.5 65ab5013cb Konzept: Verträge, Hosting-Abo und E-Mail-Zusatzspeicher
Entwurf zur Durchsicht, noch keine Umsetzung. Beschreibt den Bereich
"Verträge", das Hosting-Abo (15 €), beliebig viele Postfächer im
Wartungs-Abo, den Wechsel Hosting -> Wartung (12 neue Monate) und den
Zusatzspeicher (5 GB je 1 €/Monat, Stückzahl im Stripe-Abo) samt
betroffener Rechtstexte, Mails und Dateien.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 14:54:10 +00:00
1246 changed files with 314754 additions and 4785 deletions

No files matched your search

+5
View File
@@ -22,3 +22,8 @@ GITEA_API_TOKEN=
PREVIEW_DEPLOY_ROOT=/var/www/previews
PREVIEW_BASE_HOST=project.webklar.com
ALLOWED_CUSTOMER_STATUSES=active
# Finanzen (Bankabruf Finom über Enable Banking): beides setzen, sonst ist der Bankabruf aus.
# APP_ID = Anwendungs-ID bei Enable Banking, KEY_PATH = Pfad zum privaten Schlüssel (PEM) auf dem Server, nie im Repo.
# ENABLE_BANKING_APP_ID=
# ENABLE_BANKING_KEY_PATH=
+1 -1
View File
@@ -1,4 +1,4 @@
node_modules/
node_modules
.env
.DS_Store
preview-data/
+26
View File
@@ -0,0 +1,26 @@
// Erzeugt dist/w.js und dist/h.js: core.js + Einstieg als IIFE, ohne import/export (kein Build-Tool).
// Aufruf: node client/build.js
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const DIR = dirname(fileURLToPath(import.meta.url));
const read = (f) => readFileSync(join(DIR, f), 'utf8');
function wrap(name, entry) {
const core = read('core.js').replace(/^export\s+/gm, '');
const body = read(entry).replace(/^import\s[^\n]*\n/gm, '');
return `/* WEBklar Analyse ${name} - generiert aus client/core.js + client/${entry} (node client/build.js), nicht von Hand aendern */\n(function () {\n'use strict';\n${core}\n${body}\n})();\n`;
}
export function buildAll() {
return { w: wrap('w.js', 'w.js'), h: wrap('h.js', 'h.js') };
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const b = buildAll();
mkdirSync(join(DIR, 'dist'), { recursive: true });
writeFileSync(join(DIR, 'dist', 'w.js'), b.w);
writeFileSync(join(DIR, 'dist', 'h.js'), b.h);
console.log('dist/w.js', b.w.length, 'Bytes; dist/h.js', b.h.length, 'Bytes');
}
+127
View File
@@ -0,0 +1,127 @@
// Reine Funktionen des Analyse-Skripts (ohne DOM-Zugriff ausser an uebergebenen Objekten).
// Wird von w.js/h.js importiert; build.js fuegt alles zu je einer Datei ohne Import zusammen.
export const CONSENT_KEY = 'wk-consent';
export const CONSENT_MAX_AGE = 183 * 86400000;
export const BANNER_VERSION = '2026-10-02';
export const BANNER_TEXT = 'Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Deiner Zustimmung messen wir zusätzlich, von welcher Website Du kommst, wie lange Du bleibst, wie weit Du scrollst und wo Du klickst (Heatmap), mit einer täglich wechselnden Kennung. Die Messung betreibt für uns die WEBklar GbR in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Du abgelehnt hast. Deine Zustimmung kannst Du jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: ';
// Eigene Websites der WEBklar GbR (webklar.com, Skript-Attribut data-variant="eigen"): Sie-Form, WEBklar misst selbst
// (keine Auftragsverarbeitung). Eigene Fassung, damit eine Textaenderung nur dort neu fragt.
export const BANNER_VERSION_EIGEN = '2026-10-17';
export const BANNER_TEXT_EIGEN = 'Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Ihrer Zustimmung messen wir zusätzlich, von welcher Website Sie kommen, wie lange Sie bleiben, wie weit Sie scrollen und wo Sie klicken (Heatmap), mit einer täglich wechselnden Kennung. Wir messen selbst auf unserem Server in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Sie abgelehnt haben. Ihre Zustimmung können Sie jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: ';
export const BANNER_LINK_LABEL = 'Datenschutz';
export const BANNER_ARIA = 'Einwilligung zur Reichweitenmessung';
export const SETTINGS_LABEL = 'Datenschutz-Einstellungen';
export function vpClass(width) {
const w = Number(width);
if (w < 768) return 'm';
if (w < 1024) return 't';
return 'd';
}
export function cleanPath(href) {
let p = '/';
const h = String(href == null ? '' : href);
if (h.charAt(0) === '/') {
p = h.replace(/[?#].*$/, '') || '/'; // wie location.pathname, auch '//foo'
} else {
try { p = new URL(h, 'http://x.invalid/').pathname || '/'; } catch (e) { /* bleibt '/' */ }
}
try { p = decodeURIComponent(p); } catch (e) { /* kaputtes Encoding: roh lassen */ }
if (p.charAt(0) !== '/') p = '/' + p;
return p.slice(0, 300);
}
const stripWww = (h) => String(h).toLowerCase().replace(/^www\./, '');
export function refHost(referrer, ownHost) {
if (!referrer) return '';
try {
const u = new URL(String(referrer));
if (u.protocol !== 'http:' && u.protocol !== 'https:') return '';
const h = u.hostname.toLowerCase();
if (!h || stripWww(h) === stripWww(ownHost || '')) return '';
return h;
} catch (e) { return ''; }
}
// Nur Tag-Namen mit :nth-of-type, keine IDs/Klassen (koennten personenbezogene Texte enthalten).
export function cssPath(el) {
const parts = [];
let n = el;
while (n && n.tagName && parts.length < 6) {
const tag = String(n.tagName).toLowerCase().replace(/[^a-z0-9-]/g, '');
if (!tag) break;
let idx = 1;
for (let s = n.previousElementSibling; s; s = s.previousElementSibling) {
if (String(s.tagName).toLowerCase() === String(n.tagName).toLowerCase()) idx++;
}
parts.unshift(tag + ':nth-of-type(' + idx + ')');
if (tag === 'body' || tag === 'html') break;
n = n.parentNode;
}
let out = parts.join(' > ');
while (out.length > 200 && parts.length > 1) { parts.shift(); out = parts.join(' > '); }
return out.slice(0, 200);
}
function parseHref(a) {
const href = a && (typeof a.href === 'string' && a.href ? a.href : (a.getAttribute && a.getAttribute('href'))) || '';
return String(href).trim();
}
export function linkHost(a) {
try { return new URL(parseHref(a)).hostname.toLowerCase(); } catch (e) { return ''; }
}
export function contactKind(a, ownHost) {
const href = parseHref(a);
if (!href) return null;
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(href);
if (!scheme) return null; // relativ, Anker
const s = scheme[1].toLowerCase();
if (s === 'tel') return 'tel';
if (s === 'mailto') return 'mail';
if (s === 'whatsapp') return 'wa';
if (s !== 'http' && s !== 'https') return null;
const host = linkHost(a);
if (!host) return null;
if (host === 'wa.me' || host === 'api.whatsapp.com') return 'wa';
const own = ownHost == null ? (typeof location !== 'undefined' ? location.hostname : '') : ownHost;
return stripWww(host) === stripWww(own || '') ? null : 'out';
}
// Banner-Fassung je Variante: 'eigen' fuer WEBklars eigene Websites, alles andere ist die Kunden-Fassung.
export function bannerFor(variant) {
return variant === 'eigen'
? { version: BANNER_VERSION_EIGEN, text: BANNER_TEXT_EIGEN }
: { version: BANNER_VERSION, text: BANNER_TEXT };
}
export function readConsent(storage, now, version = BANNER_VERSION) {
try {
const raw = storage.getItem(CONSENT_KEY);
if (!raw) return null;
const c = JSON.parse(raw);
if (!c || c.v !== 1 || (c.a !== 'accept' && c.a !== 'reject') || typeof c.t !== 'number' || !isFinite(c.t)) return null;
if (c.bv !== version) return null; // andere/fehlende Fassung des Banner-Textes: neu fragen
const age = now - c.t;
if (age < 0 || age >= CONSENT_MAX_AGE) return null;
return { v: 1, a: c.a, t: c.t, bv: c.bv };
} catch (e) { return null; }
}
export function contrastOk(hex) {
if (typeof hex !== 'string' || !/^#[0-9a-f]{6}$/i.test(hex)) return false;
const lin = (i) => {
const c = parseInt(hex.slice(1 + i * 2, 3 + i * 2), 16) / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
};
const L = 0.2126 * lin(0) + 0.7152 * lin(1) + 0.0722 * lin(2);
return 1.05 / (L + 0.05) >= 4.5;
}
export function escapeHtml(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
}
+262
View File
@@ -0,0 +1,262 @@
/* WEBklar Analyse h.js - generiert aus client/core.js + client/h.js (node client/build.js), nicht von Hand aendern */
(function () {
'use strict';
// Reine Funktionen des Analyse-Skripts (ohne DOM-Zugriff ausser an uebergebenen Objekten).
// Wird von w.js/h.js importiert; build.js fuegt alles zu je einer Datei ohne Import zusammen.
const CONSENT_KEY = 'wk-consent';
const CONSENT_MAX_AGE = 183 * 86400000;
const BANNER_VERSION = '2026-10-02';
const BANNER_TEXT = 'Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Deiner Zustimmung messen wir zusätzlich, von welcher Website Du kommst, wie lange Du bleibst, wie weit Du scrollst und wo Du klickst (Heatmap), mit einer täglich wechselnden Kennung. Die Messung betreibt für uns die WEBklar GbR in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Du abgelehnt hast. Deine Zustimmung kannst Du jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: ';
// Eigene Websites der WEBklar GbR (webklar.com, Skript-Attribut data-variant="eigen"): Sie-Form, WEBklar misst selbst
// (keine Auftragsverarbeitung). Eigene Fassung, damit eine Textaenderung nur dort neu fragt.
const BANNER_VERSION_EIGEN = '2026-10-17';
const BANNER_TEXT_EIGEN = 'Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Ihrer Zustimmung messen wir zusätzlich, von welcher Website Sie kommen, wie lange Sie bleiben, wie weit Sie scrollen und wo Sie klicken (Heatmap), mit einer täglich wechselnden Kennung. Wir messen selbst auf unserem Server in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Sie abgelehnt haben. Ihre Zustimmung können Sie jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: ';
const BANNER_LINK_LABEL = 'Datenschutz';
const BANNER_ARIA = 'Einwilligung zur Reichweitenmessung';
const SETTINGS_LABEL = 'Datenschutz-Einstellungen';
function vpClass(width) {
const w = Number(width);
if (w < 768) return 'm';
if (w < 1024) return 't';
return 'd';
}
function cleanPath(href) {
let p = '/';
const h = String(href == null ? '' : href);
if (h.charAt(0) === '/') {
p = h.replace(/[?#].*$/, '') || '/'; // wie location.pathname, auch '//foo'
} else {
try { p = new URL(h, 'http://x.invalid/').pathname || '/'; } catch (e) { /* bleibt '/' */ }
}
try { p = decodeURIComponent(p); } catch (e) { /* kaputtes Encoding: roh lassen */ }
if (p.charAt(0) !== '/') p = '/' + p;
return p.slice(0, 300);
}
const stripWww = (h) => String(h).toLowerCase().replace(/^www\./, '');
function refHost(referrer, ownHost) {
if (!referrer) return '';
try {
const u = new URL(String(referrer));
if (u.protocol !== 'http:' && u.protocol !== 'https:') return '';
const h = u.hostname.toLowerCase();
if (!h || stripWww(h) === stripWww(ownHost || '')) return '';
return h;
} catch (e) { return ''; }
}
// Nur Tag-Namen mit :nth-of-type, keine IDs/Klassen (koennten personenbezogene Texte enthalten).
function cssPath(el) {
const parts = [];
let n = el;
while (n && n.tagName && parts.length < 6) {
const tag = String(n.tagName).toLowerCase().replace(/[^a-z0-9-]/g, '');
if (!tag) break;
let idx = 1;
for (let s = n.previousElementSibling; s; s = s.previousElementSibling) {
if (String(s.tagName).toLowerCase() === String(n.tagName).toLowerCase()) idx++;
}
parts.unshift(tag + ':nth-of-type(' + idx + ')');
if (tag === 'body' || tag === 'html') break;
n = n.parentNode;
}
let out = parts.join(' > ');
while (out.length > 200 && parts.length > 1) { parts.shift(); out = parts.join(' > '); }
return out.slice(0, 200);
}
function parseHref(a) {
const href = a && (typeof a.href === 'string' && a.href ? a.href : (a.getAttribute && a.getAttribute('href'))) || '';
return String(href).trim();
}
function linkHost(a) {
try { return new URL(parseHref(a)).hostname.toLowerCase(); } catch (e) { return ''; }
}
function contactKind(a, ownHost) {
const href = parseHref(a);
if (!href) return null;
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(href);
if (!scheme) return null; // relativ, Anker
const s = scheme[1].toLowerCase();
if (s === 'tel') return 'tel';
if (s === 'mailto') return 'mail';
if (s === 'whatsapp') return 'wa';
if (s !== 'http' && s !== 'https') return null;
const host = linkHost(a);
if (!host) return null;
if (host === 'wa.me' || host === 'api.whatsapp.com') return 'wa';
const own = ownHost == null ? (typeof location !== 'undefined' ? location.hostname : '') : ownHost;
return stripWww(host) === stripWww(own || '') ? null : 'out';
}
// Banner-Fassung je Variante: 'eigen' fuer WEBklars eigene Websites, alles andere ist die Kunden-Fassung.
function bannerFor(variant) {
return variant === 'eigen'
? { version: BANNER_VERSION_EIGEN, text: BANNER_TEXT_EIGEN }
: { version: BANNER_VERSION, text: BANNER_TEXT };
}
function readConsent(storage, now, version = BANNER_VERSION) {
try {
const raw = storage.getItem(CONSENT_KEY);
if (!raw) return null;
const c = JSON.parse(raw);
if (!c || c.v !== 1 || (c.a !== 'accept' && c.a !== 'reject') || typeof c.t !== 'number' || !isFinite(c.t)) return null;
if (c.bv !== version) return null; // andere/fehlende Fassung des Banner-Textes: neu fragen
const age = now - c.t;
if (age < 0 || age >= CONSENT_MAX_AGE) return null;
return { v: 1, a: c.a, t: c.t, bv: c.bv };
} catch (e) { return null; }
}
function contrastOk(hex) {
if (typeof hex !== 'string' || !/^#[0-9a-f]{6}$/i.test(hex)) return false;
const lin = (i) => {
const c = parseInt(hex.slice(1 + i * 2, 3 + i * 2), 16) / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
};
const L = 0.2126 * lin(0) + 0.7152 * lin(1) + 0.0722 * lin(2);
return 1.05 / (L + 0.05) >= 4.5;
}
function escapeHtml(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
}
// WEBklar Analyse: Heatmap-Overlay (wird von w.js nur mit gueltigem ?__wk_heat=Token geladen).
// data-embed="1" (nur in einem Rahmen): Ansicht im Geraeterahmen des Kundenportals. Dann ohne Leiste,
// Links und Formulare gesperrt, und die Klickzahl geht per postMessage nur an das Portal (Herkunft von h.js).
function main() {
const me = document.currentScript;
const token = me && me.getAttribute('data-token');
const src = String((me && (me.src || me.getAttribute('src'))) || '').replace(/[?#].*$/, '');
const base = src.replace(/[^/]*$/, '');
if (!token || !base) return;
const de = document.documentElement;
let portal = '';
try { portal = new URL(base).origin; } catch (e) { /* ohne Portal keine Einbettung */ }
const embed = !!portal && me.getAttribute('data-embed') === '1' && window.parent && window.parent !== window;
function tell(state, count) {
if (!embed) return;
try { window.parent.postMessage({ wk: 'heat', state: state, count: count }, portal); } catch (e) { /* egal */ }
}
const canvas = document.createElement('canvas');
const cs = canvas.style;
cs.position = 'absolute'; cs.left = '0'; cs.top = '0'; cs.pointerEvents = 'none'; cs.zIndex = '2147483646';
document.body.appendChild(canvas);
let points = [];
let lastW = 0, lastH = 0, timer = 0;
// Farbverlauf: transparent -> blau -> gruen -> gelb -> rot
const ramp = (function () {
const c = document.createElement('canvas'); c.width = 256; c.height = 1;
const g = c.getContext('2d');
const lg = g.createLinearGradient(0, 0, 256, 0);
lg.addColorStop(0.25, 'rgb(0,0,255)'); lg.addColorStop(0.55, 'rgb(0,255,0)'); lg.addColorStop(0.85, 'rgb(255,255,0)'); lg.addColorStop(1, 'rgb(255,0,0)');
g.fillStyle = lg; g.fillRect(0, 0, 256, 1);
return g.getImageData(0, 0, 256, 1).data;
})();
if (embed) {
// Nur ansehen: kein Seitenwechsel (er wuerde zaehlen und das Banner zeigen)
const block = function (e) {
const t = e.target;
if (e.type === 'submit' || (t && t.closest && t.closest('a,area'))) { e.preventDefault(); e.stopPropagation(); }
};
window.addEventListener('click', block, true);
window.addEventListener('submit', block, true);
// Handy und Tablet haben ueberlagernde Scrollleisten; so stimmt die Breite wie auf dem Geraet
const st = document.createElement('style');
st.textContent = 'html{scrollbar-width:none}html::-webkit-scrollbar{display:none}';
(document.head || de).appendChild(st);
start(function (n) { tell('ready', n); }, function () { tell('error', 0); });
return;
}
const host = document.createElement('div');
host.style.position = 'fixed'; host.style.left = '0'; host.style.right = '0'; host.style.top = '0'; host.style.zIndex = '2147483647';
const root = host.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = ':host{all:initial}.bar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 16px;background:#111;color:#fff;font:14px/1.4 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}button{font:600 14px/1 system-ui,sans-serif;padding:8px 14px;border:2px solid #fff;border-radius:6px;background:#fff;color:#111;cursor:pointer}button:focus-visible{outline:3px solid #1a73e8;outline-offset:2px}';
const bar = document.createElement('div');
bar.className = 'bar';
const msg = document.createElement('span');
msg.textContent = 'Heatmap wird geladen ...';
const close = document.createElement('button');
close.setAttribute('type', 'button');
close.textContent = 'Schließen';
bar.appendChild(msg); bar.appendChild(close);
root.appendChild(style); root.appendChild(bar);
document.body.appendChild(host);
function draw() {
const w = Math.max(de.scrollWidth, de.clientWidth, 1);
const h = Math.max(de.scrollHeight, de.clientHeight, 1);
lastW = w; lastH = h;
// Zeichenflaeche begrenzen (Speicher): hoechstens ca. 4 Mio. Pixel
const scale = Math.min(1, Math.sqrt(4e6 / (w * h)));
const cw = Math.max(1, Math.floor(w * scale)), ch = Math.max(1, Math.floor(h * scale));
canvas.width = cw; canvas.height = ch;
cs.width = w + 'px'; cs.height = h + 'px';
const g = canvas.getContext('2d');
g.clearRect(0, 0, cw, ch);
const r = Math.max(6, 30 * scale);
for (const p of points) {
const x = p.x * cw, y = p.y * ch;
const rg = g.createRadialGradient(x, y, 0, x, y, r);
rg.addColorStop(0, 'rgba(0,0,0,0.25)'); rg.addColorStop(1, 'rgba(0,0,0,0)');
g.fillStyle = rg; g.fillRect(x - r, y - r, r * 2, r * 2);
}
// Deckkraft-Stufen in Farben umsetzen
const img = g.getImageData(0, 0, cw, ch);
const d = img.data;
for (let i = 3; i < d.length; i += 4) {
const a = d[i];
if (!a) continue;
const k = Math.min(255, a * 3) * 4;
d[i - 3] = ramp[k]; d[i - 2] = ramp[k + 1]; d[i - 1] = ramp[k + 2];
d[i] = Math.min(200, 60 + a);
}
g.putImageData(img, 0, 0);
}
function redrawIfChanged() {
if (Math.max(de.scrollWidth, de.clientWidth) !== lastW || Math.max(de.scrollHeight, de.clientHeight) !== lastH) draw();
}
function stop() {
clearInterval(timer);
window.removeEventListener('resize', draw);
host.remove(); canvas.remove();
}
close.addEventListener('click', stop);
start(function (n) { msg.textContent = 'Heatmap – ' + n + ' Klicks (letzte 30 Tage)'; },
function () { msg.textContent = 'Heatmap-Link abgelaufen oder ungültig'; });
// Punkte laden, zeichnen und bei Groessenaenderung neu zeichnen
function start(onReady, onError) {
window.addEventListener('resize', draw);
fetch(base + 'heat?token=' + encodeURIComponent(token), { credentials: 'omit' })
.then(function (r) { if (!r.ok) throw new Error('heat'); return r.json(); })
.then(function (j) {
points = (j && Array.isArray(j.points) ? j.points : []).filter(function (p) { return p && p.x >= 0 && p.x <= 1 && p.y >= 0 && p.y <= 1; });
draw();
timer = setInterval(redrawIfChanged, 1000);
onReady(points.length);
})
.catch(function () { onError(); });
}
}
try { main(); } catch (e) { /* nie die Seite stoeren */ }
})();
+467
View File
@@ -0,0 +1,467 @@
/* WEBklar Analyse w.js - generiert aus client/core.js + client/w.js (node client/build.js), nicht von Hand aendern */
(function () {
'use strict';
// Reine Funktionen des Analyse-Skripts (ohne DOM-Zugriff ausser an uebergebenen Objekten).
// Wird von w.js/h.js importiert; build.js fuegt alles zu je einer Datei ohne Import zusammen.
const CONSENT_KEY = 'wk-consent';
const CONSENT_MAX_AGE = 183 * 86400000;
const BANNER_VERSION = '2026-10-02';
const BANNER_TEXT = 'Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Deiner Zustimmung messen wir zusätzlich, von welcher Website Du kommst, wie lange Du bleibst, wie weit Du scrollst und wo Du klickst (Heatmap), mit einer täglich wechselnden Kennung. Die Messung betreibt für uns die WEBklar GbR in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Du abgelehnt hast. Deine Zustimmung kannst Du jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: ';
// Eigene Websites der WEBklar GbR (webklar.com, Skript-Attribut data-variant="eigen"): Sie-Form, WEBklar misst selbst
// (keine Auftragsverarbeitung). Eigene Fassung, damit eine Textaenderung nur dort neu fragt.
const BANNER_VERSION_EIGEN = '2026-10-17';
const BANNER_TEXT_EIGEN = 'Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Ihrer Zustimmung messen wir zusätzlich, von welcher Website Sie kommen, wie lange Sie bleiben, wie weit Sie scrollen und wo Sie klicken (Heatmap), mit einer täglich wechselnden Kennung. Wir messen selbst auf unserem Server in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Sie abgelehnt haben. Ihre Zustimmung können Sie jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: ';
const BANNER_LINK_LABEL = 'Datenschutz';
const BANNER_ARIA = 'Einwilligung zur Reichweitenmessung';
const SETTINGS_LABEL = 'Datenschutz-Einstellungen';
function vpClass(width) {
const w = Number(width);
if (w < 768) return 'm';
if (w < 1024) return 't';
return 'd';
}
function cleanPath(href) {
let p = '/';
const h = String(href == null ? '' : href);
if (h.charAt(0) === '/') {
p = h.replace(/[?#].*$/, '') || '/'; // wie location.pathname, auch '//foo'
} else {
try { p = new URL(h, 'http://x.invalid/').pathname || '/'; } catch (e) { /* bleibt '/' */ }
}
try { p = decodeURIComponent(p); } catch (e) { /* kaputtes Encoding: roh lassen */ }
if (p.charAt(0) !== '/') p = '/' + p;
return p.slice(0, 300);
}
const stripWww = (h) => String(h).toLowerCase().replace(/^www\./, '');
function refHost(referrer, ownHost) {
if (!referrer) return '';
try {
const u = new URL(String(referrer));
if (u.protocol !== 'http:' && u.protocol !== 'https:') return '';
const h = u.hostname.toLowerCase();
if (!h || stripWww(h) === stripWww(ownHost || '')) return '';
return h;
} catch (e) { return ''; }
}
// Nur Tag-Namen mit :nth-of-type, keine IDs/Klassen (koennten personenbezogene Texte enthalten).
function cssPath(el) {
const parts = [];
let n = el;
while (n && n.tagName && parts.length < 6) {
const tag = String(n.tagName).toLowerCase().replace(/[^a-z0-9-]/g, '');
if (!tag) break;
let idx = 1;
for (let s = n.previousElementSibling; s; s = s.previousElementSibling) {
if (String(s.tagName).toLowerCase() === String(n.tagName).toLowerCase()) idx++;
}
parts.unshift(tag + ':nth-of-type(' + idx + ')');
if (tag === 'body' || tag === 'html') break;
n = n.parentNode;
}
let out = parts.join(' > ');
while (out.length > 200 && parts.length > 1) { parts.shift(); out = parts.join(' > '); }
return out.slice(0, 200);
}
function parseHref(a) {
const href = a && (typeof a.href === 'string' && a.href ? a.href : (a.getAttribute && a.getAttribute('href'))) || '';
return String(href).trim();
}
function linkHost(a) {
try { return new URL(parseHref(a)).hostname.toLowerCase(); } catch (e) { return ''; }
}
function contactKind(a, ownHost) {
const href = parseHref(a);
if (!href) return null;
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(href);
if (!scheme) return null; // relativ, Anker
const s = scheme[1].toLowerCase();
if (s === 'tel') return 'tel';
if (s === 'mailto') return 'mail';
if (s === 'whatsapp') return 'wa';
if (s !== 'http' && s !== 'https') return null;
const host = linkHost(a);
if (!host) return null;
if (host === 'wa.me' || host === 'api.whatsapp.com') return 'wa';
const own = ownHost == null ? (typeof location !== 'undefined' ? location.hostname : '') : ownHost;
return stripWww(host) === stripWww(own || '') ? null : 'out';
}
// Banner-Fassung je Variante: 'eigen' fuer WEBklars eigene Websites, alles andere ist die Kunden-Fassung.
function bannerFor(variant) {
return variant === 'eigen'
? { version: BANNER_VERSION_EIGEN, text: BANNER_TEXT_EIGEN }
: { version: BANNER_VERSION, text: BANNER_TEXT };
}
function readConsent(storage, now, version = BANNER_VERSION) {
try {
const raw = storage.getItem(CONSENT_KEY);
if (!raw) return null;
const c = JSON.parse(raw);
if (!c || c.v !== 1 || (c.a !== 'accept' && c.a !== 'reject') || typeof c.t !== 'number' || !isFinite(c.t)) return null;
if (c.bv !== version) return null; // andere/fehlende Fassung des Banner-Textes: neu fragen
const age = now - c.t;
if (age < 0 || age >= CONSENT_MAX_AGE) return null;
return { v: 1, a: c.a, t: c.t, bv: c.bv };
} catch (e) { return null; }
}
function contrastOk(hex) {
if (typeof hex !== 'string' || !/^#[0-9a-f]{6}$/i.test(hex)) return false;
const lin = (i) => {
const c = parseInt(hex.slice(1 + i * 2, 3 + i * 2), 16) / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
};
const L = 0.2126 * lin(0) + 0.7152 * lin(1) + 0.0722 * lin(2);
return 1.05 / (L + 0.05) >= 4.5;
}
function escapeHtml(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
}
// WEBklar Analyse: Einwilligungs-Banner + Ereignisse. Laeuft auf oeffentlichen Kunden-Websites.
// Keine Cookies, keine Kennung im Browser. Ohne Wahl nur pv {t,site,path}; ref/vp/cx/ck/ct/en nur nach Annehmen;
// nach Ablehnen oder bei GPC/DNT geht nichts mehr raus.
function main() {
const DOC = document;
const WIN = window;
if (WIN.__wkAnalytics) return; // doppelt eingebunden
WIN.__wkAnalytics = true;
const SELF = DOC.currentScript && DOC.currentScript.getAttribute && DOC.currentScript.getAttribute('data-site')
? DOC.currentScript : DOC.querySelector('script[src*="/a/w.js"]');
if (!SELF) return;
const site = String(SELF.getAttribute('data-site') || '');
if (!/^[a-z0-9]{16,40}$/.test(site)) return;
const colorAttr = String(SELF.getAttribute('data-color') || '');
const accent = contrastOk(colorAttr) ? colorAttr : '#111111';
// data-variant="eigen": Banner-Fassung fuer WEBklars eigene Websites (Sie-Form, ohne Auftragsverarbeiter)
const banner = bannerFor(String(SELF.getAttribute('data-variant') || ''));
const src = String(SELF.src || SELF.getAttribute('src') || '').replace(/[?#].*$/, '');
const base = src.replace(/[^/]*$/, '');
const endpoint = base + 'e';
// ---- Heatmap-Modus: nichts senden, kein Banner ----
const qs = new URLSearchParams(WIN.location.search);
const heatToken = qs.get('__wk_heat');
if (heatToken) {
// __wk_embed=1: Ansicht im Geraeterahmen des Kundenportals (h.js ohne Leiste, Links gesperrt)
const embed = qs.get('__wk_embed') === '1';
qs.delete('__wk_heat');
qs.delete('__wk_embed');
const rest = qs.toString();
try { WIN.history.replaceState(null, '', WIN.location.pathname + (rest ? '?' + rest : '') + (WIN.location.hash || '')); } catch (e) { /* egal */ }
const s = DOC.createElement('script');
s.setAttribute('src', base + 'h.js');
s.setAttribute('data-token', heatToken);
if (embed) s.setAttribute('data-embed', '1');
(DOC.head || DOC.documentElement).appendChild(s);
return;
}
// ---- Senden: Fehler werden verschluckt ----
function send(obj) {
try {
const body = JSON.stringify(obj);
fetch(endpoint, { method: 'POST', body, keepalive: true, credentials: 'omit', headers: { 'Content-Type': 'text/plain' } }).catch(function () {});
} catch (e) { /* egal */ }
}
const safe = (fn) => function () { try { return fn.apply(this, arguments); } catch (e) { /* nie die Seite stoeren */ } };
const vp = () => vpClass(WIN.innerWidth);
const ownHost = () => WIN.location.hostname;
// ---- Widerspruch (GPC/DNT) und Wahl: vor jedem Senden und vor jedem Hook ----
let storage = null;
try { storage = WIN.localStorage; } catch (e) { /* gesperrt */ }
const dnt = (v) => v === '1' || v === 'yes';
const optOut = navigator.globalPrivacyControl === true || dnt(navigator.doNotTrack) || dnt(WIN.doNotTrack) || dnt(navigator.msDoNotTrack);
if (optOut) return; // wie Ablehnen: nichts senden, kein Banner, keine Hooks
const stored = readConsent(storage, Date.now(), banner.version);
let memChoice = null;
let tracking = false;
// Wahl bei jedem Aufruf frisch lesen (Speicher, sonst nur fuer diese Seite gemerkt)
function choiceNow() {
const c = readConsent(storage, Date.now(), banner.version);
return c ? c.a : (memChoice ? memChoice.a : null);
}
const counting = () => choiceNow() !== 'reject';
// ---- Seitenaufruf (pv): ohne Wahl nur Seitenadresse, ref/vp nur nach Annehmen ----
let lastPath = null;
let pvStart = Date.now();
let maxScroll = 0;
let enSent = false;
let pvSent = false;
let cxSent = false;
function sendPv(first) {
if (!counting()) return;
lastPath = cleanPath(WIN.location.pathname);
pvStart = Date.now(); maxScroll = 0; enSent = false;
const ev = { t: 'pv', site, path: lastPath };
if (choiceNow() === 'accept') {
if (first) { ev.ref = refHost(DOC.referrer, ownHost()); cxSent = true; }
ev.vp = vp();
}
pvSent = true;
send(ev);
}
let navTimer = null;
function scheduleNav() {
if (navTimer || !counting()) return;
navTimer = setTimeout(safe(function () {
navTimer = null;
if (!counting() || cleanPath(WIN.location.pathname) === lastPath) return;
// SPA-Seitenwechsel (Router mit preventDefault, onLinkClick greift dann nicht): en der vorigen Seite zuerst
sendEn();
sendPv(false);
}), 300);
}
let hooksOn = false;
function ensureHooks() {
if (hooksOn) return;
hooksOn = true;
WIN.addEventListener('pageshow', safe(function (e) { if (e && e.persisted) sendPv(false); }));
try {
const orig = WIN.history.pushState;
WIN.history.pushState = function () { const r = orig.apply(this, arguments); try { scheduleNav(); } catch (e) { /* egal */ } return r; };
WIN.addEventListener('popstate', safe(scheduleNav));
} catch (e) { /* egal */ }
}
// Nach "Annehmen": Herkunft und Fenstergroesse einmal nachreichen, oder (noch kein pv) pv mit Daten senden
function onAccept() {
ensureHooks();
if (!pvSent) { sendPv(true); return; }
if (cxSent) return;
cxSent = true;
send({ t: 'cx', site, path: lastPath, ref: refHost(DOC.referrer, ownHost()), vp: vp() });
}
if (choiceNow() !== 'reject') { sendPv(true); ensureHooks(); }
// ---- Tracking (nur nach Annehmen erreichbar) ----
const hosts = [];
const scrollPct = () => {
const h = DOC.documentElement.scrollHeight || 1;
return Math.max(0, Math.min(100, Math.round(((WIN.scrollY || 0) + WIN.innerHeight) / h * 100)));
};
const rel = (v, total) => Math.max(0, Math.min(1, Math.round(v / (total || 1) * 10000) / 10000));
const onClick = safe(function (e) {
if (!allowed()) return;
const t = e.target;
if (!t || hosts.indexOf(t) !== -1) return;
const dw = DOC.documentElement.scrollWidth, dh = DOC.documentElement.scrollHeight;
const px = typeof e.pageX === 'number' ? e.pageX : (e.clientX || 0) + (WIN.scrollX || 0);
const py = typeof e.pageY === 'number' ? e.pageY : (e.clientY || 0) + (WIN.scrollY || 0);
const path = cleanPath(WIN.location.pathname);
send({ t: 'ck', site, path, vp: vp(), x: rel(px, dw), y: rel(py, dh), sel: cssPath(t) });
let a = t;
while (a && String(a.tagName).toUpperCase() !== 'A') a = a.parentNode;
const kind = a ? contactKind(a, ownHost()) : null;
if (kind) {
const ev = { t: 'ct', site, path, kind };
if (kind === 'out') ev.host = linkHost(a);
send(ev);
}
});
const onScroll = safe(function () { maxScroll = Math.max(maxScroll, scrollPct()); });
function sendEn() {
if (enSent || !allowed()) return;
enSent = true;
send({ t: 'en', site, path: lastPath || cleanPath(WIN.location.pathname), vp: vp(),
sec: Math.max(0, Math.min(3600, Math.round((Date.now() - pvStart) / 1000))), scroll: Math.max(maxScroll, scrollPct()) });
}
const onLeave = safe(sendEn);
// Interner Seitenwechsel per Link: en sofort senden, der Beacon beim pagehide geht in Chromium oft verloren.
// Bubbling-Listener am Dokument: laeuft nach den Handlern der Seite, defaultPrevented ist dann bekannt.
const onLinkClick = safe(function (e) {
if (e.defaultPrevented || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey || (typeof e.button === 'number' && e.button !== 0)) return;
let a = e.target;
while (a && String(a.tagName).toUpperCase() !== 'A') a = a.parentNode;
if (!a || hosts.indexOf(e.target) !== -1) return;
const tg = String(a.getAttribute('target') || '').toLowerCase();
if ((tg && tg !== '_self') || a.getAttribute('download') !== null) return;
const u = new URL(String(a.href || a.getAttribute('href') || ''), WIN.location.href);
if (u.protocol !== 'http:' && u.protocol !== 'https:') return;
if (u.origin !== WIN.location.origin || u.pathname === WIN.location.pathname) return;
sendEn();
});
const onVis = safe(function () { if (DOC.visibilityState === 'hidden') onLeave(); });
// Einwilligung unmittelbar vor jedem Senden pruefen (z.B. in einem anderen Tab widerrufen)
function allowed() {
const ok = choiceNow() === 'accept';
if (!ok && tracking) stopTracking();
return ok;
}
function startTracking() {
if (tracking) return;
tracking = true;
DOC.addEventListener('click', onClick, { capture: true, passive: true });
DOC.addEventListener('click', onLinkClick, { passive: true });
DOC.addEventListener('visibilitychange', onVis);
WIN.addEventListener('pagehide', onLeave);
WIN.addEventListener('scroll', onScroll, { passive: true });
}
function stopTracking() {
tracking = false;
DOC.removeEventListener('click', onClick, { capture: true });
DOC.removeEventListener('click', onLinkClick);
DOC.removeEventListener('visibilitychange', onVis);
WIN.removeEventListener('pagehide', onLeave);
WIN.removeEventListener('scroll', onScroll);
}
// ---- Einwilligung ----
let bannerHost = null;
let linkHostEl = null;
function choose(a) {
memChoice = null;
try { storage.setItem(CONSENT_KEY, JSON.stringify({ v: 1, a, t: Date.now(), bv: banner.version })); } catch (e) { memChoice = { a }; /* ohne Speicher: gilt nur fuer diese Seite */ }
send({ t: 'bn', site, a });
if (a === 'accept') { startTracking(); onAccept(); } else stopTracking();
closeBanner();
}
function el(tag, attrs, text) {
const n = DOC.createElement(tag);
for (const k in attrs || {}) n.setAttribute(k, attrs[k]);
if (text != null) n.textContent = text;
return n;
}
const CSS = ':host{all:initial}' +
'*{box-sizing:border-box}' +
'.bar{display:flex;align-items:center;justify-content:center;gap:16px;padding:14px 20px;background:#fff;color:#111;border-top:1px solid #d0d0d0;box-shadow:0 -2px 12px rgba(0,0,0,.12);font:15px/1.45 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}' +
'.t{margin:0;max-width:720px}' +
'.t a{color:#111;text-decoration:underline}' +
'.r{display:flex;gap:10px;flex:none}' +
'button{font:600 15px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;width:150px;height:44px;padding:0 20px;border:2px solid ' + accent + ';border-radius:8px;cursor:pointer;background:' + accent + ';color:#fff}' +
'button:focus-visible{outline:3px solid #1a73e8;outline-offset:2px}' +
'@media (max-width:767px){.bar{display:block;margin:8px;padding:16px;border:1px solid #d0d0d0;border-radius:12px}.r{flex-direction:column;margin-top:12px}button{width:100%}}';
let prevPad = '';
function applyPad() {
if (!bannerHost) return;
const h = Math.ceil(bannerHost.getBoundingClientRect().height || bannerHost.offsetHeight || 0);
DOC.body.style.paddingBottom = h + 'px';
}
const onResize = safe(applyPad);
function openBanner(focus) {
if (bannerHost) return;
const prev = DOC.activeElement;
bannerHost = DOC.createElement('div');
const st = bannerHost.style;
st.position = 'fixed'; st.left = '0'; st.right = '0'; st.bottom = '0'; st.zIndex = '2147483647';
hosts.push(bannerHost);
const root = bannerHost.attachShadow({ mode: 'open' });
root.appendChild(el('style', {}, CSS));
const bar = el('div', { class: 'bar', role: 'dialog', 'aria-label': BANNER_ARIA });
const p = el('p', { class: 't' }, banner.text);
p.appendChild(el('a', { href: '/datenschutz-analyse.html' }, BANNER_LINK_LABEL));
const row = el('div', { class: 'r' });
const no = el('button', { type: 'button' }, 'Ablehnen');
const yes = el('button', { type: 'button' }, 'Annehmen');
no.addEventListener('click', safe(function () { choose('reject'); if (prev && prev.focus) prev.focus(); }));
yes.addEventListener('click', safe(function () { choose('accept'); if (prev && prev.focus) prev.focus(); }));
row.appendChild(no); row.appendChild(yes);
bar.appendChild(p); bar.appendChild(row);
root.appendChild(bar);
DOC.body.appendChild(bannerHost);
// Platz unten schaffen, damit Footer-Links (Impressum, Datenschutz) erreichbar bleiben
prevPad = DOC.body.style.paddingBottom;
applyPad();
WIN.addEventListener('resize', onResize);
if (linkHostEl) linkHostEl.style.display = 'none';
if (focus === true) no.focus();
}
function closeBanner() {
if (!bannerHost) return;
const i = hosts.indexOf(bannerHost);
if (i !== -1) hosts.splice(i, 1);
bannerHost.remove();
bannerHost = null;
WIN.removeEventListener('resize', onResize);
DOC.body.style.paddingBottom = prevPad;
if (linkHostEl) linkHostEl.style.display = '';
}
// Einstellungsknopf: angeknabberter Keks unten rechts, dreht sich langsam (nicht bei reduzierter Bewegung).
// Beim Darueberfahren haelt er an und zeigt "Datenschutz-Einstellungen"; das ist auch sein Name fuer Screenreader.
// Inline-SVG statt data:-Grafik, damit eine strenge CSP der Website (img-src) ihn nicht blockiert.
const SETTINGS_CSS = ':host{all:initial}' +
'.b{position:relative;display:block;width:44px;height:44px;padding:2px;margin:0;border:0;border-radius:50%;background:transparent;cursor:pointer;-webkit-tap-highlight-color:transparent}' +
'.b:focus-visible{outline:3px solid #1a73e8;outline-offset:2px}' +
'.c{display:block;width:40px;height:40px;filter:drop-shadow(0 2px 3px rgba(0,0,0,.35));animation:wk-spin 9s linear infinite}' +
'.b:hover .c,.b:focus-visible .c{animation-play-state:paused}' +
'.l{position:absolute;right:52px;top:50%;transform:translate(6px,-50%);padding:7px 12px;border-radius:999px;background:#fff;color:#222;box-shadow:0 2px 10px rgba(0,0,0,.18);font:600 13px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .15s,transform .15s}' +
'.b:hover .l,.b:focus-visible .l{opacity:1;transform:translate(0,-50%)}' +
'@keyframes wk-spin{to{transform:rotate(360deg)}}' +
'@media (prefers-reduced-motion:reduce){.c{animation:none}.l{transition:none}}';
const SVG_NS = 'http://www.w3.org/2000/svg';
function svgEl(tag, attrs) {
const n = DOC.createElementNS(SVG_NS, tag);
for (const k in attrs) n.setAttribute(k, attrs[k]);
return n;
}
function cookieSvg() {
const svg = svgEl('svg', { class: 'c', viewBox: '0 0 64 64', 'aria-hidden': 'true', focusable: 'false' });
// Umriss mit Bissspur oben rechts (zwei Bissboegen, dann der grosse Kreis zurueck)
svg.appendChild(svgEl('path', { d: 'M44.84 6A10 10 0 0 0 57.5 19.37A7.5 7.5 0 0 0 60.96 33.5A29 29 0 1 1 44.84 6Z', fill: '#dfa25b', stroke: '#a5652c', 'stroke-width': '3', 'stroke-linejoin': 'round' }));
const chips = [[19, 22, 4.2, 3.3, -20], [31, 14, 3.2, 2.6, 15], [33, 33, 4, 3.2, 30], [46, 41, 3.6, 2.8, -35], [21, 45, 4, 3, 10], [36, 52, 3, 2.4, -10], [11, 34, 2.6, 2.1, 40], [47, 31, 2.4, 2, 0]];
for (const c of chips) svg.appendChild(svgEl('ellipse', { cx: c[0], cy: c[1], rx: c[2], ry: c[3], transform: 'rotate(' + c[4] + ' ' + c[0] + ' ' + c[1] + ')', fill: '#5a3216' }));
const crumbs = [[26, 28, 1.1], [40, 22, 1], [28, 40, 1.2], [14, 26, 0.9], [40, 46, 1]];
for (const c of crumbs) svg.appendChild(svgEl('circle', { cx: c[0], cy: c[1], r: c[2], fill: '#f3c98c' }));
return svg;
}
function addSettingsLink() {
linkHostEl = DOC.createElement('div');
const st = linkHostEl.style;
st.position = 'fixed'; st.right = '12px'; st.bottom = '12px'; st.zIndex = '2147483646';
hosts.push(linkHostEl);
const root = linkHostEl.attachShadow({ mode: 'open' });
root.appendChild(el('style', {}, SETTINGS_CSS));
const b = el('button', { type: 'button', class: 'b', 'aria-label': SETTINGS_LABEL });
b.appendChild(el('span', { class: 'l', 'aria-hidden': 'true' }, SETTINGS_LABEL));
b.appendChild(cookieSvg());
b.addEventListener('click', safe(function () { openBanner(true); }));
root.appendChild(b);
DOC.body.appendChild(linkHostEl);
}
DOC.addEventListener('click', safe(function (e) {
const t = e.target;
if (t && t.closest && t.closest('[data-webklar-consent]')) { if (e.preventDefault) e.preventDefault(); openBanner(true); }
}));
if (!DOC.querySelector('[data-webklar-consent]')) addSettingsLink();
// Einwilligung in anderem Tab geaendert
WIN.addEventListener('storage', safe(function (e) {
if (e.key !== null && e.key !== CONSENT_KEY) return;
const c = readConsent(storage, Date.now(), banner.version);
if (choiceNow() === 'accept') { startTracking(); onAccept(); } else stopTracking();
if (c) closeBanner();
}));
if (stored) {
if (stored.a === 'accept') startTracking();
} else {
openBanner();
send({ t: 'bn', site, a: 'shown' });
}
}
try { main(); } catch (e) { /* nie die Seite stoeren */ }
})();
+129
View File
@@ -0,0 +1,129 @@
// WEBklar Analyse: Heatmap-Overlay (wird von w.js nur mit gueltigem ?__wk_heat=Token geladen).
// data-embed="1" (nur in einem Rahmen): Ansicht im Geraeterahmen des Kundenportals. Dann ohne Leiste,
// Links und Formulare gesperrt, und die Klickzahl geht per postMessage nur an das Portal (Herkunft von h.js).
function main() {
const me = document.currentScript;
const token = me && me.getAttribute('data-token');
const src = String((me && (me.src || me.getAttribute('src'))) || '').replace(/[?#].*$/, '');
const base = src.replace(/[^/]*$/, '');
if (!token || !base) return;
const de = document.documentElement;
let portal = '';
try { portal = new URL(base).origin; } catch (e) { /* ohne Portal keine Einbettung */ }
const embed = !!portal && me.getAttribute('data-embed') === '1' && window.parent && window.parent !== window;
function tell(state, count) {
if (!embed) return;
try { window.parent.postMessage({ wk: 'heat', state: state, count: count }, portal); } catch (e) { /* egal */ }
}
const canvas = document.createElement('canvas');
const cs = canvas.style;
cs.position = 'absolute'; cs.left = '0'; cs.top = '0'; cs.pointerEvents = 'none'; cs.zIndex = '2147483646';
document.body.appendChild(canvas);
let points = [];
let lastW = 0, lastH = 0, timer = 0;
// Farbverlauf: transparent -> blau -> gruen -> gelb -> rot
const ramp = (function () {
const c = document.createElement('canvas'); c.width = 256; c.height = 1;
const g = c.getContext('2d');
const lg = g.createLinearGradient(0, 0, 256, 0);
lg.addColorStop(0.25, 'rgb(0,0,255)'); lg.addColorStop(0.55, 'rgb(0,255,0)'); lg.addColorStop(0.85, 'rgb(255,255,0)'); lg.addColorStop(1, 'rgb(255,0,0)');
g.fillStyle = lg; g.fillRect(0, 0, 256, 1);
return g.getImageData(0, 0, 256, 1).data;
})();
if (embed) {
// Nur ansehen: kein Seitenwechsel (er wuerde zaehlen und das Banner zeigen)
const block = function (e) {
const t = e.target;
if (e.type === 'submit' || (t && t.closest && t.closest('a,area'))) { e.preventDefault(); e.stopPropagation(); }
};
window.addEventListener('click', block, true);
window.addEventListener('submit', block, true);
// Handy und Tablet haben ueberlagernde Scrollleisten; so stimmt die Breite wie auf dem Geraet
const st = document.createElement('style');
st.textContent = 'html{scrollbar-width:none}html::-webkit-scrollbar{display:none}';
(document.head || de).appendChild(st);
start(function (n) { tell('ready', n); }, function () { tell('error', 0); });
return;
}
const host = document.createElement('div');
host.style.position = 'fixed'; host.style.left = '0'; host.style.right = '0'; host.style.top = '0'; host.style.zIndex = '2147483647';
const root = host.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = ':host{all:initial}.bar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 16px;background:#111;color:#fff;font:14px/1.4 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}button{font:600 14px/1 system-ui,sans-serif;padding:8px 14px;border:2px solid #fff;border-radius:6px;background:#fff;color:#111;cursor:pointer}button:focus-visible{outline:3px solid #1a73e8;outline-offset:2px}';
const bar = document.createElement('div');
bar.className = 'bar';
const msg = document.createElement('span');
msg.textContent = 'Heatmap wird geladen ...';
const close = document.createElement('button');
close.setAttribute('type', 'button');
close.textContent = 'Schließen';
bar.appendChild(msg); bar.appendChild(close);
root.appendChild(style); root.appendChild(bar);
document.body.appendChild(host);
function draw() {
const w = Math.max(de.scrollWidth, de.clientWidth, 1);
const h = Math.max(de.scrollHeight, de.clientHeight, 1);
lastW = w; lastH = h;
// Zeichenflaeche begrenzen (Speicher): hoechstens ca. 4 Mio. Pixel
const scale = Math.min(1, Math.sqrt(4e6 / (w * h)));
const cw = Math.max(1, Math.floor(w * scale)), ch = Math.max(1, Math.floor(h * scale));
canvas.width = cw; canvas.height = ch;
cs.width = w + 'px'; cs.height = h + 'px';
const g = canvas.getContext('2d');
g.clearRect(0, 0, cw, ch);
const r = Math.max(6, 30 * scale);
for (const p of points) {
const x = p.x * cw, y = p.y * ch;
const rg = g.createRadialGradient(x, y, 0, x, y, r);
rg.addColorStop(0, 'rgba(0,0,0,0.25)'); rg.addColorStop(1, 'rgba(0,0,0,0)');
g.fillStyle = rg; g.fillRect(x - r, y - r, r * 2, r * 2);
}
// Deckkraft-Stufen in Farben umsetzen
const img = g.getImageData(0, 0, cw, ch);
const d = img.data;
for (let i = 3; i < d.length; i += 4) {
const a = d[i];
if (!a) continue;
const k = Math.min(255, a * 3) * 4;
d[i - 3] = ramp[k]; d[i - 2] = ramp[k + 1]; d[i - 1] = ramp[k + 2];
d[i] = Math.min(200, 60 + a);
}
g.putImageData(img, 0, 0);
}
function redrawIfChanged() {
if (Math.max(de.scrollWidth, de.clientWidth) !== lastW || Math.max(de.scrollHeight, de.clientHeight) !== lastH) draw();
}
function stop() {
clearInterval(timer);
window.removeEventListener('resize', draw);
host.remove(); canvas.remove();
}
close.addEventListener('click', stop);
start(function (n) { msg.textContent = 'Heatmap – ' + n + ' Klicks (letzte 30 Tage)'; },
function () { msg.textContent = 'Heatmap-Link abgelaufen oder ungültig'; });
// Punkte laden, zeichnen und bei Groessenaenderung neu zeichnen
function start(onReady, onError) {
window.addEventListener('resize', draw);
fetch(base + 'heat?token=' + encodeURIComponent(token), { credentials: 'omit' })
.then(function (r) { if (!r.ok) throw new Error('heat'); return r.json(); })
.then(function (j) {
points = (j && Array.isArray(j.points) ? j.points : []).filter(function (p) { return p && p.x >= 0 && p.x <= 1 && p.y >= 0 && p.y <= 1; });
draw();
timer = setInterval(redrawIfChanged, 1000);
onReady(points.length);
})
.catch(function () { onError(); });
}
}
try { main(); } catch (e) { /* nie die Seite stoeren */ }
+335
View File
@@ -0,0 +1,335 @@
// WEBklar Analyse: Einwilligungs-Banner + Ereignisse. Laeuft auf oeffentlichen Kunden-Websites.
// Keine Cookies, keine Kennung im Browser. Ohne Wahl nur pv {t,site,path}; ref/vp/cx/ck/ct/en nur nach Annehmen;
// nach Ablehnen oder bei GPC/DNT geht nichts mehr raus.
import { cleanPath, vpClass, refHost, cssPath, contactKind, linkHost, readConsent, contrastOk, bannerFor, CONSENT_KEY, BANNER_LINK_LABEL, BANNER_ARIA, SETTINGS_LABEL } from './core.js';
function main() {
const DOC = document;
const WIN = window;
if (WIN.__wkAnalytics) return; // doppelt eingebunden
WIN.__wkAnalytics = true;
const SELF = DOC.currentScript && DOC.currentScript.getAttribute && DOC.currentScript.getAttribute('data-site')
? DOC.currentScript : DOC.querySelector('script[src*="/a/w.js"]');
if (!SELF) return;
const site = String(SELF.getAttribute('data-site') || '');
if (!/^[a-z0-9]{16,40}$/.test(site)) return;
const colorAttr = String(SELF.getAttribute('data-color') || '');
const accent = contrastOk(colorAttr) ? colorAttr : '#111111';
// data-variant="eigen": Banner-Fassung fuer WEBklars eigene Websites (Sie-Form, ohne Auftragsverarbeiter)
const banner = bannerFor(String(SELF.getAttribute('data-variant') || ''));
const src = String(SELF.src || SELF.getAttribute('src') || '').replace(/[?#].*$/, '');
const base = src.replace(/[^/]*$/, '');
const endpoint = base + 'e';
// ---- Heatmap-Modus: nichts senden, kein Banner ----
const qs = new URLSearchParams(WIN.location.search);
const heatToken = qs.get('__wk_heat');
if (heatToken) {
// __wk_embed=1: Ansicht im Geraeterahmen des Kundenportals (h.js ohne Leiste, Links gesperrt)
const embed = qs.get('__wk_embed') === '1';
qs.delete('__wk_heat');
qs.delete('__wk_embed');
const rest = qs.toString();
try { WIN.history.replaceState(null, '', WIN.location.pathname + (rest ? '?' + rest : '') + (WIN.location.hash || '')); } catch (e) { /* egal */ }
const s = DOC.createElement('script');
s.setAttribute('src', base + 'h.js');
s.setAttribute('data-token', heatToken);
if (embed) s.setAttribute('data-embed', '1');
(DOC.head || DOC.documentElement).appendChild(s);
return;
}
// ---- Senden: Fehler werden verschluckt ----
function send(obj) {
try {
const body = JSON.stringify(obj);
fetch(endpoint, { method: 'POST', body, keepalive: true, credentials: 'omit', headers: { 'Content-Type': 'text/plain' } }).catch(function () {});
} catch (e) { /* egal */ }
}
const safe = (fn) => function () { try { return fn.apply(this, arguments); } catch (e) { /* nie die Seite stoeren */ } };
const vp = () => vpClass(WIN.innerWidth);
const ownHost = () => WIN.location.hostname;
// ---- Widerspruch (GPC/DNT) und Wahl: vor jedem Senden und vor jedem Hook ----
let storage = null;
try { storage = WIN.localStorage; } catch (e) { /* gesperrt */ }
const dnt = (v) => v === '1' || v === 'yes';
const optOut = navigator.globalPrivacyControl === true || dnt(navigator.doNotTrack) || dnt(WIN.doNotTrack) || dnt(navigator.msDoNotTrack);
if (optOut) return; // wie Ablehnen: nichts senden, kein Banner, keine Hooks
const stored = readConsent(storage, Date.now(), banner.version);
let memChoice = null;
let tracking = false;
// Wahl bei jedem Aufruf frisch lesen (Speicher, sonst nur fuer diese Seite gemerkt)
function choiceNow() {
const c = readConsent(storage, Date.now(), banner.version);
return c ? c.a : (memChoice ? memChoice.a : null);
}
const counting = () => choiceNow() !== 'reject';
// ---- Seitenaufruf (pv): ohne Wahl nur Seitenadresse, ref/vp nur nach Annehmen ----
let lastPath = null;
let pvStart = Date.now();
let maxScroll = 0;
let enSent = false;
let pvSent = false;
let cxSent = false;
function sendPv(first) {
if (!counting()) return;
lastPath = cleanPath(WIN.location.pathname);
pvStart = Date.now(); maxScroll = 0; enSent = false;
const ev = { t: 'pv', site, path: lastPath };
if (choiceNow() === 'accept') {
if (first) { ev.ref = refHost(DOC.referrer, ownHost()); cxSent = true; }
ev.vp = vp();
}
pvSent = true;
send(ev);
}
let navTimer = null;
function scheduleNav() {
if (navTimer || !counting()) return;
navTimer = setTimeout(safe(function () {
navTimer = null;
if (!counting() || cleanPath(WIN.location.pathname) === lastPath) return;
// SPA-Seitenwechsel (Router mit preventDefault, onLinkClick greift dann nicht): en der vorigen Seite zuerst
sendEn();
sendPv(false);
}), 300);
}
let hooksOn = false;
function ensureHooks() {
if (hooksOn) return;
hooksOn = true;
WIN.addEventListener('pageshow', safe(function (e) { if (e && e.persisted) sendPv(false); }));
try {
const orig = WIN.history.pushState;
WIN.history.pushState = function () { const r = orig.apply(this, arguments); try { scheduleNav(); } catch (e) { /* egal */ } return r; };
WIN.addEventListener('popstate', safe(scheduleNav));
} catch (e) { /* egal */ }
}
// Nach "Annehmen": Herkunft und Fenstergroesse einmal nachreichen, oder (noch kein pv) pv mit Daten senden
function onAccept() {
ensureHooks();
if (!pvSent) { sendPv(true); return; }
if (cxSent) return;
cxSent = true;
send({ t: 'cx', site, path: lastPath, ref: refHost(DOC.referrer, ownHost()), vp: vp() });
}
if (choiceNow() !== 'reject') { sendPv(true); ensureHooks(); }
// ---- Tracking (nur nach Annehmen erreichbar) ----
const hosts = [];
const scrollPct = () => {
const h = DOC.documentElement.scrollHeight || 1;
return Math.max(0, Math.min(100, Math.round(((WIN.scrollY || 0) + WIN.innerHeight) / h * 100)));
};
const rel = (v, total) => Math.max(0, Math.min(1, Math.round(v / (total || 1) * 10000) / 10000));
const onClick = safe(function (e) {
if (!allowed()) return;
const t = e.target;
if (!t || hosts.indexOf(t) !== -1) return;
const dw = DOC.documentElement.scrollWidth, dh = DOC.documentElement.scrollHeight;
const px = typeof e.pageX === 'number' ? e.pageX : (e.clientX || 0) + (WIN.scrollX || 0);
const py = typeof e.pageY === 'number' ? e.pageY : (e.clientY || 0) + (WIN.scrollY || 0);
const path = cleanPath(WIN.location.pathname);
send({ t: 'ck', site, path, vp: vp(), x: rel(px, dw), y: rel(py, dh), sel: cssPath(t) });
let a = t;
while (a && String(a.tagName).toUpperCase() !== 'A') a = a.parentNode;
const kind = a ? contactKind(a, ownHost()) : null;
if (kind) {
const ev = { t: 'ct', site, path, kind };
if (kind === 'out') ev.host = linkHost(a);
send(ev);
}
});
const onScroll = safe(function () { maxScroll = Math.max(maxScroll, scrollPct()); });
function sendEn() {
if (enSent || !allowed()) return;
enSent = true;
send({ t: 'en', site, path: lastPath || cleanPath(WIN.location.pathname), vp: vp(),
sec: Math.max(0, Math.min(3600, Math.round((Date.now() - pvStart) / 1000))), scroll: Math.max(maxScroll, scrollPct()) });
}
const onLeave = safe(sendEn);
// Interner Seitenwechsel per Link: en sofort senden, der Beacon beim pagehide geht in Chromium oft verloren.
// Bubbling-Listener am Dokument: laeuft nach den Handlern der Seite, defaultPrevented ist dann bekannt.
const onLinkClick = safe(function (e) {
if (e.defaultPrevented || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey || (typeof e.button === 'number' && e.button !== 0)) return;
let a = e.target;
while (a && String(a.tagName).toUpperCase() !== 'A') a = a.parentNode;
if (!a || hosts.indexOf(e.target) !== -1) return;
const tg = String(a.getAttribute('target') || '').toLowerCase();
if ((tg && tg !== '_self') || a.getAttribute('download') !== null) return;
const u = new URL(String(a.href || a.getAttribute('href') || ''), WIN.location.href);
if (u.protocol !== 'http:' && u.protocol !== 'https:') return;
if (u.origin !== WIN.location.origin || u.pathname === WIN.location.pathname) return;
sendEn();
});
const onVis = safe(function () { if (DOC.visibilityState === 'hidden') onLeave(); });
// Einwilligung unmittelbar vor jedem Senden pruefen (z.B. in einem anderen Tab widerrufen)
function allowed() {
const ok = choiceNow() === 'accept';
if (!ok && tracking) stopTracking();
return ok;
}
function startTracking() {
if (tracking) return;
tracking = true;
DOC.addEventListener('click', onClick, { capture: true, passive: true });
DOC.addEventListener('click', onLinkClick, { passive: true });
DOC.addEventListener('visibilitychange', onVis);
WIN.addEventListener('pagehide', onLeave);
WIN.addEventListener('scroll', onScroll, { passive: true });
}
function stopTracking() {
tracking = false;
DOC.removeEventListener('click', onClick, { capture: true });
DOC.removeEventListener('click', onLinkClick);
DOC.removeEventListener('visibilitychange', onVis);
WIN.removeEventListener('pagehide', onLeave);
WIN.removeEventListener('scroll', onScroll);
}
// ---- Einwilligung ----
let bannerHost = null;
let linkHostEl = null;
function choose(a) {
memChoice = null;
try { storage.setItem(CONSENT_KEY, JSON.stringify({ v: 1, a, t: Date.now(), bv: banner.version })); } catch (e) { memChoice = { a }; /* ohne Speicher: gilt nur fuer diese Seite */ }
send({ t: 'bn', site, a });
if (a === 'accept') { startTracking(); onAccept(); } else stopTracking();
closeBanner();
}
function el(tag, attrs, text) {
const n = DOC.createElement(tag);
for (const k in attrs || {}) n.setAttribute(k, attrs[k]);
if (text != null) n.textContent = text;
return n;
}
const CSS = ':host{all:initial}' +
'*{box-sizing:border-box}' +
'.bar{display:flex;align-items:center;justify-content:center;gap:16px;padding:14px 20px;background:#fff;color:#111;border-top:1px solid #d0d0d0;box-shadow:0 -2px 12px rgba(0,0,0,.12);font:15px/1.45 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}' +
'.t{margin:0;max-width:720px}' +
'.t a{color:#111;text-decoration:underline}' +
'.r{display:flex;gap:10px;flex:none}' +
'button{font:600 15px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;width:150px;height:44px;padding:0 20px;border:2px solid ' + accent + ';border-radius:8px;cursor:pointer;background:' + accent + ';color:#fff}' +
'button:focus-visible{outline:3px solid #1a73e8;outline-offset:2px}' +
'@media (max-width:767px){.bar{display:block;margin:8px;padding:16px;border:1px solid #d0d0d0;border-radius:12px}.r{flex-direction:column;margin-top:12px}button{width:100%}}';
let prevPad = '';
function applyPad() {
if (!bannerHost) return;
const h = Math.ceil(bannerHost.getBoundingClientRect().height || bannerHost.offsetHeight || 0);
DOC.body.style.paddingBottom = h + 'px';
}
const onResize = safe(applyPad);
function openBanner(focus) {
if (bannerHost) return;
const prev = DOC.activeElement;
bannerHost = DOC.createElement('div');
const st = bannerHost.style;
st.position = 'fixed'; st.left = '0'; st.right = '0'; st.bottom = '0'; st.zIndex = '2147483647';
hosts.push(bannerHost);
const root = bannerHost.attachShadow({ mode: 'open' });
root.appendChild(el('style', {}, CSS));
const bar = el('div', { class: 'bar', role: 'dialog', 'aria-label': BANNER_ARIA });
const p = el('p', { class: 't' }, banner.text);
p.appendChild(el('a', { href: '/datenschutz-analyse.html' }, BANNER_LINK_LABEL));
const row = el('div', { class: 'r' });
const no = el('button', { type: 'button' }, 'Ablehnen');
const yes = el('button', { type: 'button' }, 'Annehmen');
no.addEventListener('click', safe(function () { choose('reject'); if (prev && prev.focus) prev.focus(); }));
yes.addEventListener('click', safe(function () { choose('accept'); if (prev && prev.focus) prev.focus(); }));
row.appendChild(no); row.appendChild(yes);
bar.appendChild(p); bar.appendChild(row);
root.appendChild(bar);
DOC.body.appendChild(bannerHost);
// Platz unten schaffen, damit Footer-Links (Impressum, Datenschutz) erreichbar bleiben
prevPad = DOC.body.style.paddingBottom;
applyPad();
WIN.addEventListener('resize', onResize);
if (linkHostEl) linkHostEl.style.display = 'none';
if (focus === true) no.focus();
}
function closeBanner() {
if (!bannerHost) return;
const i = hosts.indexOf(bannerHost);
if (i !== -1) hosts.splice(i, 1);
bannerHost.remove();
bannerHost = null;
WIN.removeEventListener('resize', onResize);
DOC.body.style.paddingBottom = prevPad;
if (linkHostEl) linkHostEl.style.display = '';
}
// Einstellungsknopf: angeknabberter Keks unten rechts, dreht sich langsam (nicht bei reduzierter Bewegung).
// Beim Darueberfahren haelt er an und zeigt "Datenschutz-Einstellungen"; das ist auch sein Name fuer Screenreader.
// Inline-SVG statt data:-Grafik, damit eine strenge CSP der Website (img-src) ihn nicht blockiert.
const SETTINGS_CSS = ':host{all:initial}' +
'.b{position:relative;display:block;width:44px;height:44px;padding:2px;margin:0;border:0;border-radius:50%;background:transparent;cursor:pointer;-webkit-tap-highlight-color:transparent}' +
'.b:focus-visible{outline:3px solid #1a73e8;outline-offset:2px}' +
'.c{display:block;width:40px;height:40px;filter:drop-shadow(0 2px 3px rgba(0,0,0,.35));animation:wk-spin 9s linear infinite}' +
'.b:hover .c,.b:focus-visible .c{animation-play-state:paused}' +
'.l{position:absolute;right:52px;top:50%;transform:translate(6px,-50%);padding:7px 12px;border-radius:999px;background:#fff;color:#222;box-shadow:0 2px 10px rgba(0,0,0,.18);font:600 13px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .15s,transform .15s}' +
'.b:hover .l,.b:focus-visible .l{opacity:1;transform:translate(0,-50%)}' +
'@keyframes wk-spin{to{transform:rotate(360deg)}}' +
'@media (prefers-reduced-motion:reduce){.c{animation:none}.l{transition:none}}';
const SVG_NS = 'http://www.w3.org/2000/svg';
function svgEl(tag, attrs) {
const n = DOC.createElementNS(SVG_NS, tag);
for (const k in attrs) n.setAttribute(k, attrs[k]);
return n;
}
function cookieSvg() {
const svg = svgEl('svg', { class: 'c', viewBox: '0 0 64 64', 'aria-hidden': 'true', focusable: 'false' });
// Umriss mit Bissspur oben rechts (zwei Bissboegen, dann der grosse Kreis zurueck)
svg.appendChild(svgEl('path', { d: 'M44.84 6A10 10 0 0 0 57.5 19.37A7.5 7.5 0 0 0 60.96 33.5A29 29 0 1 1 44.84 6Z', fill: '#dfa25b', stroke: '#a5652c', 'stroke-width': '3', 'stroke-linejoin': 'round' }));
const chips = [[19, 22, 4.2, 3.3, -20], [31, 14, 3.2, 2.6, 15], [33, 33, 4, 3.2, 30], [46, 41, 3.6, 2.8, -35], [21, 45, 4, 3, 10], [36, 52, 3, 2.4, -10], [11, 34, 2.6, 2.1, 40], [47, 31, 2.4, 2, 0]];
for (const c of chips) svg.appendChild(svgEl('ellipse', { cx: c[0], cy: c[1], rx: c[2], ry: c[3], transform: 'rotate(' + c[4] + ' ' + c[0] + ' ' + c[1] + ')', fill: '#5a3216' }));
const crumbs = [[26, 28, 1.1], [40, 22, 1], [28, 40, 1.2], [14, 26, 0.9], [40, 46, 1]];
for (const c of crumbs) svg.appendChild(svgEl('circle', { cx: c[0], cy: c[1], r: c[2], fill: '#f3c98c' }));
return svg;
}
function addSettingsLink() {
linkHostEl = DOC.createElement('div');
const st = linkHostEl.style;
st.position = 'fixed'; st.right = '12px'; st.bottom = '12px'; st.zIndex = '2147483646';
hosts.push(linkHostEl);
const root = linkHostEl.attachShadow({ mode: 'open' });
root.appendChild(el('style', {}, SETTINGS_CSS));
const b = el('button', { type: 'button', class: 'b', 'aria-label': SETTINGS_LABEL });
b.appendChild(el('span', { class: 'l', 'aria-hidden': 'true' }, SETTINGS_LABEL));
b.appendChild(cookieSvg());
b.addEventListener('click', safe(function () { openBanner(true); }));
root.appendChild(b);
DOC.body.appendChild(linkHostEl);
}
DOC.addEventListener('click', safe(function (e) {
const t = e.target;
if (t && t.closest && t.closest('[data-webklar-consent]')) { if (e.preventDefault) e.preventDefault(); openBanner(true); }
}));
if (!DOC.querySelector('[data-webklar-consent]')) addSettingsLink();
// Einwilligung in anderem Tab geaendert
WIN.addEventListener('storage', safe(function (e) {
if (e.key !== null && e.key !== CONSENT_KEY) return;
const c = readConsent(storage, Date.now(), banner.version);
if (choiceNow() === 'accept') { startTracking(); onAccept(); } else stopTracking();
if (c) closeBanner();
}));
if (stored) {
if (stored.a === 'accept') startTracking();
} else {
openBanner();
send({ t: 'bn', site, a: 'shown' });
}
}
try { main(); } catch (e) { /* nie die Seite stoeren */ }
+10
View File
@@ -0,0 +1,10 @@
{
"name": "webklar-analytics",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"start": "node src/index.js",
"test": "node --test test/*.test.js"
}
}
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env node
// CSV-Export einer Website (Tageswerte, Einzelereignisse, Klicks). Öffnet die Datenbank nur lesend.
//
// Ablauf (Runbook): Im Container webklar-analytics ausführen (Code liegt dort unter /analytics, schreibbar ist nur /srv/webklar-analytics):
// docker exec webklar-analytics node /analytics/scripts/export-site.mjs <siteKey> --out /srv/webklar-analytics/exports/<siteKey>
// Die CSV-Dateien enthalten Besucher-Hashes (Rechte 0600) und müssen dem Kunden als Kopie innerhalb von zwei Wochen
// übergeben werden (AVV Nr. 9); danach den Export-Ordner wieder löschen.
import { DatabaseSync } from 'node:sqlite';
import { mkdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
const RE_KEY = /^[a-z0-9]{16,40}$/;
const iso = (ts) => new Date(ts).toISOString();
function cell(v) {
if (v === null || v === undefined) return '';
let s = String(v);
if (typeof v === 'string' && /^[=+\-@\t\r]/.test(s)) s = `'${s}`;
return /[",\r\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
}
const csv = (head, rows) => [head, ...rows].map((r) => r.map(cell).join(',')).join('\r\n') + '\r\n';
export function exportSite({ dbPath, siteKey, outDir }) {
if (!RE_KEY.test(siteKey || '')) throw new Error('Ungültiger Website-Schlüssel');
const db = new DatabaseSync(dbPath, { readOnly: true });
try {
if (!db.prepare('SELECT 1 FROM sites WHERE siteKey = ?').get(siteKey)) throw new Error('Unbekannte Website');
const daily = db.prepare('SELECT day, dim, key, count FROM daily WHERE site = ? ORDER BY day, dim, key').all(siteKey);
const ev = db.prepare('SELECT ts, type, path, vp, ref, country, device, visitor, sec, scroll, kind, host FROM events_raw WHERE site = ? ORDER BY ts, id').all(siteKey);
const cl = db.prepare('SELECT ts, path, vp, x, y, sel FROM clicks WHERE site = ? ORDER BY ts, id').all(siteKey);
mkdirSync(outDir, { recursive: true });
const files = {
[`${siteKey}-tageswerte.csv`]: csv(['day', 'dim', 'key', 'count'], daily.map((r) => [r.day, r.dim, r.key, r.count])),
[`${siteKey}-ereignisse.csv`]: csv(['time', 'type', 'path', 'vp', 'ref', 'country', 'device', 'visitor', 'sec', 'scroll', 'kind', 'host'],
ev.map((r) => [iso(r.ts), r.type, r.path, r.vp, r.ref, r.country, r.device, r.visitor, r.sec, r.scroll, r.kind, r.host])),
[`${siteKey}-klicks.csv`]: csv(['time', 'path', 'vp', 'x', 'y', 'sel'], cl.map((r) => [iso(r.ts), r.path, r.vp, r.x, r.y, r.sel])),
};
for (const [n, c] of Object.entries(files)) writeFileSync(join(outDir, n), c, { encoding: 'utf8', mode: 0o600 });
return Object.keys(files).map((n) => join(outDir, n));
} finally { db.close(); }
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const args = process.argv.slice(2);
const opt = (n, d) => { const i = args.indexOf(n); return i >= 0 ? args[i + 1] : d; };
const siteKey = args.find((a, i) => !a.startsWith('--') && !(i > 0 && args[i - 1].startsWith('--')));
try {
const files = exportSite({ dbPath: opt('--db', '/srv/webklar-analytics/analytics.db'), siteKey, outDir: opt('--out', '.') });
console.log(files.join('\n'));
} catch (e) {
console.error(`Export fehlgeschlagen: ${e.message}`);
process.exit(1);
}
}
+60
View File
@@ -0,0 +1,60 @@
#!/bin/sh
# Lädt die aktuelle DB-IP-Lite-Länderdatei (monatlich) nach $ANALYTICS_DATA_DIR/geo.csv und startet
# den Dienst nur neu, wenn sich die Datei geändert hat. Läuft auf dem Host (z. B. per Cron, 1x pro Woche).
# Die alte geo.csv wird nie gelöscht, bevor die neue geprüft und umbenannt ist.
set -eu
DIR="${ANALYTICS_DATA_DIR:-/srv/webklar-analytics}"
CONTAINER="${ANALYTICS_CONTAINER:-webklar-analytics}"
NEU="$DIR/geo.csv.new"
ZIEL="$DIR/geo.csv"
MARKER="$DIR/geo.restart-pending"
fehler() { echo "Fehler (geo-update): $*" >&2; rm -f "$NEU" "$NEU.gz"; exit 1; }
command -v curl >/dev/null 2>&1 || fehler "curl fehlt."
command -v gunzip >/dev/null 2>&1 || fehler "gunzip fehlt."
[ -d "$DIR" ] || fehler "Datenverzeichnis $DIR existiert nicht."
JETZT="$(date -u +%Y-%m)"
JAHR="${JETZT%-*}"
MONAT="${JETZT#*-}"
m="$(printf '%s' "$MONAT" | sed 's/^0//')"
if [ "$m" -eq 1 ]; then VORMONAT="$((JAHR - 1))-12"; else VORMONAT="$JAHR-$(printf '%02d' "$((m - 1))")"; fi
geladen=""
for monat in "$JETZT" "$VORMONAT"; do
url="https://download.db-ip.com/free/dbip-country-lite-$monat.csv.gz"
if curl -fsSL --retry 2 --max-time 300 -o "$NEU.gz" "$url" 2>/dev/null && gunzip -c "$NEU.gz" > "$NEU" 2>/dev/null; then
geladen="$monat"
break
fi
done
rm -f "$NEU.gz"
[ -n "$geladen" ] || fehler "Download von DB-IP fehlgeschlagen (weder $JETZT noch $VORMONAT). Bestehende geo.csv bleibt unverändert."
zeilen="$(wc -l < "$NEU" | tr -d ' ')"
[ "$zeilen" -gt 100000 ] || fehler "Neue Datei hat nur $zeilen Zeilen (erwartet > 100000). Bestehende geo.csv bleibt unverändert."
grep -Eq ',"?DE"?\r?$' "$NEU" || fehler "Neue Datei enthält kein 'DE'. Bestehende geo.csv bleibt unverändert."
neustart() {
docker restart "$CONTAINER" >/dev/null || fehler "Neustart von $CONTAINER fehlgeschlagen (geo.csv ist aber aktualisiert, nächster Lauf versucht den Neustart erneut)."
rm -f "$MARKER"
echo "$CONTAINER neu gestartet."
}
if [ -f "$ZIEL" ] && cmp -s "$NEU" "$ZIEL"; then
rm -f "$NEU"
if [ -f "$MARKER" ]; then
echo "geo.csv ($geladen) unverändert, aber ein früherer Neustart steht aus."
neustart
else
echo "geo.csv ($geladen) unverändert, kein Neustart nötig."
fi
exit 0
fi
mv -f "$NEU" "$ZIEL"
: > "$MARKER" # bleibt liegen, bis der Neustart gelungen ist
echo "geo.csv aktualisiert ($geladen, $zeilen Zeilen)."
neustart
+82
View File
@@ -0,0 +1,82 @@
// GET/HEAD /a/w.js und /a/h.js (öffentlicher Listener): generierte Client-Skripte aus client/dist/.
// Einmal beim Start in den Speicher geladen (roh + gzip); fehlt eine Datei, antwortet der Pfad mit 503.
import { readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { gzipSync } from 'node:zlib';
import crypto from 'node:crypto';
const DEFAULT_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'client', 'dist');
const FILES = { '/a/w.js': 'w.js', '/a/h.js': 'h.js' };
const RE_KEY = /^[a-z0-9]{16,40}$/;
const EMPTY = Buffer.from('/* WEBklar Analyse: aus */\n');
export function createAssets({ distDir = DEFAULT_DIR, log = console, isActive = () => true } = {}) {
const assets = new Map();
for (const [path, file] of Object.entries(FILES)) {
try {
const raw = readFileSync(join(distDir, file));
assets.set(path, { raw, gz: gzipSync(raw, { level: 9 }), etag: `"${crypto.createHash('sha256').update(raw).digest('hex')}"` });
} catch (e) {
log.error(`Analytics: Skript ${file} fehlt in ${distDir} (${e.code || e.message}); ${path} antwortet mit 503`);
}
}
// true, wenn die Anfrage ein Skript betraf.
return function handle(req, res, pathname) {
if (!(pathname in FILES)) return false;
if (req.method !== 'GET' && req.method !== 'HEAD') {
res.writeHead(405, { Allow: 'GET, HEAD' });
res.end();
return true;
}
if (pathname === '/a/w.js') {
let key = '';
try { key = new URL(req.url, 'http://x').searchParams.get('s') || ''; } catch { /* leer */ }
let on = false;
try { on = RE_KEY.test(key) && !!isActive(key); } catch (e) { log.error('Analytics: isActive:', e.message); }
if (!on) {
res.writeHead(200, {
'Content-Type': 'text/javascript; charset=utf-8',
'Cache-Control': 'public, max-age=60',
Vary: 'Accept-Encoding',
'X-Content-Type-Options': 'nosniff',
'Cross-Origin-Resource-Policy': 'cross-origin',
'Access-Control-Allow-Origin': '*',
'Content-Length': EMPTY.length,
});
res.end(req.method === 'HEAD' ? undefined : EMPTY);
return true;
}
}
const a = assets.get(pathname);
if (!a) {
res.writeHead(503, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
res.end(req.method === 'HEAD' ? '' : JSON.stringify({ error: 'Skript nicht verfügbar' }));
return true;
}
const headers = {
'Content-Type': 'text/javascript; charset=utf-8',
'Cache-Control': 'public, max-age=3600',
ETag: a.etag,
Vary: 'Accept-Encoding',
'X-Content-Type-Options': 'nosniff',
'Cross-Origin-Resource-Policy': 'cross-origin',
'Access-Control-Allow-Origin': '*',
};
const inm = req.headers['if-none-match'];
if (inm && inm.split(',').some((t) => t.trim().replace(/^W\//, '') === a.etag)) {
res.writeHead(304, headers);
res.end();
return true;
}
const gzip = /\bgzip\b/i.test(req.headers['accept-encoding'] || '');
const body = gzip ? a.gz : a.raw;
if (gzip) headers['Content-Encoding'] = 'gzip';
headers['Content-Length'] = body.length;
res.writeHead(200, headers);
res.end(req.method === 'HEAD' ? undefined : body);
return true;
};
}
+12
View File
@@ -0,0 +1,12 @@
// Konfiguration aus der Umgebung; leerer adminToken sperrt alle Admin-Routen.
export function loadConfig(env = process.env) {
return {
port: Number(env.ANALYTICS_PORT) || 8090,
adminPort: Number(env.ANALYTICS_ADMIN_PORT) || 8091,
dataDir: env.ANALYTICS_DATA_DIR || '/srv/webklar-analytics',
adminToken: env.ANALYTICS_ADMIN_TOKEN || '',
heatSecret: env.ANALYTICS_HEAT_SECRET || '',
geoFile: env.ANALYTICS_GEO_FILE || '',
baseHost: env.ANALYTICS_BASE_HOST || 'project.webklar.com',
};
}
+61
View File
@@ -0,0 +1,61 @@
import { DatabaseSync } from 'node:sqlite';
const SCHEMA = `
CREATE TABLE IF NOT EXISTS sites (
siteKey TEXT PRIMARY KEY,
projectId TEXT NOT NULL,
domains TEXT NOT NULL,
active INTEGER NOT NULL DEFAULT 1,
offSince TEXT
);
CREATE TABLE IF NOT EXISTS events_raw (
id INTEGER PRIMARY KEY AUTOINCREMENT,
site TEXT NOT NULL,
ts INTEGER NOT NULL,
type TEXT NOT NULL,
path TEXT,
vp TEXT,
ref TEXT,
country TEXT,
device TEXT,
visitor TEXT,
sec INTEGER,
scroll INTEGER,
kind TEXT,
host TEXT
);
CREATE INDEX IF NOT EXISTS idx_events_site_ts ON events_raw(site, ts);
CREATE TABLE IF NOT EXISTS clicks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
site TEXT NOT NULL,
ts INTEGER NOT NULL,
path TEXT NOT NULL,
vp TEXT NOT NULL,
x REAL NOT NULL,
y REAL NOT NULL,
sel TEXT
);
CREATE INDEX IF NOT EXISTS idx_clicks_site_path_vp_ts ON clicks(site, path, vp, ts);
CREATE TABLE IF NOT EXISTS daily (
site TEXT NOT NULL,
day TEXT NOT NULL,
dim TEXT NOT NULL,
key TEXT NOT NULL,
count INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (site, day, dim, key)
);
-- Merker für die Nachtverdichtung: Tage, die schon in daily stehen (macht runNightly idempotent).
CREATE TABLE IF NOT EXISTS agg_state (
site TEXT NOT NULL,
day TEXT NOT NULL,
PRIMARY KEY (site, day)
);
`;
export function openDb(path) {
const db = new DatabaseSync(path);
if (path !== ':memory:') db.exec('PRAGMA journal_mode = WAL');
db.exec('PRAGMA secure_delete = ON');
db.exec(SCHEMA);
return db;
}
+136
View File
@@ -0,0 +1,136 @@
// Länderzuordnung per DB-IP Lite (country, CSV: start_ip,end_ip,land). Die Datei wird einmal beim
// Start geladen (IPv4 als Uint32, IPv6 als 2×64 Bit in BigUint64Array) und per Binärsuche abgefragt.
import { readFileSync } from 'node:fs';
import net from 'node:net';
const MASK64 = (1n << 64n) - 1n;
function parseV4(s) {
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(s);
if (!m) return -1;
let n = 0;
for (let i = 1; i <= 4; i++) {
const o = Number(m[i]);
if (o > 255) return -1;
n = n * 256 + o;
}
return n;
}
// IPv6 ⇒ 128-Bit-BigInt oder null (auch mit angehängter IPv4-Schreibweise).
function parseV6(s) {
if (net.isIPv6(s) === false) return null;
s = s.split('%')[0];
const dot = s.lastIndexOf('.');
if (dot !== -1) {
const v4 = parseV4(s.slice(s.lastIndexOf(':') + 1));
if (v4 < 0) return null;
s = s.slice(0, s.lastIndexOf(':') + 1) + Math.floor(v4 / 65536).toString(16) + ':' + (v4 % 65536).toString(16);
}
const [head, tail] = s.split('::');
const h = head ? head.split(':') : [];
const t = tail === undefined ? [] : (tail ? tail.split(':') : []);
const fill = tail === undefined ? 0 : 8 - h.length - t.length;
const groups = [...h, ...Array(Math.max(fill, 0)).fill('0'), ...t];
if (groups.length !== 8) return null;
let n = 0n;
for (const g of groups) n = (n << 16n) | BigInt(parseInt(g, 16));
return n;
}
const pack = (c) => (c.length === 2 && c !== 'ZZ' && /^[A-Za-z]{2}$/.test(c) ? (c.toUpperCase().charCodeAt(0) << 8) | c.toUpperCase().charCodeAt(1) : 0);
const unpack = (n) => (n ? String.fromCharCode(n >> 8, n & 255) : '');
function sortedBy(n, cmp) {
for (let i = 1; i < n; i++) if (cmp(i - 1, i) > 0) return false;
return true;
}
export function loadGeo(file) {
const none = { lookup: () => '', size: 0 };
let text;
try { text = readFileSync(file, 'utf8'); } catch (e) {
console.warn(`Analytics: Länderdatei ${file} nicht lesbar (${e.code || e.message}); Länder bleiben leer.`);
return none;
}
// Obergrenze der Zeilen für die Vorab-Reservierung
let cap = 1;
for (let i = text.indexOf('\n'); i !== -1; i = text.indexOf('\n', i + 1)) cap++;
let n4 = 0;
let n6 = 0;
const s4 = new Uint32Array(cap); const e4 = new Uint32Array(cap); const c4 = new Uint16Array(cap);
const sh = new BigUint64Array(cap); const sl = new BigUint64Array(cap);
const eh = new BigUint64Array(cap); const el = new BigUint64Array(cap); const c6 = new Uint16Array(cap);
let pos = 0;
while (pos < text.length) {
let nl = text.indexOf('\n', pos);
if (nl === -1) nl = text.length;
const line = text.slice(pos, nl).replace(/["\r]/g, '').trim();
pos = nl + 1;
if (!line) continue;
const p = line.split(',');
if (p.length < 3) continue;
const a = p[0].trim(); const b = p[1].trim(); const c = pack(p[2].trim());
if (a.includes(':')) {
const x = parseV6(a); const y = parseV6(b);
if (x === null || y === null || y < x) continue;
sh[n6] = x >> 64n; sl[n6] = x & MASK64; eh[n6] = y >> 64n; el[n6] = y & MASK64; c6[n6++] = c;
} else {
const x = parseV4(a); const y = parseV4(b);
if (x < 0 || y < x) continue;
s4[n4] = x; e4[n4] = y; c4[n4++] = c;
}
}
text = null;
// Falls die Datei nicht sortiert ist: Indexsortierung
let S4 = s4.slice(0, n4); let E4 = e4.slice(0, n4); let C4 = c4.slice(0, n4);
if (!sortedBy(n4, (i, j) => S4[i] - S4[j])) {
const idx = Array.from({ length: n4 }, (_, i) => i).sort((i, j) => S4[i] - S4[j]);
S4 = Uint32Array.from(idx, (i) => s4[i]); E4 = Uint32Array.from(idx, (i) => e4[i]); C4 = Uint16Array.from(idx, (i) => c4[i]);
}
let SH = sh.slice(0, n6); let SL = sl.slice(0, n6); let EH = eh.slice(0, n6); let EL = el.slice(0, n6); let C6 = c6.slice(0, n6);
const cmp6 = (i, j) => (SH[i] !== SH[j] ? (SH[i] < SH[j] ? -1 : 1) : (SL[i] === SL[j] ? 0 : (SL[i] < SL[j] ? -1 : 1)));
if (!sortedBy(n6, cmp6)) {
const idx = Array.from({ length: n6 }, (_, i) => i).sort(cmp6);
const pick = (A, T) => T.from(idx, (i) => A[i]);
const [a, b, c, d, e] = [pick(SH, BigUint64Array), pick(SL, BigUint64Array), pick(EH, BigUint64Array), pick(EL, BigUint64Array), pick(C6, Uint16Array)];
SH = a; SL = b; EH = c; EL = d; C6 = e;
}
const look4 = (v) => {
let lo = 0; let hi = n4 - 1; let best = -1;
while (lo <= hi) {
const mid = (lo + hi) >>> 1;
if (S4[mid] <= v) { best = mid; lo = mid + 1; } else hi = mid - 1;
}
return best >= 0 && v <= E4[best] ? unpack(C4[best]) : '';
};
const look6 = (h, l) => {
let lo = 0; let hi = n6 - 1; let best = -1;
while (lo <= hi) {
const mid = (lo + hi) >>> 1;
if (SH[mid] < h || (SH[mid] === h && SL[mid] <= l)) { best = mid; lo = mid + 1; } else hi = mid - 1;
}
if (best < 0) return '';
const inside = h < EH[best] || (h === EH[best] && l <= EL[best]);
return inside ? unpack(C6[best]) : '';
};
return {
size: n4 + n6,
lookup(ip) {
if (typeof ip !== 'string') return '';
ip = ip.trim();
if (!ip.includes(':')) {
const v = parseV4(ip);
return v < 0 ? '' : look4(v);
}
const v = parseV6(ip);
if (v === null) return '';
const h = v >> 64n; const l = v & MASK64;
if (h === 0n && (l >> 32n) === 0xffffn) return look4(Number(l & 0xffffffffn)); // ::ffff:a.b.c.d
return look6(h, l);
},
};
}
+85
View File
@@ -0,0 +1,85 @@
// GET /a/heat?token= (öffentlicher Listener): Klickpunkte für das Heatmap-Overlay.
// Token (R3): <b64url(JSON {s,p,v,e})>.<hex HMAC-SHA256(heatSecret, b64part)>, e = Ablauf in Unix-Sekunden.
import crypto from 'node:crypto';
import { hostOf, cleanPath } from './ingest.js';
const SITE_KEY = /^[a-z0-9]{16,40}$/;
const MAX_AHEAD = 30 * 60; // Sekunden
const POINTS = 5000;
const CLICK_DAYS = 30;
export function signHeatToken({ siteKey, path, vp, exp }, secret) {
const part = Buffer.from(JSON.stringify({ s: siteKey, p: path, v: vp, e: exp })).toString('base64url');
return `${part}.${crypto.createHmac('sha256', secret).update(part).digest('hex')}`;
}
// Liefert { siteKey, path, vp, exp } oder null (Signatur, Format, abgelaufen, > 30 Min voraus).
// ignoreExpiry: nur Signatur und Format prüfen (um die Website eines abgelaufenen Links zu erkennen).
export function verifyHeatToken(token, secret, nowMs, { ignoreExpiry = false } = {}) {
if (!secret || typeof token !== 'string') return null;
const m = /^([A-Za-z0-9_-]+)\.([0-9a-f]{64})$/.exec(token);
if (!m) return null;
const expect = crypto.createHmac('sha256', secret).update(m[1]).digest();
const got = Buffer.from(m[2], 'hex');
if (got.length !== expect.length || !crypto.timingSafeEqual(got, expect)) return null;
let o;
try { o = JSON.parse(Buffer.from(m[1], 'base64url').toString('utf8')); } catch { return null; }
if (!o || typeof o !== 'object' || typeof o.s !== 'string' || !SITE_KEY.test(o.s)
|| typeof o.p !== 'string' || !o.p.startsWith('/') || !['m', 't', 'd'].includes(o.v)
|| !Number.isInteger(o.e)) return null;
const nowS = nowMs / 1000;
if (!ignoreExpiry && (o.e <= nowS || o.e > nowS + MAX_AHEAD)) return null;
return { siteKey: o.s, path: o.p, vp: o.v, exp: o.e };
}
export function createHeat({ db, config = {}, now }) {
const clock = now || Date.now;
const getSite = db.prepare('SELECT domains, active FROM sites WHERE siteKey = ?');
const getClicks = db.prepare('SELECT x, y FROM clicks WHERE site = ? AND path = ? AND vp = ? AND ts >= ? ORDER BY ts DESC, id DESC LIMIT ?');
// CORS: nur die eigenen https-Domains der Website (das Overlay läuft auf der Kundenseite).
function cors(res, req, domains) {
const origin = req.headers.origin;
const host = hostOf(origin);
res.setHeader('Vary', 'Origin');
if (host && domains.includes(host)) res.setHeader('Access-Control-Allow-Origin', origin);
}
const domainsOf = (site) => { try { return JSON.parse(site.domains); } catch { return []; } };
// true, wenn die Anfrage /a/heat betraf.
return function handle(req, res, url) {
if (url.pathname !== '/a/heat') return false;
const end = (status, body, extra = {}) => {
const b = body ? JSON.stringify(body) : '';
res.writeHead(status, { 'Cache-Control': 'no-store', ...(body ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(b) } : {}), ...extra });
res.end(b);
return true;
};
const token = url.searchParams.get('token');
// Website zum Token (auch bei abgelaufenem, aber echt signiertem Link), nur für die CORS-Köpfe der Fehlerantworten.
const knownSite = () => {
const t = verifyHeatToken(token, config.heatSecret, clock(), { ignoreExpiry: true });
return t && getSite.get(t.siteKey);
};
if (req.method === 'OPTIONS') {
// Preflight: CORS-Köpfe nur, wenn das Token zu einer Website gehört und der Origin deren https-Domain ist.
const site = knownSite();
if (site) cors(res, req, domainsOf(site));
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
return end(204);
}
if (req.method !== 'GET') return end(405, null, { Allow: 'GET, OPTIONS' });
const t = verifyHeatToken(token, config.heatSecret, clock());
const site = t && getSite.get(t.siteKey);
if (!t || !site || !site.active) {
// Bekannte Website: CORS-Kopf auch bei 403, damit das Overlay "Link abgelaufen oder ungültig" zeigen kann.
const known = site || knownSite();
if (known) cors(res, req, domainsOf(known));
return end(403, { error: 'Ungültiger oder abgelaufener Link' });
}
cors(res, req, domainsOf(site));
const since = clock() - CLICK_DAYS * 86400 * 1000;
const points = getClicks.all(t.siteKey, cleanPath(t.path) || t.path, t.vp, since, POINTS).map((r) => ({ x: r.x, y: r.y }));
return end(200, { points });
};
}
+35
View File
@@ -0,0 +1,35 @@
import { mkdirSync } from 'node:fs';
import { join } from 'node:path';
import { loadConfig } from './config.js';
import { openDb } from './db.js';
import { createSalt } from './salt.js';
import { loadGeo } from './geo.js';
import { scheduleNightly } from './jobs.js';
import { createPublicServer, createAdminServer } from './server.js';
const config = loadConfig(process.env);
mkdirSync(config.dataDir, { recursive: true });
const db = openDb(join(config.dataDir, 'analytics.db'));
const geo = loadGeo(config.geoFile || join(config.dataDir, 'geo.csv'));
if (geo.size) console.log(`Analytics: ${geo.size} Länder-Bereiche geladen`);
const publicServer = createPublicServer({ config, db, geo, salt: createSalt() });
const servers = [
['öffentlich', publicServer, config.port],
['intern (Admin)', createAdminServer({ config, db, onSitesChanged: () => publicServer.invalidateSites() }), config.adminPort],
];
for (const [name, server, port] of servers) {
server.on('error', (e) => { console.error(`Analytics-Listener ${name} (Port ${port}):`, e.message); process.exit(1); });
server.listen(port, () => console.log(`Analytics-Dienst ${name} auf Port ${port}`));
}
// Nachtjob: täglich 02:30 Europe/Berlin und einmal 60 s nach dem Start
const nightly = scheduleNightly(db);
process.on('SIGTERM', () => {
nightly.stop();
let open = servers.length;
const done = () => { if (--open === 0) { db.close(); process.exit(0); } };
for (const [, server] of servers) { server.close(done); server.closeIdleConnections?.(); }
setTimeout(() => process.exit(0), 5000).unref();
});
+231
View File
@@ -0,0 +1,231 @@
// Annahme von POST /a/e. Datenschutz-Kern: IP und User-Agent leben nur in lokalen
// Variablen (Geo, Limit-Schlüssel, Besucher-Hash) und werden nie gespeichert oder geloggt.
import { classifyUa } from './ua.js';
const MAX_BODY = 4096;
const SITE_KEY = /^[a-z0-9]{16,40}$/;
const HOST = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/;
const LIMIT = 120;
const HOUR = 3600 * 1000;
const MINUTE = 60 * 1000;
const FIELDS = Object.assign(Object.create(null), {
pv: ['t', 'site', 'path', 'ref', 'vp'],
cx: ['t', 'site', 'path', 'ref', 'vp'],
bn: ['t', 'site', 'a'],
en: ['t', 'site', 'path', 'vp', 'sec', 'scroll'],
ck: ['t', 'site', 'path', 'vp', 'x', 'y', 'sel'],
ct: ['t', 'site', 'path', 'kind', 'host'],
});
const SITES_TTL = 30 * 1000;
function tooBig() { return Object.assign(new Error('zu groß'), { status: 413 }); }
function readBody(req) {
return new Promise((resolve, reject) => {
if (Number(req.headers['content-length']) > MAX_BODY) return reject(tooBig());
const chunks = [];
let size = 0;
req.on('data', (c) => {
size += c.length;
if (size > MAX_BODY) { chunks.length = 0; reject(tooBig()); return; }
chunks.push(c);
});
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
req.on('error', reject);
});
}
// Rechtester X-Forwarded-For-Eintrag (von Traefik angehängt), sonst Socket-Adresse.
function clientIp(req) {
const xff = String(req.headers['x-forwarded-for'] || '').split(',').map((s) => s.trim()).filter(Boolean);
const ip = xff.length ? xff[xff.length - 1] : (req.socket.remoteAddress || '');
return ip.replace(/^::ffff:/i, '');
}
// Nur https ohne expliziten Port zählt; alles andere ergibt '' (kein Treffer).
export function hostOf(value) {
if (typeof value !== 'string' || !value) return '';
try {
const u = new URL(value);
return u.protocol === 'https:' && !u.port ? u.hostname.toLowerCase() : '';
} catch { return ''; }
}
// Für cleanRef: Host einer beliebigen Referrer-URL (auch http, mit Port).
function anyHostOf(value) {
try { return new URL(value).hostname.toLowerCase(); } catch { return ''; }
}
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
// Kennungsartige Segmente (lange Ziffernfolge, UUID, E-Mail) werden nie gespeichert.
function looksLikeId(seg) {
let d = seg;
try { d = decodeURIComponent(seg); } catch { /* Rohwert */ }
return /\d{6,}/.test(d) || UUID.test(d) || d.includes('@');
}
export function cleanPath(p) {
if (typeof p !== 'string') return null;
const s = p.split(/[?#]/)[0];
if (!s.startsWith('/') || s.length > 300 || /[\u0000-\u001f\u007f]/.test(s)) return null;
return s.split('/').map((seg) => (looksLikeId(seg) ? ':id' : seg)).join('/');
}
function cleanHost(h) {
if (typeof h !== 'string') return null;
const s = h.toLowerCase();
return s.length <= 253 && HOST.test(s) ? s : null;
}
function cleanRef(ref, domains) {
if (typeof ref !== 'string' || !ref) return null;
const host = cleanHost(ref.includes('://') ? anyHostOf(ref) : ref);
return host && !domains.includes(host) ? host : null;
}
const cleanVp = (v) => (v === 'm' || v === 't' || v === 'd' ? v : null);
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
export function createIngest({ db, geo = { lookup: () => '' }, salt, now = Date.now }) {
const getSite = db.prepare('SELECT domains, active FROM sites WHERE siteKey = ?');
const activeSites = db.prepare('SELECT domains FROM sites WHERE active = 1');
let hostCache = null; // { at, hosts: Set } für Preflight-Prüfungen
let hostCacheStale = true;
const insEvent = db.prepare(`INSERT INTO events_raw (site, ts, type, path, vp, ref, country, device, visitor, sec, scroll, kind, host)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
const insClick = db.prepare('INSERT INTO clicks (site, ts, path, vp, x, y, sel) VALUES (?, ?, ?, ?, ?, ?, ?)');
const windows = new Map(); // rateKey -> { w, n }; rateKey ist ein HMAC, keine IP
let lastPrune = 0;
const originHost = (req) => hostOf(req.headers.origin) || (req.headers.origin ? '' : hostOf(req.headers.referer));
const parseDomains = (s) => { try { return JSON.parse(s); } catch { return []; } };
function activeHosts() {
const t = now();
if (hostCacheStale || !hostCache || t - hostCache.at >= SITES_TTL || t < hostCache.at) {
const hosts = new Set();
for (const s of activeSites.all()) for (const d of parseDomains(s.domains)) hosts.add(d);
hostCache = { at: t, hosts };
hostCacheStale = false;
}
return hostCache.hosts;
}
function limited(key) {
const t = now();
const w = Math.floor(t / MINUTE);
if (w !== lastPrune) { // alte Fenster entfernen
for (const [k, v] of windows) if (v.w !== w) windows.delete(k);
lastPrune = w;
}
let e = windows.get(key);
if (!e || e.w !== w) { e = { w, n: 0 }; windows.set(key, e); }
return ++e.n > LIMIT;
}
function cors(res, origin) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Vary', 'Origin');
}
function store(ev, site, domains, ip, ua) {
if (typeof ev.t !== 'string') return;
const spec = FIELDS[ev.t];
if (!spec) return;
const e = {};
for (const k of spec) if (k in ev) e[k] = ev[k]; // unbekannte Felder werden nie übernommen
const t = now();
if (e.t === 'bn') {
if (!['shown', 'accept', 'reject'].includes(e.a)) return;
insEvent.run(site, Math.floor(t / HOUR) * HOUR, 'bn', null, null, null, null, null, null, null, null, e.a, null);
return;
}
const path = cleanPath(e.path);
if (!path) return;
const hour = Math.floor(t / HOUR) * HOUR;
if (e.t === 'cx') {
const vp = cleanVp(e.vp);
if (!vp) return;
insEvent.run(site, hour, 'cx', path, vp, cleanRef(e.ref, domains), null, null, null, null, null, null, null);
return;
}
if (e.t === 'pv') {
const vp = e.vp === undefined ? null : cleanVp(e.vp);
if (e.vp !== undefined && !vp) return;
const c = String(geo.lookup(ip) || '');
const country = /^[A-Za-z]{2}$/.test(c) ? c.toUpperCase() : '';
insEvent.run(site, hour, 'pv', path, vp, vp ? cleanRef(e.ref, domains) : null, country, classifyUa(ua).device, null, null, null, null, null);
return;
}
if (e.t === 'ck') {
const vp = cleanVp(e.vp);
const x = num(e.x);
const y = num(e.y);
if (!vp || x === null || y === null) return;
const sel = typeof e.sel === 'string' ? e.sel.slice(0, 200) : null;
insClick.run(site, hour, path, vp, Math.round(clamp(x, 0, 1) * 1000) / 1000, Math.round(clamp(y, 0, 1) * 1000) / 1000, sel);
return;
}
const visitor = salt.visitorId(site, ip, ua);
if (e.t === 'en') {
const vp = cleanVp(e.vp);
const sec = num(e.sec);
const scroll = num(e.scroll);
if (!vp || sec === null || scroll === null) return;
insEvent.run(site, hour, 'en', path, vp, null, null, null, visitor, Math.floor(clamp(sec, 0, 3600)), Math.floor(clamp(scroll, 0, 100)), null, null);
return;
}
if (e.t === 'ct') {
if (!['tel', 'mail', 'wa', 'out'].includes(e.kind)) return;
let host = null;
if (e.kind === 'out') { host = cleanHost(e.host); if (!host) return; }
insEvent.run(site, hour, 'ct', path, null, null, null, null, visitor, null, null, e.kind, host);
}
}
// Liefert true, wenn die Anfrage /a/e betraf.
async function handle(req, res, pathname) {
if (pathname !== '/a/e') return false;
const done = (status) => {
if (status === 413) { // Rest des Bodys nicht abwarten: Antwort senden, danach Verbindung kappen
res.writeHead(413, { Connection: 'close' });
res.end(() => req.destroy());
return true;
}
res.writeHead(status); res.end(); return true;
};
if (req.method === 'OPTIONS') {
const host = originHost(req);
const origin = req.headers.origin;
if (host && origin && activeHosts().has(host)) {
cors(res, origin);
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
res.setHeader('Access-Control-Max-Age', '86400');
}
return done(204);
}
if (req.method !== 'POST') { res.setHeader('Allow', 'POST, OPTIONS'); return done(405); }
let raw;
try { raw = await readBody(req); } catch (e) { return done(e.status === 413 ? 413 : 204); }
let ev;
try { ev = JSON.parse(raw); } catch { return done(204); }
if (!ev || typeof ev !== 'object' || Array.isArray(ev) || typeof ev.site !== 'string' || !SITE_KEY.test(ev.site)) return done(204);
const site = getSite.get(ev.site);
if (!site || !site.active) return done(204);
const domains = parseDomains(site.domains);
const host = originHost(req);
if (!host || !domains.includes(host)) return done(204);
if (req.headers.origin) cors(res, req.headers.origin);
const ua = String(req.headers['user-agent'] || '');
if (classifyUa(ua).bot) return done(204);
const ip = clientIp(req);
if (limited(salt.rateKey(ev.site, ip))) return done(204);
try { store(ev, ev.site, domains, ip, ua); } catch (e) { console.error('Analytics-Annahme:', e.message); }
return done(204);
}
handle.invalidateSites = () => { hostCacheStale = true; };
return handle;
}
+158
View File
@@ -0,0 +1,158 @@
// Nachtjob: Rohdaten tageweise in `daily` verdichten, Fristen durchsetzen.
// Idempotenz: agg_state(site, day) merkt sich verdichtete Tage; ein Tag wird nie zweimal gezählt.
// Rohzeilen bleiben bis zur 30-Tage-Löschung bestehen (Statistik für heute/gestern liest sie live).
const DAY_FMT = new Intl.DateTimeFormat('en-CA', { timeZone: 'Europe/Berlin', year: 'numeric', month: '2-digit', day: '2-digit' });
const WALL_FMT = new Intl.DateTimeFormat('en-GB', {
timeZone: 'Europe/Berlin', hourCycle: 'h23', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit',
});
const DAY_MS = 86400 * 1000;
export const RAW_DAYS = 30;
export const OFF_DAYS = 60;
export const DAILY_MONTHS = 25;
// Berliner Tag von `now` minus DAILY_MONTHS Kalendermonate (Monatsende auf den letzten Tag des Zielmonats begrenzt).
export function dailyCutoffDay(now) {
const [y, m, d] = DAY_FMT.format(now).split('-').map(Number);
const idx = y * 12 + (m - 1) - DAILY_MONTHS;
const ty = Math.floor(idx / 12);
const tm = idx % 12;
const last = new Date(Date.UTC(ty, tm + 1, 0)).getUTCDate();
return `${String(ty).padStart(4, '0')}-${String(tm + 1).padStart(2, '0')}-${String(Math.min(d, last)).padStart(2, '0')}`;
}
// Kalendertag (YYYY-MM-DD) in Europe/Berlin.
export const dayOf = (ts) => DAY_FMT.format(ts);
// Verdichtet Rohzeilen EINES Tages zu [dim, key, count]-Tripeln.
export function aggregateRows(rows) {
const m = new Map();
const add = (dim, key, n = 1) => { const k = `${dim}\u0000${key}`; m.set(k, (m.get(k) || 0) + n); };
const visitors = new Set();
for (const r of rows) {
if (r.type === 'pv') {
add('pv_total', ''); add('pv_path', r.path || '');
if (r.vp != null) add('pv_ref', r.ref || ''); // nur Besucher mit Einwilligung; sonst kommt pv_ref aus cx
add('pv_country', r.country || ''); add('pv_device', r.device || '');
} else if (r.type === 'cx') {
add('pv_ref', r.ref || '');
} else if (r.type === 'en') {
add('en_count', ''); add('en_sec_sum', '', r.sec || 0); add('scroll_sum', '', r.scroll || 0);
if (r.visitor) visitors.add(r.visitor);
} else if (r.type === 'ct') {
add('ct_kind', r.kind || '');
if (r.visitor) visitors.add(r.visitor);
} else if (r.type === 'bn' && ['shown', 'accept', 'reject'].includes(r.kind)) {
add(`bn_${r.kind}`, '');
}
}
if (visitors.size) add('visitors', '', visitors.size);
return [...m].filter(([, n]) => n > 0).map(([k, n]) => { const [dim, key] = k.split('\u0000'); return [dim, key, n]; });
}
export function runNightly(db, { now = Date.now(), log = console } = {}) {
const today = dayOf(now);
const rawCut = now - RAW_DAYS * DAY_MS;
const offCut = new Date(now - OFF_DAYS * DAY_MS).toISOString();
const siteKeys = db.prepare(`SELECT DISTINCT site FROM events_raw UNION SELECT siteKey FROM sites`).all().map((r) => r.site);
const selRaw = db.prepare('SELECT ts, type, path, vp, ref, country, device, visitor, sec, scroll, kind FROM events_raw WHERE site = ? AND ts < ? ORDER BY ts');
const selDone = db.prepare('SELECT day FROM agg_state WHERE site = ?');
const insDaily = db.prepare(`INSERT INTO daily (site, day, dim, key, count) VALUES (?, ?, ?, ?, ?)
ON CONFLICT(site, day, dim, key) DO UPDATE SET count = count + excluded.count`);
const insDone = db.prepare('INSERT OR IGNORE INTO agg_state (site, day) VALUES (?, ?)');
const stats = { aggregated: 0, sitesDeleted: 0, dailyDeleted: 0, orphansMarked: 0, orphanRowsDeleted: 0 };
const tx = (fn) => {
db.exec('BEGIN');
try { fn(); db.exec('COMMIT'); } catch (e) { db.exec('ROLLBACK'); throw e; }
};
for (const site of siteKeys) {
tx(() => {
const done = new Set(selDone.all(site).map((r) => r.day));
const byDay = new Map();
for (const r of selRaw.all(site, now)) {
const d = dayOf(r.ts);
if (d >= today || done.has(d)) continue;
if (!byDay.has(d)) byDay.set(d, []);
byDay.get(d).push(r);
}
for (const [day, rows] of byDay) {
for (const [dim, key, n] of aggregateRows(rows)) insDaily.run(site, day, dim, key, n);
insDone.run(site, day);
stats.aggregated++;
}
// Fristen erst NACH der Verdichtung (Rohdaten und Klicks > 30 Tage)
db.prepare('DELETE FROM events_raw WHERE site = ? AND ts < ?').run(site, rawCut);
db.prepare('DELETE FROM clicks WHERE site = ? AND ts < ?').run(site, rawCut);
});
}
// Klicks ganz ohne Website-Zeile / Ereignisse
db.prepare('DELETE FROM clicks WHERE ts < ?').run(rawCut);
for (const { siteKey } of db.prepare('SELECT siteKey FROM sites WHERE offSince IS NOT NULL AND offSince < ?').all(offCut)) {
tx(() => {
for (const t of ['events_raw', 'clicks', 'daily', 'agg_state']) db.prepare(`DELETE FROM ${t} WHERE site = ?`).run(siteKey);
db.prepare('DELETE FROM sites WHERE siteKey = ?').run(siteKey);
stats.sitesDeleted++;
});
}
// Tageswerte älter als 25 Monate (alle Websites)
tx(() => {
const cut = dailyCutoffDay(now);
stats.dailyDeleted = Number(db.prepare('DELETE FROM daily WHERE day < ?').run(cut).changes);
db.prepare('DELETE FROM agg_state WHERE day < ?').run(cut);
});
// Ausgeschaltete Websites ohne offSince: Frist beginnt bei Entdeckung.
stats.orphansMarked = Number(db.prepare('UPDATE sites SET offSince = ? WHERE active = 0 AND offSince IS NULL').run(new Date(now).toISOString()).changes);
// Zeilen ohne Website-Eintrag
tx(() => {
for (const t of ['events_raw', 'clicks', 'daily', 'agg_state']) {
stats.orphanRowsDeleted += Number(db.prepare(`DELETE FROM ${t} WHERE site NOT IN (SELECT siteKey FROM sites)`).run().changes);
}
});
try { db.exec('PRAGMA wal_checkpoint(TRUNCATE)'); } catch (e) { log.error('Analytics-Nachtjob: wal_checkpoint:', e.message); }
return stats;
}
// Offset (ms) von Europe/Berlin gegenüber UTC zum Zeitpunkt ts.
function berlinOffset(ts) {
const p = Object.fromEntries(WALL_FMT.formatToParts(ts).map((x) => [x.type, x.value]));
return Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second) - Math.floor(ts / 1000) * 1000;
}
// Nächster Lauf um hour:minute Europe/Berlin (strikt nach now), als UTC-ms.
export function nextRunAt(now, hour = 2, minute = 30) {
const [y, m, d] = dayOf(now).split('-').map(Number);
const at = (dd) => {
const guess = Date.UTC(y, m - 1, dd, hour, minute);
return guess - berlinOffset(guess - berlinOffset(guess));
};
const t = at(d);
return t > now ? t : at(d + 1);
}
// Plant runNightly täglich 02:30 Berlin und einmal kurz nach dem Start; stop() beendet alle Timer.
// Test-Hooks: now, timers ({ setTimeout, clearTimeout }) und job (statt runNightly).
export function scheduleNightly(db, { startDelayMs = 60000, log = console, now = Date.now, timers = { setTimeout, clearTimeout }, job = runNightly } = {}) {
let timer = null;
let stopped = false;
const run = () => {
try {
const r = job(db, { now: now() });
log.log(`Analytics-Nachtjob: ${r.aggregated} Tage verdichtet, ${r.sitesDeleted} Websites gelöscht, ${r.dailyDeleted} Tageswerte > ${DAILY_MONTHS} Monate gelöscht, ${r.orphansMarked} Websites markiert, ${r.orphanRowsDeleted} verwaiste Zeilen gelöscht`);
} catch (e) { log.error('Analytics-Nachtjob:', e.message); }
};
const arm = (delay, fn) => { if (!stopped) timer = timers.setTimeout(fn, delay); };
// Ein Timer kann früh feuern (Uhrsprung, Timer-Ungenauigkeit): dann nur neu auf den Zielzeitpunkt setzen,
// nie vorzeitig laufen. Nach dem Lauf strikt hinter das Ziel planen, damit 02:30 nie doppelt läuft.
const planDaily = (target) => arm(Math.max(0, target - now()), () => {
if (now() < target) return planDaily(target);
run();
planDaily(nextRunAt(Math.max(now(), target) + 1000));
});
arm(startDelayMs, () => { run(); planDaily(nextRunAt(now())); });
return { stop: () => { stopped = true; timers.clearTimeout(timer); } };
}
+19
View File
@@ -0,0 +1,19 @@
import crypto from 'node:crypto';
const DAY = new Intl.DateTimeFormat('en-CA', { timeZone: 'Europe/Berlin', year: 'numeric', month: '2-digit', day: '2-digit' });
// Tagessalz nur im RAM; neu erzeugt, sobald sich der Kalendertag in Europe/Berlin ändert.
export function createSalt({ now = Date.now, random = () => crypto.randomBytes(32) } = {}) {
let day = null;
let salt = null;
const current = () => {
const d = DAY.format(now());
if (d !== day) { day = d; salt = random(); }
return salt;
};
const hmac = (s) => crypto.createHmac('sha256', current()).update(s).digest('hex');
return {
visitorId: (site, ip, ua) => hmac(`${site}|${ip}|${ua}`).slice(0, 16),
rateKey: (site, ip) => hmac(`${site}|${ip}`),
};
}
+149
View File
@@ -0,0 +1,149 @@
import http from 'node:http';
import crypto from 'node:crypto';
import { createIngest } from './ingest.js';
import { createSalt } from './salt.js';
import { createHeat } from './heat.js';
import { computeStats } from './stats.js';
import { createAssets } from './assets.js';
const MAX_ADMIN_BODY = 64 * 1024;
const SITE_KEY = /^[a-z0-9]{16,40}$/;
const HOSTNAME = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$/;
function tokenOk(header, token) {
if (!token) return false;
const m = /^Bearer (.+)$/.exec(header || '');
if (!m) return false;
const a = crypto.createHash('sha256').update(m[1]).digest();
const b = crypto.createHash('sha256').update(token).digest();
return crypto.timingSafeEqual(a, b);
}
function send(res, status, obj) {
const body = JSON.stringify(obj);
res.writeHead(status, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) });
res.end(body);
}
function readJson(req, max) {
return new Promise((resolve, reject) => {
const chunks = [];
let size = 0;
let tooBig = false;
req.on('data', (c) => {
if (tooBig) return; // Rest verwerfen, damit die Antwort noch ausgeliefert wird
size += c.length;
if (size > max) { tooBig = true; chunks.length = 0; return; }
chunks.push(c);
});
req.on('end', () => {
if (tooBig) return reject(Object.assign(new Error('Anfrage zu groß'), { status: 413 }));
const raw = Buffer.concat(chunks).toString('utf8');
if (!raw) return resolve({});
try { resolve(JSON.parse(raw)); } catch { reject(Object.assign(new Error('Ungültiges JSON'), { status: 400 })); }
});
req.on('error', reject);
});
}
function validateSite(b) {
if (!b || typeof b !== 'object' || Array.isArray(b)) return { error: 'Ungültige Anfrage' };
if (typeof b.projectId !== 'string' || !b.projectId || b.projectId.length > 100) return { error: 'Ungültige Projekt-ID' };
if (!Array.isArray(b.domains) || b.domains.length < 1 || b.domains.length > 5
|| !b.domains.every((d) => typeof d === 'string' && d.length <= 253 && HOSTNAME.test(d))) {
return { error: 'Ungültige Domains (1 bis 5 Hostnamen, klein geschrieben, ohne Port und Pfad)' };
}
if (new Set(b.domains).size !== b.domains.length) return { error: 'Doppelte Domains' };
if (typeof b.active !== 'boolean') return { error: 'Ungültiger Status' };
return { site: { projectId: b.projectId, domains: b.domains, active: b.active } };
}
function createHandlerServer({ config, db, role, geo, salt, now, onSitesChanged }) {
const ingest = role === 'public' ? createIngest({ db, geo, salt: salt || createSalt({ now }), now }) : null;
const heat = role === 'public' ? createHeat({ db, config, now }) : null;
const assets = role === 'public' ? createAssets({ distDir: config.distDir, isActive: (k) => !!db.prepare('SELECT 1 FROM sites WHERE siteKey = ? AND active = 1').get(k) }) : null;
const clock = now || Date.now;
const server = http.createServer(async (req, res) => {
try {
const url = new URL(req.url, 'http://x');
const parts = url.pathname.split('/').filter(Boolean);
const method = req.method;
if (ingest && await ingest(req, res, url.pathname)) return;
if (heat && heat(req, res, url)) return;
if (assets && assets(req, res, url.pathname)) return;
if (method === 'GET' && url.pathname === '/a/health') return send(res, 200, { ok: true });
if (role === 'admin' && parts[0] === 'a' && parts[1] === 'admin' && parts[2] === 'sites' && parts.length === 4
&& (method === 'PUT' || method === 'DELETE')) {
if (!tokenOk(req.headers.authorization, config.adminToken)) return send(res, 401, { error: 'Nicht autorisiert' });
const key = parts[3];
if (!SITE_KEY.test(key)) return send(res, 400, { error: 'Ungültiger Website-Schlüssel' });
if (method === 'DELETE') {
db.exec('BEGIN');
try {
db.prepare('DELETE FROM sites WHERE siteKey = ?').run(key);
db.prepare('DELETE FROM events_raw WHERE site = ?').run(key);
db.prepare('DELETE FROM clicks WHERE site = ?').run(key);
db.prepare('DELETE FROM daily WHERE site = ?').run(key);
db.prepare('DELETE FROM agg_state WHERE site = ?').run(key);
db.exec('COMMIT');
} catch (e) {
db.exec('ROLLBACK');
throw e;
}
onSitesChanged?.();
return send(res, 200, { ok: true });
}
const v = validateSite(await readJson(req, MAX_ADMIN_BODY));
if (v.error) return send(res, 400, { error: v.error });
const { projectId, domains, active } = v.site;
const old = db.prepare('SELECT offSince FROM sites WHERE siteKey = ?').get(key);
const offSince = active ? null : (old && old.offSince) || new Date().toISOString();
db.prepare(`INSERT INTO sites (siteKey, projectId, domains, active, offSince) VALUES (?, ?, ?, ?, ?)
ON CONFLICT(siteKey) DO UPDATE SET projectId = excluded.projectId, domains = excluded.domains,
active = excluded.active, offSince = excluded.offSince`)
.run(key, projectId, JSON.stringify(domains), active ? 1 : 0, offSince);
onSitesChanged?.();
return send(res, 200, { ok: true });
}
// Beenden ohne Domainliste: active=0 und offSince einmalig (Portal: stopSite)
if (role === 'admin' && method === 'POST' && parts[0] === 'a' && parts[1] === 'admin' && parts[2] === 'sites' && parts.length === 5 && parts[4] === 'off') {
if (!tokenOk(req.headers.authorization, config.adminToken)) return send(res, 401, { error: 'Nicht autorisiert' });
const key = parts[3];
if (!SITE_KEY.test(key)) return send(res, 400, { error: 'Ungültiger Website-Schlüssel' });
const old = db.prepare('SELECT offSince FROM sites WHERE siteKey = ?').get(key);
if (!old) return send(res, 404, { error: 'Website nicht gefunden' });
db.prepare('UPDATE sites SET active = 0, offSince = ? WHERE siteKey = ?').run(old.offSince || new Date().toISOString(), key);
onSitesChanged?.();
return send(res, 200, { ok: true });
}
if (role === 'admin' && method === 'GET' && parts[0] === 'a' && parts[1] === 'admin' && parts[2] === 'stats' && parts.length === 4) {
if (!tokenOk(req.headers.authorization, config.adminToken)) return send(res, 401, { error: 'Nicht autorisiert' });
const key = parts[3];
if (!SITE_KEY.test(key)) return send(res, 400, { error: 'Ungültiger Website-Schlüssel' });
const r = url.searchParams.get('range') ?? '30';
if (!['7', '30', '90'].includes(r)) return send(res, 400, { error: 'Ungültiger Zeitraum (7, 30 oder 90)' });
if (!db.prepare('SELECT 1 FROM sites WHERE siteKey = ?').get(key)) return send(res, 404, { error: 'Website nicht gefunden' });
return send(res, 200, computeStats(db, key, Number(r), clock()));
}
return send(res, 404, { error: 'Nicht gefunden' });
} catch (e) {
const status = e.status || 500;
if (status >= 500) console.error('Analytics-Fehler:', e);
if (!res.headersSent) send(res, status, { error: status === 500 ? 'Interner Fehler' : e.message });
}
});
server.invalidateSites = ingest ? ingest.invalidateSites : () => {};
return server;
}
// Öffentlicher Listener: /a/health, /a/e (Erfassung), /a/heat, /a/w.js und /a/h.js; Admin-Routen gibt es hier nicht.
export const createPublicServer = ({ config, db, geo, salt, now }) => createHandlerServer({ config, db, role: 'public', geo, salt, now });
// Interner Listener für die Website-Verwaltung.
// onSitesChanged: wird nach jeder Änderung an Websites gerufen (public.invalidateSites()).
export const createAdminServer = ({ config, db, onSitesChanged, now }) => createHandlerServer({ config, db, role: 'admin', onSitesChanged, now });
+137
View File
@@ -0,0 +1,137 @@
// Statistik für das Portal: Vergangenheit aus `daily`, noch nicht verdichtete Tage live aus den Rohdaten.
import { dayOf, aggregateRows } from './jobs.js';
const TOP = 20;
// Kalendertag (YYYY-MM-DD) um `n` Tage verschoben, rein kalendarisch (UTC-Datumsrechnung, keine Zeitumstellung).
function shiftDay(day, n) {
const [y, m, d] = day.split('-').map(Number);
return new Date(Date.UTC(y, m - 1, d + n)).toISOString().slice(0, 10);
}
// Liste der Kalendertage (YYYY-MM-DD), oldest first, endend am Tag `endDay`.
function dayList(endDay, range) {
const days = [];
for (let i = range - 1; i >= 0; i--) days.push(shiftDay(endDay, -i));
return days;
}
const top = (map) => [...map].map(([key, count]) => ({ key, count })).filter((e) => e.count > 0)
.sort((a, b) => b.count - a.count || (a.key < b.key ? -1 : 1)).slice(0, TOP);
// Sammelt die Zähler für eine zusammenhängende Tagesliste: verdichtete Tage aus `daily`,
// Tage ohne agg_state (heute, ggf. gestern vor 02:30) live aus den Rohdaten. Kein Tag zählt doppelt.
function collect(db, site, days, now) {
const first = days[0];
const last = days[days.length - 1];
const inRange = new Set(days);
const perDay = new Map(days.map((d) => [d, { pv: 0, visitors: 0, contacts: 0, en: 0, sec: 0, scroll: 0 }]));
const dims = {};
const apply = (day, dim, key, n) => {
const t = perDay.get(day);
if (dim === 'pv_total') t.pv += n;
else if (dim === 'visitors') t.visitors += n;
else if (dim === 'ct_kind') t.contacts += n;
else if (dim === 'en_count') t.en += n;
else if (dim === 'en_sec_sum') t.sec += n;
else if (dim === 'scroll_sum') t.scroll += n;
if (!dims[dim]) dims[dim] = new Map();
dims[dim].set(key, (dims[dim].get(key) || 0) + n);
};
const done = new Set(db.prepare('SELECT day FROM agg_state WHERE site = ? AND day >= ? AND day <= ?').all(site, first, last).map((r) => r.day));
for (const r of db.prepare('SELECT day, dim, key, count FROM daily WHERE site = ? AND day >= ? AND day <= ?').all(site, first, last)) {
if (inRange.has(r.day)) apply(r.day, r.dim, r.key, r.count);
}
// Ein Berliner Tag beginnt 22:00/23:00 UTC des Vortags: zwei Tage Rand reichen auf beiden Seiten, danach filtert dayOf
const [fy, fm, fd] = first.split('-').map(Number);
const [ly, lm, ld] = last.split('-').map(Number);
const lo = Date.UTC(fy, fm - 1, fd - 2);
const hi = Math.min(now, Date.UTC(ly, lm - 1, ld + 2));
const live = new Map();
for (const r of db.prepare('SELECT ts, type, path, vp, ref, country, device, visitor, sec, scroll, kind FROM events_raw WHERE site = ? AND ts >= ? AND ts <= ? ORDER BY ts')
.all(site, lo, hi)) {
const d = dayOf(r.ts);
if (!inRange.has(d) || done.has(d)) continue;
if (!live.has(d)) live.set(d, []);
live.get(d).push(r);
}
for (const [day, rows] of live) for (const [dim, key, n] of aggregateRows(rows)) apply(day, dim, key, n);
return { days, perDay, dims };
}
const avg = (sum, n) => (n ? Math.round(sum / n) : null);
// Formt gesammelte Zähler zu banner + totals + series (gleiche Form für aktuellen Zeitraum und Vorzeitraum).
function shape({ days, perDay, dims }) {
const sum = (dim) => [...(dims[dim] || new Map()).values()].reduce((a, b) => a + b, 0);
const banner = { shown: sum('bn_shown'), accept: sum('bn_accept'), reject: sum('bn_reject') };
const decided = banner.accept + banner.reject;
const en = sum('en_count');
return {
banner,
totals: {
pv: sum('pv_total'),
// Summe der Tages-Besucher (kein tagesübergreifender Abgleich möglich: Salz wechselt täglich)
visitors: sum('visitors'),
contacts: sum('ct_kind'),
consentRate: decided ? banner.accept / decided : 0,
engaged: en,
avgSec: avg(sum('en_sec_sum'), en),
avgScroll: avg(sum('scroll_sum'), en),
},
series: days.map((day) => {
const t = perDay.get(day);
return { day, pv: t.pv, visitors: t.visitors, contacts: t.contacts, avgSec: avg(t.sec, t.en) };
}),
};
}
// Frühester Berliner Tag mit Daten dieser Website ('' = keine): kleinster Tag aus daily und events_raw.
function firstDayOf(db, site) {
const fromDaily = db.prepare('SELECT MIN(day) AS d FROM daily WHERE site = ?').get(site)?.d || '';
const ts = db.prepare('SELECT MIN(ts) AS t FROM events_raw WHERE site = ?').get(site)?.t;
const fromRaw = ts == null ? '' : dayOf(ts);
if (!fromDaily) return fromRaw;
if (!fromRaw) return fromDaily;
return fromDaily < fromRaw ? fromDaily : fromRaw;
}
// Der Zeitraum sind die `range` abgeschlossenen Kalendertage, die gestern enden (wie GA4 "Letzte 28 Tage"): Der
// angebrochene heutige Tag würde gegen einen vollen Vorzeitraum als Einbruch erscheinen. Heute steht getrennt in `today`.
export function computeStats(db, site, range, now = Date.now()) {
const todayDay = dayOf(now);
const days = dayList(shiftDay(todayDay, -1), range);
const cur = collect(db, site, days, now);
const { banner, totals, series } = shape(cur);
// Heute bis jetzt: live aus den Rohdaten (der Tag ist noch nicht verdichtet)
const t = shape(collect(db, site, [todayDay], now)).series[0];
// Vorzeitraum: die `range` Kalendertage direkt vor dem aktuellen Zeitraum (kalendarisch, nie now - range * 24 h)
const prevDays = dayList(shiftDay(days[0], -1), range);
const prevCollected = collect(db, site, prevDays, now);
const prev = shape(prevCollected);
const firstDay = firstDayOf(db, site);
return {
range,
from: days[0],
to: days[days.length - 1],
firstDay,
totals,
series,
today: { day: todayDay, pv: t.pv, visitors: t.visitors, contacts: t.contacts },
pages: top(cur.dims.pv_path || new Map()),
refs: top(cur.dims.pv_ref || new Map()),
countries: top(cur.dims.pv_country || new Map()),
devices: top(cur.dims.pv_device || new Map()),
contacts: top(cur.dims.ct_kind || new Map()),
banner,
previous: {
from: prevDays[0],
to: prevDays[prevDays.length - 1],
complete: firstDay !== '' && firstDay <= prevDays[0],
totals: prev.totals,
series: prev.series,
contacts: top(prevCollected.dims.ct_kind || new Map()),
},
};
}
+10
View File
@@ -0,0 +1,10 @@
// Gröbste Bot- und Gerätetyp-Erkennung; der User-Agent wird danach verworfen.
const BOT = /bot|crawl|spider|slurp|headless|lighthouse|pingdom|uptime|curl|wget|python|go-http|java\/|facebookexternalhit|preview/i;
export function classifyUa(ua) {
const s = typeof ua === 'string' ? ua : '';
if (!s || BOT.test(s)) return { bot: true, device: 'd' };
if (/Mobi|Android.*Mobile|iPhone/i.test(s)) return { bot: false, device: 'm' };
if (/iPad|Tablet|Android(?!.*Mobile)/i.test(s)) return { bot: false, device: 't' };
return { bot: false, device: 'd' };
}
+148
View File
@@ -0,0 +1,148 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import crypto from 'node:crypto';
import http from 'node:http';
import { readFileSync, mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { gunzipSync } from 'node:zlib';
import { openDb } from '../src/db.js';
import { createPublicServer, createAdminServer } from '../src/server.js';
const DIST = new URL('../client/dist/', import.meta.url);
const KEY = 'abcdef0123456789abcd';
const OFFKEY = 'offoffoffoffoffoffoff';
async function start(config = {}) {
const db = openDb(':memory:');
db.prepare('INSERT INTO sites (siteKey, projectId, domains, active) VALUES (?,?,?,1)').run(KEY, 'p', '[]');
db.prepare('INSERT INTO sites (siteKey, projectId, domains, active) VALUES (?,?,?,0)').run(OFFKEY, 'p', '[]');
const server = createPublicServer({ config, db });
await new Promise((r) => server.listen(0, '127.0.0.1', r));
return { base: `http://127.0.0.1:${server.address().port}`, close: () => server.close() };
}
// fetch dekodiert gzip selbst; für den rohen Body http.request verwenden.
function raw(base, path, headers = {}, method = 'GET') {
return new Promise((resolve, reject) => {
const req = http.request(base + path, { method, headers }, (res) => {
const chunks = [];
res.on('data', (c) => chunks.push(c));
res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: Buffer.concat(chunks) }));
});
req.on('error', reject);
req.end();
});
}
for (const file of ['w.js', 'h.js']) {
test(`/a/${file}: Header, identity und gzip liefern den dist-Inhalt`, async () => {
const s = await start();
const want = readFileSync(new URL(file, DIST));
const etag = `"${crypto.createHash('sha256').update(want).digest('hex')}"`;
const id = await raw(s.base, `/a/${file}${file === 'w.js' ? `?s=${KEY}` : ''}`, { 'Accept-Encoding': 'identity' });
assert.equal(id.status, 200);
assert.deepEqual(id.body, want);
assert.equal(id.headers['content-type'], 'text/javascript; charset=utf-8');
assert.equal(id.headers['cache-control'], 'public, max-age=3600');
assert.equal(id.headers.etag, etag);
assert.equal(id.headers.vary, 'Accept-Encoding');
assert.equal(id.headers['x-content-type-options'], 'nosniff');
assert.equal(id.headers['cross-origin-resource-policy'], 'cross-origin');
assert.equal(id.headers['access-control-allow-origin'], '*');
assert.equal(id.headers['content-encoding'], undefined);
const gz = await raw(s.base, `/a/${file}${file === 'w.js' ? `?s=${KEY}` : ''}`, { 'Accept-Encoding': 'gzip, deflate' });
assert.equal(gz.status, 200);
assert.equal(gz.headers['content-encoding'], 'gzip');
assert.equal(gz.headers.etag, etag);
assert.ok(gz.body.length < want.length);
assert.deepEqual(gunzipSync(gz.body), want);
s.close();
});
}
test('ETag: 304 bei If-None-Match, 200 bei anderem Wert', async () => {
const s = await start();
const first = await raw(s.base, `/a/w.js?s=${KEY}`);
const hit = await raw(s.base, `/a/w.js?s=${KEY}`, { 'If-None-Match': first.headers.etag });
assert.equal(hit.status, 304);
assert.equal(hit.body.length, 0);
assert.equal(hit.headers.etag, first.headers.etag);
assert.equal(hit.headers['cache-control'], 'public, max-age=3600');
assert.equal((await raw(s.base, `/a/w.js?s=${KEY}`, { 'If-None-Match': '"anders"' })).status, 200);
s.close();
});
test('HEAD: Header wie GET, kein Body', async () => {
const s = await start();
const get = await raw(s.base, '/a/h.js', { 'Accept-Encoding': 'identity' });
const head = await raw(s.base, '/a/h.js', { 'Accept-Encoding': 'identity' }, 'HEAD');
assert.equal(head.status, 200);
assert.equal(head.body.length, 0);
assert.equal(head.headers['content-length'], String(get.body.length));
assert.equal(head.headers.etag, get.headers.etag);
assert.equal((await raw(s.base, '/a/h.js', {}, 'POST')).status, 405);
s.close();
});
test('Admin-Listener liefert die Skripte nicht', async () => {
const server = createAdminServer({ config: { adminToken: 't' }, db: openDb(':memory:') });
await new Promise((r) => server.listen(0, '127.0.0.1', r));
const base = `http://127.0.0.1:${server.address().port}`;
assert.equal((await fetch(`${base}/a/w.js`)).status, 404);
assert.equal((await fetch(`${base}/a/h.js`)).status, 404);
server.close();
});
test('fehlende dist-Datei: 503 nur für diesen Pfad, deutscher Fehler im Log', async () => {
const dir = mkdtempSync(join(tmpdir(), 'dist-'));
writeFileSync(join(dir, 'w.js'), 'console.log(1)');
const orig = console.error;
const logged = [];
console.error = (...a) => logged.push(a.join(' '));
let s;
try { s = await start({ distDir: dir }); } finally { console.error = orig; }
assert.ok(logged.some((l) => l.includes('h.js') && l.includes('fehlt')));
assert.equal((await fetch(`${s.base}/a/w.js`)).status, 200);
assert.equal((await fetch(`${s.base}/a/h.js`)).status, 503);
s.close();
});
const EMPTY = '/* WEBklar Analyse: aus */\n';
for (const [name, q] of [['unbekannter Schlüssel', '?s=unbekannt0123456789ab'], ['ungültiger Schlüssel', '?s=ABC'], ['ohne s', ''], ['ausgeschaltete Website', `?s=${OFFKEY}`]]) {
test(`w.js ${name}: leeres Skript`, async () => {
const s = await start();
const r = await raw(s.base, `/a/w.js${q}`, { 'Accept-Encoding': 'gzip' });
assert.equal(r.status, 200);
assert.equal(r.body.toString(), EMPTY);
assert.equal(r.headers['content-type'], 'text/javascript; charset=utf-8');
assert.equal(r.headers['cache-control'], 'public, max-age=60');
assert.equal(r.headers['x-content-type-options'], 'nosniff');
assert.equal(r.headers['cross-origin-resource-policy'], 'cross-origin');
assert.equal(r.headers['access-control-allow-origin'], '*');
assert.equal(r.headers.vary, 'Accept-Encoding');
assert.equal(r.headers['content-encoding'], undefined);
assert.equal(r.headers.etag, undefined);
s.close();
});
}
test('Ende-zu-Ende: Ausschalten wirkt sofort, Einschalten ebenfalls', async () => {
const db = openDb(':memory:');
const pub = createPublicServer({ config: {}, db });
const adm = createAdminServer({ config: { adminToken: 't' }, db });
await Promise.all([pub, adm].map((x) => new Promise((r) => x.listen(0, '127.0.0.1', r))));
const pb = `http://127.0.0.1:${pub.address().port}`;
const ab = `http://127.0.0.1:${adm.address().port}`;
const put = (active) => fetch(`${ab}/a/admin/sites/${KEY}`, { method: 'PUT', headers: { Authorization: 'Bearer t', 'Content-Type': 'application/json' },
body: JSON.stringify({ projectId: 'p', domains: ['abc.project.webklar.com'], active }) });
const get = async () => (await raw(pb, `/a/w.js?s=${KEY}`)).body.toString();
assert.equal(await get(), EMPTY);
assert.equal((await put(true)).status, 200);
assert.notEqual(await get(), EMPTY);
assert.equal((await put(false)).status, 200);
assert.equal(await get(), EMPTY);
assert.equal((await put(true)).status, 200);
assert.notEqual(await get(), EMPTY);
pub.close(); adm.close();
});
+709
View File
@@ -0,0 +1,709 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import vm from 'node:vm';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import {
vpClass, cleanPath, refHost, cssPath, contactKind, readConsent, contrastOk, escapeHtml,
} from '../client/core.js';
import { BANNER_VERSION, BANNER_TEXT, BANNER_LINK_LABEL, BANNER_ARIA, SETTINGS_LABEL, BANNER_VERSION_EIGEN, BANNER_TEXT_EIGEN, bannerFor } from '../client/core.js';
import { buildAll } from '../client/build.js';
const BV = '2026-10-02';
const DIST = join(dirname(fileURLToPath(import.meta.url)), '..', 'client', 'dist');
// ---------- core ----------
test('vpClass', () => {
assert.equal(vpClass(0), 'm');
assert.equal(vpClass(767), 'm');
assert.equal(vpClass(768), 't');
assert.equal(vpClass(1023), 't');
assert.equal(vpClass(1024), 'd');
assert.equal(vpClass(NaN), 'd');
});
test('cleanPath', () => {
assert.equal(cleanPath('https://x.de/a/b?q=1#h'), '/a/b');
assert.equal(cleanPath('/über%20uns?x=1'), '/über uns');
assert.equal(cleanPath('/%E0%A4%A'), '/%E0%A4%A'); // kaputtes Encoding bleibt roh
assert.equal(cleanPath('https://x.de'), '/');
assert.equal(cleanPath(''), '/');
assert.equal(cleanPath('/' + 'a'.repeat(400)).length, 300);
assert.equal(cleanPath(null), '/');
assert.equal(cleanPath('//foo/bar?x=1'), '//foo/bar');
assert.equal(cleanPath('//foo'), '//foo');
});
test('refHost', () => {
assert.equal(refHost('https://www.google.com/search?q=x', 'mein.de'), 'www.google.com');
assert.equal(refHost('https://mein.de/a', 'mein.de'), '');
assert.equal(refHost('https://www.mein.de/a', 'mein.de'), '');
assert.equal(refHost('', 'mein.de'), '');
assert.equal(refHost('kein url', 'mein.de'), '');
assert.equal(refHost('android-app://com.x', 'mein.de'), '');
assert.equal(refHost('HTTPS://Foo.DE/x', 'mein.de'), 'foo.de');
});
function el(tag, o = {}) {
const e = { tagName: tag.toUpperCase(), id: o.id || '', parentNode: o.parent || null, previousElementSibling: o.prev || null,
attrs: { class: o.cls || '' }, getAttribute(n) { return this.attrs[n] ?? null; } };
return e;
}
test('cssPath: nur Tags + nth-of-type, keine IDs/Klassen', () => {
const body = el('body');
const div = el('div', { parent: body, cls: 'hero main' });
const p1 = el('p', { parent: div });
const btn0 = el('button', { parent: div, prev: p1 });
const btn = el('button', { parent: div, prev: btn0, cls: 'cta' });
assert.equal(cssPath(btn), 'body:nth-of-type(1) > div:nth-of-type(1) > button:nth-of-type(2)');
const pii = el('a', { parent: body, id: 'max@firma.de', cls: 'team-max-mustermann' });
const sel = cssPath(pii);
assert.equal(sel, 'body:nth-of-type(1) > a:nth-of-type(1)');
assert.ok(!/max|firma|mustermann|#|\./.test(sel));
// max. 6 Ebenen
let n = el('div');
for (let i = 0; i < 9; i++) n = el('span', { parent: n });
assert.equal(cssPath(n).split(' > ').length, 6);
// stoppt bei body
const outer = el('html'); body.parentNode = outer;
assert.equal(cssPath(div).split(' > ')[0], 'body:nth-of-type(1)');
// <= 200
let deep = el('div');
for (let i = 0; i < 5; i++) deep = el('x'.repeat(40), { parent: deep });
assert.ok(cssPath(deep).length <= 200);
assert.equal(cssPath(null), '');
});
test('contactKind', () => {
const k = (href) => contactKind({ href }, 'mein.de');
assert.equal(k('tel:+49123'), 'tel');
assert.equal(k('TEL:+49123'), 'tel');
assert.equal(k('mailto:a@b.de'), 'mail');
assert.equal(k('https://wa.me/49123'), 'wa');
assert.equal(k('https://api.whatsapp.com/send?phone=1'), 'wa');
assert.equal(k('whatsapp://send?text=x'), 'wa');
assert.equal(k('https://anders.de/x'), 'out');
assert.equal(k('https://mein.de/x'), null);
assert.equal(k('https://www.mein.de/x'), null);
assert.equal(k('/kontakt'), null);
assert.equal(k('#top'), null);
assert.equal(k('javascript:void(0)'), null);
assert.equal(k(''), null);
assert.equal(contactKind(null, 'mein.de'), null);
});
test('readConsent', () => {
const now = 1_700_000_000_000;
const day = 86400000;
const st = (v) => ({ getItem: () => v });
const J = (o) => st(JSON.stringify(o));
assert.equal(BANNER_VERSION, '2026-10-02');
assert.deepEqual(readConsent(J({ v: 1, a: 'accept', bv: BV, t: now - 10 * day }), now), { v: 1, a: 'accept', t: now - 10 * day, bv: BV });
assert.equal(readConsent(J({ v: 1, a: 'reject', bv: BV, t: now - 182 * day }), now).a, 'reject');
assert.equal(readConsent(J({ v: 1, a: 'accept', bv: BV, t: now - 183 * day }), now), null);
assert.equal(readConsent(J({ v: 1, a: 'accept', t: now }), now), null, 'ohne bv');
assert.equal(readConsent(J({ v: 1, a: 'accept', bv: '2026-01-01', t: now }), now), null, 'alte Fassung');
assert.equal(readConsent(J({ v: 2, a: 'accept', bv: BV, t: now }), now), null);
assert.equal(readConsent(J({ v: 1, a: 'maybe', bv: BV, t: now }), now), null);
assert.equal(readConsent(J({ v: 1, a: 'accept', bv: BV, t: 'x' }), now), null);
assert.equal(readConsent(J({ v: 1, a: 'accept', bv: BV, t: now + day }), now), null);
assert.equal(readConsent(st('kaputt{'), now), null);
assert.equal(readConsent(st(null), now), null);
assert.equal(readConsent({ getItem() { throw new Error('blocked'); } }, now), null);
assert.equal(readConsent(null, now), null);
});
test('contrastOk', () => {
assert.equal(contrastOk('#111111'), true);
assert.equal(contrastOk('#ffffff'), false);
assert.equal(contrastOk('#ffff00'), false);
assert.equal(contrastOk('#767676'), true); // 4.54
assert.equal(contrastOk('#777777'), false); // 4.48
assert.equal(contrastOk('#00F'), false); // ungueltig
assert.equal(contrastOk(undefined), false);
});
test('escapeHtml', () => {
assert.equal(escapeHtml(`<a href="x">&'</a>`), '&lt;a href=&quot;x&quot;&gt;&amp;&#39;&lt;/a&gt;');
assert.equal(escapeHtml(null), '');
});
// ---------- dist ----------
test('dist ist aktuell', () => {
const b = buildAll();
assert.equal(readFileSync(join(DIST, 'w.js'), 'utf8'), b.w);
assert.equal(readFileSync(join(DIST, 'h.js'), 'utf8'), b.h);
assert.ok(!/^\s*(import|export)\s/m.test(b.w + b.h));
});
// ---------- Verhalten von w.js (Fake-DOM) ----------
function makeEl(tag) {
const e = {
tagName: String(tag).toUpperCase(), attrs: {}, children: [], listeners: {}, style: {}, parentNode: null, id: '',
textContent: '', focused: false, removed: false,
setAttribute(n, v) { this.attrs[n] = String(v); if (n === 'id') this.id = String(v); },
getAttribute(n) { return n in this.attrs ? this.attrs[n] : null; },
appendChild(c) { c.parentNode = this; this.children.push(c); return c; },
append(...cs) { cs.forEach((c) => this.appendChild(c)); },
addEventListener(t, f) { (this.listeners[t] ||= []).push(f); },
removeEventListener(t, f) { this.listeners[t] = (this.listeners[t] || []).filter((x) => x !== f); },
focus() { this.focused = true; },
getBoundingClientRect() { return { height: this.fakeHeight || 0 }; },
remove() { this.removed = true; if (this.parentNode) this.parentNode.children = this.parentNode.children.filter((c) => c !== this); },
closest(sel) {
const attr = /^\[([\w-]+)\]$/.exec(sel);
for (let n = this; n; n = n.parentNode) if (attr && n.attrs && attr[1] in n.attrs) return n;
return null;
},
attachShadow() { const r = makeEl('shadow'); r.isShadow = true; this.shadow = r; return r; },
click(extra = {}) { fire(this, 'click', { target: this, ...extra }); },
};
Object.defineProperty(e, 'href', { get() { return this.attrs.href || ''; } });
Object.defineProperty(e, 'className', { set(v) { this.attrs.class = v; }, get() { return this.attrs.class || ''; } });
return e;
}
function fire(node, type, ev) { for (const f of [...(node.listeners[type] || [])]) f({ type, preventDefault() {}, ...ev }); }
function walk(n, fn) { fn(n); for (const c of n.children || []) walk(c, fn); if (n.shadow) walk(n.shadow, fn); }
function findAll(root, pred) { const out = []; walk(root, (n) => { if (pred(n)) out.push(n); }); return out; }
const btn = (root, label) => findAll(root, (n) => n.tagName === 'BUTTON' && n.textContent === label)[0];
const settingsBtn = (h) => findAll(h.body, (n) => n.tagName === 'BUTTON' && n.attrs['aria-label'] === SETTINGS_LABEL)[0];
function harness({ gpc, dnt, stored, search = '', src = 'https://project.webklar.com/a/w.js', withConsentLink = false, bannerHeight = 0, bodyPad, variant } = {}) {
const sent = []; const reads = { referrer: 0, innerWidth: 0 }; const beacon = []; const fetches = []; const store = {}; let now = 1_700_000_000_000;
if (stored) store['wk-consent'] = JSON.stringify(stored);
const body = makeEl('body'); if (bodyPad !== undefined) body.style.paddingBottom = bodyPad; const html = makeEl('html');
const timers = [];
const script = makeEl('script'); script.setAttribute('src', src); script.setAttribute('data-site', 'abcdef0123456789abcd'); script.setAttribute('data-color', '#0a5a8a'); if (variant !== undefined) script.setAttribute('data-variant', variant); script.src = src;
const docL = {}; const winL = {};
const link = withConsentLink ? makeEl('a') : null; if (link) link.setAttribute('data-webklar-consent', '');
const injected = [];
if (link) link.click = () => { for (const f of [...(docL.click || [])]) f({ type: 'click', target: link, preventDefault() {} }); };
const document = {
currentScript: script, get referrer() { reads.referrer++; return 'https://www.google.com/'; }, visibilityState: 'visible',
documentElement: Object.assign(html, { scrollWidth: 1000, scrollHeight: 2000 }), body, head: makeEl('head'),
createElement(t) { const e = makeEl(t); if (bannerHeight) e.offsetHeight = bannerHeight; return e; }, activeElement: null,
createElementNS(ns, t) { const e = makeEl(t); e.ns = ns; return e; },
querySelector(s) { return s === '[data-webklar-consent]' ? link : null; },
addEventListener(t, f) { (docL[t] ||= []).push(f); },
removeEventListener(t, f) { docL[t] = (docL[t] || []).filter((x) => x !== f); },
};
body.appendChild = function (c) { c.parentNode = this; this.children.push(c); if (c.attrs.src) injected.push(c); return c; };
document.head.appendChild = body.appendChild;
const origPush = function () {};
const history = { pushState: origPush, replaceState(s, t, u) { this.replaced = u; } };
const win = {
get innerWidth() { reads.innerWidth++; return 1200; }, innerHeight: 800, scrollX: 0, scrollY: 300, document, history,
location: { hostname: 'mein.de', origin: 'https://mein.de', pathname: '/start', search, href: 'https://mein.de/start' + search, hash: '' },
addEventListener(t, f) { (winL[t] ||= []).push(f); },
removeEventListener(t, f) { winL[t] = (winL[t] || []).filter((x) => x !== f); },
};
const sandbox = {
window: win, document, history, location: win.location, URL, URLSearchParams, JSON, Math, Date: { now: () => now }, String, Number, Array, Object, RegExp, Error, parseInt,
navigator: { globalPrivacyControl: gpc, doNotTrack: dnt, sendBeacon(u, d) { beacon.push({ u, d }); return true; } },
localStorage: { getItem: (k) => (k in store ? store[k] : null), setItem: (k, v) => { store[k] = String(v); } },
setTimeout: (f, ms) => { timers.push(f); return timers.length; }, clearTimeout() {},
setInterval: () => 0, clearInterval() {}, Blob: class {}, fetch: (u, o) => { fetches.push({ u, o }); sent.push({ u, d: JSON.parse(o.body) }); return Promise.resolve(); },
};
win.localStorage = sandbox.localStorage;
sandbox.globalThis = sandbox;
vm.createContext(sandbox);
vm.runInContext(readFileSync(join(DIST, 'w.js'), 'utf8'), sandbox);
const types = () => sent.map((s) => s.d.t + (s.d.a ? ':' + s.d.a : ''));
const ck = (target, ev = {}) => { for (const f of [...(docL.click || [])]) f({ type: 'click', target, pageX: 500, pageY: 1000, clientX: 500, clientY: 700, preventDefault() {}, ...ev }); };
const leave = () => { for (const f of [...(winL.pagehide || [])]) f({}); document.visibilityState = 'hidden'; for (const f of [...(docL.visibilitychange || [])]) f({}); };
const rerun = () => vm.runInContext(readFileSync(join(DIST, 'w.js'), 'utf8'), sandbox);
const storage = () => (winL.storage || []).forEach((f) => f({ key: 'wk-consent' }));
const nav = (p) => { win.location.pathname = p; history.pushState(null, '', p); timers.splice(0).forEach((f) => f()); };
return { nav, reads, beacon, fetches, origPush, rerun, storage, sent, store, types, ck, leave, body, docL, winL, win, injected, history, timers, setNow: (n) => { now = n; }, link, document };
}
const target = () => { const b = makeEl('button'); b.parentNode = makeEl('body'); b.attrs.class = 'cta'; return b; };
const hostEl = (h) => h.body.children.find((c) => c.shadow && findAll(c.shadow, (n) => n.attrs && n.attrs.role === 'dialog').length);
const SITE = 'abcdef0123456789abcd';
const ACC0 = { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 1000 };
test('w.js: ohne Einwilligung nur pv {t,site,path} + bn shown, kein Lesen von referrer/innerWidth', () => {
const h = harness();
assert.deepEqual(h.types(), ['pv', 'bn:shown']);
assert.equal(h.sent[0].u, 'https://project.webklar.com/a/e');
assert.deepEqual(h.sent[0].d, { t: 'pv', site: SITE, path: '/start' });
assert.deepEqual(Object.keys(h.sent[0].d).sort(), ['path', 'site', 't']);
assert.deepEqual(h.sent[1].d, { t: 'bn', site: SITE, a: 'shown' });
assert.equal(h.reads.referrer, 0);
assert.equal(h.reads.innerWidth, 0);
h.ck(target()); h.leave();
assert.deepEqual(h.types(), ['pv', 'bn:shown']);
assert.equal((h.docL.click || []).length, 1); // nur die Einstellungs-Delegation
});
test('core: Banner-Konstanten', () => {
assert.match(BANNER_TEXT, /^Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung\./);
assert.match(BANNER_TEXT, /Nach „Ablehnen“ zählen wir nur noch, dass Du abgelehnt hast\./);
assert.match(BANNER_TEXT, /über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen\. Details: $/);
assert.equal(BANNER_LINK_LABEL, 'Datenschutz');
assert.equal(BANNER_ARIA, 'Einwilligung zur Reichweitenmessung');
assert.equal(SETTINGS_LABEL, 'Datenschutz-Einstellungen');
});
test('w.js: Banner-Aufbau (Dialog, Texte, gleiche Buttons, Akzent nur Annehmen)', () => {
const h = harness();
const host = hostEl(h);
assert.ok(host, 'Banner im Shadow Root');
const dlg = findAll(host.shadow, (n) => n.attrs.role === 'dialog')[0];
assert.equal(dlg.attrs['aria-label'], BANNER_ARIA);
const all = findAll(host.shadow, () => true);
const text = all.map((n) => n.textContent).join(' ');
assert.ok(all.find((n) => n.tagName === 'P').textContent.startsWith(BANNER_TEXT), 'Text beginnt mit BANNER_TEXT');
const a = all.find((n) => n.tagName === 'A');
assert.equal(a.attrs.href, '/datenschutz-analyse.html');
assert.equal(a.textContent, 'Datenschutz');
const css = all.filter((n) => n.tagName === 'STYLE').map((n) => n.textContent).join('');
assert.match(css, /#0a5a8a/);
assert.match(css, /max-width:\s*767px/);
const buttons = all.filter((n) => n.tagName === 'BUTTON');
assert.deepEqual(buttons.map((b) => b.textContent), ['Ablehnen', 'Annehmen']);
assert.ok(!buttons[0].focused, 'kein Fokusraub beim Laden');
assert.equal(buttons[0].attrs.class, buttons[1].attrs.class);
assert.ok(!/button\.|\.p\b/.test(css), 'eine Regel fuer beide Buttons');
assert.match(css, /button\{[^}]*background:#0a5a8a[^}]*color:#fff/);
assert.match(css, /button\{[^}]*[; ]width:150px/);
assert.equal((css.match(/(?<![-\w])width:/g) || []).length, 2, 'eine feste Breite fuer beide + 100% mobil');
const portal = readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'public', 'analyse.css'), 'utf8');
assert.match(portal, /\.an-bn-btn \{[^}]*[; ]width: 150px/);
assert.equal((portal.match(/\.an-bn-btn[^{]*\{/g) || []).length, 2, 'Basisregel + mobile Regel');
assert.equal(host.style.zIndex, '2147483647');
// Escape schliesst nicht
fire(dlg, 'keydown', { key: 'Escape' }); fire(host, 'keydown', { key: 'Escape' });
assert.ok(!host.removed);
});
test('w.js: Annehmen => bn accept, Speicher, ck/ct/en; Widerruf stoppt sofort', () => {
const h = harness();
const bh = hostEl(h);
btn(bh.shadow, 'Annehmen').click();
assert.deepEqual(h.types(), ['pv', 'bn:shown', 'bn:accept', 'cx']);
assert.deepEqual(JSON.parse(h.store['wk-consent']), { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 });
assert.ok(bh.removed && !hostEl(h));
const t = target();
h.ck(t);
const ck = h.sent.at(-1).d;
assert.deepEqual(ck, { t: 'ck', site: 'abcdef0123456789abcd', path: '/start', vp: 'd', x: 0.5, y: 0.5, sel: 'body:nth-of-type(1) > button:nth-of-type(1)' });
// Kontakt-Link: ck + ct (Kind-Element in <a>)
const a = makeEl('a'); a.setAttribute('href', 'tel:+4912'); a.parentNode = makeEl('body');
const span = makeEl('span'); span.parentNode = a;
const n = h.sent.length; h.ck(span);
assert.deepEqual(h.sent.slice(n).map((s) => s.d.t), ['ck', 'ct']);
assert.deepEqual(h.sent.at(-1).d, { t: 'ct', site: 'abcdef0123456789abcd', path: '/start', kind: 'tel' });
const out = makeEl('a'); out.setAttribute('href', 'https://anders.de/x'); out.parentNode = makeEl('body');
h.ck(out);
assert.deepEqual(h.sent.at(-1).d, { t: 'ct', site: 'abcdef0123456789abcd', path: '/start', kind: 'out', host: 'anders.de' });
// en einmal
h.leave(); h.leave();
const en = h.sent.filter((s) => s.d.t === 'en');
assert.equal(en.length, 1);
assert.equal(en[0].d.scroll, 55); // (300+800)/2000
assert.equal(typeof en[0].d.sec, 'number');
// Widerruf ueber Einstellungs-Link
const settings = settingsBtn(h);
assert.ok(settings, 'fester Link');
settings.click();
assert.ok(btn(hostEl(h).shadow, 'Ablehnen').focused, 'Fokus beim Wiederoeffnen');
btn(hostEl(h).shadow, 'Ablehnen').click();
assert.equal(h.types().at(-1), 'bn:reject');
const before = h.sent.length;
h.ck(target()); h.leave();
assert.equal(h.sent.length, before);
assert.equal((h.docL.click || []).length, 1); // nur die Einstellungs-Delegation
assert.equal((h.docL.visibilitychange || []).length, 0);
assert.equal((h.winL.pagehide || []).length, 0);
});
test('w.js: gespeicherte Einwilligung => kein Banner, kein bn, Tracking an', () => {
const h = harness({ stored: { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 1000 } });
assert.deepEqual(h.types(), ['pv']);
assert.equal(hostEl(h), undefined);
h.ck(target());
assert.equal(h.sent.at(-1).d.t, 'ck');
});
test('w.js: abgelaufene/abgelehnte Einwilligung', () => {
const h = harness({ stored: { v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 - 5000 } });
assert.deepEqual(h.types(), []);
h.ck(target()); h.leave();
assert.deepEqual(h.types(), []);
const h2 = harness({ stored: { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 200 * 86400000 } });
assert.deepEqual(h2.types(), ['pv', 'bn:shown']);
});
test('w.js: GPC und DNT => nichts senden, kein Banner, keine Hooks (T1)', () => {
for (const o of [{ gpc: true }, { dnt: '1' }, { dnt: 'yes' }, { gpc: true, stored: { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 } }]) {
const h = harness(o);
assert.equal(h.sent.length, 0);
assert.equal(h.winL.pageshow, undefined);
assert.equal(h.winL.popstate, undefined);
assert.equal(h.history.pushState, h.origPush);
assert.equal(h.body.children.length, 0, 'kein Banner, kein Einstellungsknopf');
h.ck(target()); h.leave(); h.nav('/neu');
assert.equal(h.sent.length, 0);
assert.equal(h.reads.referrer + h.reads.innerWidth, 0);
}
});
test('w.js: gespeichertes Ablehnen => nichts senden, nur Einstellungsknopf; spaeteres Annehmen => bn:accept, pv mit ref+vp', () => {
const h = harness({ stored: { v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 } });
assert.equal(h.sent.length, 0);
assert.equal(h.winL.pageshow, undefined);
assert.equal(h.winL.popstate, undefined);
assert.equal(h.history.pushState, h.origPush);
assert.equal(h.body.children.length, 1);
const open = settingsBtn(h);
assert.ok(open);
open.click();
assert.equal(h.sent.length, 0);
btn(hostEl(h).shadow, 'Annehmen').click();
assert.deepEqual(h.types(), ['bn:accept', 'pv']);
assert.deepEqual(h.sent[1].d, { t: 'pv', site: SITE, path: '/start', ref: 'www.google.com', vp: 'd' });
assert.ok(h.winL.pageshow && h.winL.popstate, 'Hooks nachtraeglich installiert');
h.nav('/zwei');
assert.equal(h.types().at(-1), 'pv');
assert.equal(h.sent.at(-1).d.path, '/zwei');
});
test('w.js: Ablehnen => genau ein bn:reject, danach nichts mehr (auch nicht Seitenwechsel)', () => {
const h2 = harness();
btn(hostEl(h2).shadow, 'Ablehnen').click();
assert.deepEqual(h2.types(), ['pv', 'bn:shown', 'bn:reject']);
const n = h2.sent.length;
h2.nav('/zwei'); h2.nav('/drei');
for (const f of h2.winL.pageshow || []) f({ persisted: true });
for (const f of h2.winL.popstate || []) f({});
h2.timers.splice(0).forEach((f) => f());
h2.ck(target()); h2.leave();
for (const f of h2.docL.scroll || []) f({});
assert.equal(h2.sent.length, n);
assert.equal(h2.types().filter((t) => t === 'bn:reject').length, 1);
});
test('w.js: Seitenwechsel-Timer vor dem Ablehnen sendet nichts', () => {
const h = harness();
h.win.location.pathname = '/zwei';
h.history.pushState(null, '', '/zwei'); // Timer geplant
assert.equal(h.timers.length, 1);
btn(hostEl(h).shadow, 'Ablehnen').click();
const n = h.sent.length;
h.timers.splice(0).forEach((f) => f());
assert.equal(h.sent.length, n);
});
test('w.js: Ablehnen in anderem Tab (storage) => danach kein pv beim Seitenwechsel', () => {
const h = harness();
const n = h.sent.length;
h.store['wk-consent'] = JSON.stringify({ v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 });
h.storage();
h.nav('/zwei');
for (const f of h.winL.pageshow || []) f({ persisted: true });
assert.equal(h.sent.length, n);
});
test('w.js: Annehmen auf laufender Seite => bn:accept, genau ein cx mit ref+vp; kein zweites cx', () => {
const h = harness();
assert.deepEqual(h.types(), ['pv', 'bn:shown']);
btn(hostEl(h).shadow, 'Annehmen').click();
assert.deepEqual(h.types(), ['pv', 'bn:shown', 'bn:accept', 'cx']);
assert.deepEqual(h.sent.at(-1).d, { t: 'cx', site: SITE, path: '/start', ref: refHost('https://www.google.com/', 'mein.de'), vp: 'd' });
// Widerruf, erneut annehmen
settingsBtn(h).click();
btn(hostEl(h).shadow, 'Ablehnen').click();
settingsBtn(h).click();
btn(hostEl(h).shadow, 'Annehmen').click();
assert.equal(h.sent.filter((s) => s.d.t === 'cx').length, 1);
});
test('w.js: gespeichertes Annehmen => erstes pv mit ref+vp, kein cx', () => {
const h = harness({ stored: ACC0 });
assert.deepEqual(h.types(), ['pv']);
assert.deepEqual(h.sent[0].d, { t: 'pv', site: SITE, path: '/start', ref: 'www.google.com', vp: 'd' });
});
test('w.js: T15 kein sendBeacon, fetch mit keepalive, omit, POST, text/plain', () => {
const h = harness();
assert.ok(h.fetches.length >= 1);
assert.equal(h.beacon.length, 0);
for (const f of h.fetches) {
assert.equal(f.o.keepalive, true);
assert.equal(f.o.credentials, 'omit');
assert.equal(f.o.method, 'POST');
assert.equal(f.o.headers['Content-Type'], 'text/plain');
}
});
test('w.js: T19 Platz unten solange Banner offen; Knopf mindestens 24px', () => {
const h = harness({ bannerHeight: 90, bodyPad: '5px' });
assert.equal(h.body.style.paddingBottom, '90px');
btn(hostEl(h).shadow, 'Ablehnen').click();
assert.equal(h.body.style.paddingBottom, '5px');
const g = harness({ stored: { v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 } });
const css = findAll(g.body, (n) => n.tagName === 'STYLE').map((n) => n.textContent).join('');
assert.match(css, /width:44px;height:44px/);
});
test('w.js: Einstellungsknopf ist ein angeknabberter Keks, der sich dreht (ohne Drehung bei reduzierter Bewegung)', () => {
const h = harness({ stored: { v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 } });
const b = settingsBtn(h);
assert.ok(b, 'Knopf da');
assert.equal(b.attrs.type, 'button');
assert.equal(b.textContent, '', 'kein sichtbarer Text im Knopf selbst');
// Etikett "Datenschutz-Einstellungen" erscheint beim Darueberfahren, fuer Screenreader zaehlt aria-label
const label = findAll(b, (n) => n.textContent === SETTINGS_LABEL)[0];
assert.ok(label);
assert.equal(label.attrs['aria-hidden'], 'true');
// Keks als Inline-SVG (keine data:-Grafik, die eine CSP der Website blockieren koennte)
const svg = findAll(b, (n) => n.tagName === 'SVG')[0];
assert.ok(svg);
assert.equal(svg.ns, 'http://www.w3.org/2000/svg');
assert.equal(svg.attrs['aria-hidden'], 'true');
const paths = findAll(svg, (n) => n.tagName === 'PATH');
assert.ok(paths.length >= 1, 'Umriss mit Bissspur');
assert.match(paths[0].attrs.d, /A/, 'Bogen fuer den Biss');
assert.ok(findAll(svg, (n) => n.tagName === 'ELLIPSE' || n.tagName === 'CIRCLE').length >= 5, 'Schokostueckchen');
const css = findAll(h.body, (n) => n.tagName === 'STYLE').map((n) => n.textContent).join('');
assert.match(css, /@keyframes wk-spin\{to\{transform:rotate\(360deg\)\}\}/);
assert.match(css, /animation:wk-spin \d+s linear infinite/);
assert.match(css, /@media \(prefers-reduced-motion:reduce\)\{[^}]*animation:none/);
assert.match(css, /:hover \.c,[^{]*:focus-visible \.c\{animation-play-state:paused\}/);
assert.ok(!/url\(/.test(css), 'keine externen oder data:-Grafiken');
b.click();
assert.ok(hostEl(h), 'Klick oeffnet das Banner');
});
test('w.js: [data-webklar-consent] oeffnet Banner, kein fester Link', () => {
const h = harness({ stored: { v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 }, withConsentLink: true });
assert.equal(settingsBtn(h), undefined);
assert.equal(hostEl(h), undefined);
h.link.click();
assert.ok(hostEl(h));
});
test('w.js: Heatmap-Modus sendet nichts, entfernt Parameter, laedt h.js', () => {
const h = harness({ search: '?a=1&__wk_heat=TOKEN.sig' });
assert.deepEqual(h.sent, []);
assert.equal(hostEl(h), undefined);
assert.equal(h.history.replaced, '/start?a=1');
assert.equal(h.injected.length, 1);
assert.equal(h.injected[0].attrs.src, 'https://project.webklar.com/a/h.js');
assert.equal(h.injected[0].attrs['data-token'], 'TOKEN.sig');
assert.equal(h.injected[0].attrs['data-embed'], undefined);
assert.equal((h.docL.click || []).length, 0);
});
test('w.js: Heatmap-Modus mit __wk_embed=1 entfernt beide Parameter und reicht data-embed an h.js', () => {
const h = harness({ search: '?__wk_heat=TOKEN.sig&__wk_embed=1&b=2' });
assert.deepEqual(h.sent, []);
assert.equal(h.history.replaced, '/start?b=2');
assert.equal(h.injected.length, 1);
assert.equal(h.injected[0].attrs['data-embed'], '1');
assert.equal(h.injected[0].attrs['data-token'], 'TOKEN.sig');
// __wk_embed ohne Heatmap-Token: normaler Betrieb (Banner, pv)
const n = harness({ search: '?__wk_embed=1' });
assert.deepEqual(n.types(), ['pv', 'bn:shown']);
});
test('w.js: SPA-Navigation sendet pv nur bei Pfadwechsel', () => {
const h = harness();
h.nav('/start');
assert.equal(h.sent.filter((s) => s.d.t === 'pv').length, 1);
h.win.location.pathname = '/neu';
h.history.pushState(null, '', '/neu'); h.history.pushState(null, '', '/neu');
h.timers.splice(0).forEach((f) => f());
assert.deepEqual(h.sent.filter((s) => s.d.t === 'pv').map((s) => s.d.path), ['/start', '/neu']);
assert.deepEqual(h.sent.at(-1).d, { t: 'pv', site: SITE, path: '/neu' });
});
test('w.js: wirft nie, auch wenn localStorage/sendBeacon kaputt', () => {
assert.doesNotThrow(() => {
const sandbox = { window: {}, document: { currentScript: null, querySelector() { throw new Error('x'); } }, navigator: {}, location: {} };
sandbox.window = sandbox; vm.createContext(sandbox);
vm.runInContext(readFileSync(join(DIST, 'w.js'), 'utf8'), sandbox);
});
});
test('w.js: doppelt eingebunden => nur ein pv', () => {
const h = harness();
h.rerun();
assert.deepEqual(h.types(), ['pv', 'bn:shown']);
});
test('w.js: Widerruf in anderem Tab (storage-Ereignis) stoppt Tracking sofort', () => {
const h = harness({ stored: { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 1000 } });
h.ck(target());
assert.equal(h.sent.at(-1).d.t, 'ck');
h.store['wk-consent'] = JSON.stringify({ v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 });
h.storage();
const n = h.sent.length;
h.ck(target()); h.leave();
assert.equal(h.sent.length, n);
assert.equal((h.docL.click || []).length, 1); // nur die Einstellungs-Delegation
// Annehmen in anderem Tab startet Tracking
h.store['wk-consent'] = JSON.stringify({ v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 });
h.storage();
h.ck(target());
assert.equal(h.sent.length, n + 1); // nur ck, kein cx (pv trug ref/vp schon)
assert.ok(!h.sent.some((x) => x.d.t === 'cx'));
});
test('w.js: Einwilligung wird vor jedem Senden neu geprueft (ohne storage-Ereignis)', () => {
const h = harness({ stored: { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 1000 } });
h.store['wk-consent'] = JSON.stringify({ v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 });
const n = h.sent.length;
h.ck(target()); h.leave();
assert.equal(h.sent.length, n);
assert.equal((h.docL.click || []).length, 1); // nur die Einstellungs-Delegation
});
test('w.js: SPA-pv bei akzeptierter Einwilligung; nach Widerruf kein en', () => {
const h = harness({ stored: { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 1000 } });
h.win.location.pathname = '/zwei';
h.history.pushState(null, '', '/zwei'); h.timers.splice(0).forEach((f) => f());
// en der vorigen Seite vor dem pv der neuen (Router-Links verhindern den Standard-Klick, onLinkClick greift nicht)
assert.deepEqual(h.sent.map((s) => `${s.d.t}:${s.d.path}`), ['pv:/start', 'en:/start', 'pv:/zwei']);
assert.equal(h.sent[1].d.scroll, 55);
h.ck(target());
assert.equal(h.sent.at(-1).d.path, '/zwei');
h.store['wk-consent'] = JSON.stringify({ v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 });
h.storage();
const n = h.sent.length;
h.leave();
assert.equal(h.sent.length, n);
});
test('w.js: bfcache-Rueckkehr sendet neues pv und erlaubt neues en', () => {
const h = harness({ stored: { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 1000 } });
h.leave();
assert.equal(h.sent.filter((s) => s.d.t === 'en').length, 1);
for (const f of h.winL.pageshow || []) f({ persisted: false });
assert.equal(h.sent.filter((s) => s.d.t === 'pv').length, 1);
for (const f of h.winL.pageshow || []) f({ persisted: true });
assert.equal(h.sent.filter((s) => s.d.t === 'pv').length, 2);
h.leave();
assert.equal(h.sent.filter((s) => s.d.t === 'en').length, 2);
});
test('w.js: [data-webklar-consent] spaeter hinzugefuegt funktioniert per Delegation', () => {
const h = harness({ stored: { v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 } });
// kein Link beim Laden => fester Link; spaeteres Element oeffnet ebenfalls
const late = makeEl('a'); late.setAttribute('data-webklar-consent', '');
for (const f of [...(h.docL.click || [])]) f({ type: 'click', target: late, preventDefault() {} });
assert.ok(hostEl(h));
assert.ok(btn(hostEl(h).shadow, 'Ablehnen').focused);
});
// ---------- Verweildauer bei internem Seitenwechsel ----------
const ACC = { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 1000 };
const lnk = (href, attrs = {}) => { const a = makeEl('a'); a.setAttribute('href', href); for (const k in attrs) a.setAttribute(k, attrs[k]); a.parentNode = makeEl('body'); return a; };
const enCount = (h) => h.sent.filter((s) => s.d.t === 'en').length;
test('w.js: interner Link-Klick sendet en sofort, pagehide danach nicht doppelt', () => {
const h = harness({ stored: ACC });
h.ck(lnk('/kontakt'));
assert.equal(enCount(h), 1);
assert.deepEqual(h.sent.map((s) => s.d.t), ['pv', 'ck', 'en']);
const en = h.sent.at(-1).d;
assert.equal(en.path, '/start'); assert.equal(en.scroll, 55);
h.leave();
assert.equal(enCount(h), 1);
h.ck(lnk('https://mein.de/andere?x=1#a'));
assert.equal(enCount(h), 1);
});
test('w.js: kein fruehes en bei externem Link, Anker, gleicher Seite, Modifier, Ziel/Download, defaultPrevented', () => {
const h = harness({ stored: ACC });
h.ck(lnk('https://anders.de/x'));
h.ck(lnk('#oben'));
h.ck(lnk('/start#unten'));
h.ck(lnk('/kontakt'), { ctrlKey: true });
h.ck(lnk('/kontakt'), { metaKey: true });
h.ck(lnk('/kontakt'), { shiftKey: true });
h.ck(lnk('/kontakt'), { button: 1 });
h.ck(lnk('/kontakt', { target: '_blank' }));
h.ck(lnk('/datei.pdf', { download: '' }));
h.ck(lnk('/kontakt'), { defaultPrevented: true });
h.ck(lnk('mailto:a@b.de'));
assert.equal(enCount(h), 0);
h.leave();
assert.equal(enCount(h), 1);
});
test('w.js: ohne Einwilligung oder nach Widerruf kein fruehes en', () => {
const h = harness();
h.ck(lnk('/kontakt'));
assert.equal(enCount(h), 0);
const g = harness({ stored: ACC });
g.store['wk-consent'] = JSON.stringify({ v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 });
g.ck(lnk('/kontakt'));
assert.equal(enCount(g), 0);
});
// ---------- Variante "eigen" (WEBklars eigene Websites, z. B. webklar.com) ----------
test('core: bannerFor und readConsent je Fassung', () => {
assert.deepEqual(bannerFor('eigen'), { version: BANNER_VERSION_EIGEN, text: BANNER_TEXT_EIGEN });
for (const v of ['', 'kunde', 'EIGEN', 'x', undefined]) assert.deepEqual(bannerFor(v), { version: BANNER_VERSION, text: BANNER_TEXT });
assert.equal(BANNER_VERSION_EIGEN, '2026-10-17');
assert.notEqual(BANNER_VERSION_EIGEN, BANNER_VERSION);
const now = 1_700_000_000_000;
const st = (o) => ({ getItem: () => JSON.stringify(o) });
assert.equal(readConsent(st({ v: 1, a: 'accept', bv: BV, t: now }), now, BANNER_VERSION_EIGEN), null, 'Kunden-Fassung gilt nicht fuer eigen');
assert.equal(readConsent(st({ v: 1, a: 'accept', bv: BANNER_VERSION_EIGEN, t: now }), now, BANNER_VERSION_EIGEN).a, 'accept');
assert.equal(readConsent(st({ v: 1, a: 'accept', bv: BANNER_VERSION_EIGEN, t: now }), now), null, 'ohne Fassung gilt die Kunden-Fassung');
});
test('core: Banner-Text eigen (Sie-Form, ohne Auftragsverarbeiter)', () => {
assert.match(BANNER_TEXT_EIGEN, /^Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung\./);
assert.match(BANNER_TEXT_EIGEN, /Wir messen selbst auf unserem Server in Deutschland, ohne Cookies und ohne Werbung\./);
assert.match(BANNER_TEXT_EIGEN, /Nach „Ablehnen“ zählen wir nur noch, dass Sie abgelehnt haben\./);
assert.match(BANNER_TEXT_EIGEN, /über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen\. Details: $/);
assert.doesNotMatch(BANNER_TEXT_EIGEN, /\b(Du|Dein|Deine|Deiner|Dich|Dir)\b/);
assert.doesNotMatch(BANNER_TEXT_EIGEN, /betreibt für uns/);
});
test('w.js: data-variant="eigen" => Sie-Text, eigene Fassung im Speicher', () => {
const h = harness({ variant: 'eigen' });
const host = hostEl(h);
const p = findAll(host.shadow, (n) => n.tagName === 'P')[0];
assert.ok(p.textContent.startsWith(BANNER_TEXT_EIGEN));
assert.ok(!p.textContent.startsWith(BANNER_TEXT));
btn(host.shadow, 'Annehmen').click();
assert.deepEqual(JSON.parse(h.store['wk-consent']), { v: 1, a: 'accept', bv: BANNER_VERSION_EIGEN, t: 1_700_000_000_000 });
assert.deepEqual(h.types(), ['pv', 'bn:shown', 'bn:accept', 'cx']);
});
test('w.js: eigen fragt neu bei Wahl aus der Kunden-Fassung, gilt mit eigener Fassung', () => {
const alt = harness({ variant: 'eigen', stored: { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 1000 } });
assert.ok(hostEl(alt), 'Banner erscheint erneut');
const ok = harness({ variant: 'eigen', stored: { v: 1, a: 'reject', bv: BANNER_VERSION_EIGEN, t: 1_700_000_000_000 - 1000 } });
assert.equal(hostEl(ok), undefined);
assert.deepEqual(ok.types(), []);
});
test('w.js: unbekannte Variante => Kunden-Fassung', () => {
const h = harness({ variant: 'sonstwas' });
const p = findAll(hostEl(h).shadow, (n) => n.tagName === 'P')[0];
assert.ok(p.textContent.startsWith(BANNER_TEXT));
});
test('w.js: SPA-Wechsel ohne Einwilligung sendet nur pv, kein en', () => {
const h = harness();
h.win.location.pathname = '/zwei';
h.history.pushState(null, '', '/zwei'); h.timers.splice(0).forEach((f) => f());
assert.deepEqual(h.types(), ['pv', 'bn:shown', 'pv']);
});
test('w.js: en nach SPA-Wechsel nur einmal je Seite (Link-Klick und Router)', () => {
const h = harness({ stored: { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 1000 } });
h.leave();
h.win.location.pathname = '/zwei';
h.history.pushState(null, '', '/zwei'); h.timers.splice(0).forEach((f) => f());
assert.deepEqual(h.sent.map((s) => `${s.d.t}:${s.d.path}`), ['pv:/start', 'en:/start', 'pv:/zwei']);
h.leave();
assert.deepEqual(h.sent.map((s) => `${s.d.t}:${s.d.path}`).slice(-1), ['en:/zwei']);
});
+93
View File
@@ -0,0 +1,93 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import crypto from 'node:crypto';
import { mkdtempSync, readFileSync, readdirSync, statSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { DatabaseSync } from 'node:sqlite';
import { openDb } from '../src/db.js';
import { exportSite } from '../scripts/export-site.mjs';
const KEY = 'abcdef0123456789abcd';
const OTHER = 'zzzzzzzz0123456789zz';
const SCRIPT = new URL('../scripts/export-site.mjs', import.meta.url).pathname;
function setup() {
const dir = mkdtempSync(join(tmpdir(), 'exp-'));
const dbPath = join(dir, 'a.db');
const db = openDb(dbPath);
for (const k of [KEY, OTHER]) db.prepare('INSERT INTO sites (siteKey, projectId, domains, active) VALUES (?,?,?,1)').run(k, 'p', '[]');
const ts = Date.UTC(2026, 8, 1, 10);
db.prepare("INSERT INTO daily VALUES (?, '2026-08-31','pv_total','',5)").run(KEY);
db.prepare("INSERT INTO daily VALUES (?, '2026-08-31','pv_path','/a',2)").run(KEY);
db.prepare("INSERT INTO daily VALUES (?, '2026-08-31','pv_total','',9)").run(OTHER);
db.prepare("INSERT INTO events_raw (site, ts, type, path, ref) VALUES (?,?, 'pv', '/x', ?)").run(KEY, ts, 'a,"b"\nc');
db.prepare("INSERT INTO events_raw (site, ts, type, path) VALUES (?,?, 'pv', '/o')").run(OTHER, ts);
db.prepare('INSERT INTO clicks (site, ts, path, vp, x, y, sel) VALUES (?,?,?,?,?,?,?)').run(KEY, ts, '/', 'd', 1.5, 2, '=1+1');
db.prepare('INSERT INTO clicks (site, ts, path, vp, x, y, sel) VALUES (?,?,?,?,?,?,?)').run(OTHER, ts, '/', 'd', 1, 2, 'x');
db.exec('PRAGMA wal_checkpoint(TRUNCATE)');
db.close();
return { dir, dbPath };
}
const hash = (p) => crypto.createHash('sha256').update(readFileSync(p)).digest('hex');
test('Export: drei CSV-Dateien mit Kopfzeilen, nur diese Website, Quotes und Formelschutz', () => {
const { dir, dbPath } = setup();
const before = hash(dbPath);
const out = join(dir, 'out');
exportSite({ dbPath, siteKey: KEY, outDir: out });
assert.deepEqual(readdirSync(out).sort(), [`${KEY}-ereignisse.csv`, `${KEY}-klicks.csv`, `${KEY}-tageswerte.csv`]);
const t = readFileSync(join(out, `${KEY}-tageswerte.csv`), 'utf8');
assert.equal(t.split('\r\n')[0], 'day,dim,key,count');
assert.equal(t.split('\r\n').filter(Boolean).length, 3);
const e = readFileSync(join(out, `${KEY}-ereignisse.csv`), 'utf8');
assert.equal(e.split('\r\n')[0], 'time,type,path,vp,ref,country,device,visitor,sec,scroll,kind,host');
assert.ok(e.includes('2026-09-01T10:00:00.000Z,pv,/x,,"a,""b""\nc"'));
assert.ok(!e.includes('/o'));
const c = readFileSync(join(out, `${KEY}-klicks.csv`), 'utf8');
assert.equal(c.split('\r\n')[0], 'time,path,vp,x,y,sel');
assert.equal(c.split('\r\n').filter(Boolean).length, 2);
assert.ok(c.includes(",'=1+1"));
assert.equal(hash(dbPath), before);
});
test('Export: Datenbank ist nur lesend geöffnet', () => {
const { dbPath } = setup();
const ro = new DatabaseSync(dbPath, { readOnly: true });
assert.throws(() => ro.exec("INSERT INTO sites (siteKey, projectId, domains) VALUES ('q','p','[]')"));
ro.close();
});
test('CLI: unbekannter oder ungültiger Schlüssel => Exit 1 ohne Dateien', () => {
const { dir, dbPath } = setup();
for (const k of ['unbekannt0123456789ab', 'ABC']) {
const out = join(dir, 'o-' + k);
const r = spawnSync(process.execPath, [SCRIPT, k, '--db', dbPath, '--out', out]);
assert.equal(r.status, 1);
assert.throws(() => readdirSync(out));
}
const ok = spawnSync(process.execPath, [SCRIPT, KEY, '--db', dbPath, '--out', join(dir, 'ok')]);
assert.equal(ok.status, 0);
assert.equal(readdirSync(join(dir, 'ok')).length, 3);
});
test('Export: Formelschutz nur fuer Text (Tab, CR, Minus), Zahlen bleiben unveraendert, Dateien 0600', () => {
const { dir, dbPath } = setup();
const db = new DatabaseSync(dbPath);
const ts = Date.UTC(2026, 8, 1, 10);
const ins = db.prepare('INSERT INTO clicks (site, ts, path, vp, x, y, sel) VALUES (?,?,?,?,?,?,?)');
ins.run(KEY, ts, '/', 'd', -3, -4.5, '\tcmd');
ins.run(KEY, ts, '/', 'd', 1, 2, '\rcmd');
ins.run(KEY, ts, '/', 'd', 1, 2, '-cmd');
db.close();
const out = join(dir, 'out');
const files = exportSite({ dbPath, siteKey: KEY, outDir: out });
const c = readFileSync(join(out, `${KEY}-klicks.csv`), 'utf8');
assert.ok(c.includes(',-3,-4.5,'), 'negative Zahlen ohne Praefix');
assert.ok(!c.includes("'-3"));
assert.ok(c.includes(",\"'\tcmd\"") || c.includes(",'\tcmd"), 'Tab gesichert');
assert.ok(c.includes(`"'\rcmd"`), 'CR gesichert');
assert.ok(c.includes(",'-cmd"));
for (const f of files) assert.equal(statSync(f).mode & 0o777, 0o600);
});
+52
View File
@@ -0,0 +1,52 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, writeFileSync, existsSync, readFileSync, chmodSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { spawnSync } from 'node:child_process';
const SCRIPT = new URL('../scripts/geo-update.sh', import.meta.url).pathname;
// Fremde Programme (curl, docker) sind Attrappen im PATH; docker schlaegt fehl, solange DOCKER_FAIL=1 gesetzt ist.
function setup() {
const root = mkdtempSync(join(tmpdir(), 'geoupd-'));
const bin = join(root, 'bin');
const data = join(root, 'data');
mkdirSync(bin);
mkdirSync(data);
const csv = join(root, 'src.csv');
const rows = [];
for (let i = 0; i < 100100; i++) rows.push(`1.${i % 250}.${Math.floor(i / 250) % 250}.0,1.${i % 250}.${Math.floor(i / 250) % 250}.255,AU`);
rows.push('5.9.0.0,5.9.255.255,DE');
writeFileSync(csv, rows.join('\n') + '\n');
writeFileSync(join(bin, 'curl'), `#!/bin/sh\nwhile [ "$#" -gt 0 ]; do [ "$1" = "-o" ] && out="$2"; shift; done\ngzip -c "${csv}" > "$out"\n`);
writeFileSync(join(bin, 'docker'), `#!/bin/sh\necho "$@" >> "${root}/docker.log"\n[ "\${DOCKER_FAIL:-0}" = "1" ] && exit 1\nexit 0\n`);
chmodSync(join(bin, 'curl'), 0o755);
chmodSync(join(bin, 'docker'), 0o755);
const run = (fail) => spawnSync('sh', [SCRIPT], { env: { PATH: `${bin}:${process.env.PATH}`, ANALYTICS_DATA_DIR: data, DOCKER_FAIL: fail ? '1' : '0' }, encoding: 'utf8' });
const restarts = () => (existsSync(join(root, 'docker.log')) ? readFileSync(join(root, 'docker.log'), 'utf8').trim().split('\n').length : 0);
return { data, run, restarts, marker: join(data, 'geo.restart-pending') };
}
test('geo-update: Neustart schlägt fehl -> Marker; nächster Lauf startet trotz unveränderter Datei neu und entfernt ihn', () => {
const s = setup();
const first = s.run(true);
assert.notEqual(first.status, 0, first.stderr);
assert.ok(existsSync(join(s.data, 'geo.csv')), 'neue Datei ist schon an Ort und Stelle');
assert.ok(existsSync(s.marker), 'Marker liegt');
assert.equal(s.restarts(), 1);
const second = s.run(false);
assert.equal(second.status, 0, second.stderr);
assert.equal(s.restarts(), 2, 'Neustart nachgeholt');
assert.equal(existsSync(s.marker), false);
const third = s.run(false);
assert.equal(third.status, 0);
assert.equal(s.restarts(), 2, 'unverändert und ohne Marker: kein Neustart');
});
test('geo-update: erfolgreicher Neustart hinterlässt keinen Marker', () => {
const s = setup();
assert.equal(s.run(false).status, 0);
assert.equal(s.restarts(), 1);
assert.equal(existsSync(s.marker), false);
});
+110
View File
@@ -0,0 +1,110 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { loadGeo } from '../src/geo.js';
const CSV = [
'1.0.0.0,1.0.0.255,AU',
'5.9.0.0,5.9.255.255,DE',
'5.10.0.0,5.10.0.9,ZZ',
'8.8.8.0,8.8.8.255,US',
'255.255.255.0,255.255.255.255,FR',
'2001:db8::,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff,AT',
'2a01:4f8::,2a01:4f8:ffff:ffff:ffff:ffff:ffff:ffff,DE',
'2a02:1::1,2a02:1::ff,CH',
'',
].join('\n');
function fixture(content) {
const dir = mkdtempSync(join(tmpdir(), 'geo-'));
const f = join(dir, 'geo.csv');
writeFileSync(f, content);
return f;
}
test('IPv4: Treffer und Grenzen', () => {
const g = loadGeo(fixture(CSV));
assert.equal(g.size, 8);
assert.equal(g.lookup('5.9.0.0'), 'DE');
assert.equal(g.lookup('5.9.123.4'), 'DE');
assert.equal(g.lookup('5.9.255.255'), 'DE');
assert.equal(g.lookup('5.8.255.255'), '');
assert.equal(g.lookup('5.10.0.0'), '');
assert.equal(g.lookup('1.0.0.255'), 'AU');
assert.equal(g.lookup('1.0.1.0'), '');
assert.equal(g.lookup('0.0.0.1'), '');
assert.equal(g.lookup('255.255.255.255'), 'FR');
assert.equal(g.lookup('8.8.8.8'), 'US');
});
test('IPv6: Treffer, Grenzen, Kurzschreibweise', () => {
const g = loadGeo(fixture(CSV));
assert.equal(g.lookup('2a01:4f8::'), 'DE');
assert.equal(g.lookup('2a01:4f8:1:2:3:4:5:6'), 'DE');
assert.equal(g.lookup('2A01:04F8:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF'), 'DE');
assert.equal(g.lookup('2a01:4f9::'), '');
assert.equal(g.lookup('2a01:4f7:ffff:ffff:ffff:ffff:ffff:ffff'), '');
assert.equal(g.lookup('2001:db8::1'), 'AT');
assert.equal(g.lookup('2a02:1::1'), 'CH');
assert.equal(g.lookup('2a02:1::ff'), 'CH');
assert.equal(g.lookup('2a02:1::100'), '');
assert.equal(g.lookup('2a02:1::'), '');
assert.equal(g.lookup('::1'), '');
});
test('IPv4-gemappte IPv6-Adressen zählen als IPv4', () => {
const g = loadGeo(fixture(CSV));
assert.equal(g.lookup('::ffff:5.9.1.1'), 'DE');
assert.equal(g.lookup('::FFFF:8.8.8.8'), 'US');
assert.equal(g.lookup('::ffff:808:808'), 'US');
assert.equal(g.lookup('::ffff:9.9.9.9'), '');
});
test('ZZ, ungültige Eingaben, unsortierte Datei', () => {
const g = loadGeo(fixture(CSV));
assert.equal(g.lookup('5.10.0.5'), '');
for (const bad of ['', 'abc', '1.2.3', '256.1.1.1', '1.2.3.4.5', '2001:db8::zz', ':::', '1::2::3', null, undefined, 42]) {
assert.equal(g.lookup(bad), '', String(bad));
}
const u = loadGeo(fixture('8.8.8.0,8.8.8.255,US\n1.0.0.0,1.0.0.255,AU\n2a01:4f8::,2a01:4f8::ff,DE\n2001:db8::,2001:db8::ff,AT\n'));
assert.equal(u.lookup('1.0.0.7'), 'AU');
assert.equal(u.lookup('8.8.8.8'), 'US');
assert.equal(u.lookup('2001:db8::7'), 'AT');
assert.equal(u.lookup('2a01:4f8::7'), 'DE');
});
test('fehlende Datei: alles leer, eine Warnung, kein Absturz', () => {
const warns = [];
const orig = console.warn;
console.warn = (...a) => warns.push(a.join(' '));
try {
const g = loadGeo('/nicht/vorhanden/geo.csv');
assert.equal(g.lookup('5.9.1.1'), '');
assert.equal(g.lookup('2a01:4f8::1'), '');
assert.equal(g.size, 0);
} finally { console.warn = orig; }
assert.equal(warns.length, 1);
assert.match(warns[0], /nicht lesbar/);
});
test('600k Zeilen: lädt zügig, Heap bleibt klein', () => {
const lines = [];
for (let i = 0; i < 400000; i++) lines.push(`${(i * 4096) >>> 24}.${((i * 4096) >>> 16) & 255}.${((i * 4096) >>> 8) & 255}.0,${(i * 4096) >>> 24}.${((i * 4096) >>> 16) & 255}.${((i * 4096 + 255) >>> 8) & 255}.255,${i % 7 ? 'DE' : 'AT'}`);
for (let i = 0; i < 200000; i++) lines.push(`2a00:${(i >> 8).toString(16)}:${(i & 255).toString(16)}::,2a00:${(i >> 8).toString(16)}:${(i & 255).toString(16)}:ffff:ffff:ffff:ffff:ffff,${i % 5 ? 'DE' : 'FR'}`);
const f = fixture(lines.join('\n'));
lines.length = 0;
globalThis.gc?.();
const before = process.memoryUsage().heapUsed; const rss0 = process.memoryUsage().rss;
const t0 = Date.now();
const g = loadGeo(f);
const ms = Date.now() - t0;
globalThis.gc?.();
const grow = (process.memoryUsage().heapUsed - before) / 1048576;
console.log(`# geo 600k: ${ms} ms, Heap +${grow.toFixed(1)} MB, RSS +${((process.memoryUsage().rss - rss0) / 1048576).toFixed(0)} MB, Zeilen ${g.size}`);
assert.equal(g.size, 600000);
assert.equal(g.lookup('0.0.16.5'), 'DE');
assert.equal(g.lookup('2a00:0:1::5'), 'DE');
assert.ok(grow < 100, `Heap +${grow} MB`);
});
+160
View File
@@ -0,0 +1,160 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { openDb } from '../src/db.js';
import { createPublicServer, createAdminServer } from '../src/server.js';
import { signHeatToken, verifyHeatToken } from '../src/heat.js';
const KEY = 'abcdef0123456789abcd';
const OTHER = 'zzzzzzzz0123456789zz';
const SECRET = 'geheim';
const NOW = Date.UTC(2026, 9, 10, 10, 0, 0);
const exp = (s = 600) => Math.floor(NOW / 1000) + s;
async function setup() {
const db = openDb(':memory:');
const site = db.prepare('INSERT INTO sites (siteKey, projectId, domains, active) VALUES (?,?,?,1)');
site.run(KEY, 'p', JSON.stringify(['kunde.de', 'www.kunde.de']));
site.run(OTHER, 'q', JSON.stringify(['andere.de']));
const click = db.prepare('INSERT INTO clicks (site, ts, path, vp, x, y) VALUES (?,?,?,?,?,?)');
click.run(KEY, NOW - 1000, '/', 'd', 0.1, 0.2);
click.run(KEY, NOW - 2000, '/', 'd', 0.3, 0.4);
click.run(KEY, NOW - 1000, '/', 'm', 0.9, 0.9);
click.run(KEY, NOW - 1000, '/x', 'd', 0.8, 0.8);
click.run(OTHER, NOW - 1000, '/', 'd', 0.7, 0.7);
click.run(KEY, NOW - 31 * 86400000, '/', 'd', 0.5, 0.5);
const server = createPublicServer({ config: { heatSecret: SECRET }, db, now: () => NOW });
await new Promise((r) => server.listen(0, '127.0.0.1', r));
const base = `http://127.0.0.1:${server.address().port}`;
const get = (token, headers = {}) => fetch(`${base}/a/heat?token=${encodeURIComponent(token)}`, { headers });
return { db, get, base, close: () => server.close() };
}
const tok = (o = {}, secret = SECRET) => signHeatToken({ siteKey: KEY, path: '/', vp: 'd', exp: exp(), ...o }, secret);
test('gültiges Token: neueste Punkte der Seite/Viewport, 30 Tage, no-store, CORS nur eigene Domain', async () => {
const s = await setup();
const r = await s.get(tok(), { Origin: 'https://www.kunde.de' });
assert.equal(r.status, 200);
assert.equal(r.headers.get('cache-control'), 'no-store');
assert.equal(r.headers.get('access-control-allow-origin'), 'https://www.kunde.de');
assert.deepEqual(await r.json(), { points: [{ x: 0.1, y: 0.2 }, { x: 0.3, y: 0.4 }] });
const fremd = await s.get(tok(), { Origin: 'https://evil.example' });
assert.equal(fremd.status, 200);
assert.equal(fremd.headers.get('access-control-allow-origin'), null);
const http = await s.get(tok(), { Origin: 'http://kunde.de' });
assert.equal(http.headers.get('access-control-allow-origin'), null);
assert.equal(r.headers.get('access-control-allow-origin') === '*', false);
s.close();
});
test('maximal 5000 neueste Punkte', async () => {
const s = await setup();
s.db.exec('BEGIN');
const ins = s.db.prepare('INSERT INTO clicks (site, ts, path, vp, x, y) VALUES (?,?,?,?,?,?)');
for (let i = 0; i < 5100; i++) ins.run(KEY, NOW - 10000 + i, '/many', 'd', 0.5, i < 100 ? 0.01 : 0.99);
s.db.exec('COMMIT');
const j = await (await s.get(tok({ path: '/many' }))).json();
assert.equal(j.points.length, 5000);
assert.ok(j.points.every((p) => p.y === 0.99)); // die 100 ältesten fehlen
s.close();
});
test('403: abgelaufen, > 30 Min voraus, falsches Secret, manipuliert, Müll, fremde/unbekannte Website', async () => {
const s = await setup();
const bad = [
tok({ exp: exp(-1) }),
tok({ exp: exp(30 * 60 + 5) }),
tok({}, 'anderes-secret'),
'abc', '', 'a.b', 'x'.repeat(10) + '.' + 'f'.repeat(64),
tok({ siteKey: 'nichtvorhanden0123456' }),
];
// Manipulation: Payload tauschen, Signatur behalten
const [part, sig] = tok().split('.');
for (const patch of [{ p: '/x' }, { v: 'm' }, { s: OTHER }]) {
const o = { ...JSON.parse(Buffer.from(part, 'base64url')), ...patch };
bad.push(`${Buffer.from(JSON.stringify(o)).toString('base64url')}.${sig}`);
}
for (const t of bad) assert.equal((await s.get(t)).status, 403, t.slice(0, 30));
assert.equal((await fetch(`${s.base}/a/heat`)).status, 403);
// Token der anderen Website liefert nur deren Punkte (nie die von KEY)
const j = await (await s.get(tok({ siteKey: OTHER }))).json();
assert.deepEqual(j.points, [{ x: 0.7, y: 0.7 }]);
s.close();
});
test('Token mit exp genau 30 Min voraus ist gültig; Helfer verify', () => {
assert.ok(verifyHeatToken(tok({ exp: exp(1800) }), SECRET, NOW));
assert.equal(verifyHeatToken(tok({ exp: exp(1801) }), SECRET, NOW), null);
assert.equal(verifyHeatToken(tok(), '', NOW), null); // leeres Secret sperrt
assert.equal(verifyHeatToken(tok({ vp: 'x' }), SECRET, NOW), null);
assert.equal(verifyHeatToken(tok({ path: 'kein-slash' }), SECRET, NOW), null);
const t = tok();
assert.match(t, /^[A-Za-z0-9_-]+\.[0-9a-f]{64}$/);
assert.deepEqual(JSON.parse(Buffer.from(t.split('.')[0], 'base64url')), { s: KEY, p: '/', v: 'd', e: exp() });
});
test('/a/heat nur auf dem öffentlichen Listener', async () => {
const db = openDb(':memory:');
const server = createAdminServer({ config: { heatSecret: SECRET, adminToken: 't' }, db });
await new Promise((r) => server.listen(0, '127.0.0.1', r));
const r = await fetch(`http://127.0.0.1:${server.address().port}/a/heat?token=${tok()}`);
assert.equal(r.status, 404);
server.close();
});
test('403 bei ausgeschalteter Website (active = 0), mit CORS für die eigene Domain', async () => {
const s = await setup();
s.db.prepare('UPDATE sites SET active = 0 WHERE siteKey = ?').run(KEY);
const r = await s.get(tok(), { Origin: 'https://kunde.de' });
assert.equal(r.status, 403);
assert.equal(r.headers.get('access-control-allow-origin'), 'https://kunde.de');
s.close();
});
test('403 mit CORS-Kopf, wenn die Website bekannt ist (abgelaufen, aus); sonst ohne', async () => {
const s = await setup();
const origin = { Origin: 'https://www.kunde.de' };
const abgelaufen = await s.get(tok({ exp: exp(-1) }), origin);
assert.equal(abgelaufen.status, 403);
assert.equal(abgelaufen.headers.get('access-control-allow-origin'), 'https://www.kunde.de');
// fremder Origin, falsche Signatur, unbekannte Website: kein CORS-Kopf
assert.equal((await s.get(tok({ exp: exp(-1) }), { Origin: 'https://evil.example' })).headers.get('access-control-allow-origin'), null);
assert.equal((await s.get(tok({}, 'anderes-secret'), origin)).headers.get('access-control-allow-origin'), null);
assert.equal((await s.get(tok({ siteKey: 'nichtvorhanden0123456' }), origin)).headers.get('access-control-allow-origin'), null);
assert.equal((await s.get('abc', origin)).headers.get('access-control-allow-origin'), null);
s.close();
});
test('OPTIONS-Preflight: CORS-Köpfe nur für erlaubte Origins bekannter Websites', async () => {
const s = await setup();
const pre = (token, origin) => fetch(`${s.base}/a/heat?token=${encodeURIComponent(token)}`, { method: 'OPTIONS', headers: { Origin: origin, 'Access-Control-Request-Method': 'GET' } });
const ok = await pre(tok(), 'https://kunde.de');
assert.equal(ok.status, 204);
assert.equal(ok.headers.get('access-control-allow-origin'), 'https://kunde.de');
assert.match(ok.headers.get('access-control-allow-methods'), /GET/);
assert.equal((await pre(tok({ exp: exp(-1) }), 'https://kunde.de')).headers.get('access-control-allow-origin'), 'https://kunde.de');
assert.equal((await pre(tok(), 'https://evil.example')).headers.get('access-control-allow-origin'), null);
assert.equal((await pre('abc', 'https://kunde.de')).headers.get('access-control-allow-origin'), null);
s.close();
});
test('Heatmap: gleiche Stunde (ts-Gleichstand), Limit schneidet über die niedrigste id ab', async () => {
const s = await setup();
const ins = s.db.prepare('INSERT INTO clicks (site, ts, path, vp, x, y) VALUES (?,?,?,?,?,?)');
const ts = NOW - 3600000;
ins.run(KEY, ts, '/g', 'd', 0.99, 0.99); // niedrigste id
s.db.exec('BEGIN');
for (let i = 0; i < 5000; i++) ins.run(KEY, ts, '/g', 'd', 0.5, 0.5);
s.db.exec('COMMIT');
const j = await (await s.get(tok({ path: '/g' }))).json();
assert.equal(j.points.length, 5000);
assert.ok(!j.points.some((p) => p.x === 0.99));
s.close();
});
test('Heatmap: Token-Pfad mit Kennung findet die als :id gespeicherten Klicks', async () => {
const s = await setup();
s.db.prepare('INSERT INTO clicks (site, ts, path, vp, x, y) VALUES (?,?,?,?,?,?)').run(KEY, NOW - 1000, '/kunde/:id', 'd', 0.4, 0.6);
const j = await (await s.get(tok({ path: '/kunde/12345678' }))).json();
assert.deepEqual(j, { points: [{ x: 0.4, y: 0.6 }] });
s.close();
});
+129
View File
@@ -0,0 +1,129 @@
// Verhalten von dist/h.js (Heatmap-Overlay) im Fake-DOM: eigenes Fenster und Einbettung im Portal.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import vm from 'node:vm';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const H = readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'client', 'dist', 'h.js'), 'utf8');
function makeEl(tag) {
const e = {
tagName: String(tag).toUpperCase(), nodeType: 1, attrs: {}, children: [], listeners: {}, style: { setProperty() {} }, parentNode: null,
textContent: '', removed: false,
setAttribute(n, v) { this.attrs[n] = String(v); },
getAttribute(n) { return n in this.attrs ? this.attrs[n] : null; },
appendChild(c) { c.parentNode = this; this.children.push(c); return c; },
addEventListener(t, f, o) { (this.listeners[t] ||= []).push({ f, o }); },
removeEventListener(t, f) { this.listeners[t] = (this.listeners[t] || []).filter((x) => x.f !== f); },
remove() { this.removed = true; },
attachShadow() { const r = makeEl('shadow'); this.shadow = r; return r; },
closest(sel) {
const tags = sel.split(',').map((t) => t.trim().toUpperCase());
for (let n = this; n && n.tagName; n = n.parentNode) if (tags.includes(n.tagName)) return n;
return null;
},
getContext() {
return {
createLinearGradient: () => ({ addColorStop() {} }),
createRadialGradient: () => ({ addColorStop() {} }),
fillRect() {}, clearRect() {},
getImageData: (x, y, w, hh) => ({ data: new Uint8ClampedArray(Math.max(4, w * hh * 4)) }),
putImageData() {},
};
},
};
return e;
}
async function harness({ embed = false, framed = true, fail = false, points = [{ x: 0.5, y: 0.5 }, { x: 0.1, y: 0.2 }, { x: 2, y: 0 }] } = {}) {
const body = makeEl('body'); const head = makeEl('head'); const html = makeEl('html');
Object.assign(html, { scrollWidth: 402, scrollHeight: 2000, clientWidth: 402, clientHeight: 800 });
const script = makeEl('script');
script.setAttribute('src', 'https://project.webklar.com/a/h.js');
script.setAttribute('data-token', 'TOK.sig');
if (embed) script.setAttribute('data-embed', '1');
script.src = 'https://project.webklar.com/a/h.js';
const fetches = []; const posted = []; const winL = {};
const document = {
currentScript: script, documentElement: html, body, head,
createElement: (t) => makeEl(t),
};
const win = {
document,
addEventListener(t, f, o) { (winL[t] ||= []).push({ f, o }); },
removeEventListener(t, f) { winL[t] = (winL[t] || []).filter((x) => x.f !== f); },
};
const parent = framed ? { postMessage(data, origin) { posted.push({ data, origin }); } } : win;
win.parent = parent;
const sandbox = {
window: win, document, URL, JSON, Math, String, Number, Array, Object, RegExp, Error, Uint8ClampedArray, Promise,
setInterval: () => 1, clearInterval() {},
fetch: (u, o) => {
fetches.push({ u, o });
if (fail) return Promise.resolve({ ok: false, json: () => Promise.resolve({}) });
return Promise.resolve({ ok: true, json: () => Promise.resolve({ points }) });
},
};
sandbox.globalThis = sandbox;
vm.createContext(sandbox);
vm.runInContext(H, sandbox);
await new Promise((r) => setTimeout(r, 10));
const fire = (type, target) => {
const ev = { type, target, prevented: false, stopped: false, preventDefault() { this.prevented = true; }, stopPropagation() { this.stopped = true; } };
for (const { f } of winL[type] || []) f(ev);
return ev;
};
const shadowHosts = body.children.filter((c) => c.shadow);
return { body, head, fetches, posted, winL, fire, shadowHosts };
}
const linkIn = () => { const a = makeEl('a'); a.setAttribute('href', '/kontakt'); const span = makeEl('span'); span.parentNode = a; return span; };
test('h.js eigenes Fenster: schwarze Leiste mit Klickzahl, keine Nachricht, Links bleiben klickbar', async () => {
const h = await harness();
assert.equal(h.fetches.length, 1);
assert.equal(h.fetches[0].u, 'https://project.webklar.com/a/heat?token=TOK.sig');
assert.equal(h.fetches[0].o.credentials, 'omit');
assert.equal(h.shadowHosts.length, 1, 'Leiste');
const msg = h.shadowHosts[0].shadow.children.find((c) => c.className === 'bar').children[0];
assert.equal(msg.textContent, 'Heatmap – 2 Klicks (letzte 30 Tage)');
assert.deepEqual(h.posted, []);
assert.equal(h.fire('click', linkIn()).prevented, false);
});
test('h.js eingebettet: keine Leiste, meldet Klickzahl nur an project.webklar.com', async () => {
const h = await harness({ embed: true });
assert.equal(h.shadowHosts.length, 0, 'keine Leiste');
assert.ok(h.body.children.some((c) => c.tagName === 'CANVAS'), 'Overlay da');
assert.deepEqual(JSON.parse(JSON.stringify(h.posted)), [{ data: { wk: 'heat', state: 'ready', count: 2 }, origin: 'https://project.webklar.com' }]);
// Scrollleisten ausgeblendet (Handy/Tablet haben ueberlagernde Leisten)
assert.ok(h.head.children.some((c) => c.tagName === 'STYLE' && /scrollbar-width:none/.test(c.textContent)));
});
test('h.js eingebettet: Links und Formulare gesperrt, andere Klicks nicht', async () => {
const h = await harness({ embed: true });
assert.ok((h.winL.click || []).some((l) => l.o === true), 'Klick-Sperre in der Capture-Phase');
const ev = h.fire('click', linkIn());
assert.equal(ev.prevented, true);
assert.equal(ev.stopped, true, 'Router der Seite bekommt den Klick nicht');
const area = h.fire('click', makeEl('area'));
assert.equal(area.prevented, true);
const other = h.fire('click', makeEl('button'));
assert.equal(other.prevented, false);
const sub = h.fire('submit', makeEl('form'));
assert.equal(sub.prevented, true);
});
test('h.js eingebettet: Fehler beim Laden => state error', async () => {
const h = await harness({ embed: true, fail: true });
assert.deepEqual(JSON.parse(JSON.stringify(h.posted)), [{ data: { wk: 'heat', state: 'error', count: 0 }, origin: 'https://project.webklar.com' }]);
});
test('h.js mit data-embed, aber nicht in einem Rahmen: verhaelt sich wie das eigene Fenster', async () => {
const h = await harness({ embed: true, framed: false });
assert.equal(h.shadowHosts.length, 1);
assert.deepEqual(h.posted, []);
assert.equal(h.fire('click', linkIn()).prevented, false);
});
+451
View File
@@ -0,0 +1,451 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { openDb } from '../src/db.js';
import net from 'node:net';
import { createPublicServer, createAdminServer } from '../src/server.js';
import { classifyUa } from '../src/ua.js';
import { createSalt } from '../src/salt.js';
const KEY = 'abcdef0123456789abcd';
const HOST = 'abc.project.webklar.com';
const ORIGIN = `https://${HOST}`;
const IP = '203.0.113.77';
const UA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) Mobile/15E148 Safari/604.1 GEHEIMERUA';
async function setup({ now = () => Date.UTC(2026, 9, 1, 10, 30, 45), geo = { lookup: () => 'DE' } } = {}) {
const db = openDb(':memory:');
db.prepare('INSERT INTO sites (siteKey, projectId, domains, active) VALUES (?,?,?,1)')
.run(KEY, 'p1', JSON.stringify([HOST, 'www.kunde.de']));
const server = createPublicServer({ config: {}, db, geo, now });
const invalidate = () => server.invalidateSites();
await new Promise((r) => server.listen(0, '127.0.0.1', r));
const base = `http://127.0.0.1:${server.address().port}`;
const post = (ev, { origin = ORIGIN, ua = UA, ip = IP, left = '1.2.3.4', referer, raw, type = 'text/plain' } = {}) => {
const headers = { 'Content-Type': type, 'User-Agent': ua, 'X-Forwarded-For': `${left}, ${ip}` };
if (origin) headers.Origin = origin;
if (referer) headers.Referer = referer;
return fetch(base + '/a/e', { method: 'POST', headers, body: raw ?? JSON.stringify(ev) });
};
const rows = () => db.prepare('SELECT * FROM events_raw ORDER BY id').all();
return { db, post, rows, base, server, invalidate, close: () => server.close() };
}
const pv = (o = {}) => ({ t: 'pv', site: KEY, path: '/', vp: 'd', ...o });
test('classifyUa: Bots, Geräte, leer', () => {
assert.equal(classifyUa('').bot, true);
assert.equal(classifyUa('Googlebot/2.1').bot, true);
assert.equal(classifyUa('curl/8.0').bot, true);
assert.equal(classifyUa('Mozilla/5.0 (Windows NT 10.0) Chrome/120').device, 'd');
assert.equal(classifyUa('Mozilla/5.0 (iPhone; CPU) Mobile/15E').device, 'm');
assert.equal(classifyUa('Mozilla/5.0 (Linux; Android 13; Pixel) Mobile Safari').device, 'm');
assert.equal(classifyUa('Mozilla/5.0 (iPad; CPU OS 17)').device, 't');
assert.equal(classifyUa('Mozilla/5.0 (Linux; Android 13; SM-T500) Safari').device, 't');
assert.equal(classifyUa('Mozilla/5.0 (X11; Linux) Firefox/120').bot, false);
});
test('Salz: stabil am Tag, wechselt um Mitternacht Berlin', () => {
let t = Date.UTC(2026, 9, 1, 10, 0, 0);
let n = 0;
const s = createSalt({ now: () => t, random: () => Buffer.alloc(32, ++n) });
const a = s.visitorId('s', '1.1.1.1', 'ua');
assert.match(a, /^[0-9a-f]{16}$/);
t = Date.UTC(2026, 9, 1, 21, 59, 0); // 23:59 Berlin (MESZ)
assert.equal(s.visitorId('s', '1.1.1.1', 'ua'), a);
assert.notEqual(s.visitorId('s', '1.1.1.2', 'ua'), a);
t = Date.UTC(2026, 9, 1, 22, 0, 1); // 00:00 Berlin
assert.notEqual(s.visitorId('s', '1.1.1.1', 'ua'), a);
assert.match(s.rateKey('s', '1.1.1.1'), /^[0-9a-f]{64}$/);
assert.ok(!s.rateKey('s', '1.1.1.1').includes('1.1.1.1'));
});
test('pv: gespeichert ohne visitor, mit Land und Gerät, Cookie-frei', async () => {
const t = await setup();
const res = await t.post(pv({ path: '/preise?x=1#a', ref: 'https://Google.com/search?q=1' }));
assert.equal(res.status, 204);
assert.equal(res.headers.get('set-cookie'), null);
const [r] = t.rows();
assert.equal(r.type, 'pv');
assert.equal(r.site, KEY);
assert.equal(r.path, '/preise');
assert.equal(r.ref, 'google.com');
assert.equal(r.vp, 'd');
assert.equal(r.country, 'DE');
assert.equal(r.device, 'm');
assert.equal(r.visitor, null);
assert.equal(res.headers.get('access-control-allow-origin'), ORIGIN);
assert.equal(res.headers.get('access-control-allow-credentials'), null);
t.close();
});
test('pv: ref eigene Domain wird verworfen, ungültige Felder', async () => {
const t = await setup();
await t.post(pv({ ref: 'https://www.kunde.de/x' }));
await t.post(pv({ ref: 'kein host!' }));
assert.equal(t.rows()[0].ref, null);
assert.equal(t.rows()[1].ref, null);
await t.post(pv({ path: 'kein-slash' }));
await t.post(pv({ path: '/' + 'a'.repeat(300) }));
await t.post(pv({ path: '/a\u0001b' }));
await t.post(pv({ vp: 'x' }));
await t.post({ t: 'zz', site: KEY, path: '/', vp: 'd' });
await t.post({ site: KEY, path: '/', vp: 'd' });
await t.post(null, { raw: 'kein json' });
await t.post(null, { raw: '[1]' });
assert.equal(t.rows().length, 2);
t.close();
});
test('Antwort immer 204; 413 bei großem Body; 405 bei falscher Methode', async () => {
const t = await setup();
assert.equal((await t.post(null, { raw: 'kein json' })).status, 204);
assert.equal((await t.post(pv({ pad: 'x'.repeat(5000) }))).status, 413);
assert.equal(t.rows().length, 0);
assert.equal((await fetch(t.base + '/a/e')).status, 405);
assert.equal((await fetch(t.base + '/a/e', { method: 'PUT', body: '{}' })).status, 405);
t.close();
});
test('application/json wird ebenfalls angenommen; unbekannte Felder werden nicht gespeichert', async () => {
const t = await setup();
const res = await t.post(pv({ evil: 'GEHEIM', ip: '9.9.9.9' }), { type: 'application/json' });
assert.equal(res.status, 204);
assert.equal(t.rows().length, 1);
assert.ok(!JSON.stringify(t.db.prepare('SELECT * FROM events_raw').all()).includes('GEHEIM'));
t.close();
});
test('Origin: fremd, fehlend, Referer-Fallback, inaktiv, unbekannte Site', async () => {
const t = await setup();
assert.equal((await t.post(pv(), { origin: 'https://evil.example' })).headers.get('access-control-allow-origin'), null);
await t.post(pv(), { origin: null });
assert.equal(t.rows().length, 0);
await t.post(pv(), { origin: null, referer: `${ORIGIN}/seite` });
assert.equal(t.rows().length, 1);
await t.post(pv(), { origin: 'https://www.kunde.de' });
assert.equal(t.rows().length, 2);
await t.post(pv({ site: 'zzzzzzzzzzzzzzzzzzzz' }));
assert.equal(t.rows().length, 2);
t.db.prepare('UPDATE sites SET active = 0').run();
await t.post(pv());
assert.equal(t.rows().length, 2);
t.close();
});
test('Bot und leerer User-Agent: nichts gespeichert', async () => {
const t = await setup();
await t.post(pv(), { ua: 'Googlebot/2.1' });
await t.post(pv(), { ua: '' });
assert.equal(t.rows().length, 0);
t.close();
});
test('bn: nur site, ts (Stunde), type, kind', async () => {
const t = await setup();
await t.post({ t: 'bn', site: KEY, a: 'shown', path: '/x', vp: 'm', visitor: 'v' });
await t.post({ t: 'bn', site: KEY, a: 'accept' });
await t.post({ t: 'bn', site: KEY, a: 'foo' });
const rows = t.rows();
assert.equal(rows.length, 2);
for (const r of rows) {
assert.equal(r.type, 'bn');
for (const f of ['path', 'vp', 'country', 'device', 'visitor', 'ref', 'sec', 'scroll', 'host']) assert.equal(r[f], null, f);
assert.equal(r.ts, Date.UTC(2026, 9, 1, 10, 0, 0));
}
assert.deepEqual(rows.map((r) => r.kind), ['shown', 'accept']);
t.close();
});
test('en: sec/scroll begrenzt, mit visitor', async () => {
const t = await setup();
await t.post({ t: 'en', site: KEY, path: '/a?b', vp: 't', sec: 99999, scroll: 250 });
await t.post({ t: 'en', site: KEY, path: '/a', vp: 't', sec: 12.7, scroll: -5 });
await t.post({ t: 'en', site: KEY, path: '/a', vp: 't', sec: 'x', scroll: 1 });
const rows = t.rows();
assert.equal(rows.length, 2);
assert.match(rows[0].visitor, /^[0-9a-f]{16}$/);
assert.equal(rows[0].path, '/a');
assert.equal(rows[0].sec, 3600);
assert.equal(rows[0].scroll, 100);
assert.equal(rows[1].sec, 12);
assert.equal(rows[1].scroll, 0);
t.close();
});
test('ck: in clicks, geklemmt, ohne visitor', async () => {
const t = await setup();
await t.post({ t: 'ck', site: KEY, path: '/a#h', vp: 'd', x: 0.123456, y: 1.7, sel: 'div>a'.repeat(100) });
await t.post({ t: 'ck', site: KEY, path: '/a', vp: 'd', x: 'n', y: 0.5 });
assert.equal(t.rows().length, 0);
const c = t.db.prepare('SELECT * FROM clicks').all();
assert.equal(c.length, 1);
assert.equal(c[0].path, '/a');
assert.equal(c[0].x, 0.123);
assert.equal(c[0].y, 1);
assert.equal(c[0].sel.length, 200);
assert.deepEqual(Object.keys(c[0]).sort(), ['id', 'path', 'sel', 'site', 'ts', 'vp', 'x', 'y']);
t.close();
});
test('ct: kind und host', async () => {
const t = await setup();
await t.post({ t: 'ct', site: KEY, path: '/k', kind: 'tel', host: 'ignored.de' });
await t.post({ t: 'ct', site: KEY, path: '/k', kind: 'out', host: 'Instagram.COM' });
await t.post({ t: 'ct', site: KEY, path: '/k', kind: 'out' });
await t.post({ t: 'ct', site: KEY, path: '/k', kind: 'sms' });
const rows = t.rows();
assert.equal(rows.length, 2);
assert.equal(rows[0].kind, 'tel');
assert.equal(rows[0].host, null);
assert.equal(rows[1].host, 'instagram.com');
assert.match(rows[1].visitor, /^[0-9a-f]{16}$/);
t.close();
});
test('Mengenlimit: 121. Ereignis verworfen, neue Minute frei', async () => {
let now = Date.UTC(2026, 9, 1, 10, 30, 0);
const t = await setup({ now: () => now });
for (let i = 0; i < 125; i++) await t.post(pv());
assert.equal(t.rows().length, 120);
await t.post(pv(), { ip: '198.51.100.9' });
assert.equal(t.rows().length, 121);
now += 61000;
await t.post(pv());
assert.equal(t.rows().length, 122);
t.close();
});
test('Land: nur 2 Großbuchstaben, sonst leer', async () => {
const t = await setup({ geo: { lookup: () => 'xyz' } });
await t.post(pv());
assert.equal(t.rows()[0].country, '');
t.close();
const u = await setup({ geo: { lookup: () => 'at' } });
await u.post(pv());
assert.equal(u.rows()[0].country, 'AT');
u.close();
});
test('CORS-Preflight nur für erlaubte Origin', async () => {
const t = await setup();
const ok = await fetch(t.base + '/a/e', { method: 'OPTIONS', headers: { Origin: ORIGIN, 'Access-Control-Request-Method': 'POST' } });
assert.equal(ok.status, 204);
assert.equal(ok.headers.get('access-control-allow-origin'), ORIGIN);
assert.equal(ok.headers.get('access-control-allow-credentials'), null);
const bad = await fetch(t.base + '/a/e', { method: 'OPTIONS', headers: { Origin: 'https://evil.example' } });
assert.equal(bad.status, 204);
assert.equal(bad.headers.get('access-control-allow-origin'), null);
t.close();
});
test('Datenschutz: weder IP noch User-Agent irgendwo in der Datenbank', async () => {
const t = await setup();
await t.post(pv({ ref: 'https://google.com' }));
await t.post({ t: 'bn', site: KEY, a: 'accept' });
await t.post({ t: 'en', site: KEY, path: '/a', vp: 'd', sec: 5, scroll: 50 });
await t.post({ t: 'ck', site: KEY, path: '/a', vp: 'd', x: 0.5, y: 0.5, sel: 'a' });
await t.post({ t: 'ct', site: KEY, path: '/a', kind: 'mail' });
let dump = '';
for (const { name } of t.db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all()) {
dump += JSON.stringify(t.db.prepare(`SELECT * FROM ${name}`).all());
}
for (const needle of [IP, '1.2.3.4', 'GEHEIMERUA', 'Mozilla', 'iPhone']) assert.ok(!dump.includes(needle), needle);
t.close();
});
test('t: nur eigene Feldlisten, kein Prototyp-Zugriff, keine Logzeile', async () => {
const t = await setup();
const errs = [];
const orig = console.error;
console.error = (...a) => errs.push(a.join(' '));
try {
for (const tt of ['__proto__', 'constructor', 'toString', 'hasOwnProperty', 'valueOf']) {
assert.equal((await t.post(null, { raw: JSON.stringify({ t: tt, site: KEY, path: '/', vp: 'd' }) })).status, 204);
}
assert.equal((await t.post(null, { raw: `{"t":"__proto__","site":"${KEY}","path":"/"}` })).status, 204);
assert.equal((await t.post({ t: ['pv'], site: KEY, path: '/', vp: 'd' })).status, 204);
assert.equal((await t.post({ t: { toString: 1 }, site: KEY, path: '/', vp: 'd' })).status, 204);
} finally { console.error = orig; }
assert.equal(t.rows().length, 0);
assert.deepEqual(errs, []);
t.close();
});
test('Origin/Referer: nur https ohne Port zählt', async () => {
const t = await setup();
for (const origin of [`http://${HOST}`, `https://${HOST}:8080`, `https://evil-${HOST}`, `https://${HOST}.evil.com`, `ftp://${HOST}`]) {
const res = await t.post(pv(), { origin });
assert.equal(res.headers.get('access-control-allow-origin'), null, origin);
const pre = await fetch(t.base + '/a/e', { method: 'OPTIONS', headers: { Origin: origin } });
assert.equal(pre.headers.get('access-control-allow-origin'), null, origin);
}
await t.post(pv(), { origin: null, referer: `http://${HOST}/x` });
await t.post(pv(), { origin: null, referer: `https://${HOST}:8080/x` });
assert.equal(t.rows().length, 0);
await t.post(pv(), { origin: null, referer: `https://${HOST}/x` });
assert.equal(t.rows().length, 1);
t.close();
});
function rawRequest(port, head, body = '') {
return new Promise((resolve) => {
const s = net.connect(port, '127.0.0.1');
let buf = '';
s.on('connect', () => s.write(head + '\r\n\r\n' + body));
s.on('data', (d) => { buf += d; });
s.on('close', () => resolve(buf));
s.on('error', () => resolve(buf));
setTimeout(() => { s.destroy(); }, 1500);
});
}
test('Body: früh 413 bei Content-Length > 4096, Abbruch bei gestreamtem Überlauf', async () => {
const t = await setup();
const port = t.server.address().port;
const head = `POST /a/e HTTP/1.1\r\nHost: x\r\nOrigin: ${ORIGIN}\r\nContent-Type: text/plain`;
// Content-Length 1 MB angekündigt, aber nichts gesendet: Antwort kommt trotzdem sofort
const early = await rawRequest(port, `${head}\r\nContent-Length: 1048576`);
assert.match(early, /^HTTP\/1\.1 413/);
// chunked ohne Content-Length: über 4 KB ⇒ 413 und Verbindung wird beendet
const chunk = 'x'.repeat(3000);
const body = `${chunk.length.toString(16)}\r\n${chunk}\r\n`.repeat(3);
const streamed = await rawRequest(port, `${head}\r\nTransfer-Encoding: chunked`, body);
assert.match(streamed, /^HTTP\/1\.1 413/);
assert.equal(t.rows().length, 0);
t.close();
});
test('Preflight: Hostliste gecacht (30 s), invalidateSites() und Admin-Änderung wirken sofort', async () => {
let now = Date.UTC(2026, 9, 1, 10, 0, 0);
const t = await setup({ now: () => now });
const pre = async () => (await fetch(t.base + '/a/e', { method: 'OPTIONS', headers: { Origin: ORIGIN } })).headers.get('access-control-allow-origin');
assert.equal(await pre(), ORIGIN);
t.db.prepare('UPDATE sites SET active = 0').run();
assert.equal(await pre(), ORIGIN); // noch aus dem Cache
now += 31000;
assert.equal(await pre(), null); // Cache abgelaufen
t.db.prepare('UPDATE sites SET active = 1').run();
assert.equal(await pre(), null);
t.invalidate();
assert.equal(await pre(), ORIGIN);
// Admin-Listener meldet Änderungen an den öffentlichen Listener
const admin = createAdminServer({ config: { adminToken: 'geheim' }, db: t.db, onSitesChanged: () => t.invalidate() });
await new Promise((r) => admin.listen(0, '127.0.0.1', r));
const res = await fetch(`http://127.0.0.1:${admin.address().port}/a/admin/sites/${KEY}`, {
method: 'PUT', headers: { Authorization: 'Bearer geheim', 'Content-Type': 'application/json' },
body: JSON.stringify({ projectId: 'p1', domains: [HOST], active: false }),
});
assert.equal(res.status, 200);
assert.equal(await pre(), null);
admin.close();
t.close();
});
test('X-Forwarded-For: rechtester Eintrag zählt für das Mengenlimit', async () => {
const t = await setup();
for (let i = 0; i < 120; i++) await t.post(pv(), { left: `10.0.0.${i % 250}` }); // links variiert, rechts gleich
assert.equal(t.rows().length, 120);
await t.post(pv(), { left: '10.9.9.9' }); // gleicher Bucket ⇒ verworfen
assert.equal(t.rows().length, 120);
await t.post(pv(), { left: '1.2.3.4', ip: '198.51.100.9' }); // rechts anders ⇒ eigener Bucket
assert.equal(t.rows().length, 121);
t.close();
});
test('T2: pv ohne ref/vp wird gespeichert (vp NULL, ref NULL), Gerät und Land bleiben', async () => {
const t = await setup();
await t.post({ t: 'pv', site: KEY, path: '/' });
const r = t.rows();
assert.equal(r.length, 1);
assert.equal(r[0].type, 'pv');
assert.equal(r[0].vp, null);
assert.equal(r[0].ref, null);
assert.equal(r[0].device, 'm');
assert.equal(r[0].country, 'DE');
await t.post({ t: 'pv', site: KEY, path: '/', vp: 'x' });
assert.equal(t.rows().length, 1);
t.close();
});
test('T2: cx braucht gültiges vp, speichert ref, eigene Domain ergibt NULL', async () => {
const t = await setup();
await t.post({ t: 'cx', site: KEY, path: '/', ref: 'https://www.google.com/x' });
await t.post({ t: 'cx', site: KEY, path: '/', ref: 'google.com', vp: 'x' });
assert.equal(t.rows().length, 0);
await t.post({ t: 'cx', site: KEY, path: '/', ref: 'https://www.google.com/x', vp: 'd' });
await t.post({ t: 'cx', site: KEY, path: '/', ref: 'https://www.kunde.de/x', vp: 'm' });
const r = t.rows();
assert.equal(r.length, 2);
assert.equal(r[0].type, 'cx');
assert.equal(r[0].ref, 'www.google.com');
assert.equal(r[0].vp, 'd');
assert.equal(r[0].country, null);
assert.equal(r[0].device, null);
assert.equal(r[1].ref, null);
t.close();
});
test('T3: alle Zeitstempel auf die volle Stunde', async () => {
const t = await setup();
const hour = Date.UTC(2026, 9, 1, 10, 0, 0);
await t.post(pv());
await t.post({ t: 'cx', site: KEY, path: '/', vp: 'd' });
await t.post({ t: 'en', site: KEY, path: '/', vp: 'd', sec: 5, scroll: 10 });
await t.post({ t: 'ct', site: KEY, path: '/', kind: 'tel' });
await t.post({ t: 'ck', site: KEY, path: '/', vp: 'd', x: 0.5, y: 0.5 });
const r = t.rows();
assert.deepEqual(r.map((x) => x.type), ['pv', 'cx', 'en', 'ct']);
for (const x of r) assert.equal(x.ts, hour);
const c = t.db.prepare('SELECT ts FROM clicks').all();
assert.equal(c.length, 1);
assert.equal(c[0].ts, hour);
assert.equal(t.db.prepare('SELECT COUNT(*) n FROM events_raw WHERE ts % 3600000 != 0').get().n, 0);
t.close();
});
test('T14: kennungsartige Pfadsegmente werden zu :id, alle Typen', async () => {
const t = await setup();
const cases = [
['/kunde/123456789/rechnung', '/kunde/:id/rechnung'],
['/u/3f2b8c1e-5d4a-4e0b-9a7c-1b2c3d4e5f60', '/u/:id'],
['/mail/max%40example.com', '/mail/:id'],
['/mail/max@example.com', '/mail/:id'],
['/preise/2026', '/preise/2026'],
['/blog/12345', '/blog/12345'],
['/', '/'],
['/a/123456?x=1#h', '/a/:id'],
];
for (const [p] of cases) await t.post(pv({ path: p }));
assert.deepEqual(t.rows().map((r) => r.path), cases.map((c) => c[1]));
await t.post({ t: 'cx', site: KEY, path: '/kunde/987654321', vp: 'd' });
await t.post({ t: 'en', site: KEY, path: '/kunde/987654321', vp: 'd', sec: 1, scroll: 1 });
await t.post({ t: 'ct', site: KEY, path: '/kunde/987654321', kind: 'tel' });
await t.post({ t: 'ck', site: KEY, path: '/kunde/987654321', vp: 'd', x: 0.1, y: 0.1 });
const paths = t.rows().slice(cases.length).map((r) => r.path);
assert.deepEqual(paths, ['/kunde/:id', '/kunde/:id', '/kunde/:id']);
assert.equal(t.db.prepare('SELECT path FROM clicks').get().path, '/kunde/:id');
t.close();
});
test('T14: ungültige Prozent-Kodierung fällt auf den Rohwert zurück', async () => {
const t = await setup();
await t.post(pv({ path: '/a/%E0%A4%A/123456' }));
await t.post(pv({ path: '/a/%E0%A4%A/x' }));
await t.post(pv({ path: '/a/%E0%A4%A40/x' }));
assert.deepEqual(t.rows().map((r) => r.path), ['/a/%E0%A4%A/:id', '/a/%E0%A4%A/x', '/a/%E0%A4%A40/x']);
t.close();
});
test('T3: jede gespeicherte Zeile jedes Typs liegt auf der vollen Stunde (ungerade Uhrzeit)', async () => {
const t = await setup({ now: () => Date.UTC(2026, 9, 1, 10, 37, 12, 345) });
await t.post({ t: 'bn', site: KEY, a: 'shown' });
await t.post(pv());
await t.post({ t: 'cx', site: KEY, path: '/', vp: 'd' });
await t.post({ t: 'en', site: KEY, path: '/', vp: 'd', sec: 5, scroll: 10 });
await t.post({ t: 'ct', site: KEY, path: '/', kind: 'tel' });
await t.post({ t: 'ck', site: KEY, path: '/', vp: 'd', x: 0.5, y: 0.5 });
const r = t.rows();
assert.deepEqual([...new Set(r.map((x) => x.type))].sort(), ['bn', 'ct', 'cx', 'en', 'pv']);
for (const x of r) assert.equal(x.ts % 3600000, 0);
for (const x of t.db.prepare('SELECT ts FROM clicks').all()) assert.equal(x.ts % 3600000, 0);
t.close();
});
+92
View File
@@ -0,0 +1,92 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, readFileSync, statSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { openDb } from '../src/db.js';
import { runNightly, dailyCutoffDay, DAILY_MONTHS } from '../src/jobs.js';
const KEY = 'abcdef0123456789abcd';
const NOW = Date.UTC(2026, 9, 1, 10);
const D = 86400 * 1000;
test('dailyCutoffDay: 25 Kalendermonate zurück, Monatsende begrenzt', () => {
assert.equal(DAILY_MONTHS, 25);
assert.equal(dailyCutoffDay(Date.UTC(2026, 9, 1, 10)), '2024-09-01');
assert.equal(dailyCutoffDay(Date.UTC(2026, 2, 31, 10)), '2024-02-29');
});
function mk() {
const db = openDb(':memory:');
db.prepare('INSERT INTO sites (siteKey, projectId, domains, active) VALUES (?,?,?,1)').run(KEY, 'p', '["a.de"]');
return db;
}
test('Tageswerte älter als 25 Monate werden gelöscht, Grenztag bleibt', () => {
const db = mk();
for (const day of ['2024-08-31', '2024-09-01']) {
db.prepare("INSERT INTO daily (site, day, dim, key, count) VALUES (?,?, 'pv_total','',3)").run(KEY, day);
db.prepare('INSERT INTO agg_state (site, day) VALUES (?,?)').run(KEY, day);
}
const r = runNightly(db, { now: NOW });
assert.deepEqual(db.prepare('SELECT day FROM daily').all().map((x) => x.day), ['2024-09-01']);
assert.deepEqual(db.prepare('SELECT day FROM agg_state').all().map((x) => x.day), ['2024-09-01']);
assert.equal(r.dailyDeleted, 1);
});
test('ausgeschaltete Website ohne offSince bekommt offSince, zweiter Lauf ändert nichts', () => {
const db = mk();
db.prepare('UPDATE sites SET active = 0').run();
const r = runNightly(db, { now: NOW });
assert.equal(db.prepare('SELECT offSince FROM sites').get().offSince, new Date(NOW).toISOString());
assert.equal(r.orphansMarked, 1);
runNightly(db, { now: NOW + D });
assert.equal(db.prepare('SELECT offSince FROM sites').get().offSince, new Date(NOW).toISOString());
});
test('Website mit offSince > 60 Tage wird weiter komplett gelöscht', () => {
const db = mk();
db.prepare('UPDATE sites SET active = 0, offSince = ?').run(new Date(NOW - 61 * D).toISOString());
const r = runNightly(db, { now: NOW });
assert.equal(r.sitesDeleted, 1);
assert.equal(db.prepare('SELECT COUNT(*) n FROM sites').get().n, 0);
});
test('Zeilen ohne sites-Eintrag werden gelöscht, andere bleiben', () => {
const db = mk();
const GONE = 'gonegonegonegonegone';
for (const s of [KEY, GONE]) {
db.prepare("INSERT INTO events_raw (site, ts, type) VALUES (?,?, 'pv')").run(s, NOW - 1000);
db.prepare('INSERT INTO clicks (site, ts, path, vp, x, y) VALUES (?,?,?,?,?,?)').run(s, NOW - 1000, '/', 'd', 1, 1);
db.prepare("INSERT INTO daily (site, day, dim, key, count) VALUES (?, '2026-09-01','pv_total','',1)").run(s);
db.prepare("INSERT INTO agg_state (site, day) VALUES (?, '2026-09-01')").run(s);
}
const r = runNightly(db, { now: NOW });
for (const t of ['events_raw', 'clicks', 'daily', 'agg_state']) {
assert.deepEqual(db.prepare(`SELECT DISTINCT site FROM ${t}`).all().map((x) => x.site), [KEY], t);
}
assert.ok(r.orphanRowsDeleted >= 4);
});
test('secure_delete: gelöschter Pfad steht nicht mehr in der Datei', () => {
const dir = mkdtempSync(join(tmpdir(), 'sd-'));
const p = join(dir, 'a.db');
const db = openDb(p);
assert.equal(db.prepare('PRAGMA secure_delete').get().secure_delete, 1);
db.prepare("INSERT INTO events_raw (site, ts, type, path) VALUES ('x', 1, 'pv', 'GEHEIMER-PFAD-4711')").run();
db.exec('PRAGMA wal_checkpoint(TRUNCATE)');
db.exec("DELETE FROM events_raw WHERE path = 'GEHEIMER-PFAD-4711'");
db.exec('PRAGMA wal_checkpoint(TRUNCATE)');
assert.ok(!readFileSync(p).includes('GEHEIMER-PFAD-4711'));
db.close();
});
test('runNightly leert das WAL', () => {
const dir = mkdtempSync(join(tmpdir(), 'wal-'));
const p = join(dir, 'a.db');
const db = openDb(p);
db.prepare('INSERT INTO sites (siteKey, projectId, domains, active) VALUES (?,?,?,1)').run(KEY, 'p', '[]');
runNightly(db, { now: NOW });
assert.equal(statSync(p + '-wal').size, 0);
db.close();
});
+209
View File
@@ -0,0 +1,209 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { loadConfig } from '../src/config.js';
import { openDb } from '../src/db.js';
import { createPublicServer, createAdminServer } from '../src/server.js';
const KEY = 'abcdef0123456789abcd';
async function setup(adminToken = 'geheim', make = createAdminServer) {
const db = openDb(':memory:');
const server = make({ config: { adminToken }, db });
await new Promise((r) => server.listen(0, '127.0.0.1', r));
const base = `http://127.0.0.1:${server.address().port}`;
const call = async (method, path, body, token = 'geheim', raw) => {
const headers = {};
if (token !== null) headers.Authorization = `Bearer ${token}`;
if (body !== undefined || raw !== undefined) headers['Content-Type'] = 'application/json';
const res = await fetch(base + path, { method, headers, body: raw ?? (body === undefined ? undefined : JSON.stringify(body)) });
return { status: res.status, body: await res.json() };
};
return { db, call, close: () => server.close() };
}
const row = (db, k = KEY) => db.prepare('SELECT * FROM sites WHERE siteKey = ?').get(k);
const valid = { projectId: 'p1', domains: ['abc.project.webklar.com'], active: true };
test('loadConfig: Vorgaben und Umgebung', () => {
const c = loadConfig({});
assert.deepEqual(c, { port: 8090, adminPort: 8091, dataDir: '/srv/webklar-analytics', adminToken: '', heatSecret: '', geoFile: '', baseHost: 'project.webklar.com' });
const d = loadConfig({ ANALYTICS_PORT: '9000', ANALYTICS_ADMIN_TOKEN: 't', ANALYTICS_HEAT_SECRET: 'h' });
assert.equal(d.port, 9000);
assert.equal(d.adminToken, 't');
assert.equal(d.heatSecret, 'h');
});
test('openDb ist idempotent und legt Tabellen an', () => {
const db = openDb(':memory:');
const names = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map((r) => r.name);
for (const n of ['sites', 'events_raw', 'clicks', 'daily']) assert.ok(names.includes(n), n);
});
test('GET /a/health ohne Token', async () => {
const t = await setup();
const r = await t.call('GET', '/a/health', undefined, null);
assert.equal(r.status, 200);
assert.deepEqual(r.body, { ok: true });
t.close();
});
test('Admin: 401 ohne, mit falschem Token und bei leerem adminToken', async () => {
const t = await setup();
assert.equal((await t.call('PUT', `/a/admin/sites/${KEY}`, valid, null)).status, 401);
assert.equal((await t.call('PUT', `/a/admin/sites/${KEY}`, valid, 'falsch')).status, 401);
assert.equal((await t.call('DELETE', `/a/admin/sites/${KEY}`, undefined, 'falsch')).status, 401);
assert.equal(row(t.db), undefined);
t.close();
const e = await setup('');
assert.equal((await e.call('PUT', `/a/admin/sites/${KEY}`, valid, '')).status, 401);
assert.equal((await e.call('PUT', `/a/admin/sites/${KEY}`, valid, 'x')).status, 401);
e.close();
});
test('PUT legt an und ändert', async () => {
const t = await setup();
const r = await t.call('PUT', `/a/admin/sites/${KEY}`, valid);
assert.equal(r.status, 200);
assert.equal(r.body.ok, true);
let s = row(t.db);
assert.equal(s.projectId, 'p1');
assert.deepEqual(JSON.parse(s.domains), ['abc.project.webklar.com']);
assert.equal(s.active, 1);
assert.equal(s.offSince, null);
await t.call('PUT', `/a/admin/sites/${KEY}`, { projectId: 'p2', domains: ['a.de', 'b.de'], active: true });
s = row(t.db);
assert.equal(s.projectId, 'p2');
assert.deepEqual(JSON.parse(s.domains), ['a.de', 'b.de']);
assert.equal(t.db.prepare('SELECT COUNT(*) c FROM sites').get().c, 1);
t.close();
});
test('active=false setzt offSince einmalig, active=true löscht es', async () => {
const t = await setup();
await t.call('PUT', `/a/admin/sites/${KEY}`, valid);
await t.call('PUT', `/a/admin/sites/${KEY}`, { ...valid, active: false });
const s1 = row(t.db);
assert.equal(s1.active, 0);
assert.ok(!Number.isNaN(Date.parse(s1.offSince)));
assert.match(s1.offSince, /^\d{4}-\d\d-\d\dT/);
t.db.prepare('UPDATE sites SET offSince = ? WHERE siteKey = ?').run('2026-01-01T00:00:00.000Z', KEY);
await t.call('PUT', `/a/admin/sites/${KEY}`, { ...valid, active: false });
assert.equal(row(t.db).offSince, '2026-01-01T00:00:00.000Z');
await t.call('PUT', `/a/admin/sites/${KEY}`, valid);
assert.equal(row(t.db).offSince, null);
t.close();
});
test('PUT: ungültiger siteKey 400', async () => {
const t = await setup();
for (const k of ['kurz', 'ABCDEF0123456789ABCD', 'abcdef0123456789abc!', 'a'.repeat(41)]) {
assert.equal((await t.call('PUT', `/a/admin/sites/${k}`, valid)).status, 400, k);
}
assert.equal((await t.call('DELETE', '/a/admin/sites/kurz')).status, 400);
t.close();
});
test('PUT: Domains nur Hostnamen, max 5; projectId/active geprüft', async () => {
const t = await setup();
const bad = [
['a.de:8080'], ['a.de/pfad'], ['https://a.de'], ['A.de'], ['localhost'], [''], ['-a.de'], [1], 'a.de', [],
['a.de', 'b.de', 'c.de', 'd.de', 'e.de', 'f.de'],
];
for (const domains of bad) {
const r = await t.call('PUT', `/a/admin/sites/${KEY}`, { ...valid, domains });
assert.equal(r.status, 400, JSON.stringify(domains));
assert.equal(typeof r.body.error, 'string');
}
assert.equal((await t.call('PUT', `/a/admin/sites/${KEY}`, { ...valid, projectId: '' })).status, 400);
assert.equal((await t.call('PUT', `/a/admin/sites/${KEY}`, { ...valid, active: 'ja' })).status, 400);
assert.equal((await t.call('PUT', `/a/admin/sites/${KEY}`, undefined, 'geheim', '{kaputt')).status, 400);
assert.equal(row(t.db), undefined);
assert.equal((await t.call('PUT', `/a/admin/sites/${KEY}`, { ...valid, domains: ['a.de', 'b.de', 'c.de', 'd.de', 'e.de'] })).status, 200);
t.close();
});
test('PUT: zu großer Body 413', async () => {
const t = await setup();
const r = await t.call('PUT', `/a/admin/sites/${KEY}`, { ...valid, projectId: 'x'.repeat(70000) });
assert.equal(r.status, 413);
t.close();
});
test('DELETE entfernt Website, unbekannte Route 404', async () => {
const t = await setup();
await t.call('PUT', `/a/admin/sites/${KEY}`, valid);
assert.equal((await t.call('DELETE', `/a/admin/sites/${KEY}`)).status, 200);
assert.equal(row(t.db), undefined);
assert.equal((await t.call('GET', '/a/nichts', undefined, null)).status, 404);
t.close();
});
test('Admin-Route auf dem öffentlichen Listener: 404 auch mit richtigem Token', async () => {
const t = await setup('geheim', createPublicServer);
assert.equal((await t.call('PUT', `/a/admin/sites/${KEY}`, valid)).status, 404);
assert.equal((await t.call('DELETE', `/a/admin/sites/${KEY}`)).status, 404);
assert.equal(row(t.db), undefined);
assert.equal((await t.call('GET', '/a/health', undefined, null)).status, 200);
t.close();
});
test('Doppelte Domains: 400', async () => {
const t = await setup();
assert.equal((await t.call('PUT', `/a/admin/sites/${KEY}`, { ...valid, domains: ['a.de', 'a.de'] })).status, 400);
t.close();
});
test('Datei-DB: zweimal öffnen (idempotent) und WAL', () => {
const f = join(mkdtempSync(join(tmpdir(), 'ana-')), 'a.db');
const d1 = openDb(f);
d1.prepare("INSERT INTO sites VALUES ('k','p','[]',1,NULL)").run();
assert.equal(d1.prepare('PRAGMA journal_mode').get().journal_mode, 'wal');
d1.close();
const d2 = openDb(f);
assert.equal(d2.prepare('SELECT COUNT(*) c FROM sites').get().c, 1);
assert.equal(d2.prepare('PRAGMA journal_mode').get().journal_mode, 'wal');
d2.close();
});
test('POST …/off: 401 ohne Token, 404 unbekannt, setzt active=0 und offSince einmalig', async () => {
const t = await setup();
assert.equal((await t.call('POST', `/a/admin/sites/${KEY}/off`, undefined, null)).status, 401);
assert.equal((await t.call('POST', `/a/admin/sites/${KEY}/off`, undefined, 'falsch')).status, 401);
assert.equal((await t.call('POST', `/a/admin/sites/${KEY}/off`)).status, 404);
assert.equal((await t.call('POST', '/a/admin/sites/kurz/off')).status, 400);
await t.call('PUT', `/a/admin/sites/${KEY}`, valid);
assert.equal(row(t.db).active, 1);
const r = await t.call('POST', `/a/admin/sites/${KEY}/off`);
assert.equal(r.status, 200);
const s1 = row(t.db);
assert.equal(s1.active, 0);
assert.match(s1.offSince, /^\d{4}-\d\d-\d\dT/);
assert.deepEqual(JSON.parse(s1.domains), ['abc.project.webklar.com']);
t.db.prepare('UPDATE sites SET offSince = ? WHERE siteKey = ?').run('2026-01-01T00:00:00.000Z', KEY);
assert.equal((await t.call('POST', `/a/admin/sites/${KEY}/off`)).status, 200);
assert.equal(row(t.db).offSince, '2026-01-01T00:00:00.000Z');
t.close();
});
test('POST …/off: ruft onSitesChanged, danach verwirft /a/e Ereignisse und w.js ist leer', async () => {
const db = openDb(':memory:');
const pub = createPublicServer({ config: {}, db });
const adm = createAdminServer({ config: { adminToken: 't' }, db, onSitesChanged: () => pub.invalidateSites() });
await Promise.all([pub, adm].map((x) => new Promise((r) => x.listen(0, '127.0.0.1', r))));
const pb = `http://127.0.0.1:${pub.address().port}`;
const ab = `http://127.0.0.1:${adm.address().port}`;
await fetch(`${ab}/a/admin/sites/${KEY}`, { method: 'PUT', headers: { Authorization: 'Bearer t', 'Content-Type': 'application/json' },
body: JSON.stringify({ projectId: 'p', domains: ['abc.project.webklar.com'], active: true }) });
const ev = () => fetch(`${pb}/a/e`, { method: 'POST', headers: { 'Content-Type': 'text/plain', Origin: 'https://abc.project.webklar.com', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0) Chrome/120' },
body: JSON.stringify({ t: 'pv', site: KEY, path: '/', vp: 'd' }) });
await ev();
const before = db.prepare('SELECT COUNT(*) c FROM events_raw').get().c;
assert.equal(before, 1);
assert.equal((await fetch(`${ab}/a/admin/sites/${KEY}/off`, { method: 'POST', headers: { Authorization: 'Bearer t' } })).status, 200);
await ev();
assert.equal(db.prepare('SELECT COUNT(*) c FROM events_raw').get().c, before);
assert.equal((await (await fetch(`${pb}/a/w.js?s=${KEY}`)).text()).length < 200, true);
pub.close(); adm.close();
});
+393
View File
@@ -0,0 +1,393 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { openDb } from '../src/db.js';
import { runNightly, nextRunAt, dayOf, scheduleNightly } from '../src/jobs.js';
import { createAdminServer } from '../src/server.js';
const KEY = 'abcdef0123456789abcd';
const OTHER = 'zzzzzzzz0123456789zz';
const NOW = Date.UTC(2026, 9, 10, 10, 0, 0); // 10.10.2026 12:00 Berlin
const H = 3600 * 1000;
const D = 24 * H;
function mk() {
const db = openDb(':memory:');
db.prepare('INSERT INTO sites (siteKey, projectId, domains, active) VALUES (?,?,?,1)').run(KEY, 'p', '["a.de"]');
const ev = (o) => db.prepare(`INSERT INTO events_raw (site, ts, type, path, vp, ref, country, device, visitor, sec, scroll, kind, host)
VALUES (@site,@ts,@type,@path,@vp,@ref,@country,@device,@visitor,@sec,@scroll,@kind,@host)`)
.run({ site: KEY, path: null, vp: null, ref: null, country: null, device: null, visitor: null, sec: null, scroll: null, kind: null, host: null, ...o });
return { db, ev };
}
const daily = (db, site = KEY) => db.prepare('SELECT day, dim, key, count FROM daily WHERE site = ? ORDER BY day, dim, key').all(site);
const get = (db, day, dim, key = '') => db.prepare('SELECT count FROM daily WHERE site=? AND day=? AND dim=? AND key=?').get(KEY, day, dim, key)?.count;
test('Verdichtung: Dimensionen, nur Tage < heute, idempotent', () => {
const { db, ev } = mk();
const t = NOW - D; // 9.10.
ev({ ts: t, type: 'pv', path: '/', vp: 'd', ref: 'google.com', country: 'DE', device: 'd' });
ev({ ts: t + 1, type: 'pv', path: '/', vp: 'm', ref: null, country: 'DE', device: 'm' });
ev({ ts: t + 2, type: 'pv', path: '/x', vp: 'd', ref: 'google.com', country: 'AT', device: 'd' });
ev({ ts: t + 3, type: 'en', path: '/', vp: 'd', visitor: 'v1', sec: 10, scroll: 50 });
ev({ ts: t + 4, type: 'en', path: '/x', vp: 'd', visitor: 'v1', sec: 20, scroll: 70 });
ev({ ts: t + 5, type: 'ct', path: '/', visitor: 'v2', kind: 'tel' });
ev({ ts: t + 6, type: 'ct', path: '/', visitor: 'v2', kind: 'out', host: 'x.de' });
ev({ ts: t + 7, type: 'bn', kind: 'shown' });
ev({ ts: t + 8, type: 'bn', kind: 'accept' });
ev({ ts: NOW, type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' }); // heute: unberührt
runNightly(db, { now: NOW });
const d = '2026-10-09';
assert.equal(get(db, d, 'pv_total'), 3);
assert.equal(get(db, d, 'pv_path', '/'), 2);
assert.equal(get(db, d, 'pv_path', '/x'), 1);
assert.equal(get(db, d, 'pv_ref', 'google.com'), 2);
assert.equal(get(db, d, 'pv_ref', ''), 1);
assert.equal(get(db, d, 'pv_country', 'DE'), 2);
assert.equal(get(db, d, 'pv_device', 'm'), 1);
assert.equal(get(db, d, 'visitors'), 2); // v1, v2
assert.equal(get(db, d, 'en_count'), 2);
assert.equal(get(db, d, 'en_sec_sum'), 30);
assert.equal(get(db, d, 'scroll_sum'), 120);
assert.equal(get(db, d, 'ct_kind', 'tel'), 1);
assert.equal(get(db, d, 'ct_kind', 'out'), 1);
assert.equal(get(db, d, 'bn_shown'), 1);
assert.equal(get(db, d, 'bn_accept'), 1);
assert.equal(get(db, d, 'bn_reject'), undefined);
assert.equal(db.prepare("SELECT COUNT(*) c FROM daily WHERE day = '2026-10-10'").get().c, 0);
const before = daily(db);
runNightly(db, { now: NOW });
runNightly(db, { now: NOW + H });
assert.deepEqual(daily(db), before);
});
test('DST: 23:30 UTC im Sommer gehört zum nächsten Berlin-Tag', () => {
const { db, ev } = mk();
ev({ ts: Date.UTC(2026, 6, 14, 23, 30), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' }); // 15.7. 01:30 Berlin
ev({ ts: Date.UTC(2026, 6, 14, 21, 59), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' }); // 14.7. 23:59 Berlin
assert.equal(dayOf(Date.UTC(2026, 6, 14, 23, 30)), '2026-07-15');
runNightly(db, { now: Date.UTC(2026, 6, 16, 10) });
assert.equal(get(db, '2026-07-14', 'pv_total'), 1);
assert.equal(get(db, '2026-07-15', 'pv_total'), 1);
// Winter: 23:30 UTC = 00:30 Berlin am Folgetag
assert.equal(dayOf(Date.UTC(2026, 0, 14, 23, 30)), '2026-01-15');
});
test('Aufbewahrung: Rohdaten und Klicks > 30 Tage weg, vorher verdichtet', () => {
const { db, ev } = mk();
ev({ ts: NOW - 31 * D, type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
ev({ ts: NOW - 29 * D, type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
const ins = db.prepare('INSERT INTO clicks (site, ts, path, vp, x, y) VALUES (?,?,?,?,0.5,0.5)');
ins.run(KEY, NOW - 31 * D, '/', 'd'); ins.run(KEY, NOW - 29 * D, '/', 'd');
runNightly(db, { now: NOW });
assert.equal(db.prepare('SELECT COUNT(*) c FROM events_raw').get().c, 1);
assert.equal(db.prepare('SELECT COUNT(*) c FROM clicks').get().c, 1);
assert.equal(get(db, dayOf(NOW - 31 * D), 'pv_total'), 1); // in daily erhalten
});
test('Website mit offSince > 60 Tage wird komplett gelöscht', () => {
const { db, ev } = mk();
db.prepare('INSERT INTO sites (siteKey, projectId, domains, active, offSince) VALUES (?,?,?,0,?)')
.run(OTHER, 'q', '["b.de"]', new Date(NOW - 61 * D).toISOString());
db.prepare('INSERT INTO sites (siteKey, projectId, domains, active, offSince) VALUES (?,?,?,0,?)')
.run('young0123456789abcdef', 'r', '["c.de"]', new Date(NOW - 59 * D).toISOString());
ev({ site: OTHER, ts: NOW - D, type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
ev({ ts: NOW - D, type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
db.prepare('INSERT INTO clicks (site, ts, path, vp, x, y) VALUES (?,?,?,?,0.1,0.1)').run(OTHER, NOW - D, '/', 'd');
runNightly(db, { now: NOW });
for (const t of ['sites', 'events_raw', 'clicks', 'daily', 'agg_state']) {
const col = t === 'sites' ? 'siteKey' : 'site';
assert.equal(db.prepare(`SELECT COUNT(*) c FROM ${t} WHERE ${col} = ?`).get(OTHER).c, 0, t);
}
assert.equal(db.prepare('SELECT COUNT(*) c FROM sites').get().c, 2); // KEY + junge
assert.ok(get(db, dayOf(NOW - D), 'pv_total'));
});
test('nextRunAt: 02:30 Berlin (Sommer/Winter)', () => {
assert.equal(new Date(nextRunAt(Date.UTC(2026, 9, 1, 10))).toISOString(), '2026-10-02T00:30:00.000Z');
assert.equal(new Date(nextRunAt(Date.UTC(2026, 9, 1, 0, 0))).toISOString(), '2026-10-01T00:30:00.000Z');
assert.equal(new Date(nextRunAt(Date.UTC(2026, 0, 1, 12))).toISOString(), '2026-01-02T01:30:00.000Z');
});
async function admin(db, now = () => NOW) {
const server = createAdminServer({ config: { adminToken: 'tok' }, db, now });
await new Promise((r) => server.listen(0, '127.0.0.1', r));
const base = `http://127.0.0.1:${server.address().port}`;
const get = (p, auth = 'Bearer tok') => fetch(base + p, { headers: auth ? { Authorization: auth } : {} });
return { get, close: () => server.close() };
}
test('Stats-Endpunkt: abgeschlossene Tage bis gestern, heute getrennt in today, Vergangenheit aus daily, consentRate', async () => {
const { db, ev } = mk();
const y = NOW - D;
for (let i = 0; i < 3; i++) ev({ ts: y + i, type: 'pv', path: '/a', vp: 'd', ref: 'g.de', country: 'DE', device: 'd' });
ev({ ts: y + 10, type: 'en', path: '/a', vp: 'd', visitor: 'v1', sec: 5, scroll: 5 });
ev({ ts: y + 11, type: 'bn', kind: 'accept' });
ev({ ts: y + 12, type: 'ct', path: '/a', visitor: 'v1', kind: 'tel' });
runNightly(db, { now: NOW }); // gestern -> daily
ev({ ts: NOW - H, type: 'pv', path: '/b', vp: 'm', country: 'AT', device: 'm' }); // heute live
ev({ ts: NOW - H, type: 'ct', path: '/b', visitor: 'v9', kind: 'mail' });
ev({ ts: NOW - H, type: 'bn', kind: 'accept' });
ev({ ts: NOW - H, type: 'bn', kind: 'reject' });
ev({ ts: NOW - H, type: 'bn', kind: 'reject' });
ev({ ts: NOW - H, type: 'bn', kind: 'shown' });
const a = await admin(db);
const r = await a.get(`/a/admin/stats/${KEY}?range=7`);
assert.equal(r.status, 200);
const j = await r.json();
assert.deepEqual(Object.keys(j).sort(), ['banner', 'contacts', 'countries', 'devices', 'firstDay', 'from', 'pages', 'previous', 'range', 'refs', 'series', 'to', 'today', 'totals']);
// Der Zeitraum sind die 7 abgeschlossenen Tage bis gestern (3.–9.10.); heute (10.10.) steht nur in `today`
assert.equal(j.series.length, 7);
assert.equal(j.series[0].day, '2026-10-03');
assert.equal(j.series[6].day, '2026-10-09');
assert.deepEqual(j.series.map((s) => s.pv), [0, 0, 0, 0, 0, 0, 3]);
assert.deepEqual(j.totals, { pv: 3, visitors: 1, contacts: 1, consentRate: 1, engaged: 1, avgSec: 5, avgScroll: 5 });
assert.deepEqual(j.series[6], { day: '2026-10-09', pv: 3, visitors: 1, contacts: 1, avgSec: 5 });
assert.deepEqual(j.today, { day: '2026-10-10', pv: 1, visitors: 1, contacts: 1 });
assert.equal(j.from, '2026-10-03'); assert.equal(j.to, '2026-10-09'); assert.equal(j.range, 7);
assert.deepEqual(j.pages, [{ key: '/a', count: 3 }]);
assert.deepEqual(j.refs[0], { key: 'g.de', count: 3 });
assert.deepEqual(j.countries, [{ key: 'DE', count: 3 }]);
assert.deepEqual(j.devices, [{ key: 'd', count: 3 }]);
assert.deepEqual(j.contacts, [{ key: 'tel', count: 1 }]);
assert.deepEqual(j.banner, { shown: 0, accept: 1, reject: 0 });
// range 90: 90 Tage
const j90 = await (await a.get(`/a/admin/stats/${KEY}?range=90`)).json();
assert.equal(j90.series.length, 90);
assert.equal(j90.series[89].day, '2026-10-09');
assert.equal(j90.totals.pv, 3);
assert.deepEqual(j90.today, j.today);
a.close();
});
test('Stats: today = heutiger Berliner Tag bis jetzt, auch ohne Ereignisse; Mitternacht in Berlin trennt gestern und heute', async () => {
const { db, ev } = mk();
const a = await admin(db);
assert.deepEqual((await (await a.get(`/a/admin/stats/${KEY}?range=7`)).json()).today, { day: '2026-10-10', pv: 0, visitors: 0, contacts: 0 });
a.close();
// 11.10. 00:10 Berlin (= 10.10. 22:10 UTC, Sommerzeit): 23:00 Berlin am 10.10. gehört zu gestern, 00:05 Berlin am 11.10. zu heute
const LATE = Date.UTC(2026, 9, 10, 22, 10);
ev({ ts: Date.UTC(2026, 9, 10, 21, 0), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
ev({ ts: Date.UTC(2026, 9, 10, 22, 5), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
ev({ ts: Date.UTC(2026, 9, 10, 22, 6), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
ev({ ts: Date.UTC(2026, 9, 10, 22, 7), type: 'ct', path: '/', visitor: 'v1', kind: 'wa' });
ev({ ts: Date.UTC(2026, 9, 10, 23, 0), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' }); // noch in der Zukunft: zählt nicht
const b = await admin(db, () => LATE);
const j = await (await b.get(`/a/admin/stats/${KEY}?range=7`)).json();
assert.equal(j.to, '2026-10-10');
assert.equal(j.series[6].day, '2026-10-10'); assert.equal(j.series[6].pv, 1);
assert.equal(j.totals.pv, 1);
assert.deepEqual(j.today, { day: '2026-10-11', pv: 2, visitors: 1, contacts: 1 });
assert.equal(j.contacts.length, 0); // die Kontakt-Aktion von 00:07 ist heute
b.close();
});
test('Stats: gestern noch nicht verdichtet wird live gezählt, nie doppelt', async () => {
const { db, ev } = mk();
ev({ ts: NOW - D, type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
ev({ ts: NOW - H, type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' }); // heute: nur in today
const a = await admin(db);
const stats = async () => (await (await a.get(`/a/admin/stats/${KEY}?range=7`)).json());
assert.equal((await stats()).totals.pv, 1);
runNightly(db, { now: NOW });
const j = await stats();
assert.equal(j.totals.pv, 1);
assert.equal(j.today.pv, 1);
a.close();
});
test('Stats: Vorzeitraum = die range Kalendertage vor dem aktuellen Zeitraum, complete nach firstDay', async () => {
const { db, ev } = mk();
// aktueller Zeitraum zu range 7 am 10.10.: 03.–09.10.; Vorzeitraum: 26.09.–02.10.
ev({ ts: Date.UTC(2026, 8, 30, 10), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
ev({ ts: Date.UTC(2026, 9, 1, 10), type: 'en', path: '/', vp: 'd', visitor: 'v1', sec: 20, scroll: 40 });
ev({ ts: Date.UTC(2026, 9, 2, 10), type: 'ct', path: '/', visitor: 'v2', kind: 'tel' });
const a = await admin(db);
const get = async () => (await (await a.get(`/a/admin/stats/${KEY}?range=7`)).json());
let j = await get();
assert.equal(j.firstDay, '2026-09-30');
assert.equal(j.previous.complete, false);
// 27.09. liegt noch einen Tag nach dem Beginn des Vorzeitraums (26.09.): weiter unvollständig
ev({ ts: Date.UTC(2026, 8, 27, 10), type: 'pv', path: '/x', vp: 'd', country: 'DE', device: 'd' });
j = await get();
assert.equal(j.firstDay, '2026-09-27');
assert.equal(j.previous.complete, false);
// genau am ersten Tag des Vorzeitraums: vollständig
ev({ ts: Date.UTC(2026, 8, 26, 10), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
j = await get();
assert.equal(j.firstDay, '2026-09-26');
assert.equal(j.previous.complete, true);
// 25.09. liegt vor dem Vorzeitraum und darf nicht mitzählen
ev({ ts: Date.UTC(2026, 8, 25, 10), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
j = await get();
assert.equal(j.firstDay, '2026-09-25');
assert.equal(j.previous.complete, true);
assert.equal(j.previous.from, '2026-09-26'); assert.equal(j.previous.to, '2026-10-02');
assert.equal(j.previous.series.length, 7); assert.equal(j.previous.series[4].day, '2026-09-30');
assert.equal(j.previous.totals.pv, 3); // 26.09., 27.09., 30.09.
assert.equal(j.previous.totals.engaged, 1);
assert.equal(j.previous.totals.avgSec, 20);
assert.equal(j.previous.totals.contacts, 1);
assert.deepEqual(j.previous.series[5], { day: '2026-10-01', pv: 0, visitors: 1, contacts: 0, avgSec: 20 });
assert.deepEqual(j.previous.series[6], { day: '2026-10-02', pv: 0, visitors: 1, contacts: 1, avgSec: null });
assert.deepEqual(j.previous.contacts, [{ key: 'tel', count: 1 }]);
assert.equal(j.totals.pv, 0); // aktueller Zeitraum leer
assert.equal(j.totals.avgSec, null);
a.close();
});
test('Stats: firstDay aus daily und Rohdaten, leer ohne Daten', async () => {
const { db, ev } = mk();
const a = await admin(db);
const first = async () => (await (await a.get(`/a/admin/stats/${KEY}?range=7`)).json());
let j = await first();
assert.equal(j.firstDay, '');
assert.equal(j.previous.complete, false);
// nur Rohdaten (heute)
ev({ ts: NOW - H, type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
assert.equal((await first()).firstDay, '2026-10-10');
// 40 Tage alt: nach dem Nachtlauf nur noch in daily (Rohdaten > 30 Tage gelöscht)
ev({ ts: NOW - 40 * D, type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
assert.equal((await first()).firstDay, '2026-08-31');
runNightly(db, { now: NOW });
assert.equal(db.prepare('SELECT COUNT(*) c FROM events_raw WHERE ts < ?').get(NOW - 31 * D).c, 0);
j = await first();
assert.equal(j.firstDay, '2026-08-31');
assert.equal(j.previous.complete, true);
a.close();
});
test('Stats: avgSec/avgScroll gerundet, null ohne en; Tageswert avgSec', async () => {
const { db, ev } = mk();
const t = NOW - D; // 9.10.
ev({ ts: t, type: 'en', path: '/', vp: 'd', visitor: 'v1', sec: 30, scroll: 50 });
ev({ ts: t + 1, type: 'en', path: '/a', vp: 'd', visitor: 'v2', sec: 91, scroll: 100 });
const a = await admin(db);
const j = await (await a.get(`/a/admin/stats/${KEY}?range=7`)).json();
assert.equal(j.totals.engaged, 2);
assert.equal(j.totals.avgSec, 61); // 60,5 gerundet
assert.equal(j.totals.avgScroll, 75);
assert.equal(j.series[6].avgSec, 61); // 9.10. ist der letzte abgeschlossene Tag
assert.equal(j.series[5].avgSec, null);
// ohne en: beide null, engaged 0
const leer = await (await a.get(`/a/admin/stats/${KEY}?range=30`)).json();
assert.equal(leer.previous.totals.engaged, 0);
assert.equal(leer.previous.totals.avgSec, null);
assert.equal(leer.previous.totals.avgScroll, null);
a.close();
});
test('Stats: Vorzeitraum über die Zeitumstellung lückenlos und ohne Überlappung', async () => {
// range 30 um 23:30 Berlin: Zeitumstellung am 25.10. liegt im Zeitraum. Spätabends wäre now - 30 * 24 h
// schon der 12.10. (00:30 Berlin) und ein naives Fenster würde sich überlappen; mittags fiele das nicht auf.
const NOV = Date.UTC(2026, 10, 10, 22, 30);
const { db, ev } = mk();
ev({ ts: Date.UTC(2026, 9, 10, 10), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' }); // 10.10. mittags: Vorzeitraum
ev({ ts: Date.UTC(2026, 9, 11, 10), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' }); // 11.10. mittags: aktueller Zeitraum
ev({ ts: Date.UTC(2026, 10, 10, 12), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' }); // 10.11. mittags: heute
const a = await admin(db, () => NOV);
const j = await (await a.get(`/a/admin/stats/${KEY}?range=30`)).json();
assert.equal(j.series[0].day, '2026-10-11');
assert.equal(j.series[29].day, '2026-11-09');
assert.equal(j.previous.series[29].day, '2026-10-10');
assert.equal(j.previous.series[0].day, '2026-09-11');
assert.equal(j.from, '2026-10-11'); assert.equal(j.to, '2026-11-09');
assert.equal(j.previous.from, '2026-09-11'); assert.equal(j.previous.to, '2026-10-10');
const all = [...j.series, ...j.previous.series].map((s) => s.day);
assert.equal(all.length, 60);
assert.equal(new Set(all).size, 60);
assert.equal(j.previous.totals.pv, 1); assert.equal(j.previous.series[29].pv, 1);
assert.equal(j.totals.pv, 1); assert.equal(j.series[0].pv, 1);
assert.equal(j.today.day, '2026-11-10'); assert.equal(j.today.pv, 1);
a.close();
});
test('Stats: Vorzeitraum aus daily und live, nie doppelt', async () => {
const { db, ev } = mk();
ev({ ts: Date.UTC(2026, 9, 1, 10), type: 'pv', path: '/', vp: 'd', country: 'DE', device: 'd' });
ev({ ts: Date.UTC(2026, 9, 1, 11), type: 'ct', path: '/', visitor: 'v1', kind: 'mail' });
const a = await admin(db);
const get = async () => (await (await a.get(`/a/admin/stats/${KEY}?range=7`)).json()).previous;
const live = await get(); // noch nicht verdichtet: live aus den Rohdaten
assert.equal(live.totals.pv, 1);
runNightly(db, { now: NOW }); // jetzt aus daily
const verdichtet = await get();
assert.equal(verdichtet.totals.pv, 1);
assert.deepEqual(verdichtet, live);
runNightly(db, { now: NOW + H });
assert.deepEqual(await get(), live);
a.close();
});
test('Stats: Top 20 absteigend, consentRate 0 ohne Entscheidungen, Fehlerfälle', async () => {
const { db, ev } = mk();
for (let p = 0; p < 25; p++) for (let i = 0; i <= p; i++) ev({ ts: NOW - D - 1000, type: 'pv', path: `/p${p}`, vp: 'd', country: 'DE', device: 'd' });
const a = await admin(db);
const j = await (await a.get(`/a/admin/stats/${KEY}?range=30`)).json();
assert.equal(j.pages.length, 20);
assert.equal(j.pages[0].key, '/p24');
assert.ok(j.pages.every((e, i, arr) => !i || arr[i - 1].count >= e.count));
assert.equal(j.totals.consentRate, 0);
assert.equal((await a.get(`/a/admin/stats/${KEY}?range=5`)).status, 400);
assert.equal((await a.get(`/a/admin/stats/${KEY}?range=abc`)).status, 400);
assert.equal((await a.get(`/a/admin/stats/${OTHER}?range=7`)).status, 404);
assert.equal((await a.get(`/a/admin/stats/${KEY}?range=7`, null)).status, 401);
assert.equal((await a.get(`/a/admin/stats/${KEY}?range=7`, 'Bearer nope')).status, 401);
a.close();
});
test('scheduleNightly: früh feuernder Timer läuft nicht vorzeitig und nie doppelt für dasselbe 02:30', () => {
let t = Date.UTC(2026, 9, 10, 10, 0, 0);
const pending = [];
const timers = { setTimeout: (fn, ms) => { const h = { fn, at: t + ms }; pending.push(h); return h; }, clearTimeout: (h) => { const i = pending.indexOf(h); if (i >= 0) pending.splice(i, 1); } };
const runs = [];
const quiet = { log() {}, error() {} };
const s = scheduleNightly({}, { startDelayMs: 1000, log: quiet, now: () => t, timers, job: (db, o) => { runs.push(o.now); return { aggregated: 0, sitesDeleted: 0 }; } });
const fire = (clock) => { const h = pending.shift(); t = clock ?? h.at; h.fn(); };
fire(); // Startlauf
assert.equal(runs.length, 1);
const target = nextRunAt(t);
assert.equal(pending.length, 1);
assert.equal(pending[0].at, target);
fire(target - 500); // Timer feuert 500 ms zu früh
assert.equal(runs.length, 1, 'kein vorzeitiger Lauf');
assert.equal(pending.length, 1);
assert.equal(pending[0].at, target, 'neu auf das Ziel gesetzt');
fire(target); // pünktlich
assert.equal(runs.length, 2);
assert.equal(pending[0].at, nextRunAt(target + 1000));
assert.ok(pending[0].at > target + 20 * 3600000, 'nächster Lauf am Folgetag');
fire(pending[0].at - 1); // wieder zu früh
assert.equal(runs.length, 2);
s.stop();
assert.equal(pending.length, 0);
});
test('T2: pv ohne Einwilligung + cx: pv_total 1, pv_ref nur {google.com:1}; cx zählt nie pv_total', () => {
const { db, ev } = mk();
const t = NOW - D;
ev({ ts: t, type: 'pv', path: '/', vp: null, ref: null, country: 'DE', device: 'd' });
ev({ ts: t, type: 'cx', vp: 'd', ref: 'google.com' });
ev({ ts: t, type: 'cx', vp: 'd', ref: null });
ev({ ts: t, type: 'pv', path: '/', vp: 'd', ref: null, country: 'DE', device: 'd' });
runNightly(db, { now: NOW });
const d = '2026-10-09';
assert.equal(get(db, d, 'pv_total'), 2);
assert.equal(get(db, d, 'pv_ref', 'google.com'), 1);
assert.equal(get(db, d, 'pv_ref', ''), 2); // pv mit vp und cx ohne ref
const refs = db.prepare("SELECT key FROM daily WHERE site=? AND dim='pv_ref' ORDER BY key").all(KEY).map((r) => r.key);
assert.deepEqual(refs, ['', 'google.com']);
});
test('T2: Stats-Endpunkt refs live und aus daily, nie doppelt', async () => {
const { db, ev } = mk();
ev({ ts: NOW - D, type: 'pv', path: '/', vp: null, country: 'DE', device: 'd' });
ev({ ts: NOW - D, type: 'cx', vp: 'd', ref: 'google.com' });
const a = await admin(db);
const live = await (await a.get(`/a/admin/stats/${KEY}?range=7`)).json();
assert.equal(live.totals.pv, 1);
assert.deepEqual(live.refs, [{ key: 'google.com', count: 1 }]);
runNightly(db, { now: NOW });
const agg = await (await a.get(`/a/admin/stats/${KEY}?range=7`)).json();
assert.equal(agg.totals.pv, 1);
assert.deepEqual(agg.refs, [{ key: 'google.com', count: 1 }]);
a.close();
});
Binary file not shown.
@@ -0,0 +1 @@
21d406e6836a7d0c1afdb99dc259e4174fb3fe499afcbdac30c8a45a1aa8d605
Binary file not shown.
+63
View File
@@ -0,0 +1,63 @@
# News-Pinnwand auf der Kundenwebsite einbauen
Anleitung für das WEBklar-Team. Die Pinnwand zeigt die Beiträge, die der Kunde im Portal (Tab „Funktionen“) schreibt. Den `siteKey` findest Du im Portal-Admin beim Projekt (16 Zeichen, a–z und 2–7).
## 1. Die Stelle auf der Seite
```html
<div data-webklar-news="SITEKEY" data-api="https://project.webklar.com">
<!-- statischer Leerzustand, siehe unten -->
</div>
```
- `data-webklar-news`: der siteKey des Projekts (Pflicht).
- `data-api`: Herkunft des Portals. Ohne Angabe nimmt das Skript die Herkunft aus der Adresse des Skripts. Das klappt nur, wenn es als normales `<script src>` geladen wird. Bei Modul-Import, Tag-Manager oder nachgeladenem Skript ist `data-api` Pflicht.
- `data-signature` (optional): Unterschrift unter jedem Zettel, z. B. `data-signature="Opa Martin"`. Ohne Attribut gibt es keine Unterschrift.
## 2. Das Skript (einmal pro Seite, vor `</body>`)
```html
<script src="https://project.webklar.com/embed/news.js" defer></script>
```
Keine Abhängigkeiten, keine Cookies, kein Browser-Speicher. Kundentexte werden nur als Text eingesetzt, nie als HTML. Das Skript wird 5 Minuten gecacht und darf von jeder Domain geladen werden.
## 3. Statischer Leerzustand
Alles, was im `div` steht, gehört der Kundenwebsite. Das Skript ersetzt es nur, wenn die API mindestens einen Beitrag liefert. Bei 0 Beiträgen, Fehler, ausgeschaltetem JavaScript oder unbekanntem Schlüssel bleibt es stehen. Beispiel:
```html
<div data-webklar-news="SITEKEY" data-api="https://project.webklar.com">
<h3>Hier hängt bald was Neues.</h3>
<p>Neuigkeiten findest Du bald an dieser Pinnwand.</p>
</div>
```
Gestalte den Leerzustand im Stil der Kundenseite. Das Skript fasst ihn nicht an.
## 4. Aussehen anpassen (CSS-Variablen)
Setze sie auf dem `div` oder in der Seiten-CSS, z. B. `[data-webklar-news]{ --wkn-frame:#2f5d50; }`.
| Variable | Standard |
| --- | --- |
| `--wkn-board` (Korkfläche) | `#C08A52` |
| `--wkn-frame` (Holzrahmen) | `#A8733F` |
| `--wkn-font-display` (Überschriften) | `"Fredoka", system-ui, sans-serif` |
| `--wkn-font-body` (Text) | `"Nunito", system-ui, sans-serif` |
| `--wkn-paper-sun` | `#FFF6D6` |
| `--wkn-paper-sky` | `#DDEEFB` |
| `--wkn-paper-coral` | `#FFE1DA` |
| `--wkn-paper-white` | `#fff` |
Die Schriften lädt das Skript nicht selbst. Sind sie auf der Kundenseite nicht eingebunden, greift die Systemschrift. Setze dann die Variablen auf die Schriften der Kundenseite.
Das Layout richtet sich nach der Breite des `div` (Container Query, braucht aktuelle Browser), nicht nach dem Bildschirm: ab 701 px Breite ein Korkbrett mit drei Spalten, darunter eine Wischreihe mit Punkten. Sechs Beiträge pro Seite, „Ältere Neuigkeiten ansehen“ lädt die nächsten nach. Alle Klassen beginnen mit `wkn-`, die Styles stehen einmalig in `<style id="wkn-style">`.
## 5. Datenschutzerklärung des Kunden
Das Skript lädt Beiträge und Bilder von `project.webklar.com`. Dabei sieht der Server die IP-Adresse des Besuchers. Die Datenschutzerklärung des Kunden braucht deshalb den Absatz aus `docs/funktionen/website-datenschutz-absatz.md`. Bau die Pinnwand erst ein, wenn der Absatz dort steht.
## 6. Testen
`/embed/news-demo.html` auf dem Portal ist eine nicht verlinkte Testseite. Automatische Prüfung: `node test/ui/embed.ui.mjs`.
@@ -0,0 +1,55 @@
# Mustertext: Datenschutzerklärung der Kundenwebsite (Webshop)
**Muster – keine Rechtsberatung.** Der Absatz gehört in die Datenschutzerklärung jeder Kundenwebsite, auf der der Webshop läuft. Der Händler ist Verantwortlicher und verantwortet seine Datenschutzerklärung selbst; WEBklar liefert nur diesen Baustein (Leistungsbeschreibung Webshop Nr. 5 Abs. 10 und Nr. 6). Bau den Shop erst ein, wenn der Absatz in der Datenschutzerklärung steht.
Hinweise für das Team:
- Liegt die Datenschutzerklärung in der Website, die WEBklar betreut, fügen wir den Absatz beim Einbau ein (gehört zur Einrichtung). Liegt sie woanders, bekommt der Händler den Text im Chat und fügt ihn selbst ein.
- Platzhalter in eckigen Klammern durch die Angaben des Händlers ersetzen. Nicht Zutreffendes streichen: Klarna nur, wenn der Händler Klarna in den Zahlungsarten eingeschaltet hat; den Satz zum Versanddienstleister nur, wenn er versendet; die Zeile zur Abholung nur bei Abholung.
- Anrede und Ton an die übrige Erklärung der Website anpassen (meist Sie). Die Nummerierung der Überschrift der Website beachten.
- Wenn der Händler im Impressum oder in der Erklärung einen Datenschutzbeauftragten nennt, dort dazuschreiben.
- Die Löschfristen entsprechen dem AVV Webshop Nr. 9 und Abschnitt 6d der Datenschutzerklärung von WEBklar (project.webklar.com/datenschutz.html). Ändert sich dort etwas, hier mitändern. Die Frist für Server-Protokolle (14 Tage) steht in Abschnitt 3 dieser Erklärung.
- Das Widget setzt keine Cookies. Der Warenkorb liegt in der `sessionStorage` des Tabs, das ist Absatz „Warenkorb im Browser“.
- Im sichtbaren Text der Kundenwebsite die Wünsche des Händlers beachten (etwa keine Gedankenstriche).
---
## [Nummer]. Bestellungen in unserem Webshop
### Verantwortlicher und technischer Betrieb
Verantwortlich für die Verarbeitung Ihrer Daten bei einer Bestellung ist [Name des Händlers, Anschrift, E-Mail-Adresse], also der Verkäufer. Den Webshop betreibt technisch die WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, in unserem Auftrag. Sie verarbeitet Ihre Daten nur nach unseren Weisungen und nicht für eigene Zwecke (Auftragsverarbeitung nach Art. 28 DSGVO). Der Server steht bei der Hetzner Online GmbH in Falkenstein (Deutschland); der Versand der E-Mails zu Ihrer Bestellung läuft über den E-Mail-Dienst der Hetzner Online GmbH in Nürnberg (Deutschland).
### Welche Daten wir bei einer Bestellung verarbeiten
Wenn Sie in unserem Webshop bestellen, verarbeiten wir die Artikel in Ihrem Warenkorb und Ihre Bestellung (Artikel, Preise, Versandart, Gesamtbetrag, Bestellnummer, Zeitpunkt), Ihren Namen, Ihre E-Mail-Adresse sowie Ihre Liefer- und Rechnungsanschrift, den Stand der Zahlung und den Stand Ihrer Bestellung (zum Beispiel bezahlt, versendet, abholbereit, erstattet). Name und Anschriften geben Sie auf der Zahlungsseite von Stripe ein (siehe unten); wir erhalten sie von dort, die Rechnungsanschrift nur, wenn Stripe sie für Ihre Zahlungsart abfragt. Eine Telefonnummer verlangen und speichern wir nicht. Die Verarbeitung ist nötig, um den Kaufvertrag mit Ihnen zu schließen und zu erfüllen (Art. 6 Abs. 1 lit. b DSGVO). Ohne diese Angaben können wir Ihre Bestellung nicht ausführen. Außerdem speichern wir in der noch offenen Bestellung einen Hashwert Ihrer IP-Adresse (nicht die IP-Adresse selbst; er wird mit einem geheimen Schlüssel des Servers berechnet, sodass sich daraus keine IP-Adresse ableiten lässt). Wir lassen damit je IP-Adresse höchstens drei offene, noch nicht bezahlte Bestellungen innerhalb von 30 Minuten zu und schützen den Webshop so vor automatisierten Massenbestellungen, die den Bestand blockieren. Das ist unser berechtigtes Interesse (Art. 6 Abs. 1 lit. f DSGVO); Sie können dieser Verarbeitung widersprechen (Art. 21 DSGVO). Wir verwenden den Hashwert nur dafür und löschen ihn, sobald Ihre Bestellung bezahlt oder storniert ist, spätestens mit der Löschung nicht bezahlter Bestellungen nach 30 Tagen.
### Zahlung über Stripe
Die Zahlung wickelt die Stripe Payments Europe, Limited, 1 Grand Canal Street Lower, Grand Canal Dock, Dublin, D02 H210, Irland („Stripe“) ab. Sie werden dazu auf die Zahlungsseite von Stripe weitergeleitet. Karten- und Kontodaten geben Sie nur dort ein; wir und der Betreiber des Webshops erhalten sie nicht. Stripe verarbeitet Ihre Daten teils im Rahmen der Zahlungsabwicklung für uns und teils als eigener Verantwortlicher, zum Beispiel zur Betrugsvorbeugung und zur Erfüllung gesetzlicher Pflichten; dafür gilt die Datenschutzerklärung von Stripe (stripe.com/de/privacy). Dabei können Daten an die Stripe, LLC in den USA übermittelt werden. Die Stripe, LLC ist nach dem EU-US Data Privacy Framework zertifiziert, für das ein Angemessenheitsbeschluss der EU-Kommission besteht. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO.
Welche Zahlungsarten angeboten werden, sehen Sie vor dem Kauf im Webshop. Zahlen Sie per SEPA-Lastschrift, gibt Stripe Ihre Kontodaten an die beteiligten Banken weiter und bucht den Betrag nach Ihrem Mandat ab. [Nur wenn Klarna eingeschaltet ist: Wählen Sie Klarna, übermittelt Stripe die zur Zahlung erforderlichen Daten an die Klarna Bank AB (publ), Sveavägen 46, 111 34 Stockholm, Schweden; es gelten die Datenschutzbestimmungen von Klarna, die Ihnen bei der Zahlung angezeigt werden.]
### Bestellbestätigung und weitere E-Mails
Zu Ihrer Bestellung senden wir Ihnen E-Mails an die angegebene E-Mail-Adresse: bei einer Zahlung, die noch aussteht (zum Beispiel SEPA-Lastschrift), zuerst eine Mail über den Eingang der Bestellung, Bestellbestätigung (mit den Vertragsbedingungen, der Widerrufsbelehrung und dem Muster-Widerrufsformular; es sind die Texte, die bei Ihrer Bestellung galten), Hinweise zum Versand [oder zur Abholbereitschaft], Bestätigungen zu einer Erstattung oder zu einem Widerruf und, wenn eine Zahlung nicht gelungen ist, einen Hinweis darauf. Das sind Mitteilungen zur Abwicklung Ihres Vertrags (Art. 6 Abs. 1 lit. b DSGVO), keine Werbung. Bis zum Versand liegt jede E-Mail in einer Warteschlange; nach dem Versand wird Empfängeradresse und Inhalt dort gelöscht, der Eintrag selbst nach sieben Tagen.
### Warenkorb im Browser
Ihren Warenkorb (Artikel und Mengen, keine persönlichen Daten) speichert der Webshop im Sitzungsspeicher (sessionStorage) Ihres Browsers, solange der Tab geöffnet ist. Das ist technisch erforderlich, damit der Warenkorb beim Durchklicken erhalten bleibt (§ 25 Abs. 2 Nr. 2 TDDDG); es werden keine Cookies gesetzt, und der Eintrag verschwindet, wenn Sie den Tab schließen. Beim Aufruf der Seite lädt Ihr Browser das Programm des Webshops und die Artikeldaten von der Adresse project.webklar.com. Dabei werden Ihre IP-Adresse, Datum und Uhrzeit des Aufrufs sowie Angaben zu Ihrem Browser und Betriebssystem übertragen und für 14 Tage in Server-Protokollen gespeichert, um den sicheren und störungsfreien Betrieb zu gewährleisten (Art. 6 Abs. 1 lit. f DSGVO). Zum Schutz vor Missbrauch begrenzen wir die Zahl der Bestellungen und Widerrufe je IP-Adresse; die Zeitpunkte der Aufrufe werden nur im Arbeitsspeicher des Servers gehalten. Mit Ihrer Bestellung oder Ihrem Widerruf wird Ihre IP-Adresse nicht gespeichert.
### Widerruf über die Widerrufsfunktion
Über den Link „Vertrag widerrufen“ können Sie Ihren Vertrag ohne Anmeldung online widerrufen. Dabei verarbeiten wir Ihren Namen, Ihre E-Mail-Adresse, die Bestellnummer, Ihre Erklärung und den Zeitpunkt des Eingangs. Sie erhalten sofort eine Eingangsbestätigung mit Inhalt, Datum und Uhrzeit Ihrer Erklärung. Rechtsgrundlage ist Art. 6 Abs. 1 lit. c DSGVO in Verbindung mit § 356a BGB sowie Art. 6 Abs. 1 lit. b DSGVO.
### Weitergabe
[Nur bei Versand: Zur Lieferung geben wir Ihren Namen und Ihre Lieferanschrift an das Versandunternehmen weiter, das wir beauftragen ([Name des Versanddienstleisters]).] Im Übrigen geben wir Ihre Daten nur weiter, soweit das für die Vertragsabwicklung nötig ist (Stripe und die genannten Zahlungsanbieter, Auftragsverarbeiter) oder wir gesetzlich dazu verpflichtet sind (zum Beispiel an das Finanzamt). Eine Weitergabe zu Werbezwecken findet nicht statt.
### Speicherdauer
Bestellungen, die nicht bezahlt oder die storniert wurden, löscht das Webshop-System nach 30 Tagen. Für bezahlte Bestellungen sind wir als Händler verpflichtet, Buchungsbelege und Geschäftsunterlagen nach Handels- und Steuerrecht aufzubewahren (in der Regel sechs bis zehn Jahre, § 257 HGB, § 147 AO; Art. 6 Abs. 1 lit. c DSGVO). Diese Unterlagen führen wir selbst und mit den Belegen von Stripe, nicht im Webshop-System. Im Webshop-System selbst werden Name, E-Mail-Adresse und Anschriften drei Jahre nach dem Ende des Jahres der Bestellung anonymisiert; die Bestellnummer, die Artikel und die Beträge bleiben ohne Personenbezug erhalten. Daten aus einem Widerruf werden ebenso behandelt wie die zugehörige Bestellung.
### Ihre Rechte
Sie haben das Recht auf Auskunft über Ihre Daten (Art. 15 DSGVO), auf Berichtigung (Art. 16), Löschung (Art. 17), Einschränkung der Verarbeitung (Art. 18), Datenübertragbarkeit (Art. 20) und auf Widerspruch gegen Verarbeitungen, die auf Art. 6 Abs. 1 lit. f DSGVO beruhen (Art. 21). Wenden Sie sich dazu an uns unter [E-Mail-Adresse des Händlers]; wir leiten Ihre Anfrage, soweit nötig, an den technischen Betreiber des Webshops weiter. Außerdem können Sie sich bei einer Datenschutz-Aufsichtsbehörde beschweren, etwa bei der für uns zuständigen Behörde [zuständige Aufsichtsbehörde des Händlers].
+98
View File
@@ -0,0 +1,98 @@
# Webshop auf der Kundenwebsite einbauen
Anleitung für das WEBklar-Team. Das Widget lässt Besucher auf der Website des Kunden („Händler“) Waren bestellen und bezahlen. Artikel, Versand, Zahlungsarten und Rechtstexte pflegt der Händler im Kundenportal (Tab „Funktionen“, Kachel „Webshop“). Den `siteKey` (16 Zeichen, a–z und 2–7) findest Du im Einrichtungs-Ticket und im Ticketsystem (WOMS): Kunde → Reiter „Funktionen“, Zeile Webshop. Im Ticket stehen Snippet und Schritte schon fertig; dieses Dokument ist die Referenz dahinter. Ohne aktive Funktion, ohne verbundenes Stripe-Konto, ohne bestätigte Rechtstexte oder ohne lieferbaren Artikel ist nichts bestellbar: Das Widget zeigt dann „Der Shop ist gerade geschlossen“ (oder den Ausweichtext, siehe Abschnitt 4).
Der Webshop ist an den Vertrag über den Betrieb der Website gekoppelt (Leistungsbeschreibung Webshop Nr. 10), aber **nicht in der Software**: Das Portal liest keine Abo-Daten für den Shop, schaltet ihn nicht von selbst ab, und bestellt werden kann er auch ohne Abo. Das Abschalten zum Vertragsende machen wir von Hand (Abschnitt 8).
## 1. Das Snippet
```html
<div data-webklar-shop="SITEKEY" data-api="https://project.webklar.com">
<p>Unser Shop ist gerade nicht erreichbar. Bitte schreiben Sie uns: info@beispiel.de</p> <!-- Ausweichtext, Abschnitt 4 -->
</div>
<script src="https://project.webklar.com/embed/shop.js" defer></script>
```
Das Skript steht einmal pro Seite, vor `</body>`. Es hat keine Abhängigkeiten, setzt keine Cookies, lädt nichts von Dritten und fragt nur `project.webklar.com` an. Einziger Speicher im Browser ist der Warenkorb (nur Artikelkennungen und Mengen) in der `sessionStorage` des Tabs; er ist technisch erforderlich (§ 25 Abs. 2 Nr. 2 TDDDG) und verschwindet mit dem Tab. Texte des Händlers setzt das Skript nur als Text ein, nie als HTML. Das Widget rechnet nie selbst: Preise, Summen, Versand und Steuerhinweise kommen fertig vom Server.
| Attribut | Pflicht | Bedeutung |
| --- | --- | --- |
| `data-webklar-shop` | ja | der `siteKey` des Projekts |
| `data-api` | nein | Herkunft des Portals, Standard `https://project.webklar.com`. Bei Modul-Import, Tag-Manager oder nachgeladenem Skript ist `data-api` Pflicht. |
| `data-ansicht` | nein | `widerruf` öffnet gleich die Widerrufsfunktion („Vertrag widerrufen“). Die Adresse der Seite kennt außerdem `?ansicht=widerruf` und `?ansicht=agb`. |
## 2. Wohin damit
1. **Eigene Seite „Shop“** anlegen (zum Beispiel `/shop` oder `/shop.html`) und das Snippet an die Stelle des Inhalts setzen. Das `div` braucht Breite (normaler Blockcontainer; in einem Flex- oder Grid-Element ohne Breite schrumpft es auf null). Der Warenkorb liegt nur im Tab, also darf die Seite beim Bestellen nicht in einem iframe liegen und der Händler braucht keine zweite Seite für die Kasse: Kasse, Danke-Seite und Widerruf laufen im selben Widget.
2. **Menüpunkt „Shop“** im Hauptmenü auf diese Seite, auf allen Seiten.
3. Vom Shop zurück: Nach der Zahlung bei Stripe kehrt der Käufer auf die Seite zurück, auf der er bestellt hat (nur Adressen der Website des Projekts sind erlaubt, sonst die Standalone-Seite). Die Seite darf also nicht hinter einer Weiterleitung oder einem Passwortschutz liegen.
4. **Mehrere Widgets** auf einer Seite sind nicht vorgesehen. Der Shop ist ein Widget je Website.
Aussehen: Das Widget nimmt die Akzentfarbe aus den Einstellungen des Händlers. Wie beim Terminplaner stehen die Stile in `@layer wks`, jede Regel der Kundenseite gewinnt also. Anpassen per CSS-Variablen auf dem `div`, in der Seiten-CSS oder auf `:root`, zum Beispiel `[data-webklar-shop]{ --wks-akzent:#0f766e; --wks-akzent-text:#fff; }`:
| Variable | Standard |
| --- | --- |
| `--wks-akzent` und `--wks-akzent-text` (Knöpfe; setze beide zusammen) | Akzentfarbe des Händlers, sonst `#1d4ed8` und `#fff` |
| `--wks-hintergrund` | `#fff` |
| `--wks-text` | `#1f2937` |
| `--wks-rand` | `#858f9e` |
| `--wks-radius` | `10px` |
| `--wks-schrift`, `--wks-schrift-titel` | Schrift der Seite |
Kontrast (WCAG 2.1 AA): Text auf dem Hintergrund und Schrift auf dem Akzent mindestens 4,5 : 1, Rand gegen den Hintergrund mindestens 3 : 1. Prüfe beim Abnehmen, ob globale Regeln des Themes (`button{…}`, `input{…}`) das Widget stören, und überschreibe sie gezielt (`[data-webklar-shop] .wks-btn{ … }`).
## 3. Link „Vertrag widerrufen“ auf jeder Seite (Pflicht)
Seit dem 19.06.2026 muss jeder Online-Shop eine elektronische Widerrufsfunktion bieten (§ 356a BGB). Sie muss **auf jeder Seite** der Website dauerhaft erreichbar sein, nicht nur im Shop:
- In der **Fußzeile jeder Seite** (auch Impressum, Datenschutz und 404-Seite) steht der Link **„Vertrag widerrufen“** auf `https://project.webklar.com/shop/<siteKey>/widerruf` (mit dem echten `siteKey`).
- Der Linktext lautet genau „Vertrag widerrufen“. Er darf nicht ausgeblendet, versteckt oder in einem Aufklappmenü untergebracht werden.
- Der Link braucht keine Anmeldung. Er führt zur Schaltfläche „Vertrag widerrufen“, danach zum Formular und zur Bestätigungsschaltfläche „Widerruf bestätigen“.
- Im Shop-Widget selbst steht der Knopf „Vertrag widerrufen“ zusätzlich in der Fußzeile des Widgets.
Der Händler darf den Link nicht entfernen oder ändern (Leistungsbeschreibung Nr. 5 Abs. 6). Weise ihn bei der Übergabe darauf hin.
## 4. Ausweichtext
Alles, was im `div` steht, gehört der Kundenwebsite. Das Skript ersetzt es nur, wenn die Konfiguration geladen werden konnte. Bei ausgeschaltetem JavaScript, unbekanntem Schlüssel, abgeschalteter Funktion oder Netzfehler bleibt der Text stehen. Schreib dort einen Hinweis und die Kontaktdaten des Händlers hinein. Ist der Shop zwar eingebunden, aber (noch) nicht geöffnet, zeigt das Widget selbst „Der Shop ist gerade geschlossen“.
## 5. Vor der Abnahme prüfen
**Impressum des Händlers.** Der Händler ist der Verkäufer. Sein Impressum muss ihn nennen (Name beziehungsweise Firma, vollständige Anschrift, E-Mail, Telefon, gegebenenfalls Register und USt-IdNr.). Es reicht nicht, wenn nur WEBklar als Betreiber der Website erscheint. Die Adressen von Impressum und Datenschutzerklärung (https) hat der Händler im Portal unter Rechtliches eingetragen; das Widget verlinkt sie in der Fußzeile und in der Kasse. Prüfe, dass beide Adressen auf die echten Seiten der Website führen. Fällt Dir ein offensichtlicher Mangel auf, sag es dem Händler im Chat (Leistungsbeschreibung Nr. 5 Abs. 10: Eine Prüfpflicht entsteht daraus nicht).
**Datenschutz-Absatz.** Liegt die Datenschutzerklärung in der Website, die wir betreuen, fügen wir den Absatz aus `docs/funktionen/shop-datenschutz-absatz.md` dort ein (gehört zur Einrichtung). Liegt sie woanders, bekommt der Händler den Text per Chat und fügt ihn selbst ein. Anrede und Nummerierung an die übrige Erklärung anpassen; die Angaben zur Speicherdauer entsprechen dem AVV Webshop Nr. 9 und Abschnitt 6d unserer Datenschutzerklärung. Ändert sich dort etwas, ändere den Absatz mit.
**Rechtstexte.** AGB, Widerrufsbelehrung, Muster-Widerrufsformular sowie Versand und Zahlung bestätigt der Händler im Portal (Rechtliches). Die Muster liegen in `docs/funktionen/shop-muster-rechtstexte.md`. Ohne Bestätigung öffnet der Shop nicht.
**Artikel.** Wirf einen Blick auf die Pflichtangaben: Preis, Grundpreis (immer je 1 kg, 1 l, 1 m oder 1 m²), Lieferzeit, bei Lebensmitteln die Pflichtangaben und Allergene, die Angaben zur Produktsicherheit und Widerrufsausschlüsse. Das ist Sache des Händlers; ein Hinweis im Chat hilft.
## 6. Vorschau und Testkauf
1. **Vorschau:** `https://project.webklar.com/shop/<siteKey>` zeigt das Widget auf einer neutralen Seite. Prüfe dort Artikel, Warenkorb, Versand und die Fußzeile (AGB, Widerrufsbelehrung, Versand & Zahlung, Vertrag widerrufen, Impressum, Datenschutz). Prüfe danach dasselbe auf der echten Seite der Kundenwebsite, auch auf dem Handy.
2. **Testkauf mit dem Händler.** Lege mit dem Händler einen Artikel für **1 €** an (nur für den Test, danach wieder löschen oder deaktivieren). Der Händler öffnet den Shop (Übersicht → „Shop geöffnet“, alle Haken grün), Du kaufst den Artikel auf der echten Seite mit Deiner E-Mail-Adresse und zahlst echt (das Stripe-Konto des Händlers ist ein Live-Konto). Prüfe: Bestätigungsseite „Danke“, Bestellbestätigungsmail beim Käufer (mit AGB, Widerrufsbelehrung und Muster-Widerrufsformular im Text), Mail „Neue Bestellung“ beim Händler, Bestellung im Portal als bezahlt.
3. **Widerruf testen:** Rufe den Link „Vertrag widerrufen“ aus der Fußzeile auf, erkläre den Widerruf zur Testbestellung und bestätige ihn. Du musst sofort eine Eingangsbestätigung mit Inhalt, Datum und Uhrzeit bekommen; der Händler bekommt die Mitteilung, der Widerruf steht an der Bestellung im Portal.
4. **Erstatten:** Der Händler erstattet den Testkauf im Portal (Bestellung → Erstatten). Prüfe die Erstattungsmail und dass Stripe die Rückzahlung zeigt. Den Testartikel danach löschen.
5. Ist alles in Ordnung, **schließe das Einrichtungs-Ticket**. Das schaltet die Funktion frei (Kunde erhält die Mail „Webshop bereit“). Trag die Zeit im Arbeitsblatt ein; sie wird nicht auf Abo-Stunden angerechnet (Leistungsbeschreibung Nr. 9 Abs. 4).
## 7. Was das Widget zeigt (Kurzüberblick)
1. Ganz oben: Lieferländer und akzeptierte Zahlungsarten (Beginn des Bestellvorgangs, § 312j Abs. 1 BGB).
2. Artikel (Kategorien, Raster), Detail mit Bildern, Beschreibung, Preis, Grundpreis, Varianten, Lieferhinweis, Widerrufsausschluss, Produktsicherheit und Lebensmittelangaben.
3. Warenkorb, Lieferart (Versand oder Abholung).
4. Kasse: Positionen mit Einzel- und Gesamtpreis, Versandkosten, Gesamtsumme, Lieferzeit, Zahlungsarten, Hinweis auf das Widerrufsrecht beziehungsweise seinen Ausschluss, Links zu AGB und Widerrufsbelehrung, **Hinweis auf die gesetzliche Gewährleistung** (Anhang I der Verordnung (EU) 2025/1960, hervorgehoben über dem Knopf) und der Knopf **„Zahlungspflichtig bestellen“**. Erst mit diesem Klick im Widget kommt der Vertrag zustande; Stripe Checkout ist danach nur noch die Zahlung (Name, Telefon und Lieferadresse sammelt Stripe).
5. Danke-Seite mit dem Stand der Bestellung; die Bestellbestätigungsmail geht an den Käufer.
6. Widerruf: Formular (Name, E-Mail, Bestellnummer, optional Erklärung) und „Widerruf bestätigen“.
## 8. Wenn der Vertrag über die Website endet (Abschalten von Hand)
Der Webshop läuft, solange WEBklar die Website des Händlers betreibt (Hosting-Abo, Wartungs-Abo oder ein anderer Vertrag über den Betrieb der Website; Leistungsbeschreibung Nr. 10). Die Software prüft das **nicht**. Du musst es selbst im Blick haben, wenn ein solcher Vertrag endet, gekündigt oder widerrufen wird:
1. **Vorher** schreibst Du dem Händler eine E-Mail (oder Chat): Der Vertrag endet am TT.MM.JJJJ, damit endet auch der Webshop, WEBklar schaltet ihn zum Vertragsende ab. Er soll offene Bestellungen und Widerrufe abarbeiten, seine Bestellungen als CSV exportieren (Aufbewahrungspflichten sind seine Sache) und seine Käufer informieren, dass der Shop nicht mehr angeboten wird.
2. **Zum Vertragsende** im Ticketsystem (WOMS): Kunde → Reiter „Funktionen“ → Webshop → **„Abschalten“**. Danach nimmt der Shop keine Bestellungen mehr an, das Widget zeigt den Ausweichtext, unbezahlte Bestellungen laufen aus.
3. Der Händler kann Bestellungen und Widerrufe danach **90 Tage** lang im Portal lesen und exportieren. Danach löscht der tägliche Job automatisch alle Shopdaten (Artikel, Bilder, Bestellungen, Widerrufe, Einstellungen); das Einrichtungs-Ticket bleibt als Arbeitsnachweis ohne Käuferdaten. Die Widerrufsfunktion bleibt bis zu dieser Löschung erreichbar, solange das Widget eingebunden ist.
4. Nimm nach dem Abschalten den Link „Vertrag widerrufen“ aus der Fußzeile und das Snippet aus der Website, sobald die 90 Tage um sind (oder der Händler den Shop nicht mehr braucht), und entferne den Shop-Menüpunkt.
5. Beginnt **innerhalb der 90 Tage** wieder ein Vertrag über den Betrieb der Website, schaltest Du den Webshop auf Wunsch des Händlers in Textform ohne weitere Kosten wieder ein (WOMS → Funktionen → Webshop → Aktivieren; Daten und Einstellungen sind noch da). Nach der Löschung ist eine neue Anfrage und Einrichtung nötig (neuer Preis).
6. **Erstattung (Leistungsbeschreibung Nr. 10 Abs. 6):** Endet der Vertrag über den Betrieb der Website, **bevor** wir den Webshop freigeschaltet haben (Ticket noch offen, Funktion im Zustand „in Einrichtung“), erstatten wir die 299 € vollständig, in Stripe von Hand. In allen anderen Fällen (Webshop war schon freigeschaltet, Abschalten auf Wunsch nach Abs. 7, Abschalten wegen verbotener Waren) gibt es keine Erstattung; nur der Widerruf der Bestellung richtet sich nach Nr. 9 Abs. 6.
7. Widerruft ein Verbraucher oder Existenzgründer die Bestellung der Einrichtung (Seite „Vertrag widerrufen“, Auswahl „Webshop (Einrichtung)“), kommt die Mitteilung T09 mit Fristende und Preis; es wird nichts automatisch geändert. Du prüfst, erstattest in Stripe innerhalb von 14 Tagen (gegebenenfalls abzüglich Wertersatz für den begonnenen Einbau) und schaltest den Webshop ab. Mit dem Widerruf endet der Webshop.
Das Stripe-Konto des Händlers bleibt in jedem Fall beim Händler; wir rühren es nicht an.
+91
View File
@@ -0,0 +1,91 @@
# Webshop – Livegang
Reihenfolge für den Inhaber. Jeder Schritt braucht seine Freigabe; nichts davon hat Claude gegen Produktion oder das
Live-Stripe-Konto ausgeführt. Code: Portal `feature/webshop` (Worktree `dev-webshop`), WOMS `feature/webshop`
(Worktree `ticket.webklar.com/dev-webshop`). Beide Zweige bauen auf `feature/terminplaner-tp2` auf.
## 0. Voraussetzung
- **TP2 (Terminplaner-Verkauf) ist live.** Der Webshop nutzt das Funktionen-Register aus TP2. Erst TP2 nach `main`
(Portal) bzw. `test` (WOMS), dann diesen Zweig darauf mergen (`git merge origin/main` im Worktree, Tests laufen lassen).
- Der Zweig `feature/oeffnungszeiten` baut ebenfalls auf TP2 auf. Wer zuerst live geht, ist egal; der zweite merged den
ersten nach (Konflikte erwartbar in `server/config.js` features, `public/app.js`, `public/funktionen.js`,
`test/mailTemplates.test.js` (Vorlagen-IDs), `server/index.js`).
## 1. Stripe-Dashboard (WEBklar-Konto, live)
1. **Connect aktivieren**: Einstellungen → Connect → Plattformprofil ausfüllen: „Plattform/Software, deren Nutzer an
eigene Kunden verkaufen“, Geldfluss **Direct Charges**, **keine** Plattformgebühr, Konten mit eigenem vollen
Stripe-Dashboard (Standard-Konten). Danach unter Connect → Branding Name „WEBklar“, Icon und Farbe für das Onboarding.
2. **Connect-Webhook** anlegen: Entwickler → Webhooks → Endpunkt hinzufügen, **„Ereignisse in verbundenen Konten“**,
URL `https://project.webklar.com/api/shop/stripe-webhook`, Ereignisse:
`account.updated`, `checkout.session.completed`, `checkout.session.async_payment_succeeded`,
`checkout.session.async_payment_failed`, `checkout.session.expired`, `charge.refunded`.
Das Signatur-Geheimnis (`whsec_…`) kommt in Schritt 2 in die `.env`.
3. Nichts weiter: Händler-Konten legt das Portal selbst an, wenn ein Händler „Stripe-Konto einrichten“ klickt.
## 2. Server-Konfiguration
In `/home/webklar/apps/.env`:
```
SHOP_ENABLED=pilot
SHOP_PILOT_EMAILS=kenso.gri@gmail.com
STRIPE_CONNECT_WEBHOOK_SECRET=whsec_…
```
In `/home/webklar/apps/docker-compose.yml` beim Dienst `kundenbereich-server` die drei Variablen durchreichen
(`SHOP_ENABLED=${SHOP_ENABLED:-off}`, `SHOP_PILOT_EMAILS=${SHOP_PILOT_EMAILS:-}`,
`STRIPE_CONNECT_WEBHOOK_SECRET=${STRIPE_CONNECT_WEBHOOK_SECRET:-}`) und danach
`docker compose -f /home/webklar/apps/docker-compose.yml up -d --no-deps kundenbereich-server`
(ein `docker restart` liest die `.env` nicht neu). Artikelbilder liegen unter `build/upload-data/shop/` (gitignored,
überleben Deploys wie die News-Fotos).
## 3. Datenbank und Stripe-Preis (vor dem Deploy)
```bash
cd /home/webklar/apps/kundenbereich.webklar.com/build
docker exec -w /app webklar-kundenbereich-server node scripts/shop-setup.mjs --check
docker exec -w /app webklar-kundenbereich-server node scripts/shop-setup.mjs
docker exec -w /app webklar-kundenbereich-server node scripts/stripe-products.mjs --pruefen
docker exec -w /app webklar-kundenbereich-server node scripts/stripe-products.mjs --anlegen # legt shopSetup 299 € an
```
`shop-setup.mjs` legt fünf Collections an (`shopConfig`, `shopProducts`, `shopOrders`, `shopWiderrufe`,
`shopRechtstexteArchiv`). Die Käufer-Mails laufen über `termineOutbox`; die Collection kommt aus dem TP2-Livegang
(`scripts/termine-setup.mjs`). Die Skripte müssen aus dem neuen Code laufen (nach Schritt 4 oder vorher aus dem Worktree
mit derselben Umgebung).
## 4. Deploy
- Portal: `feature/webshop` → `main` pushen (Webhook deployt).
- WOMS: `feature/webshop` → `test` pushen; danach im WOMS „Rechtliches“: `scripts/setup-legal.mjs` laufen lassen (neue
Karten „Leistungsbeschreibung Webshop“, „AVV Webshop“) und die Fassungen 2026-10-05 hochladen
(`public/rechtliches/leistungsbeschreibung-webshop-2026-10-05.pdf`, `avv-webshop-2026-10-05.pdf`).
- Prüfen: `https://project.webklar.com/rechtliches/leistungsbeschreibung-webshop.html` und
`…/avv-webshop.html` antworten 200; `https://project.webklar.com/embed/shop.js` antwortet 200.
## 5. Pilot mit dem Testkonto
1. Im Portal als Testkunde: Funktionen → Webshop → Anfrage stellen (Projekt `test-website`).
2. WOMS → Kunde → Funktionen → Webshop → „Anfrage annehmen“ (Mail K24 kommt an).
3. Einrichtung: entweder bestellen (299 €, danach in Stripe erstatten) oder im WOMS „Aktivieren (ohne Zahlung)“
(Pilot-Adressen brauchen keine Textform).
4. Einbau nach `docs/funktionen/shop-einbau.md` auf der Test-Website, Ticket schließen → Freischaltung, Mail K16.
5. Portal → Webshop → Übersicht → „Stripe-Konto einrichten“ (echtes Onboarding mit den Daten von WEBklar oder einem
Testbetrieb; Stripe prüft Identität und Bankkonto).
6. Einstellungen ausfüllen (Steuerart wählen!), einen Artikel für 1,00 € anlegen, Rechtstexte: Muster einsetzen,
Platzhalter ersetzen, Impressum-/Datenschutz-URL, bestätigen, „Shop öffnen“.
7. Testkauf auf der Website mit echter Karte → Mails G09 (Käufer) und K25 (Händler) prüfen → im Portal „Versendet“
(G10) → „Erstatten“ (G13) → im Stripe-Dashboard des Händlers sichtbar.
8. Widerruf über den Fußzeilen-Link testen (G12 mit Datum und Uhrzeit, K26 an den Händler).
## 6. Freigabe für alle
- Kanzlei-Paket `docs/rechtliches/webshop-kanzlei-paket.md` an die Kanzlei, Befund einarbeiten (neue Fassung, falls
sich Texte ändern; ausgelieferte Fassungen werden nie überschrieben).
- Checkliste in Abschnitt 7 des Kanzlei-Pakets abhaken.
- `SHOP_ENABLED=on`, Compose neu anwenden (wie Schritt 2).
## Zurück
`SHOP_ENABLED=off` blendet die Kachel aus und stoppt neue Bestellungen. Bestehende Shops (Zustand aktiv oder
abgeschaltet) behalten ihre Widerrufsfunktion, ihre Fristen und die Löschjobs laufen weiter. Einzelne Shops schaltet das
Team im WOMS ab (Funktionen → Webshop → „Abschalten“), die Daten werden 90 Tage danach gelöscht.
+166
View File
@@ -0,0 +1,166 @@
# Shop: Muster-Rechtstexte (Widerrufsbelehrung, Widerrufsformular, AGB)
**Muster – keine Rechtsberatung.** Diese Texte sind Muster, die das Kundenportal im Webshop vorbefüllt (`server/services/shop/rechtstexte.js`, dort mit den Angaben aus den Shop-Einstellungen). Der Händler prüft sie, passt sie an und bestätigt sie selbst; WEBklar berät nicht rechtlich. Diese Datei enthält dieselben Texte mit Platzhaltern in eckigen Klammern zur Prüfung durch eine Kanzlei (Plan 2026-10-05, Task C5; Offene Punkte 5 und 9 der Recherche).
Stand: 2026-10-05.
## Quellen und Fundstellen
- Widerrufsbelehrung: Anlage 1 zu Art. 246a § 1 Abs. 2 S. 2 EGBGB, Fassung 2026 mit dem Baustein zur Widerrufsfunktion (https://www.gesetze-im-internet.de/bgbeg/BJNR006049896.html). Ein richtig ausgefülltes Muster erfüllt die Informationspflicht (Art. 246a § 1 Abs. 2 S. 3 EGBGB).
- Elektronische Widerrufsfunktion: § 356a BGB (seit 19.06.2026), Hinweispflicht in der Belehrung nach Art. 246a § 1 Abs. 2 S. 1 Nr. 1 EGBGB.
- Widerrufsformular: Anlage 2 zu Art. 246a § 1 Abs. 2 S. 1 Nr. 1 EGBGB.
- Ausschluss und vorzeitiges Erlöschen: § 312g Abs. 2 Nr. 1, 2, 3 BGB; Information nach Art. 246a § 1 Abs. 3 EGBGB.
- Gewährleistungshinweis: Art. 246a § 1 Abs. 1 S. 1 Nr. 11 EGBGB i. V. m. Durchführungsverordnung (EU) 2025/1960 Anhang I (seit 27.09.2026).
- Streitbeilegung: § 36 VSBG. Die EU-Plattform zur Online-Streitbeilegung wurde am 20.07.2025 eingestellt; es gibt keinen Link darauf.
- Button und Vertragsschluss: § 312j Abs. 3 und 4 BGB, § 312i BGB. Recherche: `/root/reports/webshop-2026-10-05/recherche-recht-stripe.md` (B.1 bis B.10).
## Hinweise zur Prüfung (nicht Teil der Texte)
- Fristbeginn: Die Belehrung enthält beide gesetzlichen Alternativen (eine Ware bzw. die Waren; mehrere Waren einer Bestellung, getrennt geliefert: die letzte Ware), weil der Warenkorb beides erzeugen kann. Teilsendungen und regelmäßige Lieferung sind nicht vorgesehen.
- Rücksendekosten: Standard ist „Käufer trägt“. Variante „Händler trägt“ steht unten.
- Abholung: Bietet der Shop Abholung an, kommt der Satz „Haben Sie die Waren bei uns abgeholt …“ hinzu.
- Ausschlussgründe: Der letzte Absatz der Belehrung nennt § 312g Abs. 2 Nr. 1, 2 und 3 BGB in allgemeiner Form. Ob und für welche Waren er zutrifft, entscheidet der Händler; Einordnung von Lieferung und Abholung von Speisen (§ 312g Abs. 2 Nr. 9, § 312 Abs. 2 Nr. 8 BGB) ist offen (Recherche, Offene Punkte 6).
- Gewährleistungshinweis: amtliche Grafik und wörtlicher Text aus Anhang I eingesetzt; offen bleibt nur der Abgleich der Grafikdatei mit der Originaldatei der Kommission.
- Widerrufsfunktion: Erreichbarkeit ohne Login bei Gastbestellungen, Identifikation der Bestellung und Zeitstempel mit der Kanzlei abstimmen (Recherche, Offene Punkte 7).
## Widerrufsbelehrung
Widerrufsbelehrung
Widerrufsrecht
Sie haben das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen.
Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag, an dem Sie oder ein von Ihnen benannter Dritter, der nicht der Beförderer ist, die Waren in Besitz genommen haben bzw. hat.
Haben Sie mehrere Waren im Rahmen einer einheitlichen Bestellung bestellt und werden diese getrennt geliefert, beträgt die Widerrufsfrist vierzehn Tage ab dem Tag, an dem Sie oder ein von Ihnen benannter Dritter, der nicht der Beförderer ist, die letzte Ware in Besitz genommen haben bzw. hat.
Um Ihr Widerrufsrecht auszuüben, müssen Sie uns ([Name des Händlers], [Straße Nr.], [PLZ] [Ort], E-Mail: [E-Mail-Adresse]) mittels einer eindeutigen Erklärung (z. B. ein mit der Post versandter Brief oder eine E-Mail) über Ihren Entschluss, diesen Vertrag zu widerrufen, informieren. Sie können dafür das beigefügte Muster-Widerrufsformular verwenden, das jedoch nicht vorgeschrieben ist.
Sie können Ihr Widerrufsrecht auch online unter [Internetadresse der Widerrufsfunktion im Shop] ausüben. Wenn Sie diese Online-Funktion nutzen, übermitteln wir Ihnen auf einem dauerhaften Datenträger (z. B. durch eine E-Mail) unverzüglich eine Eingangsbestätigung mit Informationen zum Inhalt der Widerrufserklärung sowie dem Datum und der Uhrzeit ihres Eingangs.
Zur Wahrung der Widerrufsfrist reicht es aus, dass Sie die Mitteilung über die Ausübung des Widerrufsrechts vor Ablauf der Widerrufsfrist absenden.
Folgen des Widerrufs
Wenn Sie diesen Vertrag widerrufen, haben wir Ihnen alle Zahlungen, die wir von Ihnen erhalten haben, einschließlich der Lieferkosten (mit Ausnahme der zusätzlichen Kosten, die sich daraus ergeben, dass Sie eine andere Art der Lieferung als die von uns angebotene, günstigste Standardlieferung gewählt haben), unverzüglich und spätestens binnen vierzehn Tagen ab dem Tag zurückzuzahlen, an dem die Mitteilung über Ihren Widerruf dieses Vertrags bei uns eingegangen ist. Für diese Rückzahlung verwenden wir dasselbe Zahlungsmittel, das Sie bei der ursprünglichen Transaktion eingesetzt haben, es sei denn, mit Ihnen wurde ausdrücklich etwas anderes vereinbart; in keinem Fall werden Ihnen wegen dieser Rückzahlung Entgelte berechnet.
Wir können die Rückzahlung verweigern, bis wir die Waren wieder zurückerhalten haben oder bis Sie den Nachweis erbracht haben, dass Sie die Waren zurückgesandt haben, je nachdem, welches der frühere Zeitpunkt ist.
Sie haben die Waren unverzüglich und in jedem Fall spätestens binnen vierzehn Tagen ab dem Tag, an dem Sie uns über den Widerruf dieses Vertrags unterrichten, an uns ([Name des Händlers], [Straße Nr.], [PLZ] [Ort]) zurückzusenden oder zu übergeben. Die Frist ist gewahrt, wenn Sie die Waren vor Ablauf der Frist von vierzehn Tagen absenden.
Sie tragen die unmittelbaren Kosten der Rücksendung der Waren.
Sie müssen für einen etwaigen Wertverlust der Waren nur aufkommen, wenn dieser Wertverlust auf einen zur Prüfung der Beschaffenheit, Eigenschaften und Funktionsweise der Waren nicht notwendigen Umgang mit ihnen zurückzuführen ist.
Ausschluss bzw. vorzeitiges Erlöschen des Widerrufsrechts
Das Widerrufsrecht besteht nicht bei Verträgen zur Lieferung von Waren, die nicht vorgefertigt sind und für deren Herstellung eine individuelle Auswahl oder Bestimmung durch den Verbraucher maßgeblich ist oder die eindeutig auf die persönlichen Bedürfnisse des Verbrauchers zugeschnitten sind (§ 312g Abs. 2 Nr. 1 BGB), z. B. Waren mit Wunschaufdruck oder Maßanfertigungen.
Das Widerrufsrecht besteht nicht bei Verträgen zur Lieferung von Waren, die schnell verderben können oder deren Verfallsdatum schnell überschritten würde (§ 312g Abs. 2 Nr. 2 BGB), z. B. frische Lebensmittel.
Das Widerrufsrecht erlischt vorzeitig bei Verträgen zur Lieferung versiegelter Waren, die aus Gründen des Gesundheitsschutzes oder der Hygiene nicht zur Rückgabe geeignet sind, wenn ihre Versiegelung nach der Lieferung entfernt wurde (§ 312g Abs. 2 Nr. 3 BGB).
Ende der Widerrufsbelehrung
### Variante: Händler trägt die Rücksendekosten
Statt „Sie tragen die unmittelbaren Kosten der Rücksendung der Waren.“ steht:
Wir tragen die Kosten der Rücksendung der Waren.
### Variante: Abholung angeboten
Nach dem Absatz zur Rücksendung folgt zusätzlich:
Haben Sie die Waren bei uns abgeholt, übergeben Sie diese zur Rückgabe an unserer Anschrift: [Abholadresse].
## Widerrufsformular
Muster-Widerrufsformular
(Wenn Sie den Vertrag widerrufen wollen, dann füllen Sie bitte dieses Formular aus und senden Sie es zurück.)
An [Name des Händlers], [Straße Nr.], [PLZ] [Ort], E-Mail: [E-Mail-Adresse]:
Hiermit widerrufe(n) ich/wir (*) den von mir/uns (*) abgeschlossenen Vertrag über den Kauf der folgenden Waren (*)/die Erbringung der folgenden Dienstleistung (*)
– Bestellt am (*)/erhalten am (*)
– Name des/der Verbraucher(s)
– Anschrift des/der Verbraucher(s)
– Unterschrift des/der Verbraucher(s) (nur bei Mitteilung auf Papier)
– Datum
(*) Unzutreffendes streichen.
## Allgemeine Geschäftsbedingungen
Die Kopfzeile „Muster – bitte prüfen und anpassen“ steht im vorbefüllten Text und wird vom Händler nach der Prüfung entfernt. Die Zahlungsarten, Länder und Abholung werden aus den Shop-Einstellungen eingesetzt; die Platzhalter unten zeigen den Standardfall.
Muster – bitte prüfen und anpassen
Allgemeine Geschäftsbedingungen
§ 1 Geltung und Anbieter
Diese Allgemeinen Geschäftsbedingungen gelten für alle Bestellungen von Verbrauchern in unserem Online-Shop. Anbieter und Vertragspartner ist [Name des Händlers], [Straße Nr.], [PLZ] [Ort], E-Mail: [E-Mail-Adresse].
§ 2 Vertragsschluss
Die Darstellung der Waren im Shop ist kein verbindliches Angebot. Mit dem Klick auf die Schaltfläche „Zahlungspflichtig bestellen“ geben Sie eine verbindliche Bestellung ab. Den Eingang Ihrer Bestellung bestätigen wir Ihnen sofort auf einer Bestätigungsseite im Shop. Bei den Zahlungsarten Kreditkarte, Apple Pay, Google Pay, Klarna erhalten Sie die Bestellbestätigung per E-Mail unmittelbar nach erfolgreicher Zahlung; mit dieser E-Mail nehmen wir Ihre Bestellung an und der Vertrag kommt zustande. Bei SEPA-Lastschrift senden wir Ihnen zunächst eine E-Mail über den Eingang Ihrer Bestellung; der Vertrag kommt in diesem Fall erst zustande, wenn wir Ihnen nach Eingang der Zahlung die Bestellbestätigung per E-Mail senden. Bestellungen, die nicht bezahlt werden, werden nach Ablauf der Zahlungsfrist verworfen.
§ 3 Preise und Versandkosten
Es gelten die zum Zeitpunkt der Bestellung im Shop angegebenen Preise in Euro. Alle Preise enthalten die gesetzliche Umsatzsteuer. Versandkosten und etwaige weitere Kosten werden vor Abgabe der Bestellung gesondert ausgewiesen und richten sich nach dem Zielland. Einzelheiten finden Sie unter „Versand & Zahlung“.
§ 4 Zahlung
Die Zahlung erfolgt über den Zahlungsdienstleister Stripe. Es stehen folgende Zahlungsarten zur Verfügung: Kreditkarte, Apple Pay, Google Pay, Klarna, SEPA-Lastschrift. Bei SEPA-Lastschrift versenden wir die Ware erst nach Eingang der Zahlung.
§ 5 Lieferung und Abholung
Wir liefern in folgende Länder: Deutschland. Die Lieferzeit beträgt [Lieferzeit], gerechnet ab Zahlungseingang. Ist eine Ware nicht verfügbar, informieren wir Sie unverzüglich.
§ 6 Eigentumsvorbehalt
Die Waren bleiben bis zur vollständigen Bezahlung unser Eigentum.
§ 7 Widerrufsrecht
Verbrauchern steht ein gesetzliches Widerrufsrecht zu. Einzelheiten ergeben sich aus der Widerrufsbelehrung, die Sie unter „Widerrufsbelehrung“ im Shop finden.
§ 8 Gewährleistung
Es gelten die gesetzlichen Mängelhaftungsrechte. Der Hinweis auf das gesetzliche Gewährleistungsrecht steht vor Abgabe der Bestellung im Shop.
§ 9 Technische Schritte, Vertragstext und Sprache
Zum Vertragsschluss führen folgende Schritte: Sie legen die gewünschten Waren in den Warenkorb, prüfen die Bestellübersicht mit allen Angaben und klicken auf „Zahlungspflichtig bestellen“. Danach zahlen Sie bei unserem Zahlungsdienstleister Stripe. Der Vertrag kommt nach § 2 mit unserer Bestellbestätigung per E-Mail zustande. Vor dem Klick auf „Zahlungspflichtig bestellen“ können Sie Eingabefehler erkennen und berichtigen: Im Warenkorb ändern Sie Mengen oder entfernen Waren, aus der Bestellübersicht gelangen Sie über „Zurück zum Warenkorb“ dorthin zurück, und Ihre Angaben zu Adresse und Zahlung prüfen und korrigieren Sie bei Stripe, bevor Sie die Zahlung abschließen. Wir speichern den Vertragstext nach Vertragsschluss nicht für Sie abrufbar. Ihre Bestelldaten, diese Allgemeinen Geschäftsbedingungen und die Widerrufsbelehrung erhalten Sie mit der Bestellbestätigung per E-Mail; bitte bewahren Sie diese E-Mail auf. Die Vertragssprache ist Deutsch. Wir unterwerfen uns keinen besonderen Verhaltenskodizes.
§ 10 Streitbeilegung
[Name des Händlers] ist nicht bereit und nicht verpflichtet, an einem Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen (§ 36 VSBG).
Zu § 2: Der Satz zur sofortigen Bestellbestätigung nennt nur die Zahlungsarten Karte/Apple Pay/Google Pay/Klarna; der SEPA-Satz erscheint nur, wenn SEPA-Lastschrift aktiv ist. Ohne SEPA entfällt er, bei ausschließlich SEPA entfällt der Satz davor. Der Ablauf im Shop: Bestätigungsseite sofort nach der Bestellung; bei Karte/Klarna folgt die Bestellbestätigung (G09, Vertragsschluss) unmittelbar nach der Zahlung; bei SEPA zuerst die Eingangsmail (G15), die Bestellbestätigung (G09) erst nach Zahlungseingang.
Zu § 9: enthält die Angaben nach Art. 246c EGBGB (technische Schritte, Eingabefehler erkennen und berichtigen, Speicherung des Vertragstextes, Sprache, Verhaltenskodizes).
Bei Kleinunternehmern steht in § 3 statt „Alle Preise enthalten die gesetzliche Umsatzsteuer.“ der Satz „Gemäß § 19 UStG wird keine Umsatzsteuer berechnet und ausgewiesen.“
## Hinweis auf das gesetzliche Gewährleistungsrecht (Kasse und Bestellbestätigung)
Amtliche Grafik nach Durchführungsverordnung (EU) 2025/1960 Anhang I (ABl. L vom 2.10.2025), nicht editierbar, online farbig: `public/img/gewaehrleistung-eu.webp` (aus dem Amtsblatt ausgeschnitten; vor Livegang mit der Originaldatei der Kommission abgleichen, Kanzlei-Frage). Wörtlicher Text der Grafik (Alternativtext, Bestellbestätigung):
GESETZLICHE GEWÄHRLEISTUNG
Mindestens zwei Jahre gesetzliche Gewährleistung der Vertragsmäßigkeit für Waren, die in der Europäischen Union verkauft werden.
Verbraucherinnen und Verbraucher können ihre Rechte im Rahmen des gesetzlichen Gewährleistungsrechts geltend machen, z. B. wenn die Waren nicht der Beschreibung entsprechen, nicht bestimmungsgemäß funktionieren.
Verkäufer haften für jede Vertragswidrigkeit, die zum Zeitpunkt der Lieferung der Waren bestand und innerhalb des Zeitraums der gesetzlichen Gewährleistung erkennbar wird. Verkäufer müssen in solchen Fällen Folgendes anbieten: kostenlose Nachbesserung oder kostenlose Ersatzlieferung, in bestimmten Fällen eine Preisminderung oder eine vollständige Erstattung des Kaufpreises.
In einigen Ländern gilt ein längerer Zeitraum für die gesetzliche Gewährleistung. Für gebrauchte Waren kann ein kürzerer Zeitraum gelten, jedoch nicht weniger als ein Jahr.
Für weitere Informationen zu Ihren Rechten in einem bestimmten Land scannen Sie den nachstehenden QR-Code oder fragen Sie den Verkäufer. europa.eu/youreurope/garantien
Was ist zu tun, wenn Sie vertragswidrige Waren erhalten? 1. Melden Sie dem Verkäufer das Problem so bald wie möglich. 2. Legen Sie einen Kaufnachweis vor, z. B. die Quittung, Rechnung oder einen Kontoauszug.
Verkäufer und Hersteller können auch gewerbliche Garantien gewähren, die unabhängig von der gesetzlichen Gewährleistung gelten. Diese GARAN-Kennzeichnung zeigt beispielsweise, dass der Hersteller eine gewerbliche Haltbarkeitsgarantie ohne zusätzliche Kosten gewährt, die die gesamte Ware abdeckt.
+165
View File
@@ -0,0 +1,165 @@
# Terminplaner auf der Kundenwebsite einbauen
Anleitung für das WEBklar-Team. Das Widget lässt Gäste auf der Website des Kunden Termine buchen. Leistungen, Personen und Öffnungszeiten pflegt der Kunde im Portal (Tab „Funktionen“, Kachel „Terminplaner“). Den `siteKey` (16 Zeichen, a–z und 2–7) und den fertigen Einbau-Code findest Du im Portal: in der Kundenansicht des Kunden unter Terminplaner → Einstellungen, Abschnitt „Einbau auf deiner Website“ (mit Knopf „Code kopieren“). Den `siteKey` zeigt auch das Ticketsystem (WOMS): Kunde → Reiter „Funktionen“, Zeile Terminplaner, mit Kopierknopf. Ohne aktive Funktion, ohne gespeicherte Einstellung und ohne Telefonnummer des Betriebs (bei Verbrauchern auch ohne Anschrift) ist nichts buchbar: Das Widget zeigt dann den Ausweichtext der Seite. Die Anleitung für einen bestimmten Kunden erzeugt das Ticketsystem (WOMS) selbst: Kunde → Reiter „Funktionen“ → Anleitung des Projekts, mit den Einstellungen des Betriebs, dem fertigen Einbau-Code, dem CSS-Vorschlag, den Datenschutz-Absätzen, der Abnahme-Checkliste und den Warnungen. Im Einrichtungs-Ticket steht davon die Kurzfassung (Stand der Einstellungen, Einbau-Code, Schritte) mit dem Link auf diese Anleitung. Dieses Dokument ist die allgemeine Referenz dahinter: Es erklärt Variablen, Stile, Datenschutz und API, die für jeden Betrieb gelten.
## 1. Das Snippet
```html
<div data-webklar-termine="SITEKEY" data-api="https://project.webklar.com" data-modus="inline">
<p>Termine telefonisch unter 06301 1234.</p> <!-- Ausweichtext, siehe Abschnitt 4 -->
</div>
<script src="https://project.webklar.com/embed/termine.js" defer></script>
```
Das Skript steht einmal pro Seite, vor `</body>`. Es hat keine Abhängigkeiten, setzt keine Cookies, speichert nichts im Browser und lädt nichts von Dritten. Texte der Kunden setzt es nur als Text ein, nie als HTML. Es wird 5 Minuten gecacht und darf von jeder Domain geladen werden.
## 2. Variablen (Attribute am `div`)
| Attribut | Pflicht | Bedeutung |
| --- | --- | --- |
| `data-webklar-termine` | ja | der `siteKey` des Projekts |
| `data-api` | nein | Herkunft des Portals, Standard `https://project.webklar.com`. Ohne Angabe nimmt das Skript die Herkunft seiner eigenen Adresse. Das klappt nur bei einem normalen `<script src>`. Bei Modul-Import, Tag-Manager oder nachgeladenem Skript ist `data-api` Pflicht. |
| `data-modus` | nein | `inline` (Standard): die Buchungsstrecke steht direkt in der Seite und lädt beim Aufruf der Seite. `knopf`: ein Knopf öffnet die Strecke in einem Dialog (`<dialog>`). Beim Laden der Seite ruft das Widget einmal die Konfiguration ab (für den Knopf „Vertrag widerrufen“, siehe Abschnitt 5), alles Weitere erst beim Klick. |
| `data-knopf` | nein | Text des Knopfes im Modus `knopf`, Standard „Termin buchen“. |
| `data-leistung` | nein | ID einer Leistung (`s_` plus 8 Zeichen, steht im Portal bei der Leistung). Die Leistung ist vorgewählt, der Schritt „Leistung wählen“ entfällt. Praktisch für einen Knopf neben einer Leistung auf der Seite. Eine unbekannte ID wird ignoriert. |
Alle Klassen beginnen mit `wkt-`. Die Stile stehen einmalig in `<style id="wkt-style">` und zwar in `@layer wkt`. Das heißt: **jede** Regel der Kundenseite (ohne Layer) gewinnt gegen die Standard-Stile. Das ist gewollt, hat aber eine Folge: Hat das Theme der Kundenseite globale Regeln wie `button{…}` oder `input{…}`, greifen sie auch im Widget. Prüfe das beim Abnehmen und überschreibe bei Bedarf gezielt, z. B. `[data-webklar-termine] .wkt-btn{ … }`.
Das Layout richtet sich nach der Breite des `div` (Container Query, braucht aktuelle Browser), nicht nach dem Bildschirm. Das `div` muss eine Breite haben (normaler Blockcontainer; in einem Flex- oder Grid-Element ohne Breite schrumpft es auf null).
### Zwei Widgets auf einer Seite
Das geht. Jedes Widget bekommt eigene IDs, die Stile werden nur einmal eingefügt. Beispiel: zwei Knöpfe neben zwei Leistungen.
```html
<div data-webklar-termine="SITEKEY" data-modus="knopf" data-knopf="Damenschnitt buchen" data-leistung="s_abcd1234"></div>
<div data-webklar-termine="SITEKEY" data-modus="knopf" data-knopf="Färben buchen" data-leistung="s_efgh5678"></div>
```
## 3. Aussehen anpassen (CSS-Variablen)
Setze die Variablen auf dem `div`, in der Seiten-CSS oder auf `:root`, z. B. `[data-webklar-termine]{ --wkt-akzent:#0f766e; }`. Alle sind optional.
| Variable | Standard |
| --- | --- |
| `--wkt-akzent` (Knöpfe, aktive Tage, Rahmen der freien Tage) | die Betriebsfarbe aus den Einstellungen, sonst `#1d4ed8` |
| `--wkt-akzent-text` (Schrift auf dem Akzent) | passend zur Betriebsfarbe (hell oder dunkel), sonst `#fff` |
| `--wkt-hintergrund` | `#fff` |
| `--wkt-text` | `#1f2937` |
| `--wkt-rand` (Eingabefelder, Karten) | `#858f9e` |
| `--wkt-radius` | `10px` |
| `--wkt-schrift` | die Schrift der Seite (`inherit`) |
| `--wkt-schrift-titel` (Überschriften) | wie `--wkt-schrift` |
Die Betriebsfarbe gilt nur, solange `--wkt-akzent` nicht gesetzt ist; die Schrift darauf (Weiß, dunkles Grau oder Schwarz) wählt das Skript so, dass sie mindestens 4,5 : 1 erreicht. Setzt Du `--wkt-akzent`, setze auch `--wkt-akzent-text`. Im Modus `knopf` kennt das Widget die Betriebsfarbe erst nach dem Klick: Der Knopf selbst nimmt `--wkt-akzent` der Seite oder `#1d4ed8`. Setze `--wkt-akzent` dort also immer.
Kontrast (WCAG 2.1 AA): Text auf dem Hintergrund mindestens 4,5 : 1, Schrift auf dem Akzent mindestens 4,5 : 1, `--wkt-rand` gegen den Hintergrund mindestens 3 : 1 (sonst sind Eingabefelder schwer zu erkennen). Der Fokusrahmen nimmt die Textfarbe.
### Stil 1: hell und freundlich (Salon, Kosmetik)
```css
[data-webklar-termine]{
--wkt-akzent:#9d4b6f;
--wkt-akzent-text:#fff;
--wkt-hintergrund:#fff;
--wkt-text:#3b2a33;
--wkt-rand:#8c7580;
--wkt-radius:14px;
--wkt-schrift:"Nunito", system-ui, sans-serif;
--wkt-schrift-titel:"Fredoka", system-ui, sans-serif;
}
```
### Stil 2: dunkel (Tattoo, Studio, Bar)
```css
[data-webklar-termine]{
--wkt-akzent:#f5c542;
--wkt-akzent-text:#111;
--wkt-hintergrund:#18181b;
--wkt-text:#f4f4f5;
--wkt-rand:#8b8b94;
--wkt-radius:4px;
--wkt-schrift:"Inter", system-ui, sans-serif;
}
```
### Stil 3: schlicht und eckig (Kanzlei, Handwerk, Beratung)
```css
[data-webklar-termine]{
--wkt-akzent:#1f3a5f;
--wkt-akzent-text:#fff;
--wkt-hintergrund:#fff;
--wkt-text:#1a1a1a;
--wkt-rand:#767676;
--wkt-radius:0;
--wkt-schrift:Georgia, "Times New Roman", serif;
--wkt-schrift-titel:system-ui, sans-serif;
}
```
Die Schriften lädt das Skript nicht selbst. Sind sie auf der Kundenseite nicht eingebunden, greift die Systemschrift.
## 4. Ausweichtext
Alles, was im `div` steht, gehört der Kundenwebsite. Das Skript ersetzt es nur, wenn die Konfiguration geladen werden konnte. Bei ausgeschaltetem JavaScript, unbekanntem Schlüssel, abgeschalteter Funktion, fehlenden Pflichtangaben des Betriebs oder Netzfehler beim Laden bleibt der Text stehen. Schreib dort die Telefonnummer des Betriebs hinein und gestalte den Text im Stil der Seite.
Im Modus `knopf` ersetzt der Knopf den Text auf der Seite. Der Text erscheint dafür im Dialog, wenn das Laden beim Klick scheitert.
Bricht das Netz mitten in der Strecke ab, zeigt das Widget „Gerade nicht erreichbar. Bitte rufen Sie an: {Telefon des Betriebs}“ (bei Anrede „du“: „Bitte ruf an“).
## 5. Was das Widget zeigt
1. Leistung wählen (Name, Dauer, Preis, Kurzbeschreibung) mit der Zahlungsart („Bezahlung vor Ort: …“). Entfällt bei `data-leistung`.
2. Person wählen („Egal“ zuerst). Nur wenn mehrere Personen infrage kommen und der Betrieb die Wahl erlaubt; bei Plätzen entfällt der Schritt.
3. Tag (Monatsraster, nur Tage mit freien Zeiten sind wählbar) und Uhrzeit. Alle Daten und Uhrzeiten kommen vom Server in Berliner Zeit. Das Widget rechnet nie mit dem Datum oder der Zeitzone des Browsers.
4. Angaben: Name, E-Mail, Telefon, Nachricht und Zusatzfelder nach den Einstellungen des Betriebs.
5. Zusammenfassung mit Beschreibung der Leistung, Stornobedingungen (nur bei einer Leistung mit Preis; daneben der Link „AGB des Betriebs“, wenn im Portal eine https-Adresse hinterlegt ist), Widerrufsbelehrung (aufklappbar, bei Verbraucher-Betrieben), Datenschutz-Hinweis, den Haken (Storno nur bei einer Leistung mit Preis, vorzeitiger Beginn, Erinnerung) und dem Knopf („Zahlungspflichtig buchen“, „Termin buchen“ oder „Termin anfragen“, kommt vom Server).
6. „Fast geschafft“: Der Gast bestätigt den Termin über den Link in der E-Mail. Die Reservierung gilt 30 Minuten.
Im Fuß stehen „Vertrag widerrufen“ als zweiter Knopf (kein bloßer Textlink; für Verbraucher bei jeder Leistung, ob sofort buchbar oder als Anfrage, weil auch ein angenommener Anfrage-Vertrag widerrufen werden kann), „Datenschutz“ (Link auf die Datenschutzerklärung des Betriebs) und klein „Terminbuchung über WEBklar“. Im Modus `knopf` steht der Fuß nur im geöffneten Dialog. Damit die Widerrufsfunktion (§ 356a BGB) trotzdem dauerhaft auffindbar ist, lädt das Widget dort schon beim Laden der Seite die Konfiguration (ein einziger Abruf, sonst nichts) und setzt, wenn der Betrieb ein Widerrufsrecht hat, den Knopf „Vertrag widerrufen“ als zweiten Knopf neben den Buchungsknopf. Er öffnet den Dialog gleich beim Widerrufsformular. Anrede (Sie oder Du) und Betriebsfarbe kommen aus den Einstellungen. Die rechtlich vorgeschriebenen Texte (Haken, Belehrung) stehen immer in der Sie-Form.
Bedienung nur mit der Tastatur: Tab und Umschalt+Tab zwischen den Elementen, im Monatsraster Pfeiltasten (Tag), Pos1 und Ende (Wochenanfang und -ende), Bild ↑ und Bild ↓ (Monat), Eingabe oder Leertaste wählt den Tag. Im Dialog schließt Esc.
## 6. Datenschutzerklärung des Kunden
Das Skript lädt beim Aufruf der Seite die Konfiguration (Texte, Leistungen) von `project.webklar.com`; im Modus `inline` folgen die Termine beim Durchklicken, im Modus `knopf` erst beim Klick auf den Knopf. Dabei sieht der Server die IP-Adresse des Besuchers. Die Datenschutzerklärung der Website braucht deshalb den Absatz „Online-Terminbuchung“ aus `docs/rechtliches/termine-datenschutz-absatz.md` (zwei Fassungen: Sie und Du, nimm die zur Erklärung der Website passende). Die `[[wenn …]]`-Sätze richten sich nach den Einstellungen des Betriebs (Telefon-Feld, Nachricht, Erinnerung, Verbraucher). Bau das Widget erst ein, wenn der Absatz in der Datenschutzerklärung steht. Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar den Absatz beim Einbau ein (Teil der Einrichtung, nicht auf das Stundenkontingent angerechnet); sonst erhält der Kunde den Absatz als Textbaustein per Chat und fügt ihn selbst ein. Die URL der Datenschutzerklärung (und des Impressums) muss in den Betriebsangaben im Portal stehen, sonst fehlt der Link im Fuß.
## 7. Abnahme-Checkliste
- [ ] Funktion im Ticketsystem aktiviert (Zustand „aktiv“), `TERMINE_ENABLED` auf `pilot` oder `on`, Kunde im Pilot. `siteKey` und Einbau-Code aus dem Portal (Terminplaner → Einstellungen → „Einbau auf deiner Website“) übernommen.
- [ ] Einstellungen im Portal gespeichert, Telefon und Anschrift des Betriebs eingetragen (das Portal weist darauf hin, solange etwas fehlt), URLs von Impressum und Datenschutz mit `https://`.
- [ ] Datenschutz-Absatz steht in der Datenschutzerklärung der Website (Abschnitt 6).
- [ ] Ausweichtext prüfen: JavaScript aus, Funktion im Ticketsystem ausgeschaltet oder falscher `siteKey`: Der Text steht, nichts ist kaputt.
- [ ] Komplett durchbuchen bis „Fast geschafft“; die Bestätigungs-E-Mail kommt an, der Link bestätigt, die zweite E-Mail (mit Kalenderdatei) kommt an, im Portal steht der Termin. Danach den Testtermin absagen (Verwaltungslink aus der E-Mail oder im Portal).
- [ ] Bei Verbraucher-Betrieb (auch mit nur Anfrage-Leistungen): Widerrufsbelehrung aufklappbar, bei Preis erscheint der Haken für vorzeitigen Beginn bei Terminen in den nächsten 14 Tagen, „Vertrag widerrufen“ steht als Knopf im Fuß und das Formular funktioniert.
- [ ] Modus `knopf` bei Verbraucher-Betrieb: Neben dem Buchungsknopf steht dauerhaft der Knopf „Vertrag widerrufen“ (nicht erst im Dialog) und öffnet den Dialog gleich beim Widerrufsformular; ohne Widerrufsrecht (Unternehmen, Freizeitleistung) fehlt er.
- [ ] Nur mit der Tastatur bedienen: Fokus immer sichtbar, Monatsraster mit Pfeilen, Pos1/Ende und Bild ↑/↓, Esc schließt den Dialog und der Fokus geht zum Knopf zurück.
- [ ] Handy (390 px Breite): kein seitliches Scrollen, Knöpfe gut treffbar, Dialog passt in den Bildschirm.
- [ ] Zoom 200 % und große Schrift: nichts wird abgeschnitten.
- [ ] Kontrast der gesetzten Variablen geprüft (Abschnitt 3), globale Regeln des Themes (`button`, `input`) stören nicht.
- [ ] Zwei Widgets auf einer Seite (falls geplant) stören sich nicht.
- [ ] Anderer Browser, andere Zeitzone (z. B. im Browser auf New York stellen): dieselben Tage und Uhrzeiten wie in Berlin.
- [ ] Entwicklerwerkzeuge: keine Cookies, nichts in Local/Session Storage, keine Anfrage an fremde Hosts außer dem Portal.
- [ ] Automatische Prüfung gegen die Testseite: `nice node test/ui/termine-widget.ui.mjs` (startet einen eigenen Testserver, Screenshots unter `/root/reports/terminplaner-ui/`).
## 8. API-Kurzreferenz
Alle Pfade unter `https://project.webklar.com/api/public/termine/v1`. Antworten sind JSON. Fehler haben die Form `{ "fehler": { "feld": "Text" }, "code": "…" }` (`_` = Fehler ohne Feld). Ist der `siteKey` unbekannt, die Funktion nicht aktiv oder der Betrieb noch nicht buchbar, antworten alle Pfade unter `/:siteKey` mit `200 { "aktiv": false }`. Appwrite nicht erreichbar: `503 { "fehler": { "_": "Gerade nicht erreichbar" } }`. Die Antworten enthalten nie interne IDs.
| Methode und Pfad | Zweck |
| --- | --- |
| `GET /:siteKey/config` | Betrieb (Name, Telefon, Adresse, URLs von Impressum, Datenschutz und AGB), Anrede, Farbe, Leistungen mit Knopftext, Personen, Regeln, Zahlungsart, Stornotext, `widerruf`, Texte der Haken und der Widerrufsbelehrung, Datenschutz-Hinweis, `formToken`. Eine Minute gecacht; das Widget lädt sie nach 100 Minuten neu. |
| `GET /:siteKey/tage?leistung=&ressource=&monat=YYYY-MM` | Tage mit freien Zeiten plus Kalenderdaten des Monats (`monat`) |
| `GET /:siteKey/zeiten?leistung=&ressource=&tag=YYYY-MM-DD` | freie Startzeiten, `datumLang`, `vorzeitigNoetig` (Haken für vorzeitigen Beginn an diesem Tag) |
| `POST /:siteKey/buchungen` | Formular absenden. Antwort `201 { ok, email: "m***@gmail.com", minuten }`. Honeypot `kontakt_url` und Formular-Token (Fehlercode `neu_laden`) schützen vor Automaten. |
| `POST /:siteKey/widerruf` | Widerruf ohne Link: `{ name, email, nummer }`, Antwort immer gleich `{ ok, eingang: { datum, uhrzeit } }` |
| `POST /termin/vorschau`, `bestaetigen`, `absagen`, `verschieben`, `widerruf`, `tage`, `zeiten` | Seiten der Gäste (Token aus dem Link der E-Mail, `termin.html`) |
| `GET /kalender/:siteKey/:feedToken.ics` | Kalender-Abo des Betriebs je Person (geheimer Link aus dem Portal) |
POST-Anfragen sind nur von Seiten des Betriebs erlaubt (Host des Projekts bzw. `liveDomain` samt `www`-Gegenstück) und vom Portal selbst; andere Herkunft: `403`. Grenzen: 120 Abrufe pro Minute und 10 Buchungen pro 10 Minuten je IP, 60 Buchungen pro Stunde je Funktion. Mehr in `server/routes/publicTermine.js`.
## 9. Testen
`/embed/termine-demo.html` auf dem Portal ist eine nicht verlinkte Testseite (Strecke inline und als Knopf). Sie braucht einen Testbetrieb mit dem `siteKey` `abcdefghijklmnop`. Automatische Prüfungen: `node --test test/termineEmbedStatic.test.js` (Quelltext) und `nice node test/ui/termine-widget.ui.mjs` (Browser, eigener Testserver mit Nachbau der Datenbank; vorher `free -m`).
+46
View File
@@ -0,0 +1,46 @@
# Buchungskalender: API-Vertrag
Verbindlich für Strang 2 (Widget und Buchungsseite, bauen gegen die öffentliche Schnittstelle) und Strang 3 (Portal, baut gegen die Kunden-API). Task 6 bis 8 erfüllen ihn. Fehlerform überall `{ fehler: code, felder?: {feld: text} }`.
## Livegang-Reihenfolge
Vor dem Anheben von `UNTERKUNFT_ENABLED` über `off` zuerst `scripts/unterkunft-setup.mjs` (legt die Collections samt Index `idx_account` an) und das termine-Setup (Attribut `anhang` der Mail-Warteschlange) auf Produktion laufen lassen.
## Öffentlich (`/api/public/unterkunft/v1`)
- `GET /:siteKey` → `{ aktiv:true, anbieter:{ name, ort, datenschutzUrl }, akzentfarbe, formToken, unterkuenfte:[{ id, name, maxPersonen, mindestNaechte, checkIn, checkOut }] }` oder `{ aktiv:false }`.
- `GET /:siteKey/belegung?unterkunft=<id>&von=YYYY-MM-DD&bis=YYYY-MM-DD` → `{ belegt:[{von,bis}], heute:'YYYY-MM-DD', bisMax:'YYYY-MM-DD' }` (Quellen nicht offengelegt). Liegt `bis` hinter `bisMax` (heute + `horizontMonate`), wird es auf `bisMax` gekappt (kein 400); ein Zeitraum mit `bis < von` oder ungültigem Datum ergibt 400 `ungueltig`.
- `POST /:siteKey/anfrage` Body `{ unterkunft, von, bis, personen, name, firma, email, telefon, nachricht, formToken, kontakt_url }` → `201 { nummer }`.
- `GET /:siteKey/buchung/:id?t=` → `{ status, nummer, akzentfarbe, unterkunft:{name,checkIn,checkOut}, von, bis, naechte, personen, posten, summen, steuerart, fristAt, stornobedingungen, anbieter:{firma,inhaber,strasse,plz,ort,email,telefon,datenschutzUrl}, gast:{name,firma}, rechnungsadresse?, zahlarten:['vorort'|'online'], zahlart?, bezahlt:boolean, rechnungNummer? }`.
- `POST /:siteKey/buchung/:id?t=` nach Fristablauf: 410 `{ fehler:'frist_abgelaufen' }`. Body `{ rechnungsadresse:{name,firma,strasse,plz,ort,land}, zahlart }` → `{ status:'gebucht', rechnungNummer, zahlungUrl? }` (bei `online` direkt Checkout-URL).
- `POST /:siteKey/buchung/:id/zahlung?t=` → `{ url }`.
- `GET /:siteKey/buchung/:id/rechnung.pdf?t=&art=rechnung|storno` → PDF.
## Kunde (`/api/unterkunft`, Session, `?projectId=` steht überall in der Query, auch bei POST/PUT)
Antwortformen, die die Portal-Oberfläche erwartet (Geld in Cent, Daten `YYYY-MM-DD`; Fehler überall `{ fehler, felder? }`, nie `tokenHash`/`ipHash`):
- `GET /status` → `{ state, steuerart?, stripe:{ status:'keins'|'onboarding'|'aktiv', vorhandenesShopKonto }, onlineZahlung, unterkuenfte:Zahl, rechnungenVorhanden }`; bei `state:'off'` zusätzlich `anfrage:{ status:'offen'|'abgelehnt'|'angenommen', at, begruendung?, anfrage:{ art, einheiten, portale, nachricht } } | null`.
- `GET /einstellungen` → `{ einstellungen:{ firma, inhaber, strasse, plz, ort, email, telefon, steuernummer, ustId, iban, bic, bank, rechnungsPraefix, akzentfarbe, onlineZahlung:boolean, rechnungAuftrag:boolean }, stripe:{ status, vorhandenesShopKonto }, rechnungenVorhanden }`.
- `PUT /einstellungen` Body = genau diese Felder ohne Wrapper. `rechnungAuftrag` (boolean) leitet der Server aus `rechnungAuftragAt` ab: `true` setzt es, falls leer, `false` widerruft (null). `onlineZahlung` nur `true`, wenn Stripe aktiv ist; der Präfix ist bei vorhandenen Rechnungen nicht änderbar. Feldfehler als `felder.<name>`.
- `GET /unterkuenfte` → `{ unterkuenfte:[{ id, name, beschreibung, maxPersonen, mindestNaechte, checkIn, checkOut, sort, aktiv, icalImporte:[{ id, name, url, letzterAbrufAt, letzterErfolgAt, fehler }], icalExportUrl }] }` (`icalExportUrl` = volle URL mit Token).
- `POST /unterkuenfte`, `PUT /unterkuenfte/:id` Body `{ name, beschreibung, maxPersonen, mindestNaechte, checkIn, checkOut, sort, aktiv }` → `{ unterkunft }`; Deaktivieren = `PUT` mit `aktiv:false`. `DELETE /unterkuenfte/:id`.
- `GET|PUT /unterkuenfte/:id/preise` → `{ preise:{ staffeln:[{abNaechten,preisProNachtCent,personenCent?}], reinigungCent, personenInklusive, aufpreisPersonNachtCent, direktrabattProzent, steuerart:'kleinunternehmer'|'regel', ustUebernachtung, ustReinigung, fristTage, stornobedingungen } | null }`; PUT sendet genau diese Felder.
- Preismodell „pauschal“ (ohne `personenCent`): `preisProNachtCent` gilt für `personenInklusive` Gäste, jede weitere Person kostet `aufpreisPersonNachtCent` je Nacht (Posten „Zusatzperson“).
- Preismodell „je Personenzahl“: jede Staffel trägt `personenCent:[c1,c2,…,ck]` = Preis pro Nacht für die ganze Unterkunft bei 1, 2, …, k Gästen (k = 2 bis 10, Werte ganze Cent 100 bis 100000). Entweder haben alle Staffeln `personenCent` mit gleicher Länge k oder keine; sonst 400 `felder.staffeln`. Der Server setzt `preisProNachtCent = personenCent[0]` (eine mitgeschickte Zahl wird überschrieben). Eine Staffel gilt weiterhin ab ihrer Nächtezahl für alle Nächte. Der Posten heißt „Übernachtung, 7 Nächte, 3 Personen“; `personenInklusive` wird ignoriert. Bei mehr als k Gästen gilt `ck` plus der Posten „Zusatzperson“ (`aufpreisPersonNachtCent`, nur wenn größer 0) für die Gäste über k. Der Direktrabatt gilt auf Übernachtung und Zusatzpersonen.
- Kein neues Schema: `personenCent` liegt im JSON-Text `ukPreise.staffeln` (2000 Zeichen; der Höchstfall 6 Staffeln × 10 Werte braucht rund 800).
- `GET /unterkuenfte/:id/kalender?von&bis` → `{ bereiche:[{ von, bis (Abreisetag, exklusiv), quelle:'gebucht'|'reserviert'|'ical'|'manuell' (auch 'ical:<importId>', 'anfrage:<id>' zählt als gebucht), anfrageNummer?, sperreId?, notiz? }] }`.
- `POST /unterkuenfte/:id/sperren` `{ von, bis, notiz }` → 201, 409 `zeitraum_belegt`; `DELETE /sperren/:id`.
- `POST /unterkuenfte/:id/ical-importe` `{ name, url }` (nur https; Feldfehler `felder.url`) → 201; `DELETE /unterkuenfte/:id/ical-importe/:importId`; `POST /unterkuenfte/:id/ical-export/erneuern`.
- `GET /anfragen?filter=offen|gebucht|alle` → `{ anfragen:[{ id, nummer, von, bis, naechte, personen, gast:{name}, status, summen:{bruttoCent}|null, unterkunftName }] }`.
- `GET /anfragen/:id` → `{ anfrage:{ id, nummer, status, von, bis, naechte, personen, unterkunftId, unterkunftName, gast:{name,firma,email,telefon,nachricht}, rechnungsadresse|null, posten|null, summen|null, steuerart, zahlart|null, bezahltVia|null, bezahltAt|null, erstattetCent, rechnungId|null, rechnungNummer|null, stornoRechnungId|null, fristAt|null, nachrichtVermieter }, vorschlag:{ posten, summen } }` (`vorschlag` = `berechnePreis`).
- `POST /anfragen/:id/freigeben` `{ posten, fristTage, nachricht }` → `{ anfrage, buchungsLink }`; `POST /anfragen/:id/ablehnen` `{ text }`; `POST /anfragen/:id/bezahlt`; `POST /anfragen/:id/stornieren` `{ grund, erstattungCent }` (502 `erstattung_fehlgeschlagen`, wenn die Stornierung galt, die Erstattung aber scheiterte).
- `POST /preisvorschau` `{ unterkunftId, von, bis, personen }` → `{ naechte, posten, summen:{ bruttoCent } }` (rechnet mit den gespeicherten Preisen).
- `GET /rechnungen` → `{ rechnungen:[{ id, nummer, datum, art:'rechnung'|'storno', gastName, bruttoCent (Storno negativ), anfrageNummer? }] }`; `GET /rechnungen/:id.pdf` (nur Einzel-PDF, kein ZIP, Spec N7).
- `POST /stripe/verbinden` → `{ url }`; `POST /stripe/vorhandenes-konto` und `POST /stripe/aktualisieren` → `{ stripe:{ status } }`.
- `POST /anfrage-funktion` `{ art:'ferienwohnung'|'monteurzimmer'|'pension'|'sonstiges' ('monteur' wird als Alias angenommen), einheiten:1-999, portale:Liste aus 'booking'|'airbnb', nachricht }` → `{ anfrage }`; Feldfehler `felder.art|einheiten|portale|nachricht`; 409, wenn schon eine Anfrage läuft. `POST /anfrage-funktion/zurueckziehen`.
## Team (`/api/team/unterkunft`)
- `GET /:customerId` → `{ projects:[{ projectId, name, state, anfrage:{ status:''|'offen'|'angenommen'|'abgelehnt'|'zurueckgezogen', at, art:'ferienwohnung'|'monteurzimmer'|'pension'|'sonstiges', einheiten, portale:boolean, nachricht, entschiedenAt, entschiedenVon, begruendung }, stripe, unterkuenfte:Zahl, offeneAnfragen:Zahl }] }` (Hülle wie `GET /api/team/shop/:customerId`).
- `POST /:customerId/anfrage` `{ projectId, entscheidung:'annehmen'|'ablehnen', begruendung }` → `{ ok, anfrage, mailFehler? }`.
@@ -0,0 +1,57 @@
# Mustertext: Datenschutzerklärung der Kundenwebsite (Buchungskalender)
**Muster – keine Rechtsberatung.** Entwurf, Prüfung durch Kanzlei offen (`docs/rechtliches/unterkunft-kanzlei-fragen.md`, Frage U12). Der Absatz gehört in die Datenschutzerklärung jeder Kundenwebsite, auf der der Buchungskalender läuft. Der Vermieter ist Verantwortlicher und verantwortet seine Datenschutzerklärung selbst; WEBklar liefert nur diesen Baustein (Leistungsbeschreibung Buchungskalender Nr. 5 Abs. 10 und Nr. 6). Bau das Widget erst ein, wenn der Absatz in der Datenschutzerklärung steht.
Hinweise für das Team:
- Liegt die Datenschutzerklärung in der Website, die WEBklar betreut, fügen wir den Absatz beim Einbau ein (gehört zur Einrichtung). Liegt sie woanders, bekommt der Vermieter den Text im Chat und fügt ihn selbst ein.
- Platzhalter in eckigen Klammern durch die Angaben des Vermieters ersetzen. Nicht Zutreffendes streichen: Den Absatz „Zahlung über Stripe“ nur, wenn der Vermieter die Online-Zahlung eingeschaltet hat; den Satz zu anderen Plattformen nur, wenn er Kalender einbindet.
- Anrede und Ton an die übrige Erklärung der Website anpassen (meist Sie). Die Nummerierung der Überschrift der Website beachten.
- Wenn der Vermieter im Impressum oder in der Erklärung einen Datenschutzbeauftragten nennt, dort dazuschreiben.
- Die Löschfristen entsprechen dem AVV Buchungskalender Nr. 9 und Abschnitt 6f der Datenschutzerklärung von WEBklar (project.webklar.com/datenschutz.html, nach dem Livegang). Ändert sich dort etwas, hier mitändern. Die Frist für Server-Protokolle (14 Tage) steht in Abschnitt 3 dieser Erklärung.
- Das Widget setzt keine Cookies.
- Im sichtbaren Text der Kundenwebsite die Wünsche des Vermieters beachten (etwa keine Gedankenstriche).
---
## [Nummer]. Anfragen und Buchungen über unseren Buchungskalender
### Verantwortlicher und technischer Betrieb
Verantwortlich für die Verarbeitung Ihrer Daten bei einer Anfrage oder Buchung ist [Name des Vermieters, Anschrift, E-Mail-Adresse], also der Vermieter. Den Buchungskalender betreibt technisch die WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, in unserem Auftrag. Sie verarbeitet Ihre Daten nur nach unseren Weisungen und nicht für eigene Zwecke (Auftragsverarbeitung nach Art. 28 DSGVO). Der Server steht bei der Hetzner Online GmbH in Falkenstein (Deutschland); der Versand der E-Mails zu Ihrer Anfrage und Buchung läuft über den E-Mail-Dienst der Hetzner Online GmbH in Nürnberg (Deutschland).
### Welche Daten wir bei einer Anfrage und Buchung verarbeiten
Wenn Sie über den Buchungskalender anfragen, verarbeiten wir die gewünschte Unterkunft, den Zeitraum und die Zahl der Personen, Ihren Namen, Ihre E-Mail-Adresse und, wenn Sie es angeben, Ihre Firma, Ihre Telefonnummer und Ihre Nachricht. Bitte schreiben Sie keine Angaben zu Ihrer Gesundheit oder anderen besonders schutzbedürftigen Umständen in die Nachricht. Die Verarbeitung ist nötig, um Ihre Anfrage zu beantworten und gegebenenfalls einen Vertrag mit Ihnen zu schließen (Art. 6 Abs. 1 lit. b DSGVO). Ohne diese Angaben können wir Ihre Anfrage nicht bearbeiten. Eine Anfrage ist unverbindlich. Geben wir sie frei, erhalten Sie eine E-Mail mit dem Preis, den Stornobedingungen und einem Link zur Buchungsseite; erst dort buchen Sie verbindlich.
Wenn Sie buchen, verarbeiten wir zusätzlich Ihre Rechnungsadresse (Name oder Firma, Straße, Postleitzahl, Ort, Land), die gewählte Zahlart, die Buchung mit ihrem Stand (zum Beispiel gebucht, bezahlt, storniert) und die Rechnung. Die Rechnung wird in unserem Namen und in unserem Auftrag von der WEBklar GbR erstellt (§ 14 Abs. 2 Satz 4 UStG) und Ihnen als PDF mit der Buchungsbestätigung gesendet.
Außerdem speichern wir in der Anfrage einen Hashwert Ihrer IP-Adresse (nicht die IP-Adresse selbst; er wird mit einem geheimen Schlüssel des Servers berechnet, sodass sich daraus keine IP-Adresse ableiten lässt). Wir begrenzen damit die Zahl der Anfragen je IP-Adresse und schützen den Buchungskalender so vor automatisierten Massenanfragen. Das ist unser berechtigtes Interesse (Art. 6 Abs. 1 lit. f DSGVO); Sie können dieser Verarbeitung widersprechen (Art. 21 DSGVO). Wir verwenden den Hashwert nur dafür und entfernen ihn, sobald wir Ihre Anfrage freigeben oder ablehnen, spätestens nach 30 Tagen.
### Zahlung über Stripe
[Nur aufnehmen, wenn die Online-Zahlung eingeschaltet ist.] Wenn Sie online zahlen, werden Sie auf die Zahlungsseite der Stripe Payments Europe, Limited, 1 Grand Canal Street Lower, Grand Canal Dock, Dublin, D02 H210, Irland („Stripe“) weitergeleitet. Die Zahlung geht direkt auf unser Stripe-Konto. Online können Sie derzeit nur per Karte zahlen (auch mit Apple Pay und Google Pay), nicht per SEPA-Lastschrift. Kartendaten geben Sie nur dort ein; wir und der technische Betreiber des Buchungskalenders erhalten sie nicht. Beim Start der Zahlung übergibt der technische Betreiber Stripe die Bezeichnung des Aufenthalts mit der Rechnungsnummer, den Betrag, die Kennungen der Buchung, Ihre E-Mail-Adresse (zur Vorbelegung der Zahlungsseite) und als Rücksprungadresse den geheimen Link zur Buchungsseite; dieser Link ist damit für Stripe und in unserem Stripe-Dashboard sichtbar, gilt aber nur bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage) und öffnet nur Ihre eine Buchungsseite. Geht eine Zahlung erst nach einer Stornierung oder nach dem Ablauf der Reservierung ein oder zahlen Sie eine bereits bezahlte Buchung ein zweites Mal, wird der Betrag automatisch in voller Höhe an Sie zurückerstattet. Stripe verarbeitet Ihre Daten teils im Rahmen der Zahlungsabwicklung für uns und teils als eigener Verantwortlicher, zum Beispiel zur Betrugsvorbeugung und zur Erfüllung gesetzlicher Pflichten; dafür gilt die Datenschutzerklärung von Stripe (stripe.com/de/privacy). Dabei können Daten an die Stripe, LLC in den USA übermittelt werden. Die Stripe, LLC ist nach dem EU-US Data Privacy Framework zertifiziert, für das ein Angemessenheitsbeschluss der EU-Kommission besteht. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO. Zahlen Sie vor Ort, entfällt dieser Absatz für Sie.
### E-Mails zu Ihrer Anfrage und Buchung
Zu Ihrer Anfrage und Buchung senden wir Ihnen E-Mails an die angegebene E-Mail-Adresse: eine Bestätigung, dass Ihre Anfrage eingegangen ist, die Freigabe mit dem Link zur Buchungsseite oder die Absage, die Buchungsbestätigung mit den Vertragsangaben, den Stornobedingungen und der Rechnung, die Bestätigung eines Zahlungseingangs, einen Hinweis, wenn eine Reservierung abgelaufen ist, und bei einer Stornierung die Stornorechnung. Das sind Mitteilungen zur Abwicklung Ihres Vertrags (Art. 6 Abs. 1 lit. b DSGVO), keine Werbung. Bis zum Versand liegt jede E-Mail in einer Warteschlange; nach dem Versand wird Empfängeradresse und Inhalt dort gelöscht, der Eintrag selbst nach sieben Tagen. Der Link zur Buchungsseite enthält ein geheimes Kennzeichen, das wir nur als Prüfsumme speichern und das bei einer Online-Zahlung an Stripe übergeben wird (siehe oben); behandeln Sie ihn vertraulich.
### Besuch der Seite
Beim Aufruf der Seite lädt Ihr Browser das Programm des Buchungskalenders und die Belegung von der Adresse project.webklar.com. Dabei werden Ihre IP-Adresse, Datum und Uhrzeit des Aufrufs sowie Angaben zu Ihrem Browser und Betriebssystem übertragen und für 14 Tage in Server-Protokollen gespeichert, um den sicheren und störungsfreien Betrieb zu gewährleisten (Art. 6 Abs. 1 lit. f DSGVO). Der Buchungskalender setzt keine Cookies. Zum Schutz vor Missbrauch begrenzen wir die Zahl der Anfragen je IP-Adresse; die Zeitpunkte der Aufrufe werden nur im Arbeitsspeicher des Servers gehalten. Mit Ihrer Anfrage wird Ihre IP-Adresse nicht gespeichert, sondern nur der oben beschriebene Hashwert.
### Abgleich mit anderen Plattformen
[Nur aufnehmen, wenn der Vermieter Kalender einbindet.] Wir gleichen die Belegung unserer Unterkünfte mit [Booking.com, Airbnb, …] über Kalenderdateien (iCal) ab, damit eine Unterkunft nicht doppelt vergeben wird. Dabei werden nur belegte Zeiträume ausgetauscht, keine Namen oder anderen Angaben zu Gästen.
### Weitergabe
Wir geben Ihre Daten nur weiter, soweit das für die Vertragsabwicklung nötig ist (technischer Betreiber des Buchungskalenders als Auftragsverarbeiter, Stripe bei Online-Zahlung) oder wir gesetzlich dazu verpflichtet sind (zum Beispiel an das Finanzamt oder, soweit vorgeschrieben, an die Meldebehörde). Eine Weitergabe zu Werbezwecken findet nicht statt.
### Speicherdauer
Anfragen, aus denen keine Buchung wird, anonymisiert das System nach sechs Monaten. Für gebuchte Aufenthalte sind wir als Vermieter verpflichtet, Rechnungen und Buchungsbelege nach Handels- und Steuerrecht aufzubewahren (in der Regel acht bis zehn Jahre, § 257 HGB, § 147 AO; Art. 6 Abs. 1 lit. c DSGVO); dafür bewahrt das System die Rechnungen und die Rechnungsadresse acht Jahre auf, solange der Buchungskalender läuft. Name, Firma, E-Mail-Adresse, Telefonnummer und Nachricht eines gebuchten Aufenthalts werden drei Jahre nach dem Ende des Jahres Ihrer Abreise anonymisiert.
### Ihre Rechte
Sie haben das Recht auf Auskunft über Ihre Daten (Art. 15 DSGVO), auf Berichtigung (Art. 16), Löschung (Art. 17), Einschränkung der Verarbeitung (Art. 18), Datenübertragbarkeit (Art. 20) und auf Widerspruch gegen Verarbeitungen, die auf Art. 6 Abs. 1 lit. f DSGVO beruhen (Art. 21). Wenden Sie sich dazu an uns unter [E-Mail-Adresse des Vermieters]; wir leiten Ihre Anfrage, soweit nötig, an den technischen Betreiber des Buchungskalenders weiter. Außerdem können Sie sich bei einer Datenschutz-Aufsichtsbehörde beschweren, etwa bei der für uns zuständigen Behörde [zuständige Aufsichtsbehörde des Vermieters].
+91
View File
@@ -0,0 +1,91 @@
# Buchungskalender auf der Kundenwebsite einbauen
Anleitung für das WEBklar-Team. Das Widget lässt Besucher auf der Website des Kunden („Vermieter“) einen Zeitraum im Kalender wählen und eine unverbindliche Anfrage senden. Unterkünfte, Preise, Anfragen, Rechnungen und die Verbindung zu Stripe pflegt der Vermieter im Kundenportal (Tab „Funktionen“, Kachel „Buchungskalender“, Reiter „Buchungen“). Den `siteKey` (16 Zeichen, a–z und 2–7) findest Du im Einrichtungs-Ticket und im Ticketsystem (WOMS): Kunde → Reiter „Funktionen“, Zeile Buchungskalender. Im Ticket stehen Snippet und Schritte schon fertig; dieses Dokument ist die Referenz dahinter. Ohne aktive Funktion oder ohne freigeschaltete Unterkunft ist nichts buchbar: Das Widget zeigt dann den Ausweichtext (Abschnitt 4). API-Vertrag: `docs/funktionen/unterkunft-api.md`.
Der Buchungskalender ist an den Vertrag über den Betrieb der Website gekoppelt (Leistungsbeschreibung Buchungskalender Nr. 10), aber **nicht in der Software**: Das Portal schaltet ihn nicht von selbst ab. Das Abschalten zum Vertragsende machen wir von Hand (Abschnitt 8).
## 1. Das Snippet
```html
<div data-webklar-unterkunft="SITEKEY" data-api="https://project.webklar.com">
<p>Der Buchungskalender ist gerade nicht erreichbar. Bitte schreiben Sie uns: info@beispiel.de</p> <!-- Ausweichtext, Abschnitt 4 -->
</div>
<script src="https://project.webklar.com/embed/unterkunft.js" defer></script>
```
Das Skript steht einmal pro Seite, vor `</body>`. Es hat keine Abhängigkeiten, setzt keine Cookies, lädt nichts von Dritten und fragt nur `project.webklar.com` an. Texte des Vermieters setzt das Skript nur als Text ein, nie als HTML. Das Widget rechnet nie selbst: Preise, Summen und belegte Nächte kommen fertig vom Server (die Anfrage ist unverbindlich; der Preis wird erst bei der Freigabe festgelegt).
| Attribut | Pflicht | Bedeutung |
| --- | --- | --- |
| `data-webklar-unterkunft` | ja | der `siteKey` des Projekts |
| `data-api` | nein | Herkunft des Portals, Standard `https://project.webklar.com`. Bei Modul-Import, Tag-Manager oder nachgeladenem Skript ist `data-api` Pflicht. |
## 2. Wohin damit
1. Auf der Seite einen **Bereich „Anfrage“** (Anker `#anfrage` oder eine eigene Seite) anlegen und das Snippet an die Stelle des Formulars setzen. Ein bisheriges Anfrageformular wird ersetzt; Telefon und E-Mail des Vermieters stehen unter dem Kalender. Das `div` braucht Breite (normaler Blockcontainer; in einem Flex- oder Grid-Element ohne Breite schrumpft es auf null).
2. Layout: Das Widget zeigt zwei Spalten (Kalender links, Formular rechts) und unter 760 px Breite beide untereinander; der Kalender zeigt einen Monat, ab 1100 px Breite zwei.
3. Menüpunkt oder Knopf „Anfragen“ auf den Bereich verlinken.
4. **Mehrere Widgets** auf einer Seite sind nicht vorgesehen. Der Buchungskalender ist ein Widget je Website; mehrere Unterkünfte wählt der Gast im Widget (bei nur einer Unterkunft entfällt die Auswahl).
Aussehen: Das Widget nimmt die Akzentfarbe aus den Einstellungen des Vermieters. Die Stile stehen in `@layer wku`, jede Regel der Kundenseite gewinnt also. Anpassen per CSS-Variablen auf dem `div`, in der Seiten-CSS oder auf `:root`, zum Beispiel `[data-webklar-unterkunft]{ --wku-akzent:#8a5a2b; --wku-akzent-text:#fff; }`:
| Variable | Standard |
| --- | --- |
| `--wku-akzent` und `--wku-akzent-text` (Auswahl, Knöpfe; setze beide zusammen) | Akzentfarbe des Vermieters, sonst Standardblau und `#fff` |
| `--wku-hintergrund` | `#fff` |
| `--wku-text` | `#1f2937` |
| `--wku-rand` | `#858f9e` |
| `--wku-radius` | `10px` |
| `--wku-schrift`, `--wku-schrift-titel` | Schrift der Seite |
Kontrast (WCAG 2.1 AA): Text auf dem Hintergrund und Schrift auf dem Akzent mindestens 4,5 : 1, Rand gegen den Hintergrund mindestens 3 : 1. Prüfe beim Abnehmen, ob globale Regeln des Themes (`button{…}`, `input{…}`) das Widget stören, und überschreibe sie gezielt (`[data-webklar-unterkunft] .wku-btn{ … }`).
## 3. Datenschutz-Absatz (Pflicht vor dem Einbau)
Bau das Widget erst ein, wenn der Absatz aus `docs/funktionen/unterkunft-datenschutz-absatz.md` in der Datenschutzerklärung der Website steht. Liegt sie in der Website, die WEBklar betreut, fügst Du ihn ein (gehört zur Einrichtung), Platzhalter und nicht Zutreffendes angepasst. Liegt sie woanders, schickst Du ihn dem Vermieter im Chat (Leistungsbeschreibung Nr. 5 Abs. 10). Dasselbe gilt für Impressum und Verlinkung der Datenschutzerklärung: Die Adresse der Datenschutzerklärung trägt der Vermieter in den Einstellungen ein; das Widget und die Buchungsseite verlinken sie.
## 4. Ausweichtext
Der Inhalt des `div` bleibt stehen, solange das Widget nicht lädt oder die Funktion nicht aktiv ist (zum Beispiel vor der Freischaltung, bei `UNTERKUNFT_ENABLED=off`, bei Wartung). Schreib dort Kontaktdaten des Vermieters hinein, damit niemand ohne Möglichkeit zur Anfrage bleibt. Ein aktives Widget ersetzt den Inhalt.
## 5. Vor der Abnahme prüfen
- Im Portal: mindestens eine aktive Unterkunft mit Preisen und Stornobedingungen, Firmendaten vollständig, Steuernummer oder USt-ID, Steuerart gewählt. Sonst ist die Funktion nicht freischaltbar.
- Der Datenschutz-Absatz steht in der Datenschutzerklärung.
- Auf dem Handy und am Desktop: Anreise und Abreise wählbar, belegte Nächte nicht wählbar, Mindestnächte und Höchstzahl der Personen werden geprüft, Anfrage senden zeigt die Nummer.
- Tastatur: Pfeiltasten im Kalender, Enter wählt; jeder Tag hat ein `aria-label` mit Datum und Status.
- Kein Konflikt mit globalen Stilen des Themes (Abschnitt 2).
- Testanfrage mit eigener Adresse: E-Mail G16 (Eingang) beim Gast, K36 beim Vermieter.
## 6. Vorschau und Testanfrage
1. Vorschau-Subdomain des Projekts (`<subdomain>.project.webklar.com`) ansehen.
2. Anfrage senden, im Portal unter „Buchungen → Anfragen“ öffnen, Preis prüfen, **Freigeben** (E-Mail G17 mit Buchungslink).
3. Buchungslink öffnen: Rechnungsadresse eintragen, Zahlart „vor Ort“ wählen, **„Zahlungspflichtig buchen“**. E-Mail G19 mit Rechnungs-PDF prüfen (Rechnung trägt Name und Daten des Vermieters, keinen WEBklar-Hinweis).
4. Online-Zahlung nur testen, wenn das Stripe-Konto verbunden ist (kleiner Betrag, danach im Portal stornieren und erstatten).
5. Im Portal „bezahlt (vor Ort)“ setzen, dann stornieren (Stornorechnung, E-Mail G22).
6. Testanfragen danach im Portal stornieren oder ablehnen; Sperren des Tests im Kalender entfernen.
## 7. Was das Widget zeigt (Kurzüberblick)
- Kalender mit belegten Nächten (durchgestrichen); vergangene Tage und Zeiträume über eine belegte Nacht hinweg sind nicht wählbar. Anreise am Abreisetag eines anderen Gastes ist möglich.
- Zusammenfassung des Zeitraums („12.–30. Nov. · 18 Nächte“), Personenzahl, Name, E-Mail, Telefon und Nachricht (optional), Firma (optional), Datenschutzhinweis.
- Schaltfläche „Anfrage senden“ (unverbindlich). Danach die Anfragenummer und der Hinweis, dass der Vermieter sich meldet.
- Die Buchung selbst läuft nicht im Widget, sondern auf der Buchungsseite bei WEBklar (Link in der E-Mail G17).
## 8. Wenn der Vertrag über die Website endet (Abschalten von Hand)
Der Buchungskalender läuft, solange WEBklar die Website des Vermieters betreibt (Leistungsbeschreibung Nr. 10). Die Software prüft das **nicht**. Du musst es selbst im Blick haben, wenn ein solcher Vertrag endet, gekündigt oder widerrufen wird:
1. **Vorher** schreibst Du dem Vermieter eine E-Mail (oder Chat): Der Vertrag endet am TT.MM.JJJJ, damit endet auch der Buchungskalender. Er soll **seine Rechnungen als PDF herunterladen** (WEBklar löscht sie 90 Tage nach dem Abschalten), offene Anfragen bearbeiten und seine Gäste informieren.
2. **Zum Vertragsende** im Ticketsystem (WOMS): Kunde → Reiter „Funktionen“ → Buchungskalender → **„Abschalten“**. Danach nimmt das Widget keine Anfragen mehr an und zeigt den Ausweichtext; Reservierungen laufen aus.
3. Der Vermieter kann Anfragen, Buchungen und Rechnungen danach **90 Tage** lang im Portal lesen und die Rechnungen herunterladen. Danach löscht der tägliche Job automatisch alle Daten des Buchungskalenders; das Einrichtungs-Ticket bleibt als Arbeitsnachweis ohne Gästedaten.
4. Nimm nach den 90 Tagen das Snippet und den Menüpunkt aus der Website.
5. Beginnt **innerhalb der 90 Tage** wieder ein Vertrag über den Betrieb der Website, schaltest Du den Buchungskalender auf Wunsch des Vermieters in Textform ohne weitere Kosten wieder ein (WOMS → Funktionen → Buchungskalender → Aktivieren).
6. **Erstattung (Leistungsbeschreibung Nr. 10 Abs. 6):** Endet der Vertrag, bevor wir den Buchungskalender freigeschaltet haben, erstatten wir den gezahlten Einrichtungspreis (199 €) vollständig, in Stripe von Hand. Sonst gibt es keine Erstattung.
Das Stripe-Konto des Vermieters bleibt in jedem Fall beim Vermieter; wir rühren es nicht an.
## 9. Kalenderabgleich (iCal) einrichten
Im Portal unter Buchungen → Unterkünfte & Preise → Kalenderabgleich: Der Vermieter trägt die iCal-Adresse von Booking.com oder Airbnb als Import ein (Name und `https`-Adresse) und kopiert die **Export-Adresse** des Buchungskalenders zu Booking.com („Kalender synchronisieren“) oder Airbnb. Der Abgleich ist zeitversetzt (Minuten bis Stunden, Leistungsbeschreibung Nr. 4); sag das dem Vermieter, damit er vor jeder Freigabe seine anderen Plattformen prüft.
+125
View File
@@ -0,0 +1,125 @@
# Buchungskalender – Livegang
Reihenfolge für den Inhaber. Jeder Schritt braucht seine Freigabe; nichts davon hat Claude gegen Produktion, das Live-Stripe-Konto oder die Website Donau Home ausgeführt. Code: Portal `feature/unterkunft`, WOMS `feature/unterkunft`. Erste Kundin ist Mimi (Donau Home, MIMI FEWO, Website `WEBklar/donau-home`, lokaler Klon `/root/reports/donau-home-website`).
**Wichtig beim Pushen:** Den Feature-Zweig nie unter seinem eigenen Namen pushen. Der Portal-Webhook deployt jeden Zweig. Immer mit Zielzweig:
- Portal: `git push origin feature/unterkunft:main`
- WOMS: `git push origin feature/unterkunft:test`
## 0. Voraussetzung
- TP2, Webshop und Öffnungszeiten-Zweig liegen schon auf `origin/main` (Portal) bzw. `test` (WOMS) oder werden vorher gemerged: Das Funktionen-Register, die Mail-Warteschlange (`termineOutbox`) und die Connect-Anbindung stammen von dort. `git merge origin/main` im Worktree, Tests laufen lassen. Erwartbare Konflikte: `server/config.js` (features), `public/funktionen.js`, `test/mailTemplates.test.js` (Vorlagen-IDs: K36 bis K35, G16 bis G22, T23 sind für den Buchungskalender vergeben, vor dem Merge gegen `main` prüfen).
- Die Rechtstexte der Fassung 2026-10-07 liegen als HTML und PDF im Zweig (`public/rechtliches/*-2026-10-07.*`, siehe Schritt 6).
## 1. Inhaber prüft die Texte
Vom Inhaber freizugeben, bevor irgendetwas live geht:
- `docs/rechtliches/leistungsbeschreibung-unterkunft.md` und `avv-unterkunft.md` (beide „Entwurf – Prüfung durch Kanzlei offen“),
- `docs/rechtliches/unterkunft-datenschutz-6f-entwurf.md`,
- `docs/funktionen/unterkunft-datenschutz-absatz.md` und `unterkunft-storno-muster.md`,
- `docs/rechtliches/unterkunft-kanzlei-fragen.md` (Fragen U1 bis U12 für die Kanzlei).
**Achtung Widerrufsbelehrung:** Die Datei `widerrufsbelehrung-funktionen-2026-10-07.*` ist eine Kopie des gemeinsamen Textes der Funktionen (Quelle `docs/rechtliches/widerrufsbelehrung-funktionen.md`). Ändert sich dieser Text (neue Fassung beim Terminplaner oder Webshop), braucht der Buchungskalender eine neue Fassung mit neuem Datum; eine ausgelieferte Datei wird nie an Ort und Stelle neu gebaut.
Ändert sich ein Text, gibt es eine neue Fassung (neues Datum in `config.unterkunft.fassung` und im Zustimmungs-Pin der Bestelltexte); ausgelieferte Fassungen werden nie überschrieben.
## 2. Datenbank (nach dem Portal-Deploy, vor dem Pilot)
**Zeitpunkt:** Die Skripte laufen mit dem NEUEN Code, also erst nach dem Portal-Deploy (Schritt 5) und vor dem Einschalten des Piloten (`UNTERKUNFT_ENABLED=pilot`, Schritt 4 Teil 2), oder vorher aus dem Worktree `dev-unterkunft` (`node scripts/…` mit denselben Umgebungswerten gegen die Produktionsdatenbank). Der Schalter bleibt bis zum grünen `--check` auf `off`. Reihenfolge der Skripte: erst `termine-setup.mjs --check` und anwenden (Attribut `anhang`), dann `unterkunft-setup.mjs`:
```bash
cd /home/webklar/apps/kundenbereich.webklar.com/build
docker exec -w /app webklar-kundenbereich-server node scripts/termine-setup.mjs --check
docker exec -w /app webklar-kundenbereich-server node scripts/termine-setup.mjs # zieht das Attribut anhang der Mail-Warteschlange nach (Rechnung als Anhang)
docker exec -w /app webklar-kundenbereich-server node scripts/unterkunft-setup.mjs --check
docker exec -w /app webklar-kundenbereich-server node scripts/unterkunft-setup.mjs # legt ukUnterkuenfte, ukPreise, ukSperren, ukAnfragen, ukRechnungen, ukEinstellungen samt Indizes an
```
Wichtige Indizes (ohne sie sind Suche, Eindeutigkeit und Abfragen unsicher oder langsam; das Skript legt sie an, `--check` meldet fehlende): `idx_account` (ukEinstellungen, stripeAccountId), `idx_export_token` (ukUnterkuenfte, icalExportToken), `uniq_feature_nummer` (ukRechnungen, featureId+nummer, **unique**: verhindert doppelte Rechnungsnummern), `idx_bis` (ukSperren und ukAnfragen, Belegungsabfrage). Fehlt `termineOutbox.anhang`, meldet `unterkunft-setup.mjs --check` es ebenfalls (Exit 1) und der normale Lauf legt es an.
Die Skripte müssen aus dem neuen Code laufen (siehe Zeitpunkt oben). `termine-setup.mjs` ändert nichts an vorhandenen Daten, es fügt nur fehlende Attribute und Indizes hinzu.
## 3. Stripe-Preis (ebenfalls nach dem Portal-Deploy oder aus dem Worktree, vor dem Pilot)
```bash
docker exec -w /app webklar-kundenbereich-server node scripts/stripe-products.mjs --pruefen
docker exec -w /app webklar-kundenbereich-server node scripts/stripe-products.mjs --anlegen # legt unterkunftSetup (199 €, Lookup-Key webklar_unterkunft_einrichtung_19900) an
docker exec -w /app webklar-kundenbereich-server node scripts/stripe-products.mjs --pruefen # Preis-Lookup nach --anlegen prüfen
```
Der vorhandene Connect-Webhook (`https://project.webklar.com/api/shop/stripe-webhook`) bleibt unverändert; er leitet nach dem Metadatum `wkUk` an den Buchungskalender weiter. Kontrolle im Stripe-Dashboard: Die Ereignisse `account.updated`, `checkout.session.completed`, `checkout.session.async_payment_succeeded`, `checkout.session.async_payment_failed`, `checkout.session.expired` und `charge.refunded` sind schon eingetragen (Webshop-Livegang).
## 4. Server-Konfiguration (Pilot)
Sicherungen zuerst: `cp /home/webklar/apps/docker-compose.yml /home/webklar/apps/docker-compose.yml.bak-2026-10-07-unterkunft` und `(umask 077; cp /home/webklar/apps/.env /home/webklar/apps/.env.bak-2026-10-07-unterkunft)` (`.env` enthält Zugangsdaten).
**Teil 1 (vor dem Push):** In `/home/webklar/apps/.env` zunächst nur
```
UNTERKUNFT_ENABLED=off
UNTERKUNFT_PILOT_EMAILS=kenso.gri@gmail.com,<Mimis Portal-Adresse>
```
In `/home/webklar/apps/docker-compose.yml` beim Dienst `kundenbereich-server` neben `ZEITEN_ENABLED` und `SHOP_ENABLED` zwei Zeilen:
```
- UNTERKUNFT_ENABLED=${UNTERKUNFT_ENABLED:-off}
- UNTERKUNFT_PILOT_EMAILS=${UNTERKUNFT_PILOT_EMAILS:-}
```
**Teil 2 (nach Push, grünem Portal-Deploy und Schritt 2/3):** `UNTERKUNFT_ENABLED=pilot` setzen und Compose neu anwenden (derselbe Befehl).
Prüfen mit `docker compose -f /home/webklar/apps/docker-compose.yml config -q` (keine Ausgabe = gültig), dann übernehmen mit `docker compose -f /home/webklar/apps/docker-compose.yml up -d --no-deps kundenbereich-server`. Ein bloßes `docker restart` liest neue Umgebungswerte nicht. Das alles vor dem Push (Schritt 5): Der Container läuft dann mit dem alten Code und den neuen Werten (die kennt er nicht, harmlos), und der Neustart durch den Deploy nach dem Push ist der einzige mit dem neuen Code. Rückweg: `UNTERKUNFT_ENABLED=off`, Compose neu anwenden; das Schema ist rein additiv, es gibt nichts zurückzubauen.
## 5. Push (durch den Inhaber)
- Portal: `git push origin feature/unterkunft:main` (der Webhook deployt),
- WOMS: `git push origin feature/unterkunft:test` **erst nach grünem Portal-Deploy** (das WOMS ruft die neue Portal-Schnittstelle, z. B. das Feld `unterkunft` der Funktionen-Antwort, auf).
Nie `git push origin feature/unterkunft` (eigener Name). Danach (Portal) prüfen: `https://project.webklar.com/embed/unterkunft.js` antwortet 200; `https://project.webklar.com/rechtliches/leistungsbeschreibung-unterkunft.html` und `…/avv-unterkunft.html` antworten 200.
## 6. Rechtstexte in WOMS „Rechtliches“ hochladen (macht Claude)
Das macht Claude selbst (Wunsch des Inhabers). Dafür legt der Inhaber vorher den **kurzlebigen** Appwrite-Schlüssel mit dem Recht für Rechtliches unter `/root/.appwrite-rechtliches.key` ab und löscht ihn danach. Claude legt (falls nötig, `scripts/setup-legal.mjs` im WOMS) die Karten „Leistungsbeschreibung Buchungskalender“ und „AVV Buchungskalender“ an und lädt die Fassung 2026-10-07 hoch:
- `public/rechtliches/leistungsbeschreibung-unterkunft-2026-10-07.pdf`
- `public/rechtliches/avv-unterkunft-2026-10-07.pdf`
(Die Widerrufsbelehrung `widerrufsbelehrung-funktionen-2026-10-07.pdf` ist ein Anhang der Bestellmail und der Bestellseite; sie ist inhaltlich die der Funktionen.)
## 7. Mimi freischalten und Donau Home einbauen
**Für Mimi ist der Buchungskalender kostenlos** (Entscheidung des Inhabers, 2026-10-07): keine Funktions-Anfrage, keine Bestellung und keine Zahlung der 199 €, kein Stripe-Checkout für die Einrichtung; freigeschaltet wird nur über „Aktivieren (ohne Zahlung)“. In der Textform-Nachricht an Mimi ausdrücklich schreiben, dass die Einrichtung für sie kostenlos ist (abweichend vom Preis in der Leistungsbeschreibung, Abschnitt „9. Preis, Abrechnung und Widerruf“ Abs. 1), damit keine Zahlungspflicht entsteht.
0. **Vorher:** die Zustimmung von Mimi in Textform einholen (Chat im Kundenportal oder E-Mail: Leistungsbeschreibung und AVV Buchungskalender in der Fassung 2026-10-07). Datum und Weg trägt das Team beim Aktivieren im WOMS ein; ohne beides schaltet das Portal nicht ein. Das Portal speichert den Nachweis und schickt Mimi die Unterlagen als PDF (K16).
1. WOMS → Kunde (Mimi) → Funktionen → Buchungskalender → **„Aktivieren (ohne Zahlung)“** (`action: 'activate'`; der Weg für Kunden, die die Funktion als Teil ihres Projekts bekommen; verlangt Datum und Weg der Zustimmung in Textform, auch im Pilot).
2. Mimi richtet im Portal ein: Unterkünfte, Preise, Stornobedingungen (Muster `docs/funktionen/unterkunft-storno-muster.md`), Firmendaten, Steuerart, optional Stripe; mit dem Team zusammen.
3. **siteKey in Donau Home:** den `siteKey` aus dem Ticket oder dem WOMS in `/root/reports/donau-home-website/index.html` im Abschnitt `#anfrage` einsetzen (`data-webklar-unterkunft="SITEKEY"`), das Skript `https://project.webklar.com/embed/unterkunft.js` einbinden, CSS-Variablen (`--wku-*`) auf Palette A (Eiche & Leinen) und Schrift Figtree stellen, den Datenschutz-Absatz in die Datenschutzerklärung der Website einfügen, den Einleitungstext anpassen. Einbau nach `docs/funktionen/unterkunft-einbau.md`.
4. Donau Home pushen (Inhaber, nicht Claude); die Vorschau zeigt das Widget, solange `UNTERKUNFT_ENABLED=pilot` mit Mimis siteKey läuft.
## 8. Pilot mit Mimi
1. Testanfrage im Widget (eigene Adresse): Mails G16 und K36 prüfen.
2. Im Portal freigeben (G17), Buchungslink öffnen, „vor Ort“ buchen (G19 mit Rechnung als PDF, K37).
3. Rechnung prüfen: Mimis Name und Anschrift, Steuernummer oder USt-ID, Nummer lückenlos, Steuerart, kein WEBklar-Hinweis.
4. Mit Stripe-Konto: Online-Zahlung mit kleinem Betrag testen (G20), dann stornieren mit Erstattung (G22, Stornorechnung).
5. Stornieren (Stornorechnung), Testbuchungen wieder entfernen; iCal-Import und -Export mit einem Test-Kalender prüfen.
## 9. Datenschutz § 6f übernehmen (VOR dem Pilotstart, also vor Schritt 8)
Den Abschnitt 6f aus `docs/rechtliches/unterkunft-datenschutz-6f-entwurf.md` in `public/datenschutz.html` übernehmen (hinter 6d, vor Abschnitt 7; Abschnitt 12 anpassen), danach `FORCE=1 scripts/build-legal-pdfs.sh datenschutz`. Erst mit der Freigabe des Inhabers, aber vor dem ersten Gast-Test mit Mimi: Die Anfrageformulare verweisen auf die Datenschutzerklärung.
## 10. Freigabe für alle
- Kanzlei-Fragen (`unterkunft-kanzlei-fragen.md`) beantworten lassen, Befund einarbeiten (neue Fassung, falls sich Texte ändern; neue Texte hochladen wie in Schritt 6).
- Dann `UNTERKUNFT_ENABLED=on` in `.env`, Compose neu anwenden (wie Schritt 4).
## PDFs der Rechtstexte erzeugen
Die PDFs entstehen aus den HTML-Seiten auf dem Host mit Chrome (Memory prüfen, `free -m` > 1500 MB verfügbar):
```bash
node --max-old-space-size=512 scripts/build-legal-html.mjs unterkunft # schreibt die HTML-Seiten
node --max-old-space-size=512 scripts/build-legal-html.mjs unterkunft --check # Exit 0 = ok
nice scripts/build-legal-pdfs.sh unterkunft # druckt die PDFs samt .sha256
```
Änderst Du einen Text, HTML neu erzeugen und die PDFs neu drucken (Prüfsumme entscheidet, was neu gedruckt wird; `FORCE=1` druckt alle neu).
@@ -0,0 +1,36 @@
# Muster: Stornobedingungen für den Buchungskalender
**Vorlage, keine Rechtsberatung.** Entwurf, Prüfung durch Kanzlei offen (`docs/rechtliches/unterkunft-kanzlei-fragen.md`, Frage U11). Der Vermieter ist für seine Stornobedingungen allein verantwortlich (Leistungsbeschreibung Buchungskalender Nr. 5 Abs. 3 und Nr. 6). Das Portal bietet diesen Text in den Einstellungen („Mustertext Stornobedingungen zum Übernehmen“) an; der Vermieter prüft ihn, passt ihn an seinen Betrieb an und kann ihn von einer Rechtsanwältin oder einem Rechtsanwalt prüfen lassen. Ohne Stornobedingungen kann der Buchungskalender nicht freigeschaltet werden.
Hinweise für das Team:
- Die Bedingungen werden bei der Freigabe einer Anfrage als Schnappschuss in der Anfrage gespeichert; die Buchungsseite und die Buchungsbestätigung zeigen genau diese Fassung. Eine spätere Änderung gilt nur für neue Freigaben.
- Der Buchungskalender erstattet eine Online-Zahlung nur auf Anweisung des Vermieters (Stornieren mit Erstattungsbetrag 0 bis Gesamtbetrag); die Staffel unten ist Text, die Software wendet sie nicht von selbst an. Der Vermieter trägt den Betrag beim Stornieren selbst ein.
- Storniert der Vermieter, schließt der Buchungskalender eine noch offene Zahlungsseite bei Stripe. Geht trotzdem eine Zahlung nach der Stornierung oder dem Ablauf der Reservierung ein, und bei einer zweiten Zahlung auf eine bezahlte Buchung, erstattet WEBklar sie automatisch in voller Höhe über das Stripe-Konto des Vermieters (dauerhafte Weisung, der Stand der Buchung bleibt). Das ist keine Erstattung nach Nr. 3 des Musters, sondern die Rückgabe einer irrtümlichen Zahlung.
- Zahlt der Gast vor Ort, entfällt eine Erstattung; die Stornierung stellt die Stornorechnung aus.
- Gäste können nicht selbst stornieren: Sie schreiben dem Vermieter. Der Text sagt das.
- Prozentsätze und Fristen sind Platzhalter und Beispiele; der Vermieter wählt eigene Werte. Pauschalen müssen den üblichen Schaden angemessen berücksichtigen (§ 309 Nr. 5 BGB); bei Verbrauchern sind überhöhte Pauschalen unwirksam.
---
## Stornobedingungen (Muster)
**1. Stornierung durch den Gast.** Eine Stornierung der Buchung ist nur in Textform möglich (E-Mail an [E-Mail-Adresse des Vermieters]) und wird erst mit unserer Bestätigung wirksam; wir stornieren die Buchung in unserem System und senden Ihnen eine Stornorechnung. Maßgeblich für die Frist ist der Eingang Ihrer Nachricht bei uns.
**2. Kosten der Stornierung.** Bei einer Stornierung berechnen wir folgenden Anteil des Gesamtpreises (ohne Endreinigung, soweit der Aufenthalt nicht angetreten wird):
- bis [30] Tage vor der Anreise: kostenfrei,
- [29] bis [14] Tage vor der Anreise: [30] Prozent,
- [13] bis [7] Tage vor der Anreise: [60] Prozent,
- ab [6] Tage vor der Anreise und bei Nichtanreise: [90] Prozent.
Ihnen bleibt der Nachweis gestattet, dass uns kein oder ein geringerer Schaden entstanden ist. Uns bleibt unser Anspruch unberührt, wenn wir die Unterkunft anderweitig vermieten können: In diesem Fall berechnen wir nur den tatsächlichen Ausfall.
**3. Erstattung.** Haben Sie bereits online bezahlt, erstatten wir den Teil des Preises, der nach Nr. 2 nicht anfällt, auf das Zahlungsmittel, mit dem Sie bezahlt haben. Die Erstattung geht über unser Stripe-Konto; die Gutschrift auf Ihrem Konto kann einige Tage dauern. Haben Sie noch nicht bezahlt, zahlen Sie den nach Nr. 2 anfallenden Betrag vor Ort oder nach Rechnung.
**4. Verspätete Anreise und vorzeitige Abreise.** Reisen Sie später an oder früher ab, besteht kein Anspruch auf Erstattung des Preises für die nicht genutzten Nächte, es sei denn, wir können die Unterkunft anderweitig vermieten.
**5. Stornierung durch uns.** Können wir die Unterkunft aus einem Grund, den wir zu vertreten haben, nicht bereitstellen, stornieren wir die Buchung, bieten nach Möglichkeit eine gleichwertige Unterkunft an und erstatten bereits gezahlte Beträge vollständig.
**6. Kein Widerrufsrecht.** Für die Buchung zu einem bestimmten Zeitraum besteht kein gesetzliches Widerrufsrecht (§ 312g Abs. 2 Nr. 9 BGB). Diese Stornobedingungen gelten stattdessen.
**7. Reiserücktrittsversicherung.** Wir empfehlen Ihnen eine Reiserücktrittsversicherung.
@@ -0,0 +1,48 @@
# Mustertext: Datenschutzerklärung der Kundenwebsite (News-Pinnwand, Öffnungszeiten live)
Entwurf, Freigabe Nutzer ausstehend. Der Absatz gehört in die Datenschutzerklärung jeder Kundenwebsite, auf der die News-Pinnwand läuft. Bau die Pinnwand erst ein, wenn er dort steht. Dasselbe gilt für „Öffnungszeiten live“: Der Absatz „Öffnungszeiten auf dieser Website“ (je Variante unten) gehört in die Erklärung jeder Website mit dieser Funktion; bau die Funktion erst ein, wenn er dort steht (Einbau-Anleitung: `docs/funktionen/zeiten-einbau.md`).
Hinweise für das Team:
- Variante A, wenn WEBklar die Website hostet (Abo). Variante B, wenn die Website woanders liegt und die Pinnwand von `project.webklar.com` lädt.
- Im sichtbaren Text der Kundenwebsite keine Gedankenstriche (Wunsch von Emma's Schatzkiste). Den Text unverändert übernehmen oder ohne Gedankenstriche anpassen.
- Anrede und Ton an die übrige Erklärung der Website anpassen (meist Sie). `[Überschrift]`-Nummerierung der Website beachten.
- Die Löschfrist von 14 Tagen für Server-Protokolle entspricht Abschnitt 3 der Datenschutzerklärung von WEBklar (project.webklar.com/datenschutz.html). Ändert sich dort die Frist, hier mitändern.
- Beide Varianten setzen voraus, dass das Skript keine Cookies setzt und nichts im Browser speichert. Das gilt für `news.js` (nur lesende Abrufe) und für `zeiten.js` (ein Abruf je Seite, keine Anfragen an Dritte).
- „Öffnungszeiten auf dieser Website“: den Absatz und nur die Bausteine übernehmen, die die Seite wirklich zeigt. Der Baustein „Route“ gehört dazu, wenn die Kontakt-Leiste den Knopf „Route“ hat, der Baustein „WhatsApp“, wenn sie den Knopf „WhatsApp“ hat. Der WhatsApp-Baustein ist der „WhatsApp-Hinweis“, den der Kunde im Portal mit einem Haken bestätigt, bevor die WhatsApp-Schaltfläche erscheint. Beide Dienste öffnen sich erst nach dem Klick des Besuchers. Anschriften von Google und WhatsApp vor der Übernahme prüfen. Auch hier gilt der Entwurfsstatus: erst nach der Freigabe durch den Inhaber (Rechtsrunde „Öffnungszeiten live“) verwenden.
## Variante A: Website wird von WEBklar gehostet
### Neuigkeiten auf dieser Website
Auf dieser Website zeigen wir unter „Neuigkeiten“ kurze Beiträge mit Text und Fotos. Beim Aufruf der Seite lädt Ihr Browser diese Beiträge und Bilder von dem Server, den die WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, in unserem Auftrag betreibt und der bei der Hetzner Online GmbH in Falkenstein (Deutschland) steht. Dabei werden Ihre IP-Adresse, Datum und Uhrzeit des Aufrufs sowie Angaben zu Ihrem Browser und Betriebssystem übertragen und kurzzeitig in Server-Protokollen gespeichert. Die Pinnwand setzt keine Cookies und speichert nichts in Ihrem Browser. Wir brauchen diese Daten, um die Beiträge auszuliefern und den Betrieb sicher zu halten. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO. Unser berechtigtes Interesse liegt in der Darstellung aktueller Informationen und im sicheren, funktionsfähigen Betrieb der Website. Die Protokolle werden nach 14 Tagen gelöscht.
### Öffnungszeiten auf dieser Website
Auf dieser Website zeigen wir unsere Öffnungszeiten und ob wir gerade geöffnet haben. Dazu können Hinweise, zum Beispiel zu Betriebsurlaub, am Seitenanfang und auf dem Handy eine Leiste mit Knöpfen zum Anrufen und für die Route kommen. Beim Aufruf der Seite lädt Ihr Browser ein kleines Programm und unsere Öffnungszeiten von dem Server, den die WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, in unserem Auftrag betreibt und der bei der Hetzner Online GmbH in Falkenstein (Deutschland) steht. Dabei werden Ihre IP-Adresse, Datum und Uhrzeit des Aufrufs sowie Angaben zu Ihrem Browser und Betriebssystem übertragen und kurzzeitig in Server-Protokollen gespeichert. Das Programm setzt keine Cookies und speichert nichts in Ihrem Browser. Wir brauchen diese Daten, um die Öffnungszeiten auszuliefern und den Betrieb sicher zu halten. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO. Unser berechtigtes Interesse liegt in der Darstellung aktueller Öffnungszeiten und im sicheren, funktionsfähigen Betrieb der Website. Die Protokolle werden nach 14 Tagen gelöscht. Für Suchmaschinen stehen unsere Öffnungszeiten außerdem in maschinenlesbarer Form im Seitencode.
#### Baustein Route (nur aufnehmen, wenn die Leiste den Knopf „Route“ zeigt)
Der Knopf „Route“ ist ein Link zu Google Maps. Erst wenn Sie darauf klicken, öffnet sich Google Maps (Google Ireland Limited, Gordon House, Barrow Street, Dublin 4, Irland) mit unserer Adresse als Ziel. Vorher werden keine Daten an Google übertragen. Nach dem Klick gelten die Datenschutzhinweise von Google.
#### Baustein WhatsApp (nur aufnehmen, wenn die Leiste den Knopf „WhatsApp“ zeigt)
Der Knopf „WhatsApp“ ist ein Link zu WhatsApp. Erst wenn Sie darauf klicken, öffnet sich WhatsApp (WhatsApp Ireland Limited, Merrion Road, Dublin 4, D04 X2K5, Irland, ein Unternehmen von Meta) mit unserer Nummer und gegebenenfalls einem vorgeschlagenen Starttext Ihrer Nachricht. Vorher werden keine Daten an WhatsApp oder Meta übertragen. Die Nachricht senden Sie selbst ab; nach dem Klick gelten die Datenschutzhinweise von WhatsApp.
## Variante B: Website liegt bei einem anderen Anbieter
### Neuigkeiten auf dieser Website
Auf dieser Website zeigen wir unter „Neuigkeiten“ kurze Beiträge mit Text und Fotos. Diese Beiträge und Bilder lädt Ihr Browser beim Aufruf der Seite von project.webklar.com, einem Server der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, der bei der Hetzner Online GmbH in Falkenstein (Deutschland) steht. Dabei werden Ihre IP-Adresse, Datum und Uhrzeit des Aufrufs sowie Angaben zu Ihrem Browser und Betriebssystem an project.webklar.com übertragen und kurzzeitig in Server-Protokollen gespeichert. Die Pinnwand setzt keine Cookies und speichert nichts in Ihrem Browser. Wir nutzen den Dienst, damit wir Ihnen aktuelle Informationen zeigen können, ohne die Website jedes Mal neu zu veröffentlichen. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO. Unser berechtigtes Interesse liegt in der einfachen und aktuellen Darstellung unserer Neuigkeiten. Die Protokolle werden nach 14 Tagen gelöscht. WEBklar verarbeitet die Daten in unserem Auftrag. [Prüfen: AVV mit WEBklar erforderlich]
### Öffnungszeiten auf dieser Website
Auf dieser Website zeigen wir unsere Öffnungszeiten und ob wir gerade geöffnet haben. Dazu können Hinweise, zum Beispiel zu Betriebsurlaub, am Seitenanfang und auf dem Handy eine Leiste mit Knöpfen zum Anrufen und für die Route kommen. Diese Angaben lädt Ihr Browser beim Aufruf der Seite als kleines Programm und als Öffnungszeiten von project.webklar.com, einem Server der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, der bei der Hetzner Online GmbH in Falkenstein (Deutschland) steht. Dabei werden Ihre IP-Adresse, Datum und Uhrzeit des Aufrufs sowie Angaben zu Ihrem Browser und Betriebssystem an project.webklar.com übertragen und kurzzeitig in Server-Protokollen gespeichert. Das Programm setzt keine Cookies und speichert nichts in Ihrem Browser. Wir nutzen den Dienst, damit unsere Öffnungszeiten auf der Website überall gleich und aktuell erscheinen, ohne dass wir die Website bei jeder Änderung neu veröffentlichen müssen. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO. Unser berechtigtes Interesse liegt in der einfachen und aktuellen Darstellung unserer Öffnungszeiten. Die Protokolle werden nach 14 Tagen gelöscht. Für Suchmaschinen stehen unsere Öffnungszeiten außerdem in maschinenlesbarer Form im Seitencode. WEBklar verarbeitet die Daten in unserem Auftrag. [Prüfen: AVV mit WEBklar erforderlich]
#### Baustein Route (nur aufnehmen, wenn die Leiste den Knopf „Route“ zeigt)
Der Knopf „Route“ ist ein Link zu Google Maps. Erst wenn Sie darauf klicken, öffnet sich Google Maps (Google Ireland Limited, Gordon House, Barrow Street, Dublin 4, Irland) mit unserer Adresse als Ziel. Vorher werden keine Daten an Google übertragen. Nach dem Klick gelten die Datenschutzhinweise von Google.
#### Baustein WhatsApp (nur aufnehmen, wenn die Leiste den Knopf „WhatsApp“ zeigt)
Der Knopf „WhatsApp“ ist ein Link zu WhatsApp. Erst wenn Sie darauf klicken, öffnet sich WhatsApp (WhatsApp Ireland Limited, Merrion Road, Dublin 4, D04 X2K5, Irland, ein Unternehmen von Meta) mit unserer Nummer und gegebenenfalls einem vorgeschlagenen Starttext Ihrer Nachricht. Vorher werden keine Daten an WhatsApp oder Meta übertragen. Die Nachricht senden Sie selbst ab; nach dem Klick gelten die Datenschutzhinweise von WhatsApp.
+123
View File
@@ -0,0 +1,123 @@
# Öffnungszeiten live auf der Kundenwebsite einbauen
Anleitung für das WEBklar-Team. „Öffnungszeiten live“ zeigt die Zeiten, die der Kunde im Portal einträgt (Tab „Funktionen“, Kachel „Öffnungszeiten live“), auf seiner Website: als Tabelle, als Live-Status und als Kurzanzeige, dazu eine Hinweis-Leiste, auf dem Handy eine Kontakt-Leiste und Google-Angaben (JSON-LD) im Seitencode. Den `siteKey` (16 Zeichen, a–z und 2–7) findest Du im WOMS: Kunde → Reiter „Funktionen“, Zeile „Öffnungszeiten live“ (mit Kopierknopf). Im Einrichtungs-Ticket „Öffnungszeiten live einbauen – <Projekt>“ stehen Einbau-Zeile, Ergebnis des automatischen Einbaus (`einbau`), Beispiele der Platzhalter mit den Plan-Kurznamen und die Prüfliste. Dieses Dokument ist die Referenz dahinter. Die Datenschutz-Absätze für die Kundenseite stehen in `docs/funktionen/website-datenschutz-absatz.md`.
Die Spec ist `docs/superpowers/specs/2026-10-04-oeffnungszeiten-live-design.md` (Abschnitte 4.2, 4.3, 4.5, 4.6).
## 1. Was automatisch geschieht und was Du tust
| Teil | Wer | Wie |
| --- | --- | --- |
| Einbau-Zeile (Skript) | Runner, automatisch | beim nächsten Veröffentlichen in jede HTML-Datei vor `</head>`, nur wenn die Funktion in `setup` oder `active` steht |
| Hinweis-Leiste, Kontakt-Leiste, JSON-LD | Skript | kommen mit den Daten, ohne Platzhalter |
| Platzhalter (Tabelle, Status, Badge) mit Ersatz-HTML | Du | an die Stellen setzen, an denen feste Zeiten stehen, und die festen Zeiten ersetzen |
| Datenschutz-Absatz auf der Website | Du | Abschnitt 8, Punkt 5 |
| Einbau-Zeile bei `einbau: manuell` oder wenn die Seite woanders liegt | Du | Abschnitt 9 |
Der Einbau ist **ein Einbau je Website** und ist im Preis (30 € oder im Wartungs-Abo ohne Aufpreis) enthalten. Er wird nicht auf das Stundenkontingent angerechnet. Sonderwünsche (zusätzliche Stellen, Sondergestaltung) gehen über ein Angebot oder, mit Abo, über Stunden.
## 2. Die Einbau-Zeile
```html
<script data-wk-zeiten src="https://project.webklar.com/embed/zeiten.js" data-site="SITEKEY" defer></script>
```
- Ins `<head>` jeder HTML-Seite, einmal pro Seite. Der Runner setzt sie vor `</head>` ein (liegt kein `</head>` vor, nach `<body>`); eine schon vorhandene Zeile mit `data-wk-zeiten` ersetzt er, der Einbau ist also wiederholbar und doppelt nichts.
- `data-site` ist der `siteKey`. Optional `data-api="https://project.webklar.com"` (nur am Script-Tag, nie an einem Platzhalter); ohne Angabe nimmt das Skript die Herkunft seiner eigenen Adresse (das klappt nur bei einem normalen `<script src>`; bei Tag-Manager oder nachgeladenem Skript ist `data-api` Pflicht). Den siteKey darf auch ein Platzhalter tragen (`data-webklar-zeiten="SITEKEY"`).
- Die Adresse des Skripts kommt aus der Runner-Variable `ZEITEN_PUBLIC_BASE` (Vorgabe `https://project.webklar.com/embed/`, muss mit `https://` beginnen und mit `/` enden). Für die Produktion nichts ändern.
- Das Skript hat keine Abhängigkeiten, setzt keine Cookies (keine Cookies, kein `localStorage`, kein `sessionStorage`), fragt nur `project.webklar.com` ab und lädt nichts von Dritten. Kundentexte setzt es nur als Text ein, nie als HTML. Es wird 5 Minuten gecacht und darf von jeder Domain geladen werden. Ein Abruf je Seite für alle Teile, danach höchstens alle 10 Minuten.
- Hat die Seite eine Content-Security-Policy, muss `project.webklar.com` in `script-src` und `connect-src` stehen, und `style-src` muss Inline-Stile erlauben (`'unsafe-inline'`): das Skript legt ein `<style>`-Element an (Tabelle, Status, Leisten). Ohne diese Erlaubnis erscheinen sie ohne Gestaltung.
## 3. Platzhalter
Ein Platzhalter ist ein Element mit dem Attribut `data-webklar-zeiten`. Innen steht das Ersatz-HTML (Abschnitt 4).
```html
<div data-webklar-zeiten data-modus="tabelle" data-plan="oeffnungszeiten">…Ersatz-HTML…</div>
<div data-webklar-zeiten data-modus="status" data-plan="oeffnungszeiten">Mo–Fr 9–18 Uhr</div>
<span data-webklar-zeiten data-modus="badge" data-plan="oeffnungszeiten">Mo–Fr 9–18 Uhr</span>
```
| `data-modus` | Zeigt | Gedacht für |
| --- | --- | --- |
| `tabelle` | Wochentabelle (echtes `<table>` mit Caption und Zeilenköpfen, heutiger Tag mit `aria-current="date"`), darunter „Abweichungen in den nächsten 14 Tagen“ | Kontaktseite, Footer |
| `status` | Punkt und Text, zum Beispiel „Jetzt geöffnet · schließt um 18 Uhr“, ohne `aria-live` | Kopfbereich, Hero |
| `badge` | Kurzform „Geöffnet“, „Geschlossen“, „Nach Vereinbarung“ | Menü, Header |
- `data-plan` ist der **Kurzname** des Zeitplans (Kleinbuchstaben, Ziffern und Bindestrich, 1–24 Zeichen, nach dem Anlegen unveränderlich). Der Hauptplan heißt `oeffnungszeiten`. Weitere Pläne (zum Beispiel `kueche`, `lieferung`) hat der Kunde im Editor angelegt; die Kurznamen stehen im Editor und im Einrichtungs-Ticket („Pläne (Kurznamen für data-plan)“). Ohne `data-plan` gilt der Hauptplan.
- **Einzelseiten-Apps (Vite/React):** Das Skript beobachtet `<body>` und füllt Platzhalter, die erst nach einem Seitenwechsel auf die Seite kommen, nach etwa 0,2 Sekunden (auch einen wieder eingehängten Knoten). Setze Platzhalter nicht in Teile, die die App beim Rendern entfernt oder neu aufbaut: ein neu gebauter Knoten startet wieder mit seinem Ersatz-HTML, und was das Skript hineingeschrieben hat, kann die App überschreiben.
- Ein **unbekannter** `data-plan` oder `data-modus` lässt das Ersatz-HTML stehen und schreibt eine Warnung in die Konsole. Es gibt kein stilles Umschalten auf den Hauptplan. Bei Fehlern, ohne JavaScript, bei Daten älter als 24 Stunden und nach dem Abschalten bleibt ebenfalls das Ersatz-HTML.
- Das Skript füllt die Platzhalter ab `setup`, damit Du vor dem Schließen des Tickets prüfen kannst.
- Gestaltung: Light DOM, alle Klassen beginnen mit `wkz-`, die Stile stehen in `@layer wkz`. **Jede** Regel der Kundenseite (ohne Layer) gewinnt also gegen die Standard-Stile. Schrift und Farben erbt die Seite. Variablen (alle optional, am Platzhalter oder auf `:root`): `--wkz-offen`, `--wkz-zu`, `--wkz-unklar` (Farben der Punkte), `--wkz-offen-hell`, `--wkz-zu-hell`, `--wkz-unklar-hell`, `--wkz-offen-text`, `--wkz-zu-text`, `--wkz-unklar-text` (Badge), `--wkz-rand`, `--wkz-heute`. Kontrast mindestens 4,5 : 1 prüfen.
- Hat das Theme globale Regeln wie `table{…}` oder `td{…}`, greifen sie auch in der Tabelle. Prüfe das bei der Abnahme und überschreibe gezielt, zum Beispiel `[data-webklar-zeiten] table{ … }`.
## 4. Ersatz-HTML
Jeder Platzhalter behält eine **statische Ersatz-Tabelle**: Sie steht ohne JavaScript, bei einem Fehler und nach der Abschaltung. Ohne sie wäre die Stelle leer.
1. WOMS → Funktionen → Ersatz-HTML kopieren (je Plan ein Knopf; bei mehreren Plänen mit Namen dahinter). Erst nach dem ersten Speichern des Kunden; vorher meldet das WOMS „Der Kunde hat noch keine Zeiten eingetragen“ (die Schnittstelle antwortet 409: `GET /api/team/funktionen/:customerId/zeiten/ersatz?projectId=…&plan=<kurz>`).
2. In jeden Platzhalter einsetzen. Das Ersatz-HTML ist eine schlichte Wochentabelle als reines ASCII mit Entities (`&auml;`, `&#8211;`), also in jeder Dateikodierung sicher. Ein Fensterende um Mitternacht steht als `00:00` („18:00–00:00“), ein Fenster von 00:00 bis 24:00 als „rund um die Uhr“.
3. Die Tabelle zeigt den Wochenplan **zum Zeitpunkt des Einbaus**. Ändert der Kunde seinen Wochenplan später, bleibt das Ersatz-HTML zunächst stehen (mit Skript unsichtbar).
4. **Beim Abschalten** der Funktion, wenn der aktuelle Wochenplan des Hauptplans vom Stand beim Einbau abweicht, entsteht ein WOMS-Ticket **„Ersatz-Tabelle aktualisieren – <Projekt>“** mit dem aktuellen Ersatz-HTML. Ersetze die Tabelle in allen Platzhaltern (gehört zum enthaltenen Einbau, Leistungsbeschreibung Nr. 4 Abs. 3). Die Mail K30 sagt dem Kunden, dass wieder feste Zeiten auf der Seite stehen. Liegt die Seite woanders, schicke dem Kunden den Text.
## 5. Header-Selektor (`navSelektor`)
Die Hinweis-Leiste steht als erstes Kind von `<body>` im Fluss und schiebt den Inhalt nach unten. Feste und klebende Elemente mit `top: 0` rückt das Skript, solange die Leiste sichtbar ist, um `max(0, Leistenhöhe − Scrollposition)` nach unten (Inline-`top`, beim Entfernen der Leiste zurückgesetzt) und erhöht `scroll-padding-top`.
Überlappt die Leiste trotzdem den Kopf der Seite (zum Beispiel ein fester Header mit `top: 12px`, mit `transform` oder in einem Wrapper), setze im WOMS bei Kunde → Funktionen → „Öffnungszeiten live“ das Feld **Header-Selektor** (Team-Einstellung `navSelektor`):
- ein CSS-Selektor des Headers, höchstens 120 Zeichen, erlaubt sind nur `A–Z a–z 0–9`, Leerzeichen und `# . - _ > : [ ] = " '`, nicht erlaubt `<`, `{`, `}` und `;`. Beispiel: `header.site-header` oder `#nav`.
- Das Skript wendet ihn mit `querySelectorAll` in einem `try/catch` an; ein falscher Selektor bricht nichts, er bewirkt nur nichts.
- Leer lassen = Standard (nur `top: 0`). Speichern mit `teamVersion` (bei 409 „Inzwischen geändert – bitte neu laden“).
- Prüfe nach dem Setzen: oben auf der Seite, beim Scrollen (die Leiste scrollt weg, der Header klebt wieder oben) und auf dem Handy.
## 6. Leisten ausschalten
Im selben Bereich (WOMS → Funktionen → „Öffnungszeiten live“) gibt es zwei Haken: **`kontaktleisteAus`** („Kontakt-Leiste aus“) und **`hinweisleisteAus`** („Hinweis-Leiste aus“). Sie gelten je Website.
- Die **Kontakt-Leiste** erscheint nur bei `(max-width: 768px)`, unten fest mit `z-index: 40`, Knöpfe „Anrufen“, „Route“ und, wenn der Kunde sie eingerichtet hat, „WhatsApp“. Hat die Seite selbst ein festes Element am unteren Rand mit einem `a[href^="tel:"]`, bleibt sie **automatisch aus**. Setze den Haken, wenn die Seite eine eigene Anruf-Leiste hat, die das Skript nicht erkennt, oder der Kunde die Leiste nicht möchte.
- WhatsApp und die Route (Google Maps) öffnen erst beim Klick. Den Haken „Ich habe den WhatsApp-Hinweis in meine Datenschutzerklärung aufgenommen“ setzt der Kunde im Portal; in der Kundenansicht setzt ihn kein Mitarbeiter.
- Die **Hinweis-Leiste** zeigt zu jedem Zeitpunkt den gültigen Hinweis mit dem höchsten Rang (Wichtig, Urlaub, Aktion, Info). Sie hat keinen Schließen-Knopf (das Skript speichert nichts im Browser).
## 7. JSON-LD
Das Skript pflegt die Öffnungszeiten im Seitencode: Gibt es einen `application/ld+json`-Block mit einem Knoten vom Typ `LocalBusiness` oder einem Untertyp (auch in `@graph`), ersetzt es dort **nur** `openingHoursSpecification` und `specialOpeningHoursSpecification` (alles andere im Block bleibt); unlesbare Blöcke bleiben unberührt. Gibt es keinen, legt es einen eigenen Block `[data-wkz-jsonld]` mit `name`, `address` und `url` an. Prüfe auf der Seite, ob JSON-LD mit festen Öffnungszeiten an einer anderen Stelle steht (zum Beispiel als Mikrodaten im Footer); das würde dem Skript widersprechen.
## 8. Prüfliste
Das Einrichtungs-Ticket nennt dieselben fünf Punkte:
1. **Feste Zeiten im Repo suchen und ersetzen**: `grep -ril "öffnungszeit" .` und Ähnliches (`geöffnet`, `Mo-Fr`, `Uhr`), auch Footer, Kontaktseite, `<meta>` und eigene Status-Skripte. Jede Stelle wird ein Platzhalter mit Ersatz-HTML oder fällt weg.
2. **Header**: Überlappt die Hinweis-Leiste den Kopf der Seite? Sonst `navSelektor` setzen (Abschnitt 5).
3. **Anruf-Leiste**: Hat die Seite schon eine eigene feste Anruf-Leiste? Dann `kontaktleisteAus` (Abschnitt 6), damit sie nicht doppelt erscheint.
4. **Vorhandenes JSON-LD** (Öffnungszeiten der Seite) prüfen, damit nichts doppelt oder widersprüchlich steht (Abschnitt 7).
5. **Datenschutz-Absatz auf der Website**: den Absatz „Öffnungszeiten auf dieser Website“ aus `docs/funktionen/website-datenschutz-absatz.md` einsetzen (Variante A bei WEBklar-Hosting, Variante B bei Fremdhosting). Die Bausteine „Route“ und „WhatsApp“ nur, wenn die Kontakt-Leiste die Knöpfe zeigt. Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, setzt Du den Absatz ein (gehört zum Einbau, nicht auf das Stundenkontingent anrechnen); liegt sie woanders (externer Generator), schicke dem Kunden den Textbaustein, er fügt ihn selbst ein. Die Texte sind Entwurf, bis die Rechtsrunde sie freigibt.
Zur Abnahme außerdem: Seite in der Vorschau öffnen und mit ausgeschaltetem JavaScript ansehen (Ersatz-Tabelle muss stehen), Konsole auf Warnungen zu `data-plan` prüfen, auf einem Handy (höchstens 768 px Breite) die Kontakt-Leiste, den Regler „Uhrzeit testen“ im Editor gegen die Anzeige halten, Kontrast und Tippflächen (mindestens 44 px) ansehen.
## 9. Sperrliste `ZEITEN_KEIN_NEUBAU` und Seiten ohne automatischen Einbau
**Neubau und Einsetzen.** Bei jedem Übergang in `setup` oder `active` (Einschalten im Abo, Zahlung, Aktivierung durch das Team, Rückkehr des Abos, Behalten) stößt das Portal einen Neubau der Seite an (höchstens einer gleichzeitig). Das Ergebnis steht in `einbau`: `angestossen` (Auftrag angenommen, die Zeile erscheint nach dem Veröffentlichen) oder `einbau: manuell`. Änderungen an Zeiten und Leisten brauchen keinen Neubau.
`einbau: manuell` heißt: kein Repo, Vorschau aus, Dev-Modus, Projekt auf der Sperrliste, Fehler oder Runner ausgelastet. Dann setzt Du die Einbau-Zeile von Hand ins Repo.
**Sperrliste.** `ZEITEN_KEIN_NEUBAU` (Umgebungsvariable des Portals, Liste von Subdomains, durch Komma getrennt): Für diese Projekte gibt es weder einen angestoßenen Neubau noch das automatische Einsetzen bei normalen Deploys; die Funktion zählt sie immer als manuell. Vorgabe ist `emmas-schatzkiste` (Emmas Seiten, deren Dateien teilweise nicht in Git liegen). **Achtung:** Setzt Du die Variable, **ersetzt** sie die Vorgabe. Willst Du eine Seite zusätzlich sperren, nenne `emmas-schatzkiste` mit in der Liste. Änderung in `compose` und `.env` des Portals, danach das Portal mit `docker compose -f /home/webklar/apps/docker-compose.yml up -d --no-deps kundenbereich-server` neu erstellen (kein `docker restart`: es liest geänderte Werte nicht neu ein). Für gesperrte Seiten setzt Du die Zeile und die Platzhalter von Hand ein (im Repo, soweit die Dateien dort liegen, sonst direkt auf dem Server) und prüfst nach jedem Deploy, ob sie noch da sind.
**Seite liegt woanders** (anderer Hoster, keine WEBklar-Veröffentlichung): Das Ticket enthält die Einbau-Zeile; Du setzt sie ein, soweit WEBklar Zugriff auf die Seite hat. Hat WEBklar keinen Zugriff, schicke dem Kunden im Chat Einbau-Zeile, Platzhalter-Beispiele und Ersatz-HTML und bitte ihn, sie selbst oder durch seinen Anbieter einzufügen. Schließe das Ticket erst, wenn die Anzeige auf der Live-Seite stimmt: Öffne die Seite, sieh in den Netzwerk-Aufrufen nach der Antwort von `/api/public/zeiten/v1/<siteKey>` (`"aktiv": true`) und nach der gefüllten Tabelle.
## 10. Freischalten und Ende
- **Freischalten:** Der Job setzt `active` (und sendet K16), wenn das Ticket geschlossen ist und der Kunde Zeiten eingetragen hat (Hauptplan gespeichert, mindestens ein Tag nicht „zu“). Sonst bleibt `setup`, das WOMS zeigt „wartet auf die Zeiten des Kunden“. Solange der Kunde nichts eingetragen hat, kannst Du die Tabelle nicht einbauen.
- **Ende des Abos** (Gratisweg): K29 mit Frist zum Behalten (30 Tage), danach `disabled` und K30, Eingaben noch 30 Tage, dann gelöscht. Das Ticket „Ersatz-Tabelle aktualisieren“ gehört zum Abschalten (Abschnitt 4).
- **Abschalten durch das Team** (Grund `team`): keine automatische Löschung.
## 11. Häufige Fehlerbilder
| Symptom | Meist liegt es an |
| --- | --- |
| Es erscheint nur das Ersatz-HTML | Funktion nicht in `setup`/`active`, `einbau: manuell` und Zeile fehlt, unbekannter `data-plan`, kein JavaScript, Daten älter als 24 Stunden, CSP blockiert `project.webklar.com` |
| Hinweis-Leiste verdeckt den Header | `navSelektor` nicht gesetzt (Abschnitt 5) |
| Zwei Anruf-Leisten auf dem Handy | `kontaktleisteAus` setzen (Abschnitt 6) |
| Kontakt-Leiste fehlt auf dem Handy | Seite hat schon eine feste Anruf-Leiste unten, oder Breite über 768 px, oder `kontaktleisteAus` |
| Öffnungszeiten doppelt im Suchergebnis oder Test | festes JSON-LD an anderer Stelle (Abschnitt 7) |
| Tabelle sieht fremd aus | globale Regeln des Themes (Abschnitt 3) |
+27 -15
View File
@@ -4,21 +4,23 @@
<meta charset="utf-8">
<title>Empfehlen für WEBklar – Leitfaden</title>
<style>
@page { size: A4; margin: 13mm 16mm 12mm; }
@page { size: A4; margin: 10mm 13mm 9mm; }
* { box-sizing: border-box; }
body { margin: 0; color: #1d1d1f; font: 10pt/1.45 "DejaVu Sans", system-ui, sans-serif; }
body { margin: 0; color: #1d1d1f; font: 9.5pt/1.38 "DejaVu Sans", system-ui, sans-serif; }
.brand { font-weight: 800; letter-spacing: 0.12em; font-size: 10pt; color: #6d1fd1; }
h1 { font-size: 20pt; line-height: 1.15; margin: 3mm 0 1.5mm; }
.lead { font-size: 10.5pt; color: #444; margin: 0 0 4mm; }
h2 { font-size: 12pt; margin: 4.5mm 0 2mm; }
ol.steps { list-style: none; padding: 0; margin: 0; display: grid; gap: 3mm; }
h1 { font-size: 19pt; line-height: 1.15; margin: 2.5mm 0 1.2mm; }
.lead { font-size: 10.5pt; color: #444; margin: 0 0 2.5mm; }
h2 { font-size: 12pt; margin: 3mm 0 1.2mm; }
ol.steps { list-style: none; padding: 0; margin: 0; display: grid; gap: 1.5mm; }
ol.steps li { display: grid; grid-template-columns: 9mm 1fr; gap: 3mm; align-items: start; }
.num { width: 8mm; height: 8mm; border-radius: 50%; background: #6d1fd1; color: #fff; font-weight: 700; display: flex; align-items: center; justify-content: center; }
ol.steps strong { display: block; }
.box { border: 1px solid #e0dccf; border-radius: 3mm; padding: 3mm 4mm; margin: 3.5mm 0; background: #faf9f6; }
.box { border: 1px solid #e0dccf; border-radius: 3mm; padding: 2mm 4mm; margin: 2.5mm 0; background: #faf9f6; }
.rules { margin: 0; padding-left: 5mm; }
.rules li { margin: 1.4mm 0; }
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 4mm; }
.rules li { margin: 0.8mm 0; }
.abos { margin: 1mm 0; padding-left: 4.5mm; }
.abos li { margin: 0.6mm 0; }
.grid2 { display: grid; grid-template-columns: 1fr 1.2fr; gap: 4mm; }
.muted { color: #666; font-size: 9pt; }
.contact { font-size: 10pt; }
.do::marker { content: "✓ "; color: #2f7d3b; }
@@ -39,14 +41,24 @@
Frag den Betrieb, ob du seine Kontaktdaten an uns weitergeben darfst. Oder gib ihm unsere Kontaktdaten (unten).
Schick uns dann Name des Betriebs, Ansprechpartner, Telefon oder E-Mail, worum es kurz geht, und dass er mit der Weitergabe einverstanden ist.</div></li>
<li><span class="num">3</span><div><strong>Den Rest machen wir</strong>
Beratung, Angebot und Website übernehmen wir. Für das Wartungs-Abo bekommt der Kunde von uns einen Zugang zum Kundenportal
und schließt es dort <b>selbst online</b> ab: ohne Unterschrift, ohne Termin, Zahlung sicher über Stripe.</div></li>
Beratung, Angebot und Website übernehmen wir. Für das Hosting- oder Wartungs-Abo bekommt der Kunde von uns einen Zugang zum Kundenportal
und schließt es dort im Bereich „Verträge“ <b>selbst online</b> ab: ohne Unterschrift, ohne Termin, Zahlung sicher über Stripe.</div></li>
</ol>
<div class="box">
<strong>Das Wartungs-Abo auf einen Blick</strong><br>
50 € pro Monat, Mindestlaufzeit 12 Monate, danach monatlich kündbar. Enthalten sind Betrieb und Pflege der Website,
1 Domain, 1 E-Mail-Postfach und 1 Stunde Arbeitszeit pro Monat (sammelt sich an, jede Stunde 12 Monate gültig).
<strong>Unsere Abos auf einen Blick</strong> · beide mit Mindestlaufzeit 12 Monate, danach monatlich kündbar
<ul class="abos">
<li><b>Hosting-Abo, 20,00 € pro Monat</b> (Endpreis, gemäß § 19 UStG ohne Umsatzsteuer): nur statische Websites (ohne CMS, Login,
Datenbank oder Formulare); 1 Domain, 1 E-Mail-Postfach mit 5 GB; keine Pflege der Website und kein Stundenkontingent
(Änderungen nur nach Freigabe, gesondert berechnet).</li>
<li><b>Wartungs-Abo, 50,00 € pro Monat</b> (Endpreis, gemäß § 19 UStG ohne Umsatzsteuer): alle Websites und Web-Anwendungen; Betrieb und Pflege,
1 Domain, beliebig viele Postfächer mit zusammen 5 GB, 1 Stunde Arbeitszeit pro Monat (nicht genutzte Zeit bleibt 12 Monate verfügbar).</li>
<li><b>In beiden Abos:</b> E-Mail-Zusatzspeicher je 5 GB für 1,00 € pro Monat (Endpreis, gemäß § 19 UStG ohne Umsatzsteuer).
Außerdem kann der Kunde die freiwillige Übernahmegutschrift beantragen, wenn er Domain, Hosting oder E-Mail beim bisherigen Anbieter
im Voraus bezahlt hat: Nach unserer Prüfung verrechnen wir den doppelt bezahlten Anteil mit seinen Monatsgebühren (keine Auszahlung).</li>
<li><b>Wechsel</b> vom Hosting- ins Wartungs-Abo im Kundenportal unter „Verträge“ (Hosting-Abo läuft, nicht gekündigt, keine offene Zahlung);
es beginnt eine neue Mindestlaufzeit von 12 Monaten.</li>
</ul>
Alle Details stehen in den Abo-Bedingungen: project.webklar.com/abo-bedingungen.html
</div>
@@ -61,7 +73,7 @@
</ul>
<ul class="rules">
<li class="dont">Keine Werbe-E-Mails, WhatsApp- oder SMS-Nachrichten und keine Werbeanrufe an Leute, die dem nicht zugestimmt haben.</li>
<li class="dont">Keine Zusagen zu Preisen, Rabatten, Terminen oder Leistungen, die nicht auf webklar.com oder in unserem Angebot stehen.</li>
<li class="dont">Keine Zusagen zu Preisen, Rabatten, Terminen oder Leistungen, die nicht auf webklar.com, in den Abo-Bedingungen oder in unserem Angebot stehen.</li>
<li class="dont">Nichts für WEBklar unterschreiben, keine Verträge abschließen, kein Geld annehmen.</li>
<li class="dont">Kundendaten nicht sammeln, speichern oder anders verwenden.</li>
</ul>
@@ -0,0 +1,64 @@
# Ergänzung der Abo-Bedingungen: Website-Analyse (Vorschlag)
> **Entwurf – vor Verwendung anwaltlich prüfen lassen.** `public/abo-bedingungen.html` wird NICHT geändert; Nutzer lädt die neue Fassung in WOMS „Rechtliches“ hoch.
Bezug: aktuelle Abo-Bedingungen (Aufbau § 1 bis § 16, Leistungsbeschreibung B = Wartungs-Abo, Anlagen 1 bis 3). Die Paragraphenfolge bleibt unverändert, es entstehen keine neuen Verweise in anderen §§.
## 1. Wartungs-Abo, Nr. 1 „Umfang“: neuer Spiegelstrich (nach „Servicezeiten und Reaktion“)
```diff
tägliche Datensicherung (§ 3 Abs. 5)
1 Stunde Arbeitszeit je bezahltem Monat; nicht genutzte Zeit bleibt 12 Monate verfügbar (§ 4)
Servicezeiten und Reaktion (§ 3 Abs. 6)
+ Website-Analyse für öffentliche Websites auf Wunsch des Kunden (Leistungsbeschreibung „Website-Analyse“, § 3 Abs. 8)
```
## 2. § 3 Leistungen: neuer Absatz (8) nach Absatz (7)
```diff
+ (8) Website-Analyse
+ Im Wartungs-Abo kann der Kunde im Kundenportal für seine öffentliche, bei uns gehostete Website eine
+ Reichweitenmessung mit Cookie-Banner einschalten. Umfang, Voraussetzungen, Pflichten des Kunden und Ende
+ der Leistung ergeben sich aus der Leistungsbeschreibung „Website-Analyse“. Die Website-Analyse ist im
+ Hosting-Abo nicht enthalten. Wir schulden dafür keine bestimmte Verfügbarkeit; ein Ausfall der Analyse
+ beeinträchtigt die Website nicht. Sie endet mit dem Ausschalten durch den Kunden oder mit dem Ende des
+ Wartungs-Abos; die Daten löschen wir 60 Tage danach.
```
## 3. § 6 Pflichten des Kunden: neuer Absatz nach dem Absatz zu Impressum und Datenschutzerklärung
```diff
Für Impressum, Datenschutzerklärung und Inhalte seiner Website ist der Kunde verantwortlich. Die Angaben zum Hosting, ... stehen in den Anlagen 1 bis 3.
+ Schaltet der Kunde die Website-Analyse ein, ist er für die Messung auf seiner Website Verantwortlicher. Er
+ entfernt oder umgeht das Cookie-Banner nicht, bindet ohne Abstimmung mit uns keine weiteren Mess- oder
+ Tracking-Werkzeuge ein und ergänzt seine Datenschutzerklärung um einen Hinweis auf die von uns erzeugte
+ Zusatzseite „/datenschutz-analyse.html“.
```
## 4. § 11 Vertragsende: neuer Satz am Ende
```diff
+ Mit dem Vertragsende endet auch die Website-Analyse; wir speichern dann keine Messdaten mehr, entfernen
+ Skript und Banner beim nächsten Veröffentlichen der Website und löschen die Messdaten 60 Tage nach Abschaltung.
```
## 5. § 14 Datenschutz, Absatz (2): Ergänzung
```diff
- Soweit wir im Rahmen eines Abos personenbezogene Daten im Auftrag des Kunden verarbeiten, etwa E-Mails in seinen Postfächern, Eingaben in Kontaktformulare oder Server-Protokolle seiner Website, gilt der Auftragsverarbeitungsvertrag in Anlage 1.
+ Soweit wir im Rahmen eines Abos personenbezogene Daten im Auftrag des Kunden verarbeiten, etwa E-Mails in seinen Postfächern, Eingaben in Kontaktformulare oder Server-Protokolle seiner Website, gilt der Auftragsverarbeitungsvertrag in Anlage 1. Für die Website-Analyse gilt zusätzlich der Auftragsverarbeitungsvertrag Website-Analyse, dem der Kunde beim Einschalten im Kundenportal zustimmt.
```
## 6. Anlage 3 Unterauftragsverarbeiter: neue Zeile
```diff
Hetzner Online GmbH ... | E-Mail-Postfächer (Webhosting) | Deutschland (Rechenzentrum Nürnberg)
+ Hetzner Online GmbH ... | Server für die Website-Analyse (Messdienst, nur bei eingeschalteter Funktion) | Deutschland (Rechenzentrum Falkenstein)
```
## 7. Fassung und Datum
Neue Fassung der Abo-Bedingungen mit neuem Stand-Datum hochladen; § 15 (Änderungen dieser Bedingungen) regelt die Mitteilung. Die Website-Analyse ist eine zusätzliche Leistung ohne Aufpreis, erhöht Preise nicht und greift nicht in bestehende Leistungen ein.
<!-- Prüfpunkt Anwalt P10: Wirksame Einbeziehung der geänderten Fassung bei Bestandskunden (§ 15, § 308 Nr. 5 / § 307 BGB); Wettbewerbs-/Transparenzfragen zur kostenlosen Zusatzleistung. -->
<!-- Prüfpunkt Anwalt P14: Verweis Wartungs-Abo (Leistungsbeschreibung B) und Hosting-Abo-Ausschluss konsistent mit § 9 Wechsel. -->
+155
View File
@@ -0,0 +1,155 @@
# Abo-Bedingungen Fassung 6 (Entwurf)
Fassung 2026-10-16-v6 · Stand 16.10.2026 · Entwurf – Prüfung durch Kanzlei offen · Entwurf zur Freigabe, keine Rechtsberatung · Basis: Fassung 5 (Stand 30.09.2026, `public/vertragsunterlagen/v5/abo-bedingungen.html`)
Dieser Entwurf listet alle Änderungen gegenüber Fassung 5 im Wortlaut. Die Paragrafenfolge § 1 bis § 16 und alle Anker bleiben, damit Querverweise in Mails und der Paragraf zum Vertragsende (§ 11) stimmen. Die Sätze unter jeder Überschrift sind die Pflichtsätze; sie stehen unverändert in der Seite `public/neu/rechtliches/abo-bedingungen.html`. Zu jeder Stelle nennt die Zeile „Begründung“ die Entscheidung des Inhabers (E…), die Festlegung nach dem Builder-Check (C…) oder den Spec-Abschnitt und verweist bei offenen Punkten auf die Entscheidungsliste (`docs/rechtliches/kundenansicht-entscheidungen.md`, R… und KQ…). Voraussetzung für den Livegang ist die Freigabe durch den Inhaber (E20).
## Das Wichtigste auf einen Blick
Das Hosting-Abo enthält die Website-Analyse, das Wartungs-Abo zusätzlich News-Pinnwand, Terminplaner, Webshop, Buchungskalender und Öffnungszeiten live. Das Einschalten kostet nichts extra; die Einrichtung geht vom Stundenkontingent ab (§ 4 Abs. 8).
**Einordnung.** Neue Zeile „Funktionen“ in der Übersichtsbox, hinter der Zeile „Wartungs-Abo“. Die Box behält ihre Preiszeilen und die Anrede „Ihr“.
**Begründung.** E6 (die Funktionen stecken im Abo: Hosting-Abo mit Analyse, Wartungs-Abo mit allen Funktionen), E7 und E17 (Einrichtung vom Stundenkonto, keine Extra-Rechnung), C10 (Analyse im Hosting-Abo), Spec 5.9. Die Zeile nennt für Funktionen keinen Preis. Offen: Geltung für laufende Abos (R2, KQ1), Anrede der Rechtsdokumente (R12).
## § 1 Abs. 2
Die Bestellseiten richten sich nach der Kundenart, die wir im Kundenkonto hinterlegt haben („Betrieb“ oder „privat“). Wir hinterlegen sie nach den Angaben des Kunden; ist keine hinterlegt, gilt der Kunde als privat. Ob der Kunde Verbraucher oder Unternehmer ist, richtet sich nach dem Zweck, zu dem er den Vertrag abschließt, nicht nach dieser Angabe.
**Einordnung.** Hängt an den bestehenden Absatz 2 an („Sie gelten für Verbraucher und Unternehmer“).
**Begründung.** E2 bis E4 und C13: Die Kundenart legt das Team beim Anlegen fest, leer gilt als privat, und die Bestellseiten richten sich danach. Rechtlich entscheidet der Zweck des Vertrags (§§ 13, 14 BGB), nicht die hinterlegte Angabe; der Satz sagt das ausdrücklich, damit die Angabe nicht als Selbstauskunft oder als Rechtsfolge gelesen wird. Offen: Belastbarkeit der Kundenart (R5, KQ4).
## § 2 Abs. 2 c
Direkt über der Schaltfläche steht der Hinweis, dass diese Bedingungen mit den Leistungsbeschreibungen, dem Auftragsverarbeitungsvertrag (Anlage 1) und der Widerrufsbelehrung gelten, jeweils mit Fassung und Link; die Unterlagen erhält der Kunde nach der Bestellung zusätzlich als PDF per E-Mail. Ist die Kundenart „privat“ hinterlegt, kann der Kunde dort durch ein Kästchen verlangen, dass wir sofort mit der Leistung beginnen; ohne das Kästchen beginnen wir erst nach Ablauf der 14-tägigen Widerrufsfrist. Ist die Kundenart „Betrieb“ hinterlegt, beginnen wir mit Vertragsschluss, ohne dass der Kunde etwas markieren muss.
**Einordnung.** Ersetzt den bisherigen Buchstaben c (Zustimmung zu den Bedingungen und Wahl des Beginns). Die Buchstaben a, b, d und e bleiben.
**Begründung.** Spec 5.6, E1, E3, E4, C4 und recht.md Kurzfassung 3 und 4: Eine Linkzeile über dem Knopf ersetzt die Häkchen für Bedingungen, Leistungsbeschreibung, Auftragsverarbeitung und Belehrung; übrig bleibt das eine Häkchen „Bitte beginnt sofort …“ nur für Privatkunden in der Widerrufsfrist. Bei Betrieben (kein Widerrufsrecht) beginnt die Leistung mit Vertragsschluss. Die Kundenart kommt vom Server (§ 1 Abs. 2). Offen: Wortlaut des Häkchens ohne Normzitat (R6, KQ5), Belehrung nur per Link und PDF (R11, KQ6), AVV per Klick ohne eigenes Häkchen (R10, KQ7).
## § 2 Abs. 6
Wir speichern außerdem den Wortlaut der angezeigten Hinweise und Kästchen, die hinterlegte Kundenart sowie Zeit, IP-Adresse und Browserkennung des Klicks.
**Einordnung.** Hängt an den bestehenden Absatz 6 (Vertragssprache und Speicherung des Vertragstextes) an.
**Begründung.** Nachweise (Global Constraints der Übersicht, N34): Wortlaut, Fassung, Zeit, IP-Adresse, Browserkennung und Konto werden weiter gespeichert, ab jetzt mit der Kundenart zum Klickzeitpunkt. Der Satz macht das für den Kunden sichtbar; die Datenschutzerklärung nennt die Speicherung heute schon (Prüfpunkte D1 bis D3). Offen: Datenschutz-Prüfpunkte (R14).
## § 3 Abs. 8
Zum Abo gehören Funktionen, die der Kunde im Kundenportal unter „Funktionen“ einschaltet. Das Hosting-Abo enthält die Website-Analyse. Das Wartungs-Abo enthält News-Pinnwand, Terminplaner, Webshop, Buchungskalender, Öffnungszeiten live und Website-Analyse. Umfang, Voraussetzungen und Pflichten jeder Funktion beschreiben ihre Leistungsbeschreibung und, soweit die Funktion personenbezogene Daten im Auftrag des Kunden verarbeitet, ihr Auftragsverarbeitungsvertrag, jeweils in der Fassung, die das Kundenportal beim Einschalten anzeigt. Eine Funktion kostet keine zusätzliche Gebühr; die Einrichtung richtet sich nach § 4 Abs. 8 und 9.
Vorbehaltlich § 11 Abs. 4 läuft eine Funktion nur, solange das Abo läuft, das sie enthält. Das gilt auch für eine Funktion, die wir auf Wunsch des Kunden schon bei der Erstellung seiner Website eingebaut haben. Funktionen, die der Kunde vor Veröffentlichung dieser Fassung einzeln gekauft hat, laufen nach den Bedingungen weiter, unter denen er sie gekauft hat.
**Einordnung.** Neuer Absatz „(8) Funktionen“ nach Absatz (7) mit zwei Absätzen.
**Begründung.** E6, E9, E10, E21, C10, C11, C12 und Spec 5.5, 5.9: Funktionen sind Teil des Abos und kosten keine zusätzliche Gebühr; die Einrichtung läuft nach § 4 Abs. 8 und 9. Eine Funktion läuft nur mit dem Abo, das sie enthält, auch wenn sie schon bei der Website-Erstellung eingebaut wurde (E21). Einzeln gekaufte Funktionen behalten ihre Bedingungen (E10). Offen: Geltung für laufende Abos (R2, KQ1), Einrichtung beim Projektstart (R4, KQ8), Ende und Fristen (R8, KQ8), Analyse im Hosting-Abo (R9).
## § 4 Abs. 2
Einen Vorschuss nach Absatz 8 gleichen die folgenden Kontingente zuerst aus.
**Einordnung.** Hängt als letzter Satz an den bestehenden Absatz 2 (Verfügbarkeit und Verfall der Kontingente) an.
**Begründung.** E17: Ein offener Vorschuss wird vor allem anderen aus den folgenden Monatskontingenten ausgeglichen (FIFO, Spec 5.5). Der Satz verknüpft die bestehende Verbrauchsregel mit Absatz 8. Offen: Vorschuss-Regel insgesamt (R3, KQ2).
## § 4 Abs. 8
Die Einrichtung einer Funktion rechnen wir als Arbeitszeit auf das Stundenkontingent an. Angerechnet wird die tatsächlich aufgewendete Zeit nach Absatz 4. Reicht das verfügbare Kontingent nicht aus, führen wir die Einrichtung trotzdem ohne weitere Rückfrage aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen, sobald sie bereitstehen. Dafür berechnen wir nichts, weder als Mehraufwand nach Absatz 6 noch in einer gesonderten Rechnung. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest; der Kunde schuldet ihn nicht.
**Einordnung.** Neuer Absatz 8 nach Absatz 7.
**Begründung.** E7, E8 und E17 (E17 ersetzt die Absprache aus E8), Spec 5.5: Die Einrichtung startet ohne Rückfrage und geht mit der echten Arbeitszeit (Absatz 4) vom Stundenkonto ab. Reicht das Kontingent nicht, ist die fehlende Zeit ein Vorschuss, nie Mehraufwand nach Absatz 6; ein Rest bei Vertragsende verfällt zugunsten des Kunden. Normale Arbeit außerhalb von Einrichtungen bleibt beim Mehraufwand nach Absatz 6. Offen: Vorschuss ohne Obergrenze (R3, KQ2).
## § 4 Abs. 9
Im Hosting-Abo ist die Einrichtung der Website-Analyse für den Kunden kostenlos; ein Stundenkontingent gibt es dort nicht (Absatz 7). Lässt der Kunde eine Funktion auf seinen Wunsch schon bei der Erstellung seiner Website einbauen, rechnen wir die Einrichtung nicht über das Stundenkontingent ab, sondern über die Kosten des Projekts, wie wir es mit ihm vereinbart haben.
**Einordnung.** Neuer Absatz 9 nach Absatz 8.
**Begründung.** E9, E21, C6 und C7, Spec 5.5: Im Hosting-Abo gibt es kein Kontingent (Absatz 7), die Einrichtung der Analyse ist dort kostenlos (C6). Bei einer Funktion, die schon bei der Erstellung der Website eingebaut wird, zählen die Einrichtungs-Arbeitsblätter nicht aufs Stundenkonto, sondern laufen über die Projektkosten, wie mit dem Kunden vereinbart (C7). Offen: Einrichtung beim Projektstart (R4, KQ8), Analyse im Hosting-Abo (R9).
## § 6 Abs. 6
Schaltet der Kunde die Website-Analyse ein, ist er für die Messung auf seiner Website Verantwortlicher. Er entfernt oder umgeht das Cookie-Banner nicht, bindet ohne Abstimmung mit uns keine weiteren Mess- oder Tracking-Werkzeuge ein und ergänzt seine Datenschutzerklärung um einen Hinweis auf die von uns erzeugte Zusatzseite „/datenschutz-analyse.html“.
**Einordnung.** Neuer Absatz 6 nach Absatz 5.
**Begründung.** C10 und Spec 5.5 (Analyse): Wer die Website-Analyse einschaltet, ist für die Messung auf seiner Website Verantwortlicher. Der Absatz stellt die Pflichten aus der Abo-Ergänzung Analyse (Cookie-Banner, keine weiteren Mess- oder Tracking-Werkzeuge ohne Abstimmung, Hinweis auf die Zusatzseite) in die Abo-Bedingungen, weil die Analyse jetzt auch im Hosting-Abo liegt. Die Tatsachenangabe zur Zielgruppe bleibt als Zeile mit Häkchen in der Oberfläche (Spec 5.5, N22). Offen: Analyse im Hosting-Abo (R9, KQ8), Ausschlüsse als Vertragsklausel (R13, KQ9).
## § 9 Abs. 6
Will der Kunde im Hosting-Abo eine Funktion einschalten, die nur das Wartungs-Abo enthält, führt ihn das Kundenportal zum Wechsel. Nach Wirksamkeit des Wechsels schaltet er die Funktion in einem eigenen Vorgang ein. Der Wechsel bleibt eine eigene Bestellung nach Absatz 2.
**Einordnung.** Neuer Absatz 6 nach Absatz 5.
**Begründung.** E14 und C5: Wer im Hosting-Abo eine Funktion einschalten will, die nur das Wartungs-Abo enthält, wird zum Wechsel geführt und schaltet danach in einem eigenen Vorgang ein. Der Wechsel bleibt eine eigene Bestellung mit eigener Widerrufsfrist (§ 2 Abs. 5); das Einschalten ist keine Bestellung. Offen: Sofortbeginn des Wechsels nur mit Häkchen bei Privatkunden (R6, KQ5).
## § 11 Abs. 4
Mit dem Ende des Abos, das eine Funktion enthält, endet die Funktion; abweichend davon läuft Öffnungszeiten live noch 30 Tage unverändert weiter. Wir schalten sie ab und löschen ihre Daten nach den Fristen in ihrer Leistungsbeschreibung; die Messdaten der Website-Analyse löschen wir 60 Tage nach der Abschaltung. Beginnt innerhalb dieser Fristen wieder ein Abo, das die Funktion enthält, schalten wir sie mit den vorhandenen Daten wieder ein. Wechselt der Kunde vom Wartungs- ins Hosting-Abo, bleibt die Website-Analyse; die übrigen Funktionen enden. Das Gleiche gilt, wenn der Kunde den Wechsel ins Wartungs-Abo widerruft.
**Einordnung.** Neuer Absatz 4 nach Absatz 3.
**Begründung.** E21, C12 und Nachtrag N31 (mit P4 und P6 abgestimmt): Funktionen enden mit dem Abo, das sie enthält. Terminplaner, News, Webshop und Buchungskalender schalten spätestens am Folgetag ab (Tagesjob); Öffnungszeiten live läuft nach dem Ende noch 30 Tage unverändert weiter, ein „Behalten“ gegen Zahlung gibt es nicht mehr. Gelöscht wird nach den Fristen der Leistungsbeschreibungen: 30 Tage (Terminplaner, News, Öffnungszeiten live), 90 Tage (Webshop, Buchungskalender), 60 Tage für die Messdaten der Analyse. Ein neues passendes Abo in der Frist holt die Funktion zurück. Beim Wechsel ins Hosting-Abo und beim Widerruf des Wechsels bleibt die Analyse (E6, C10). Offen: R8 (KQ8).
## § 14 Abs. 2
Für Funktionen, die personenbezogene Daten im Auftrag des Kunden verarbeiten, gilt zusätzlich der Auftragsverarbeitungsvertrag der Funktion, dem der Kunde beim Einschalten mit dem Klick auf „Einschalten“ zustimmt.
**Einordnung.** Hängt an den bestehenden Absatz 2 (Auftragsverarbeitungsvertrag in Anlage 1) an.
**Begründung.** Spec 5.9 (AVV je Funktion): Option A der AVV-Struktur, die Auftragsverarbeitungsverträge der Funktionen bleiben eigene Verträge mit neuer Vorbemerkung, Anlage 1 bleibt der Rahmen; der Klick auf „Einschalten“ unter der Linkzeile ist die Zustimmung. Offen: Struktur (R10, KQ7), Klick statt eigenes Häkchen (KQ7).
## § 15
Fassung 6 gilt für Abos, die ab ihrer Veröffentlichung abgeschlossen werden. Für Abos, die nach einer früheren Fassung abgeschlossen wurden, gelten diese Fassungen weiter. Schaltet der Kunde mit einem solchen Abo eine Funktion ein, gelten für die Funktion ihre Leistungsbeschreibung und ihr Auftragsverarbeitungsvertrag in der angezeigten Fassung, einschließlich der Regeln zur Einrichtung und zum Stundenkontingent.
**Einordnung.** Neuer zweiter Absatz hinter dem bisherigen Text. Der bisherige Text bleibt wörtlich.
**Begründung.** C11 und E10: Fassung 6 gilt für neue Abschlüsse; laufende Abos der Fassungen 3 bis 5 behalten ihren Vertrag. Beim Einschalten gelten die Leistungsbeschreibung und der Auftragsverarbeitungsvertrag der Funktion über die Linkzeile und den Klick, auch mit den Regeln zur Einrichtung und zum Stundenkonto. Die Änderung ist für laufende Abos vorteilhaft (Spec 5.9), deshalb keine Zustimmungsabfrage nach dem bisherigen Text. Offen: Info-Mail an laufende Abos (R2, KQ1).
## Leistungsbeschreibung A Nr. 1
Website-Analyse im Kundenportal (§ 3 Abs. 8)
**Einordnung.** Neuer Listenpunkt am Ende von Nr. 1 „Umfang“.
**Begründung.** E6 und C10: Das Hosting-Abo enthält die Website-Analyse. Offen: Analyse im Hosting-Abo (R9, KQ8).
## Leistungsbeschreibung B Nr. 1
Funktionen im Kundenportal: News-Pinnwand, Terminplaner, Webshop, Buchungskalender, Öffnungszeiten live und Website-Analyse (§ 3 Abs. 8)
**Einordnung.** Neuer Listenpunkt am Ende von Nr. 1 „Umfang“.
**Begründung.** E6: Das Wartungs-Abo enthält alle Funktionen und als Obermenge die Analyse.
## Widerruf: Hinweise
Das Einschalten einer Funktion ist kein eigener Vertrag, sondern Teil des Abos. Es verlängert die Widerrufsfrist des Abos nicht. Widerruft ein Verbraucher das Abo, endet auch die Funktion (§ 11 Abs. 4).
**Einordnung.** Neue Nummer 5 am Ende der „Hinweise zum Widerruf“. Widerrufsbelehrung und Muster-Widerrufsformular bleiben wortgleich mit Fassung 5.
**Begründung.** Spec 5.5 und 5.9, recht.md § 2: Das Einschalten löst keine neue Zahlungspflicht aus und ist kein eigener Vertrag; deshalb gibt es keine eigene Widerrufsfrist. Bei Widerruf des Abos endet die Funktion (§ 11 Abs. 4). Wie sich die Einrichtungszeit innerhalb der Frist auf den Wertersatz auswirkt, regelt das Häkchen für Privatkunden (§ 2 Abs. 2 c); die Entscheidung dazu steht in R7. Offen: R7 (KQ3), Belehrung per Link (R11, KQ6).
## Anlage 3
Server für die Website-Analyse (Messdienst, nur bei eingeschalteter Funktion)
Deutschland (Rechenzentrum Falkenstein)
**Einordnung.** Neue Tabellenzeile hinter den beiden Hetzner-Zeilen (Unternehmen wie dort, Leistung und Ort wie im Pflichtsatz).
**Begründung.** Spec 5.9 (AVV je Funktion), Art. 28 Abs. 2 DSGVO: Der Server des Messdienstes der Website-Analyse (Hetzner, Rechenzentrum Falkenstein, wie in der Auftragsverarbeitung der Analyse) wird als Unterauftragsverarbeiter genannt, solange die Analyse eingeschaltet ist. Offen: AVV-Struktur (R10, KQ7).
## Was gleich bleibt
- Preise: Hosting-Abo 20,00 € pro Monat, Wartungs-Abo 50,00 € pro Monat, E-Mail-Zusatzspeicher 1,00 € pro Monat je Paket von 5 GB, Mehraufwand 100 € pro Stunde (alle Endpreise, `§ 19 UStG`). Funktionen bekommen keinen Preis.
- Laufzeiten: Mindestlaufzeit zwölf Monate ab Leistungsbeginn, Kündigung zum Ende der Mindestlaufzeit mit einem Monat Frist, danach monatlich (§ 8), Sonderkündigung bei Geschäftsaufgabe.
- Paragrafenfolge § 1 bis § 16 und alle Anker (`p1` bis `p16`, `wechsel`, `zusatzspeicher`, `hosting`, `wartung`, `widerruf`, `formular`, `widerruf-hinweise`, `avv`, `tom`, `sub`).
- Widerrufsbelehrung und Muster-Widerrufsformular: wortgleich mit Fassung 5; die Hinweise zum Widerruf Nr. 1 bis 4 bleiben.
- § 10 E-Mail-Zusatzspeicher mit Absatz 3 (Bereitstellung sofort, kein Wertersatz) und den Hinweisen zum Widerruf Nr. 4.
- Anlage 1 (Auftragsverarbeitungsvertrag) und Anlage 2 (technische und organisatorische Maßnahmen); Anlage 3 behält ihre beiden Hetzner-Zeilen.
- Die Rechtsdokumente bleiben in der bisherigen Anrede („der Kunde“, in der Übersichtsbox „Ihr“); nur Oberflächentexte stehen in „du“ (R12).
+149
View File
@@ -0,0 +1,149 @@
# Allgemeine Geschäftsbedingungen
**Fassung 2026-09-30** · Wortgleiche Kopie der Allgemeinen Geschäftsbedingungen auf webklar.com/agb
## Kaufvertrag – WEBklar (Modularer Projektvertrag)
**zwischen**
WEBklar (im Folgenden „Anbieter“)
**und**
Kunde laut Angebot (im Folgenden „Kunde“)
## 1. Vertragsgegenstand
(1) Gegenstand dieses Vertrages ist die Erbringung der im Angebot definierten Leistungen.
(2) Der Vertrag besteht aus diesem Grundvertrag sowie den ausgewählten Leistungsmodulen.
(3) Maßgeblich ist das jeweils angenommene Angebot von WEBklar.
## 2. Leistungsart
(1) Sämtliche Leistungen von WEBklar stellen Dienst- und Entwicklungsleistungen dar.
(2) Ein bestimmter wirtschaftlicher, technischer oder rechtlicher Erfolg wird nicht geschuldet.
(3) WEBklar erbringt keinen laufenden Betrieb, sofern dieser nicht explizit vereinbart wurde.
## 3. Leistungsmodul A – Webseite (einmalig)
(aktiv, wenn im Angebot enthalten)
(1) WEBklar erstellt eine individuelle Webseite gemäß Angebot.
(2) Die Umsetzung erfolgt nach den vom Kunden gelieferten Inhalten und Vorgaben.
(3) Zusätzliche Leistungen wie Domain, Hosting, Wartung oder SEO sind nicht Bestandteil, sofern sie nicht gesondert beauftragt wurden.
(4) Der Kunde ist nicht berechtigt, Änderungen am Quellcode selbst vorzunehmen.
(5) Änderungen erfolgen ausschließlich durch WEBklar gegen gesonderte Vergütung.
## 4. Leistungsmodul B – Automatisierung / Virtualisierung (einmalig)
(aktiv, wenn im Angebot enthalten)
(1) WEBklar entwickelt individuelle Automatisierungen, Apps oder Virtualisierungssysteme.
(2) Die Leistung stellt eine reine Entwicklungsleistung dar.
(3) Optional kann eine Beratungsleistung Bestandteil des Projektes sein.
(4) Ein laufender Betrieb, Monitoring oder Wartung ist nicht geschuldet, außer dies wurde explizit vereinbart.
(5) Der Kunde entscheidet über Inhalte, Daten und Prozesse und trägt dafür die rechtliche Verantwortung.
## 5. Leistungsmodul C – Hosting
(aktiv, wenn im Angebot enthalten)
(1) Hosting bietet WEBklar als Hosting-Abo an. Für das Hosting-Abo und das Wartungs-Abo gelten die Vertragsbedingungen für die WEBklar-Abos mit Leistungsbeschreibungen; sie gehen diesen AGB vor.
(2) Bestehende Hosting-Verträge mit jährlicher Laufzeit behalten die bei ihrem Abschluss vereinbarten Bedingungen. Abweichend davon können Verbraucher solche Verträge nach einer automatischen Verlängerung jederzeit mit einer Frist von einem Monat kündigen.
(3) WEBklar ist berechtigt, externe Anbieter (z. B. Rechenzentren) einzusetzen.
## 6. Quellcode und Eigentum
### 6.1 Webseite
(1) Der Kunde erhält ein einfaches, zeitlich unbegrenztes Nutzungsrecht an der fertigen Webseite.
(2) Der Quellcode der Webseite wird nur auf ausdrückliche Anfrage und nach Vereinbarung herausgegeben.
(3) Ohne Vereinbarung verbleibt der Quellcode bei WEBklar und wird archiviert.
### 6.2 Apps, Automatisierungen und Backend
(1) Der Quellcode von Apps, Automatisierungen und Backend-Systemen verbleibt vollständig bei WEBklar.
(2) Eine Herausgabe erfolgt ausschließlich nach gesonderter schriftlicher Vereinbarung.
(3) Der Kunde erhält lediglich Zugriff auf die Bedienoberfläche bzw. das Frontend.
## 7. Mitwirkungspflichten des Kunden
(1) Der Kunde stellt alle benötigten Inhalte, Daten und Freigaben rechtzeitig bereit.
(2) Verzögerungen durch fehlende Mitwirkung gehen nicht zu Lasten von WEBklar.
(3) WEBklar ist nicht verpflichtet, rechtliche Prüfungen der Inhalte vorzunehmen.
## 8. Abnahme
(1) Nach Fertigstellung wird dem Kunden die Leistung zur Abnahme bereitgestellt.
(2) Erfolgt innerhalb von 14 Tagen keine Rückmeldung, gilt die Leistung als abgenommen.
(3) Nach Abnahme sind nur noch kostenpflichtige Änderungen möglich.
## 9. Vergütung und Zahlung
(1) Die Vergütung ergibt sich aus dem Angebot.
(2) Wird nach Aufwand abgerechnet, erfasst WEBklar die Arbeitszeit je Arbeitsschritt in Schritten von 15 Minuten; angefangene 15 Minuten werden aufgerundet. Wirkt eine weitere Person unterstützend mit, rechnet WEBklar für sie die Hälfte der Arbeitszeit des Arbeitsschritts an, aufgerundet auf volle 15 Minuten; bei mehreren unterstützenden Personen gilt das für jede. Jeder Arbeitsschritt und jede Unterstützung ist im Kundenportal einsehbar.
(3) Projektleistungen sind nach Vereinbarung fällig.
(4) Für die Abos gelten die Vertragsbedingungen für die WEBklar-Abos.
## 10. Zahlungsverzug
(1) Bei Zahlungsverzug erfolgen bis zu zwei Mahnungen.
(2) Danach ist WEBklar berechtigt:
- Leistungen zu sperren
- Verzugszinsen zu berechnen
- den Vertrag außerordentlich zu kündigen
## 11. Haftung
(1) WEBklar haftet unbeschränkt bei Vorsatz und grober Fahrlässigkeit, bei Verletzung von Leben, Körper oder Gesundheit, nach dem Produkthaftungsgesetz und soweit WEBklar eine Garantie übernommen hat.
(2) Bei leicht fahrlässiger Verletzung einer wesentlichen Vertragspflicht haftet WEBklar nur für den vertragstypischen, vorhersehbaren Schaden. Wesentlich sind Pflichten, deren Erfüllung die ordnungsgemäße Durchführung des Vertrags überhaupt erst ermöglicht und auf deren Einhaltung der Kunde regelmäßig vertrauen darf. Im Übrigen ist die Haftung für leichte Fahrlässigkeit ausgeschlossen.
(3) Der Kunde bewahrt Kopien der Inhalte und Daten auf, die er WEBklar übergibt oder selbst einpflegt. Für den Verlust solcher Inhalte und Daten haftet WEBklar außer in den Fällen von Absatz 1 nur in Höhe des Aufwands, der für ihre Wiederherstellung nötig gewesen wäre, wenn der Kunde diese Kopien aufbewahrt hätte.
(4) Diese Regeln gelten auch zugunsten der Mitarbeiter und Beauftragten von WEBklar.
## 12. Kündigung
(1) Laufzeitverträge können zum Ende der jeweiligen Laufzeit gekündigt werden. Laufzeit und Kündigung der Abos richten sich nach den Vertragsbedingungen für die WEBklar-Abos.
(2) Das Recht zur außerordentlichen Kündigung bleibt unberührt.
## 13. Referenzen
WEBklar darf das Projekt nur nach ausdrücklicher Zustimmung des Kunden als Referenz verwenden.
## 14. Schlussbestimmungen
(1) Es gilt deutsches Recht.
(2) Gerichtsstand ist der Sitz von WEBklar, soweit zulässig.
(3) Sollten einzelne Bestimmungen unwirksam sein, bleibt der Vertrag im Übrigen wirksam.
+42
View File
@@ -0,0 +1,42 @@
# Archiv der Analyse-Texte
Jede ausgelieferte Fassung bleibt hier nachvollziehbar. `BANNER_VERSION` im Client (`analytics/client/core.js`) und `config.analyse.avvVersion` tragen dieselbe Fassung.
| Fassung | gültig ab | Banner-Text | Zusatzseite |
|---|---|---|---|
| 2026-10-02 | offen bis Livegang | [banner-2026-10-02.md](banner-2026-10-02.md) (`BANNER_VERSION` 2026-10-02) | [datenschutz-analyse-2026-10-02.html](datenschutz-analyse-2026-10-02.html) |
Änderung der Zusatzseite am 02.10.2026 (Fassung bleibt 2026-10-02, Banner-Text unverändert): Der Knopf zum Wiederöffnen des Banners ist jetzt ein Keks-Symbol (Name für Screenreader und Beschriftung beim Darüberfahren weiter „Datenschutz-Einstellungen“). Die Zusatzseite nennt deshalb bei Widerruf und Widerspruch zusätzlich „(das Keks-Symbol)“. Die vorige Formulierung war bis dahin nur auf der WEBklar-Testseite (test-website.project.webklar.com) ausgeliefert, keiner Kunden-Website: [datenschutz-analyse-2026-10-02-erstfassung.html](datenschutz-analyse-2026-10-02-erstfassung.html). In WOMS → Rechtliches als neue Version der Karte „datenschutz-website-analyse“ hinterlegt.
## Eigene Website webklar.com
Auf webklar.com läuft dieselbe Messung mit WEBklar als Verantwortlichem (keine AVV). Das Skript-Tag trägt `data-variant="eigen"`; der Banner hat dann eine eigene Sie-Fassung (`BANNER_VERSION_EIGEN`), die Zusatzseite liegt im Repository der Website (`public/datenschutz-analyse.html`), die Kopie hier ist byte-gleich zu der ausgelieferten Fassung. Der Abschnitt 4a der Datenschutzerklärung fasst sie zusammen.
| Fassung | gültig ab | Banner-Text | Zusatzseite |
|---|---|---|---|
| 2026-10-17 | offen bis Livegang | [banner-eigen-2026-10-17.md](banner-eigen-2026-10-17.md) (`BANNER_VERSION_EIGEN` 2026-10-17) | [datenschutz-analyse-webklar-com-2026-10-17.html](datenschutz-analyse-webklar-com-2026-10-17.html) |
AVV und Leistungsbeschreibung je Fassung: `public/rechtliches/avv-website-analyse-<Fassung>.html|pdf` und `leistungsbeschreibung-website-analyse-<Fassung>.html|pdf`. „Gültig ab“ wird beim Livegang eingetragen.
## Terminplaner
Die Texte des Terminplaners (Leistungsbeschreibung, AVV, Widerrufsbelehrung mit Muster-Widerrufsformular) tragen die Fassung `config.termine.avvVersion`. Quelle ist je Dokument eine Markdown-Datei unter `docs/rechtliches/`; `node scripts/build-legal-html.mjs termine` erzeugt daraus die HTML-Seiten, `scripts/build-legal-pdfs.sh termine` die PDFs (die Mail K15 hängt sie an). Eine ausgelieferte Fassung wird nie überschrieben: jede Änderung nach der Übergabe an die Kanzlei oder dem Livegang bekommt ein neues Datum und neue Dateien.
| Fassung | gültig ab | Leistungsbeschreibung | AVV | Widerrufsbelehrung |
|---|---|---|---|---|
| 2026-10-05 | offen bis Livegang | [HTML](../../../public/rechtliches/leistungsbeschreibung-terminplaner-2026-10-05.html) · [PDF](../../../public/rechtliches/leistungsbeschreibung-terminplaner-2026-10-05.pdf) | [HTML](../../../public/rechtliches/avv-terminplaner-2026-10-05.html) · [PDF](../../../public/rechtliches/avv-terminplaner-2026-10-05.pdf) | [HTML](../../../public/rechtliches/widerrufsbelehrung-funktionen-2026-10-05.html) · [PDF](../../../public/rechtliches/widerrufsbelehrung-funktionen-2026-10-05.pdf) |
Adressen ohne Fassung (`/rechtliches/leistungsbeschreibung-terminplaner.html`, `/rechtliches/avv-terminplaner.html`, `/rechtliches/widerrufsbelehrung-funktionen.html`) liefern die Seite der aktuellen Fassung; Mails und Zustimmungstexte verlinken nur die Adressen mit Fassung. „Gültig ab“ wird beim Livegang eingetragen.
## Terminplaner-Gästetexte
Die Texte, die ein Gast beim Buchen sieht (Hinweis am Formular, Texte der Haken, Gesundheitshinweis, Knopftexte, Widerrufsbelehrung für Gäste) und der Datenschutz-Absatz für die Website des Betriebs tragen die Fassung `RECHTSTEXTE_VERSION` in `server/services/termine/rechtstexte.js`. Der Nachweis einer Buchung speichert die Fassung und Hashes der Texte, nicht die Texte selbst (AVV Terminplaner Nr. 8 Abs. 5); den Wortlaut je Fassung hält dieses Archiv. `node scripts/termine-texte-archiv.mjs` erzeugt `termine-gaestetexte-<Fassung>.md` aus den Texten (Betriebsangaben als Platzhalter), `--check` vergleicht mit der eingecheckten Datei. Eine Datei einer früheren Fassung wird nie neu erzeugt oder geändert (`test/termineRechtstexteArchiv.test.js` pinnt ihren Hash); jede Textänderung bekommt ein neues Datum und eine neue Datei. Das Archiv bleibt bis drei Jahre nach dem Ende des Terminplaners erhalten.
| Fassung | gültig ab | Archiv |
|---|---|---|
| 2026-10-02 | 2026-10-03 (Pilot) | [termine-gaestetexte-2026-10-02.md](termine-gaestetexte-2026-10-02.md) |
| 2026-10-05 | offen bis Livegang | [termine-gaestetexte-2026-10-05.md](termine-gaestetexte-2026-10-05.md) |
„Gültig ab“ einer neuen Fassung wird beim Livegang eingetragen; ihr Zeitraum endet mit dem „gültig ab“ der nächsten.
Änderungen der Fassung 2026-10-05 gegenüber 2026-10-02 (Textänderungen: nur diese beiden Stellen): Der Haken für die Erinnerung endet mit dem Satz „Diese Einwilligung kann ich jederzeit widerrufen.“ (Art. 7 Abs. 3 DSGVO); der Datenschutz-Absatz sagt bei der Abschaltung: „Wird die Online-Terminbuchung abgeschaltet, zum Beispiel weil unser Vertrag mit WEBklar endet, werden alle Termindaten 30 Tage nach dem Abschalten gelöscht.“ Außerdem ändert sich die Regel zum Haken „Stornobedingungen“, nicht sein Text: Pflicht ist er nur noch bei einer Leistung mit Preis, bei kostenlosen Leistungen fragt der Terminplaner nicht danach (die Fassung 2026-10-02 verlangte ihn bei jeder Leistung, sobald der Betrieb Stornobedingungen hat). Buchungen, die vor dem Livegang der neuen Fassung entstanden sind, behalten im Nachweis die Fassung 2026-10-02.
@@ -0,0 +1,23 @@
# Banner-Text Fassung 2026-10-02
Quelle: `analytics/client/core.js` (Konstanten `BANNER_*`). Wortgleich zu `banner-portal-mailtexte.md` § 1.
- `BANNER_VERSION`: 2026-10-02
## Fließtext (BANNER_TEXT)
Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Deiner Zustimmung messen wir zusätzlich, von welcher Website Du kommst, wie lange Du bleibst, wie weit Du scrollst und wo Du klickst (Heatmap), mit einer täglich wechselnden Kennung. Die Messung betreibt für uns die WEBklar GbR in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Du abgelehnt hast. Deine Zustimmung kannst Du jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: [Datenschutz](/datenschutz-analyse.html)
## aria-label (BANNER_ARIA)
Einwilligung zur Reichweitenmessung
## Schaltflächen
„Ablehnen“ · „Annehmen“ (gleich gestaltet, Ablehnen zuerst)
## Einstellungs-Link (SETTINGS_LABEL)
Datenschutz-Einstellungen
Ab 02.10.2026: als angeknabbertes, sich langsam drehendes Keks-Symbol unten rechts (44 px); „Datenschutz-Einstellungen“ ist sein Name (`aria-label`) und erscheint beim Darüberfahren und bei Tastaturfokus. Bei „Bewegung reduzieren“ dreht er sich nicht. Bis dahin: Textknopf „Datenschutz-Einstellungen“.
@@ -0,0 +1,13 @@
# Banner-Text Fassung 2026-10-17 (eigene Websites der WEBklar GbR)
Quelle: `analytics/client/core.js` (Konstanten `BANNER_TEXT_EIGEN`, `BANNER_VERSION_EIGEN`). Gilt nur, wenn das Skript-Tag `data-variant="eigen"` trägt, derzeit nur auf webklar.com. Kunden-Websites behalten die Fassung 2026-10-02 ([banner-2026-10-02.md](banner-2026-10-02.md)).
- `BANNER_VERSION_EIGEN`: 2026-10-17
## Fließtext (BANNER_TEXT_EIGEN)
Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Ihrer Zustimmung messen wir zusätzlich, von welcher Website Sie kommen, wie lange Sie bleiben, wie weit Sie scrollen und wo Sie klicken (Heatmap), mit einer täglich wechselnden Kennung. Wir messen selbst auf unserem Server in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Sie abgelehnt haben. Ihre Zustimmung können Sie jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: [Datenschutz](/datenschutz-analyse.html)
## aria-label, Schaltflächen, Einstellungs-Link
Wie in der Kunden-Fassung: „Einwilligung zur Reichweitenmessung“; „Ablehnen“ · „Annehmen“ (gleich gestaltet, Ablehnen zuerst); Keks-Symbol „Datenschutz-Einstellungen“ unten rechts.
@@ -0,0 +1,84 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex">
<title>Datenschutz – Reichweitenmessung</title>
<style>
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;line-height:1.6;color:#111;background:#fff;max-width:46rem;margin:0 auto;padding:1.5rem 1rem 16rem}
h1{font-size:1.6rem}h2{font-size:1.15rem;margin-top:2rem}
.stand{color:#555;font-size:.9rem}
ul{padding-left:1.25rem}
a{color:#0b57d0}
.box{border:2px solid #111;border-radius:10px;padding:.25rem 1rem;margin:1.5rem 0}
</style>
</head>
<body>
<p><a href="/">← Zur Startseite</a></p>
<h1>Datenschutz bei der Reichweitenmessung</h1>
<p class="stand">Ergänzung zur Datenschutzerklärung dieser Website · Stand: 02.10.2026</p>
<p>Diese Seite beschreibt nur die Reichweitenmessung. Alle übrigen Datenschutzhinweise findest Du in der Datenschutzerklärung dieser Website; Impressum und Datenschutzerklärung erreichst Du über die Startseite.</p>
<h2>Verantwortlicher</h2>
<p>{{company}}<br>{{#address}}{{address}}<br>{{/address}}E-Mail: <a href="mailto:{{email}}">{{email}}</a></p>
<p>Falls es einen Datenschutzbeauftragten gibt, findest Du seine Kontaktdaten in der Datenschutzerklärung dieser Website.</p>
<h2>Worum es geht</h2>
<p>Wir möchten verstehen, wie diese Website genutzt wird, um sie zu verbessern: Welche Seiten werden gelesen, woher kommen die Besucher, wo wird geklickt? Dafür nutzen wir eine eigene Messung ohne Cookies und ohne Dienste von Google, Meta oder anderen Werbenetzwerken. Wir nutzen die Daten nicht für Werbung und bilden keine Profile einzelner Besucher. Die Messung läuft auf einem Server in Deutschland.</p>
<p>Damit die Messung arbeiten kann, lädt Dein Browser bei jedem Seitenaufruf ein Skript von einem Server der WEBklar GbR. Dabei erhält dieser Server, wie bei jeder Datei, die eine Website lädt, technisch Deine IP-Adresse und Dein Browser-Kennzeichen (User-Agent). Das gilt auch, wenn Du ablehnst oder „Global Privacy Control“ aktiviert hast. Wir speichern und protokollieren beides nicht.</p>
<h2>Solange Du im Banner nichts ausgewählt hast</h2>
<p>Dann zählen wir den Aufruf der Seite. Das Skript übermittelt dafür nur die Adresse der Seite und die Angabe, dass das Banner angezeigt wurde. Gespeichert werden:</p>
<ul>
<li>der Seitenpfad (ohne Suchbegriffe oder Parameter hinter dem „?“),</li>
<li>das Land, das wir aus Deiner IP-Adresse ableiten,</li>
<li>die Geräteklasse (Handy, Tablet oder Computer), die wir aus dem Browser-Kennzeichen ableiten,</li>
<li>der Zeitpunkt, auf die volle Stunde gerundet, und</li>
<li>ein Zähler dafür, dass das Banner angezeigt wurde.</li>
</ul>
<p>IP-Adresse und Browser-Kennzeichen verwenden wir nur kurz im Arbeitsspeicher: um Land und Geräteklasse zu bestimmen, um automatische Abrufe durch Programme (Bots) auszusortieren und um zu verhindern, dass die Messung durch massenhafte Anfragen verfälscht wird. Für diese Mengenbegrenzung bilden wir aus Deiner IP-Adresse einen Prüfwert (Hash), den wir nur im Arbeitsspeicher halten und nicht speichern. Eine Kennung, mit der wir Dich wiedererkennen könnten, speichern wir ohne Deine Einwilligung nicht.</p>
<p>Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO. Unser berechtigtes Interesse ist, die Nutzung unserer Website in Zahlen zu kennen, das Banner zu betreiben und die Messung vor Missbrauch zu schützen.</p>
<h2>Wenn Du „Ablehnen“ wählst</h2>
<p>Dann zählen wir einmal, dass abgelehnt wurde, und das Skript sendet danach nichts mehr, auch keine Seitenaufrufe. Sendet Dein Browser das Signal „Global Privacy Control“ oder „Do Not Track“, zeigen wir das Banner gar nicht an und zählen nichts.</p>
<h2>Wenn Du „Annehmen“ wählst</h2>
<p>Dann messen wir zusätzlich:</p>
<ul>
<li>von welcher Website Du gekommen bist (nur der Domainname),</li>
<li>die Größenklasse Deines Browserfensters,</li>
<li>eine Besucher-Kennung, die täglich wechselt. Wir berechnen sie aus IP-Adresse und Browser-Kennzeichen mit einem Schlüssel, der jeden Tag neu erzeugt und nicht gespeichert wird. Am selben Tag könnten wir die Kennung damit nachrechnen. Nach dem Wechsel des Schlüssels lässt sie sich nicht mehr aus Deiner IP-Adresse berechnen und nicht mit Kennungen anderer Tage verknüpfen,</li>
<li>wie lange Du auf einer Seite bleibst (ab dem Laden der Seite) und wie weit Du scrollst,</li>
<li>wo auf der Seite geklickt wird: die Klickposition und den Aufbau des angeklickten Elements, ohne Texte und ohne Eingaben in Formulare (für eine „Heatmap“),</li>
<li>ob Du auf eine Telefonnummer, eine E-Mail-Adresse, einen WhatsApp-Link oder einen Link zu einer anderen Website klickst (dann mit deren Domainname, nicht mit der Nummer oder Adresse selbst), und</li>
<li>einen Zähler dafür, dass Du „Annehmen“ gewählt hast.</li>
</ul>
<p>Rechtsgrundlage ist Deine Einwilligung nach § 25 Abs. 1 TDDDG und Art. 6 Abs. 1 lit. a DSGVO.</p>
<h2>Was in Deinem Browser gespeichert wird</h2>
<p>Wir speichern Deine Auswahl („Annehmen“ oder „Ablehnen“) mit Datum und der Fassung des Banner-Textes im lokalen Speicher Deines Browsers (Eintrag „wk-consent“), damit wir Dich nicht bei jedem Seitenaufruf erneut fragen. Das ist ohne Einwilligung zulässig (§ 25 Abs. 2 Nr. 2 TDDDG). Die Auswahl gilt 183 Tage, danach fragen wir erneut. Du kannst den Eintrag jederzeit selbst löschen, indem Du die Website-Daten in Deinem Browser löschst.</p>
<h2>Speicherdauer</h2>
<p>Einzelne Seitenaufrufe, Ereignisse und Klickpositionen löschen wir nach 30 Tagen. Danach bleiben nur zusammengefasste Tageswerte, also Summen je Tag, etwa Aufrufe je Seite oder je Land, ohne Besucher-Kennung. Wir löschen sie nach 25 Monaten, spätestens rund 60 Tage nach dem Ende der Messung. Unser Dienstleister sichert den Server täglich; gelöschte Daten bleiben dort bis zu sieben Tage erhalten.</p>
<h2>Empfänger</h2>
<p>Die Messung betreibt für uns die WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, als Auftragsverarbeiter. WEBklar darf die Daten nur auf unsere Weisung und für unsere Zwecke verwenden. Der Server steht bei der Hetzner Online GmbH in Deutschland, die als weiterer Auftragsverarbeiter eingesetzt wird. Eine Übermittlung in Länder außerhalb der EU bzw. des EWR findet nicht statt.</p>
<h2>Freiwilligkeit</h2>
<p>Du bist weder gesetzlich noch vertraglich verpflichtet, uns diese Daten bereitzustellen. Wenn Du ablehnst oder Deine Einwilligung widerrufst, kannst Du die Website trotzdem uneingeschränkt nutzen. Eine automatisierte Entscheidungsfindung findet nicht statt.</p>
<h2>Einwilligung widerrufen</h2>
<p>Du kannst Deine Einwilligung jederzeit mit Wirkung für die Zukunft widerrufen: Klicke auf „Datenschutz-Einstellungen“ (in der Regel unten rechts auf jeder Seite) und wähle „Ablehnen“. Die Rechtmäßigkeit der Verarbeitung bis zum Widerruf bleibt unberührt.</p>
<div class="box">
<h2>Widerspruchsrecht</h2>
<p>Du kannst der Zählung nach Art. 6 Abs. 1 lit. f DSGVO jederzeit aus Gründen widersprechen, die sich aus Deiner besonderen Situation ergeben (Art. 21 DSGVO). Am einfachsten wählst Du im Banner oder über „Datenschutz-Einstellungen“ „Ablehnen“ oder aktivierst in Deinem Browser das Signal „Global Privacy Control“. Dann sendet das Skript keine Daten mehr. Du kannst Deinen Widerspruch auch an die oben genannte E-Mail-Adresse richten.</p>
</div>
<h2>Deine Rechte</h2>
<p>Du hast das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung und Datenübertragbarkeit (Art. 15 bis 18 und 20 DSGVO). In den Messdaten speichern wir keine Namen, keine IP-Adressen und keine Kennung, die länger als einen Tag gilt, und wir runden Zeitpunkte auf die volle Stunde. Deshalb können wir gespeicherte Messdaten in der Regel nicht Dir zuordnen (Art. 11 DSGVO). Anders kann es sein, solange die Zugriffsprotokolle des Webservers noch vorhanden sind (14 Tage). Wenn Du uns Datum, ungefähre Uhrzeit, besuchte Seiten und, wenn Du möchtest, Deine IP-Adresse nennst, prüfen wir, ob eine Zuordnung möglich ist. Ist das nicht möglich, teilen wir Dir das mit.</p>
<p>Mit Deinen Anliegen kannst Du Dich jederzeit an die oben genannte Adresse wenden. Außerdem hast Du das Recht, Dich bei einer Datenschutz-Aufsichtsbehörde zu beschweren (Art. 77 DSGVO).</p>
</body>
</html>
@@ -0,0 +1,84 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex">
<title>Datenschutz – Reichweitenmessung</title>
<style>
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;line-height:1.6;color:#111;background:#fff;max-width:46rem;margin:0 auto;padding:1.5rem 1rem 16rem}
h1{font-size:1.6rem}h2{font-size:1.15rem;margin-top:2rem}
.stand{color:#555;font-size:.9rem}
ul{padding-left:1.25rem}
a{color:#0b57d0}
.box{border:2px solid #111;border-radius:10px;padding:.25rem 1rem;margin:1.5rem 0}
</style>
</head>
<body>
<p><a href="/">← Zur Startseite</a></p>
<h1>Datenschutz bei der Reichweitenmessung</h1>
<p class="stand">Ergänzung zur Datenschutzerklärung dieser Website · Stand: 02.10.2026</p>
<p>Diese Seite beschreibt nur die Reichweitenmessung. Alle übrigen Datenschutzhinweise findest Du in der Datenschutzerklärung dieser Website; Impressum und Datenschutzerklärung erreichst Du über die Startseite.</p>
<h2>Verantwortlicher</h2>
<p>{{company}}<br>{{#address}}{{address}}<br>{{/address}}E-Mail: <a href="mailto:{{email}}">{{email}}</a></p>
<p>Falls es einen Datenschutzbeauftragten gibt, findest Du seine Kontaktdaten in der Datenschutzerklärung dieser Website.</p>
<h2>Worum es geht</h2>
<p>Wir möchten verstehen, wie diese Website genutzt wird, um sie zu verbessern: Welche Seiten werden gelesen, woher kommen die Besucher, wo wird geklickt? Dafür nutzen wir eine eigene Messung ohne Cookies und ohne Dienste von Google, Meta oder anderen Werbenetzwerken. Wir nutzen die Daten nicht für Werbung und bilden keine Profile einzelner Besucher. Die Messung läuft auf einem Server in Deutschland.</p>
<p>Damit die Messung arbeiten kann, lädt Dein Browser bei jedem Seitenaufruf ein Skript von einem Server der WEBklar GbR. Dabei erhält dieser Server, wie bei jeder Datei, die eine Website lädt, technisch Deine IP-Adresse und Dein Browser-Kennzeichen (User-Agent). Das gilt auch, wenn Du ablehnst oder „Global Privacy Control“ aktiviert hast. Wir speichern und protokollieren beides nicht.</p>
<h2>Solange Du im Banner nichts ausgewählt hast</h2>
<p>Dann zählen wir den Aufruf der Seite. Das Skript übermittelt dafür nur die Adresse der Seite und die Angabe, dass das Banner angezeigt wurde. Gespeichert werden:</p>
<ul>
<li>der Seitenpfad (ohne Suchbegriffe oder Parameter hinter dem „?“),</li>
<li>das Land, das wir aus Deiner IP-Adresse ableiten,</li>
<li>die Geräteklasse (Handy, Tablet oder Computer), die wir aus dem Browser-Kennzeichen ableiten,</li>
<li>der Zeitpunkt, auf die volle Stunde gerundet, und</li>
<li>ein Zähler dafür, dass das Banner angezeigt wurde.</li>
</ul>
<p>IP-Adresse und Browser-Kennzeichen verwenden wir nur kurz im Arbeitsspeicher: um Land und Geräteklasse zu bestimmen, um automatische Abrufe durch Programme (Bots) auszusortieren und um zu verhindern, dass die Messung durch massenhafte Anfragen verfälscht wird. Für diese Mengenbegrenzung bilden wir aus Deiner IP-Adresse einen Prüfwert (Hash), den wir nur im Arbeitsspeicher halten und nicht speichern. Eine Kennung, mit der wir Dich wiedererkennen könnten, speichern wir ohne Deine Einwilligung nicht.</p>
<p>Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO. Unser berechtigtes Interesse ist, die Nutzung unserer Website in Zahlen zu kennen, das Banner zu betreiben und die Messung vor Missbrauch zu schützen.</p>
<h2>Wenn Du „Ablehnen“ wählst</h2>
<p>Dann zählen wir einmal, dass abgelehnt wurde, und das Skript sendet danach nichts mehr, auch keine Seitenaufrufe. Sendet Dein Browser das Signal „Global Privacy Control“ oder „Do Not Track“, zeigen wir das Banner gar nicht an und zählen nichts.</p>
<h2>Wenn Du „Annehmen“ wählst</h2>
<p>Dann messen wir zusätzlich:</p>
<ul>
<li>von welcher Website Du gekommen bist (nur der Domainname),</li>
<li>die Größenklasse Deines Browserfensters,</li>
<li>eine Besucher-Kennung, die täglich wechselt. Wir berechnen sie aus IP-Adresse und Browser-Kennzeichen mit einem Schlüssel, der jeden Tag neu erzeugt und nicht gespeichert wird. Am selben Tag könnten wir die Kennung damit nachrechnen. Nach dem Wechsel des Schlüssels lässt sie sich nicht mehr aus Deiner IP-Adresse berechnen und nicht mit Kennungen anderer Tage verknüpfen,</li>
<li>wie lange Du auf einer Seite bleibst (ab dem Laden der Seite) und wie weit Du scrollst,</li>
<li>wo auf der Seite geklickt wird: die Klickposition und den Aufbau des angeklickten Elements, ohne Texte und ohne Eingaben in Formulare (für eine „Heatmap“),</li>
<li>ob Du auf eine Telefonnummer, eine E-Mail-Adresse, einen WhatsApp-Link oder einen Link zu einer anderen Website klickst (dann mit deren Domainname, nicht mit der Nummer oder Adresse selbst), und</li>
<li>einen Zähler dafür, dass Du „Annehmen“ gewählt hast.</li>
</ul>
<p>Rechtsgrundlage ist Deine Einwilligung nach § 25 Abs. 1 TDDDG und Art. 6 Abs. 1 lit. a DSGVO.</p>
<h2>Was in Deinem Browser gespeichert wird</h2>
<p>Wir speichern Deine Auswahl („Annehmen“ oder „Ablehnen“) mit Datum und der Fassung des Banner-Textes im lokalen Speicher Deines Browsers (Eintrag „wk-consent“), damit wir Dich nicht bei jedem Seitenaufruf erneut fragen. Das ist ohne Einwilligung zulässig (§ 25 Abs. 2 Nr. 2 TDDDG). Die Auswahl gilt 183 Tage, danach fragen wir erneut. Du kannst den Eintrag jederzeit selbst löschen, indem Du die Website-Daten in Deinem Browser löschst.</p>
<h2>Speicherdauer</h2>
<p>Einzelne Seitenaufrufe, Ereignisse und Klickpositionen löschen wir nach 30 Tagen. Danach bleiben nur zusammengefasste Tageswerte, also Summen je Tag, etwa Aufrufe je Seite oder je Land, ohne Besucher-Kennung. Wir löschen sie nach 25 Monaten, spätestens rund 60 Tage nach dem Ende der Messung. Unser Dienstleister sichert den Server täglich; gelöschte Daten bleiben dort bis zu sieben Tage erhalten.</p>
<h2>Empfänger</h2>
<p>Die Messung betreibt für uns die WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, als Auftragsverarbeiter. WEBklar darf die Daten nur auf unsere Weisung und für unsere Zwecke verwenden. Der Server steht bei der Hetzner Online GmbH in Deutschland, die als weiterer Auftragsverarbeiter eingesetzt wird. Eine Übermittlung in Länder außerhalb der EU bzw. des EWR findet nicht statt.</p>
<h2>Freiwilligkeit</h2>
<p>Du bist weder gesetzlich noch vertraglich verpflichtet, uns diese Daten bereitzustellen. Wenn Du ablehnst oder Deine Einwilligung widerrufst, kannst Du die Website trotzdem uneingeschränkt nutzen. Eine automatisierte Entscheidungsfindung findet nicht statt.</p>
<h2>Einwilligung widerrufen</h2>
<p>Du kannst Deine Einwilligung jederzeit mit Wirkung für die Zukunft widerrufen: Klicke auf „Datenschutz-Einstellungen“ (das Keks-Symbol, in der Regel unten rechts auf jeder Seite) und wähle „Ablehnen“. Die Rechtmäßigkeit der Verarbeitung bis zum Widerruf bleibt unberührt.</p>
<div class="box">
<h2>Widerspruchsrecht</h2>
<p>Du kannst der Zählung nach Art. 6 Abs. 1 lit. f DSGVO jederzeit aus Gründen widersprechen, die sich aus Deiner besonderen Situation ergeben (Art. 21 DSGVO). Am einfachsten wählst Du im Banner oder über „Datenschutz-Einstellungen“ (das Keks-Symbol) „Ablehnen“ oder aktivierst in Deinem Browser das Signal „Global Privacy Control“. Dann sendet das Skript keine Daten mehr. Du kannst Deinen Widerspruch auch an die oben genannte E-Mail-Adresse richten.</p>
</div>
<h2>Deine Rechte</h2>
<p>Du hast das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung und Datenübertragbarkeit (Art. 15 bis 18 und 20 DSGVO). In den Messdaten speichern wir keine Namen, keine IP-Adressen und keine Kennung, die länger als einen Tag gilt, und wir runden Zeitpunkte auf die volle Stunde. Deshalb können wir gespeicherte Messdaten in der Regel nicht Dir zuordnen (Art. 11 DSGVO). Anders kann es sein, solange die Zugriffsprotokolle des Webservers noch vorhanden sind (14 Tage). Wenn Du uns Datum, ungefähre Uhrzeit, besuchte Seiten und, wenn Du möchtest, Deine IP-Adresse nennst, prüfen wir, ob eine Zuordnung möglich ist. Ist das nicht möglich, teilen wir Dir das mit.</p>
<p>Mit Deinen Anliegen kannst Du Dich jederzeit an die oben genannte Adresse wenden. Außerdem hast Du das Recht, Dich bei einer Datenschutz-Aufsichtsbehörde zu beschweren (Art. 77 DSGVO).</p>
</body>
</html>
@@ -0,0 +1,94 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex">
<title>Datenschutz bei der Reichweitenmessung – WEBklar</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<style>
:root { color-scheme: light; }
* { box-sizing: border-box; }
body { margin: 0; background: #f7f6f3; color: #1d1d1f; font: 16px/1.6 system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; }
main { max-width: 780px; margin: 0 auto; padding: 44px 20px 12rem; }
.brand { font-weight: 800; letter-spacing: 0.12em; font-size: 13px; color: #6d1fd1; }
h1 { font-size: 30px; line-height: 1.2; margin: 6px 0 6px; }
h2 { font-size: 20px; margin: 36px 0 10px; padding-top: 14px; border-top: 1px solid #e3e0da; }
p, li { margin: 6px 0; }
ul { padding-left: 24px; }
.meta { color: #6b6b70; font-size: 14px; }
.box { background: #fff; border: 2px solid #6d1fd1; border-radius: 14px; padding: 16px 20px; margin: 18px 0; }
a { color: #6d1fd1; }
nav.top { font-size: 14px; margin-bottom: 22px; }
</style>
</head>
<body>
<main>
<nav class="top"><a href="/">← Zur Startseite</a> · <a href="https://project.webklar.com/datenschutz.html">Datenschutzerklärung</a> · <a href="/impressum">Impressum</a></nav>
<div class="brand">WEBKLAR</div>
<h1>Datenschutz bei der Reichweitenmessung</h1>
<p class="meta">Ergänzung zu unserer Datenschutzerklärung (Abschnitt 4a) · gilt für webklar.com · Stand 17.10.2026</p>
<p>Diese Seite beschreibt nur die Reichweitenmessung auf webklar.com. Alle übrigen Hinweise finden Sie in unserer <a href="https://project.webklar.com/datenschutz.html">Datenschutzerklärung</a>.</p>
<h2>Verantwortlicher</h2>
<p>WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler<br>
E-Mail: <a href="mailto:support@webklar.com">support@webklar.com</a></p>
<p>Einen Datenschutzbeauftragten haben wir nicht benannt, weil wir dazu gesetzlich nicht verpflichtet sind.</p>
<h2>Worum es geht</h2>
<p>Wir möchten verstehen, wie unsere Website genutzt wird, um sie zu verbessern: Welche Seiten werden gelesen, woher kommen die Besucher, wo wird geklickt? Dafür nutzen wir unsere eigene Website-Analyse, dieselbe, die wir auch unseren Kunden anbieten. Sie kommt ohne Cookies und ohne Dienste von Google, Meta oder anderen Werbenetzwerken aus. Wir nutzen die Daten nicht für Werbung und bilden keine Profile einzelner Besucher. Die Messung läuft auf unserem Server in Deutschland.</p>
<p>Damit die Messung arbeiten kann, lädt Ihr Browser bei jedem Seitenaufruf ein Skript von unserem Server (project.webklar.com). Dabei erhält dieser Server, wie bei jeder Datei, die eine Website lädt, technisch Ihre IP-Adresse und Ihr Browser-Kennzeichen (User-Agent). Das gilt auch, wenn Sie ablehnen oder „Global Privacy Control“ aktiviert haben. Beim Abruf des Skripts und bei der Messung speichern und protokollieren wir beides nicht.</p>
<h2>Solange Sie im Banner nichts ausgewählt haben</h2>
<p>Dann zählen wir den Aufruf der Seite. Das Skript übermittelt dafür nur die Adresse der Seite und die Angabe, dass das Banner angezeigt wurde. Gespeichert werden:</p>
<ul>
<li>der Seitenpfad (ohne Suchbegriffe oder Parameter hinter dem „?“),</li>
<li>das Land, das wir aus Ihrer IP-Adresse ableiten,</li>
<li>die Geräteklasse (Handy, Tablet oder Computer), die wir aus dem Browser-Kennzeichen ableiten,</li>
<li>der Zeitpunkt, auf die volle Stunde gerundet, und</li>
<li>ein Zähler dafür, dass das Banner angezeigt wurde.</li>
</ul>
<p>IP-Adresse und Browser-Kennzeichen verwenden wir nur kurz im Arbeitsspeicher: um Land und Geräteklasse zu bestimmen, um automatische Abrufe durch Programme (Bots) auszusortieren und um zu verhindern, dass die Messung durch massenhafte Anfragen verfälscht wird. Für diese Mengenbegrenzung bilden wir aus Ihrer IP-Adresse einen Prüfwert (Hash), den wir nur im Arbeitsspeicher halten und nicht speichern. Eine Kennung, mit der wir Sie wiedererkennen könnten, speichern wir ohne Ihre Einwilligung nicht.</p>
<p>Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO. Unser berechtigtes Interesse ist, die Nutzung unserer Website in Zahlen zu kennen, das Banner zu betreiben und die Messung vor Missbrauch zu schützen.</p>
<h2>Wenn Sie „Ablehnen“ wählen</h2>
<p>Dann zählen wir einmal, dass abgelehnt wurde, und das Skript sendet danach nichts mehr, auch keine Seitenaufrufe. Sendet Ihr Browser das Signal „Global Privacy Control“ oder „Do Not Track“, zeigen wir das Banner gar nicht an und zählen nichts.</p>
<h2>Wenn Sie „Annehmen“ wählen</h2>
<p>Dann messen wir zusätzlich:</p>
<ul>
<li>von welcher Website Sie gekommen sind (nur der Domainname),</li>
<li>die Größenklasse Ihres Browserfensters,</li>
<li>eine Besucher-Kennung, die täglich wechselt. Wir berechnen sie aus IP-Adresse und Browser-Kennzeichen mit einem Schlüssel, der jeden Tag neu erzeugt und nicht gespeichert wird. Am selben Tag könnten wir die Kennung damit nachrechnen. Nach dem Wechsel des Schlüssels lässt sie sich nicht mehr aus Ihrer IP-Adresse berechnen und nicht mit Kennungen anderer Tage verknüpfen,</li>
<li>wie lange Sie auf einer Seite bleiben (ab dem Laden der Seite) und wie weit Sie scrollen,</li>
<li>wo auf der Seite geklickt wird: die Klickposition und den Aufbau des angeklickten Elements, ohne Texte und ohne Eingaben in Formulare (für eine „Heatmap“),</li>
<li>ob Sie auf eine Telefonnummer, eine E-Mail-Adresse, einen WhatsApp-Link oder einen Link zu einer anderen Website klicken (dann mit deren Domainname, nicht mit der Nummer oder Adresse selbst), und</li>
<li>einen Zähler dafür, dass Sie „Annehmen“ gewählt haben.</li>
</ul>
<p>Rechtsgrundlage ist Ihre Einwilligung nach § 25 Abs. 1 TDDDG und Art. 6 Abs. 1 lit. a DSGVO.</p>
<h2>Was in Ihrem Browser gespeichert wird</h2>
<p>Wir speichern Ihre Auswahl („Annehmen“ oder „Ablehnen“) mit Datum und der Fassung des Banner-Textes im lokalen Speicher Ihres Browsers (Eintrag „wk-consent“), damit wir Sie nicht bei jedem Seitenaufruf erneut fragen. Das ist ohne Einwilligung zulässig (§ 25 Abs. 2 Nr. 2 TDDDG). Die Auswahl gilt 183 Tage, danach fragen wir erneut. Sie können den Eintrag jederzeit selbst löschen, indem Sie die Website-Daten in Ihrem Browser löschen.</p>
<h2>Speicherdauer</h2>
<p>Einzelne Seitenaufrufe, Ereignisse und Klickpositionen löschen wir nach 30 Tagen. Danach bleiben nur zusammengefasste Tageswerte, also Summen je Tag, etwa Aufrufe je Seite oder je Land, ohne Besucher-Kennung. Wir löschen sie nach 25 Monaten. Beenden wir die Messung, löschen wir alle Messdaten spätestens rund 60 Tage danach. Unser Dienstleister sichert den Server täglich; gelöschte Daten bleiben dort bis zu sieben Tage erhalten.</p>
<h2>Empfänger</h2>
<p>Die Messung betreiben wir selbst. Die Daten sehen nur unsere Mitarbeiter. Der Server steht bei der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, in Deutschland; Hetzner ist für uns als Auftragsverarbeiter tätig. Eine Übermittlung in Länder außerhalb der EU bzw. des EWR findet nicht statt.</p>
<h2>Freiwilligkeit</h2>
<p>Sie sind weder gesetzlich noch vertraglich verpflichtet, uns diese Daten bereitzustellen. Wenn Sie ablehnen oder Ihre Einwilligung widerrufen, können Sie unsere Website trotzdem uneingeschränkt nutzen. Eine automatisierte Entscheidungsfindung findet nicht statt.</p>
<div class="box">
<h2 style="border:0;margin-top:0;padding-top:0">Einwilligung widerrufen</h2>
<p>Sie können Ihre Einwilligung jederzeit mit Wirkung für die Zukunft widerrufen: Klicken Sie auf „Datenschutz-Einstellungen“ (das Keks-Symbol unten rechts auf jeder Seite) und wählen Sie „Ablehnen“. Die Rechtmäßigkeit der Verarbeitung bis zum Widerruf bleibt unberührt.</p>
<h2 style="border:0;padding-top:0">Widerspruchsrecht</h2>
<p>Sie können der Zählung nach Art. 6 Abs. 1 lit. f DSGVO jederzeit aus Gründen widersprechen, die sich aus Ihrer besonderen Situation ergeben (Art. 21 DSGVO). Am einfachsten wählen Sie im Banner oder über „Datenschutz-Einstellungen“ (das Keks-Symbol) „Ablehnen“ oder aktivieren in Ihrem Browser das Signal „Global Privacy Control“. Dann sendet das Skript keine Daten mehr. Sie können Ihren Widerspruch auch an die oben genannte E-Mail-Adresse richten.</p>
</div>
<h2>Ihre Rechte</h2>
<p>Sie haben das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung und Datenübertragbarkeit (Art. 15 bis 18 und 20 DSGVO). In den Messdaten speichern wir keine Namen, keine IP-Adressen und keine Kennung, die länger als einen Tag gilt, und wir runden Zeitpunkte auf die volle Stunde. Deshalb können wir gespeicherte Messdaten in der Regel nicht Ihnen zuordnen (Art. 11 DSGVO). Anders kann es sein, solange die Zugriffsprotokolle unseres Webservers noch vorhanden sind (Abschnitt 3 der Datenschutzerklärung). Wenn Sie uns Datum, ungefähre Uhrzeit, besuchte Seiten und, wenn Sie möchten, Ihre IP-Adresse nennen, prüfen wir, ob eine Zuordnung möglich ist. Ist das nicht möglich, teilen wir Ihnen das mit.</p>
<p>Mit Ihren Anliegen können Sie sich jederzeit an die oben genannte Adresse wenden. Außerdem haben Sie das Recht, sich bei einer Datenschutz-Aufsichtsbehörde zu beschweren (Art. 77 DSGVO), für uns zuständig ist der Landesbeauftragte für den Datenschutz und die Informationsfreiheit Rheinland-Pfalz.</p>
</main>
</body>
</html>
@@ -0,0 +1,141 @@
# Gästetexte des Terminplaners, Fassung 2026-10-02
Dieses Archiv hält den Wortlaut der Gästetexte fest, die ein Gast in dieser Fassung sieht (AVV Terminplaner Nr. 8 Abs. 5). Der Nachweis einer Buchung speichert die Fassung, den Zeitpunkt, den Text des Buchungsknopfs, die Anrede und Hashes (SHA-256) der übrigen Texte, nicht die Texte selbst.
Fassung der Gästetexte: **2026-10-02** (`RECHTSTEXTE_VERSION` in `server/services/termine/rechtstexte.js`). Der Zeitraum der Verwendung ergibt sich aus „gültig ab“ dieser Fassung und der nächsten Fassung in der Tabelle „Terminplaner-Gästetexte“ in `README.md` dieses Ordners.
Erzeugt mit `node scripts/termine-texte-archiv.mjs`, geprüft mit `--check`. Nicht von Hand ändern: eine Textänderung bekommt eine neue Fassung und eine neue Datei; diese Datei bleibt, wie sie war.
Die Betriebsangaben stehen als Platzhalter: `{{Name des Betriebs}}`, `{{Anschrift}}`, `{{Telefon}}` und `{{E-Mail}}` stammen aus der Einstellung des Betriebs, `{{Stunden}}` ist die gewählte Vorlaufzeit der Erinnerung (der Nachweis speichert sie), die Aufbewahrung nach dem Termin ist mit der Vorgabe von 90 Tagen dargestellt (einstellbar von 14 bis 365 Tagen).
## 1. Hinweis unter dem Buchungsformular (Datenschutz)
### Sie-Fassung
> Datenschutz. Wir verwenden Ihre Angaben, um Ihren Termin zu verwalten und zu bestätigen (Art. 6 Abs. 1 lit. b DSGVO). Betrieben wird der Terminkalender in unserem Auftrag von der WEBklar GbR (Server: Hetzner, Deutschland). Wir speichern Ihre Daten bis 90 Tage nach dem Termin, abgesagte Termine 30 Tage, nicht bestätigte Buchungen 48 Stunden nach Ablauf der Bestätigungsfrist. Es werden keine Cookies gesetzt. Bitte machen Sie keine Angaben zu Ihrer Gesundheit. Mehr erfahren Sie in unserer Datenschutzerklärung. Verantwortlich: {{Name des Betriebs}}, {{Anschrift}}.
### Du-Fassung
> Datenschutz. Wir verwenden deine Angaben, um deinen Termin zu verwalten und zu bestätigen (Art. 6 Abs. 1 lit. b DSGVO). Betrieben wird der Terminkalender in unserem Auftrag von der WEBklar GbR (Server: Hetzner, Deutschland). Wir speichern deine Daten bis 90 Tage nach dem Termin, abgesagte Termine 30 Tage, nicht bestätigte Buchungen 48 Stunden nach Ablauf der Bestätigungsfrist. Es werden keine Cookies gesetzt. Bitte mach keine Angaben zu deiner Gesundheit. Mehr erfährst du in unserer Datenschutzerklärung. Verantwortlich: {{Name des Betriebs}}, {{Anschrift}}.
## 2. Haken
Die Haken stehen immer in der Sie-Form, auch bei Betrieben, die ihre Gäste duzen.
### Vorzeitiger Beginn (Pflicht bei einer Leistung mit Preis für Verbraucher, wenn der Termin vor Ablauf der Widerrufsfrist beginnt, bei Terminanfragen immer)
> Ich verlange ausdrücklich, dass {{Name des Betriebs}} mit der Leistung vor Ablauf der Widerrufsfrist beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen Erbringung der Leistung erlischt (§ 356 Abs. 5 Nr. 2 BGB). Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB).
### Stornobedingungen (Pflicht, wenn der Betrieb Stornobedingungen hat; der Text der Bedingungen steht beim Betrieb und trägt eine eigene Version)
> Ich habe die Stornobedingungen gelesen.
### Erinnerung (freiwillig, nie vorangekreuzt, nur wenn der Betrieb Erinnerungen anbietet)
> Ich möchte {{Stunden}} Stunden vor dem Termin eine Erinnerung per E-Mail erhalten.
## 3. Hinweis zu Gesundheitsangaben
Steht über dem Nachrichtenfeld des Formulars.
> Bitte keine Gesundheitsangaben.
## 4. Knopftexte
| Leistung | Knopf im Widget | Knopf auf der Bestätigungsseite |
|---|---|---|
| Sofort buchbar, ohne Preis | Termin buchen | Termin bestätigen |
| Sofort buchbar, mit Preis | Zahlungspflichtig buchen | Zahlungspflichtig bestätigen |
| Terminanfrage | Termin anfragen | Anfrage absenden |
Der Text des Buchungsknopfs im Widget steht im Nachweis (`knopf`).
## 5. Widerrufsbelehrung mit Muster-Widerrufsformular für Gäste
Sie-Form, mit den Betriebsangaben als Platzhalter.
> Widerrufsbelehrung
>
> Widerrufsrecht
>
> Sie haben das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen.
>
> Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag des Vertragsabschlusses.
>
> Um Ihr Widerrufsrecht auszuüben, müssen Sie uns ({{Name des Betriebs}}, {{Anschrift}}, Telefon: {{Telefon}}, E-Mail: {{E-Mail}}) mittels einer eindeutigen Erklärung (z. B. ein mit der Post versandter Brief oder eine E-Mail) über Ihren Entschluss, diesen Vertrag zu widerrufen, informieren. Sie können dafür das beigefügte Muster-Widerrufsformular verwenden, das jedoch nicht vorgeschrieben ist.
>
> Sie können Ihr Widerrufsrecht auch online unter „Vertrag widerrufen“ (Schaltfläche im Terminbereich auf unserer Website sowie Link „Termin verwalten“ in Ihrer Bestätigungs-E-Mail) ausüben. Wenn Sie diese Online-Funktion nutzen, übermitteln wir Ihnen auf einem dauerhaften Datenträger (z. B. durch eine E-Mail) unverzüglich eine Eingangsbestätigung mit Informationen zum Inhalt der Widerrufserklärung sowie dem Datum und der Uhrzeit ihres Eingangs.
>
> Zur Wahrung der Widerrufsfrist reicht es aus, dass Sie die Mitteilung über die Ausübung des Widerrufsrechts vor Ablauf der Widerrufsfrist absenden.
>
> Folgen des Widerrufs
>
> Wenn Sie diesen Vertrag widerrufen, haben wir Ihnen alle Zahlungen, die wir von Ihnen erhalten haben, einschließlich der Lieferkosten (mit Ausnahme der zusätzlichen Kosten, die sich daraus ergeben, dass Sie eine andere Art der Lieferung als die von uns angebotene, günstigste Standardlieferung gewählt haben), unverzüglich und spätestens binnen vierzehn Tagen ab dem Tag zurückzuzahlen, an dem die Mitteilung über Ihren Widerruf dieses Vertrags bei uns eingegangen ist. Für diese Rückzahlung verwenden wir dasselbe Zahlungsmittel, das Sie bei der ursprünglichen Transaktion eingesetzt haben, es sei denn, mit Ihnen wurde ausdrücklich etwas anderes vereinbart; in keinem Fall werden Ihnen wegen dieser Rückzahlung Entgelte berechnet.
>
> Haben Sie verlangt, dass die Dienstleistungen während der Widerrufsfrist beginnen soll, so haben Sie uns einen angemessenen Betrag zu zahlen, der dem Anteil der bis zu dem Zeitpunkt, zu dem Sie uns von der Ausübung des Widerrufsrechts hinsichtlich dieses Vertrags unterrichten, bereits erbrachten Dienstleistungen im Vergleich zum Gesamtumfang der im Vertrag vorgesehenen Dienstleistungen entspricht.
>
> Muster-Widerrufsformular
>
> (Wenn Sie den Vertrag widerrufen wollen, dann füllen Sie bitte dieses Formular aus und senden Sie es zurück.)
>
> – An {{Name des Betriebs}}, {{Anschrift}}, {{E-Mail}}:
> – Hiermit widerrufe(n) ich/wir (*) den von mir/uns (*) abgeschlossenen Vertrag über den Kauf der folgenden Waren (*)/die Erbringung der folgenden Dienstleistung (*)
> – Bestellt am (*)/erhalten am (*)
> – Name des/der Verbraucher(s)
> – Anschrift des/der Verbraucher(s)
> – Unterschrift des/der Verbraucher(s) (nur bei Mitteilung auf Papier)
> – Datum
>
> (*) Unzutreffendes streichen.
## 6. Datenschutz-Absatz „Online-Terminbuchung“ für die Datenschutzerklärung der Website des Betriebs
Hier mit allen bedingten Sätzen (Telefon-Feld, Nachricht oder Zusatzfelder, Erinnerung, Widerruf, Telefonnummer des Betriebs); ein Betrieb ohne die jeweilige Funktion hat den Satz nicht.
### Sie-Fassung
> Online-Terminbuchung
>
> Auf unserer Website können Sie Termine online buchen. Dafür verarbeiten wir die folgenden Daten:
>
> Welche Daten? Ihren Namen, Ihre E-Mail-Adresse, den gewählten Termin, die gewählte Leistung und die gewählte Person oder den gewählten Platz, auf Wunsch Ihre Telefonnummer und die Angaben, die Sie uns im Formular mitteilen. Außerdem speichern wir, wann Sie den Termin bestätigt haben, welche Fassung der Hinweise Sie bei der Buchung gesehen haben und welche Zustimmungen Sie erteilt haben, zum Beispiel ob Sie eine Erinnerung gewünscht haben.
>
> Wofür und auf welcher Grundlage? Wir brauchen die Daten, um Ihren Termin zu verwalten, Ihnen den Termin zu bestätigen und Sie über Änderungen oder Absagen zu informieren. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO (Vertrag über den gebuchten Termin, bei einer Terminanfrage vorvertragliche Maßnahmen auf Ihre Anfrage). Wenn Sie eine Erinnerung vor dem Termin wünschen, beruht diese auf Ihrer Einwilligung nach Art. 6 Abs. 1 lit. a DSGVO. Sie können sie jederzeit mit Wirkung für die Zukunft widerrufen, indem Sie uns eine kurze Nachricht senden oder den Termin über den Link in der E-Mail absagen. Widerrufen Sie einen Vertrag über die Schaltfläche „Vertrag widerrufen“, speichern wir Ihre Erklärung mit Datum und Uhrzeit und senden Ihnen eine Eingangsbestätigung (Art. 6 Abs. 1 lit. c DSGVO in Verbindung mit § 356a BGB). Zum Schutz vor Missbrauch (zum Beispiel automatisierten Buchungen) und um die Zahl der Anfragen zu begrenzen, merken wir uns Ihre IP-Adresse kurzzeitig nur im Arbeitsspeicher des Servers, nicht dauerhaft (Art. 6 Abs. 1 lit. f DSGVO; unser berechtigtes Interesse ist der sichere Betrieb der Terminbuchung).
>
> E-Mails. Sie erhalten eine E-Mail, in der Sie Ihre E-Mail-Adresse und den Termin per Link bestätigen, danach eine Bestätigung und, wenn Sie das angekreuzt haben, eine Erinnerung. Sagen wir den Termin ab oder verschieben ihn, informieren wir Sie ebenfalls per E-Mail. Die E-Mails enthalten keine Werbung und keine Tracking-Pixel. Absender ist {{Name des Betriebs}}, Ihre Antworten gehen an uns. Bis zum Versand liegt die E-Mail in einer Warteschlange; nach dem Versand wird dort die Empfängeradresse samt Inhalt gelöscht, nicht zustellbare E-Mails nach 48 Stunden.
>
> Wer verarbeitet die Daten in unserem Auftrag? Den Terminkalender betreibt die WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, als unser Auftragsverarbeiter nach Art. 28 DSGVO. Die Daten liegen auf Servern der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, in Deutschland (Rechenzentren Falkenstein und Nürnberg). Der E-Mail-Versand läuft ebenfalls über Hetzner. Eine Übermittlung in Drittländer findet nicht statt.
>
> Beim Aufruf der Terminbuchung. Beim Aufruf dieser Seite lädt Ihr Browser das Programm der Terminbuchung vom Server der WEBklar GbR (project.webklar.com), beim Öffnen der Terminbuchung zusätzlich die Termine und Texte. Dabei werden technisch bedingt Ihre IP-Adresse und Angaben zu Ihrem Browser übertragen. Der Server speichert sie kurzzeitig in Protokollen und löscht sie nach 14 Tagen (Art. 6 Abs. 1 lit. f DSGVO; berechtigtes Interesse ist der sichere und funktionsfähige Betrieb). Wir setzen dafür keine Cookies und speichern nichts auf Ihrem Gerät.
>
> Wie lange speichern wir? Nicht bestätigte Buchungen (der Bestätigungslink gilt 30 Minuten) löschen wir 48 Stunden nach Ablauf dieser Frist. Bestätigte Termine löschen wir 90 Tage nach dem Termin, abgesagte oder abgelehnte Termine 30 Tage nach der Absage oder Ablehnung. Terminanfragen, die wir bis zum Terminbeginn nicht beantworten, behandeln wir als abgelehnt. Wird die Online-Terminbuchung abgeschaltet, löschen wir alle Termindaten 30 Tage nach dem Abschalten. Gesetzliche Aufbewahrungspflichten für Geschäftsunterlagen (zum Beispiel Rechnungen) bleiben davon unberührt; sie erfüllen wir außerhalb des Terminkalenders.
>
> Pflicht zur Angabe. Ohne Name, E-Mail-Adresse und Terminwahl können wir keinen Termin online vergeben. Sie können uns auch telefonisch erreichen: {{Telefon}}. Eine automatisierte Entscheidungsfindung findet nicht statt. Bitte machen Sie bei der Buchung keine Angaben zu Ihrer Gesundheit.
>
> Ihre Rechte. Sie haben das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung und Datenübertragbarkeit sowie das Recht, der Verarbeitung auf Grundlage berechtigter Interessen zu widersprechen (Art. 15 bis 21 DSGVO). Eine erteilte Einwilligung können Sie jederzeit widerrufen. Sie können sich bei einer Datenschutzaufsichtsbehörde beschweren. Verantwortlich ist {{Name des Betriebs}}, {{Anschrift}}, E-Mail {{E-Mail}}.
### Du-Fassung
> Online-Terminbuchung
>
> Auf unserer Website kannst du Termine online buchen. Dafür verarbeiten wir die folgenden Daten:
>
> Welche Daten? Deinen Namen, deine E-Mail-Adresse, den gewählten Termin, die gewählte Leistung und die gewählte Person oder den gewählten Platz, auf Wunsch deine Telefonnummer und die Angaben, die du uns im Formular mitteilst. Außerdem speichern wir, wann du den Termin bestätigt hast, welche Fassung der Hinweise du bei der Buchung gesehen hast und welche Zustimmungen du erteilt hast, zum Beispiel ob du eine Erinnerung gewünscht hast.
>
> Wofür und auf welcher Grundlage? Wir brauchen die Daten, um deinen Termin zu verwalten, dir den Termin zu bestätigen und dich über Änderungen oder Absagen zu informieren. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO (Vertrag über den gebuchten Termin, bei einer Terminanfrage vorvertragliche Maßnahmen auf deine Anfrage). Wenn du eine Erinnerung vor dem Termin wünschst, beruht diese auf deiner Einwilligung nach Art. 6 Abs. 1 lit. a DSGVO. Du kannst sie jederzeit mit Wirkung für die Zukunft widerrufen, indem du uns eine kurze Nachricht sendest oder den Termin über den Link in der E-Mail absagst. Widerrufst du einen Vertrag über die Schaltfläche „Vertrag widerrufen“, speichern wir deine Erklärung mit Datum und Uhrzeit und senden dir eine Eingangsbestätigung (Art. 6 Abs. 1 lit. c DSGVO in Verbindung mit § 356a BGB). Zum Schutz vor Missbrauch (zum Beispiel automatisierten Buchungen) und um die Zahl der Anfragen zu begrenzen, merken wir uns deine IP-Adresse kurzzeitig nur im Arbeitsspeicher des Servers, nicht dauerhaft (Art. 6 Abs. 1 lit. f DSGVO; unser berechtigtes Interesse ist der sichere Betrieb der Terminbuchung).
>
> E-Mails. Du erhältst eine E-Mail, in der du deine E-Mail-Adresse und den Termin per Link bestätigst, danach eine Bestätigung und, wenn du das angekreuzt hast, eine Erinnerung. Sagen wir den Termin ab oder verschieben ihn, informieren wir dich ebenfalls per E-Mail. Die E-Mails enthalten keine Werbung und keine Tracking-Pixel. Absender ist {{Name des Betriebs}}, deine Antworten gehen an uns. Bis zum Versand liegt die E-Mail in einer Warteschlange; nach dem Versand wird dort die Empfängeradresse samt Inhalt gelöscht, nicht zustellbare E-Mails nach 48 Stunden.
>
> Wer verarbeitet die Daten in unserem Auftrag? Den Terminkalender betreibt die WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, als unser Auftragsverarbeiter nach Art. 28 DSGVO. Die Daten liegen auf Servern der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, in Deutschland (Rechenzentren Falkenstein und Nürnberg). Der E-Mail-Versand läuft ebenfalls über Hetzner. Eine Übermittlung in Drittländer findet nicht statt.
>
> Beim Aufruf der Terminbuchung. Beim Aufruf dieser Seite lädt dein Browser das Programm der Terminbuchung vom Server der WEBklar GbR (project.webklar.com), beim Öffnen der Terminbuchung zusätzlich die Termine und Texte. Dabei werden technisch bedingt deine IP-Adresse und Angaben zu deinem Browser übertragen. Der Server speichert sie kurzzeitig in Protokollen und löscht sie nach 14 Tagen (Art. 6 Abs. 1 lit. f DSGVO; berechtigtes Interesse ist der sichere und funktionsfähige Betrieb). Wir setzen dafür keine Cookies und speichern nichts auf deinem Gerät.
>
> Wie lange speichern wir? Nicht bestätigte Buchungen (der Bestätigungslink gilt 30 Minuten) löschen wir 48 Stunden nach Ablauf dieser Frist. Bestätigte Termine löschen wir 90 Tage nach dem Termin, abgesagte oder abgelehnte Termine 30 Tage nach der Absage oder Ablehnung. Terminanfragen, die wir bis zum Terminbeginn nicht beantworten, behandeln wir als abgelehnt. Wird die Online-Terminbuchung abgeschaltet, löschen wir alle Termindaten 30 Tage nach dem Abschalten. Gesetzliche Aufbewahrungspflichten für Geschäftsunterlagen (zum Beispiel Rechnungen) bleiben davon unberührt; sie erfüllen wir außerhalb des Terminkalenders.
>
> Pflicht zur Angabe. Ohne Name, E-Mail-Adresse und Terminwahl können wir keinen Termin online vergeben. Du kannst uns auch telefonisch erreichen: {{Telefon}}. Eine automatisierte Entscheidungsfindung findet nicht statt. Bitte mach bei der Buchung keine Angaben zu deiner Gesundheit.
>
> Deine Rechte. Du hast das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung und Datenübertragbarkeit sowie das Recht, der Verarbeitung auf Grundlage berechtigter Interessen zu widersprechen (Art. 15 bis 21 DSGVO). Eine erteilte Einwilligung kannst du jederzeit widerrufen. Du kannst dich bei einer Datenschutzaufsichtsbehörde beschweren. Verantwortlich ist {{Name des Betriebs}}, {{Anschrift}}, E-Mail {{E-Mail}}.
## 7. Nachweis
Der Nachweis speichert Hashes der Texte mit den Betriebsangaben zum Buchungszeitpunkt (⚖ Q26).
@@ -0,0 +1,141 @@
# Gästetexte des Terminplaners, Fassung 2026-10-05
Dieses Archiv hält den Wortlaut der Gästetexte fest, die ein Gast in dieser Fassung sieht (AVV Terminplaner Nr. 8 Abs. 5). Der Nachweis einer Buchung speichert die Fassung, den Zeitpunkt, den Text des Buchungsknopfs, die Anrede und Hashes (SHA-256) der übrigen Texte, nicht die Texte selbst.
Fassung der Gästetexte: **2026-10-05** (`RECHTSTEXTE_VERSION` in `server/services/termine/rechtstexte.js`). Der Zeitraum der Verwendung ergibt sich aus „gültig ab“ dieser Fassung und der nächsten Fassung in der Tabelle „Terminplaner-Gästetexte“ in `README.md` dieses Ordners.
Erzeugt mit `node scripts/termine-texte-archiv.mjs`, geprüft mit `--check`. Nicht von Hand ändern: eine Textänderung bekommt eine neue Fassung und eine neue Datei; diese Datei bleibt, wie sie war.
Die Betriebsangaben stehen als Platzhalter: `{{Name des Betriebs}}`, `{{Anschrift}}`, `{{Telefon}}` und `{{E-Mail}}` stammen aus der Einstellung des Betriebs, `{{Stunden}}` ist die gewählte Vorlaufzeit der Erinnerung (der Nachweis speichert sie), die Aufbewahrung nach dem Termin ist mit der Vorgabe von 90 Tagen dargestellt (einstellbar von 14 bis 365 Tagen).
## 1. Hinweis unter dem Buchungsformular (Datenschutz)
### Sie-Fassung
> Datenschutz. Wir verwenden Ihre Angaben, um Ihren Termin zu verwalten und zu bestätigen (Art. 6 Abs. 1 lit. b DSGVO). Betrieben wird der Terminkalender in unserem Auftrag von der WEBklar GbR (Server: Hetzner, Deutschland). Wir speichern Ihre Daten bis 90 Tage nach dem Termin, abgesagte Termine 30 Tage, nicht bestätigte Buchungen 48 Stunden nach Ablauf der Bestätigungsfrist. Es werden keine Cookies gesetzt. Bitte machen Sie keine Angaben zu Ihrer Gesundheit. Mehr erfahren Sie in unserer Datenschutzerklärung. Verantwortlich: {{Name des Betriebs}}, {{Anschrift}}.
### Du-Fassung
> Datenschutz. Wir verwenden deine Angaben, um deinen Termin zu verwalten und zu bestätigen (Art. 6 Abs. 1 lit. b DSGVO). Betrieben wird der Terminkalender in unserem Auftrag von der WEBklar GbR (Server: Hetzner, Deutschland). Wir speichern deine Daten bis 90 Tage nach dem Termin, abgesagte Termine 30 Tage, nicht bestätigte Buchungen 48 Stunden nach Ablauf der Bestätigungsfrist. Es werden keine Cookies gesetzt. Bitte mach keine Angaben zu deiner Gesundheit. Mehr erfährst du in unserer Datenschutzerklärung. Verantwortlich: {{Name des Betriebs}}, {{Anschrift}}.
## 2. Haken
Die Haken stehen immer in der Sie-Form, auch bei Betrieben, die ihre Gäste duzen.
### Vorzeitiger Beginn (Pflicht bei einer Leistung mit Preis für Verbraucher, wenn der Termin vor Ablauf der Widerrufsfrist beginnt, bei Terminanfragen immer)
> Ich verlange ausdrücklich, dass {{Name des Betriebs}} mit der Leistung vor Ablauf der Widerrufsfrist beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen Erbringung der Leistung erlischt (§ 356 Abs. 5 Nr. 2 BGB). Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB).
### Stornobedingungen (Pflicht bei einer Leistung mit Preis, wenn der Betrieb Stornobedingungen hat; bei kostenlosen Leistungen fragt der Terminplaner nicht danach; der Text der Bedingungen steht beim Betrieb und trägt eine eigene Version)
> Ich habe die Stornobedingungen gelesen.
### Erinnerung (freiwillig, nie vorangekreuzt, nur wenn der Betrieb Erinnerungen anbietet)
> Ich möchte {{Stunden}} Stunden vor dem Termin eine Erinnerung per E-Mail erhalten. Diese Einwilligung kann ich jederzeit widerrufen.
## 3. Hinweis zu Gesundheitsangaben
Steht über dem Nachrichtenfeld des Formulars.
> Bitte keine Gesundheitsangaben.
## 4. Knopftexte
| Leistung | Knopf im Widget | Knopf auf der Bestätigungsseite |
|---|---|---|
| Sofort buchbar, ohne Preis | Termin buchen | Termin bestätigen |
| Sofort buchbar, mit Preis | Zahlungspflichtig buchen | Zahlungspflichtig bestätigen |
| Terminanfrage | Termin anfragen | Anfrage absenden |
Der Text des Buchungsknopfs im Widget steht im Nachweis (`knopf`).
## 5. Widerrufsbelehrung mit Muster-Widerrufsformular für Gäste
Sie-Form, mit den Betriebsangaben als Platzhalter.
> Widerrufsbelehrung
>
> Widerrufsrecht
>
> Sie haben das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen.
>
> Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag des Vertragsabschlusses.
>
> Um Ihr Widerrufsrecht auszuüben, müssen Sie uns ({{Name des Betriebs}}, {{Anschrift}}, Telefon: {{Telefon}}, E-Mail: {{E-Mail}}) mittels einer eindeutigen Erklärung (z. B. ein mit der Post versandter Brief oder eine E-Mail) über Ihren Entschluss, diesen Vertrag zu widerrufen, informieren. Sie können dafür das beigefügte Muster-Widerrufsformular verwenden, das jedoch nicht vorgeschrieben ist.
>
> Sie können Ihr Widerrufsrecht auch online unter „Vertrag widerrufen“ (Schaltfläche im Terminbereich auf unserer Website sowie Link „Termin verwalten“ in Ihrer Bestätigungs-E-Mail) ausüben. Wenn Sie diese Online-Funktion nutzen, übermitteln wir Ihnen auf einem dauerhaften Datenträger (z. B. durch eine E-Mail) unverzüglich eine Eingangsbestätigung mit Informationen zum Inhalt der Widerrufserklärung sowie dem Datum und der Uhrzeit ihres Eingangs.
>
> Zur Wahrung der Widerrufsfrist reicht es aus, dass Sie die Mitteilung über die Ausübung des Widerrufsrechts vor Ablauf der Widerrufsfrist absenden.
>
> Folgen des Widerrufs
>
> Wenn Sie diesen Vertrag widerrufen, haben wir Ihnen alle Zahlungen, die wir von Ihnen erhalten haben, einschließlich der Lieferkosten (mit Ausnahme der zusätzlichen Kosten, die sich daraus ergeben, dass Sie eine andere Art der Lieferung als die von uns angebotene, günstigste Standardlieferung gewählt haben), unverzüglich und spätestens binnen vierzehn Tagen ab dem Tag zurückzuzahlen, an dem die Mitteilung über Ihren Widerruf dieses Vertrags bei uns eingegangen ist. Für diese Rückzahlung verwenden wir dasselbe Zahlungsmittel, das Sie bei der ursprünglichen Transaktion eingesetzt haben, es sei denn, mit Ihnen wurde ausdrücklich etwas anderes vereinbart; in keinem Fall werden Ihnen wegen dieser Rückzahlung Entgelte berechnet.
>
> Haben Sie verlangt, dass die Dienstleistungen während der Widerrufsfrist beginnen soll, so haben Sie uns einen angemessenen Betrag zu zahlen, der dem Anteil der bis zu dem Zeitpunkt, zu dem Sie uns von der Ausübung des Widerrufsrechts hinsichtlich dieses Vertrags unterrichten, bereits erbrachten Dienstleistungen im Vergleich zum Gesamtumfang der im Vertrag vorgesehenen Dienstleistungen entspricht.
>
> Muster-Widerrufsformular
>
> (Wenn Sie den Vertrag widerrufen wollen, dann füllen Sie bitte dieses Formular aus und senden Sie es zurück.)
>
> – An {{Name des Betriebs}}, {{Anschrift}}, {{E-Mail}}:
> – Hiermit widerrufe(n) ich/wir (*) den von mir/uns (*) abgeschlossenen Vertrag über den Kauf der folgenden Waren (*)/die Erbringung der folgenden Dienstleistung (*)
> – Bestellt am (*)/erhalten am (*)
> – Name des/der Verbraucher(s)
> – Anschrift des/der Verbraucher(s)
> – Unterschrift des/der Verbraucher(s) (nur bei Mitteilung auf Papier)
> – Datum
>
> (*) Unzutreffendes streichen.
## 6. Datenschutz-Absatz „Online-Terminbuchung“ für die Datenschutzerklärung der Website des Betriebs
Hier mit allen bedingten Sätzen (Telefon-Feld, Nachricht oder Zusatzfelder, Erinnerung, Widerruf, Telefonnummer des Betriebs); ein Betrieb ohne die jeweilige Funktion hat den Satz nicht.
### Sie-Fassung
> Online-Terminbuchung
>
> Auf unserer Website können Sie Termine online buchen. Dafür verarbeiten wir die folgenden Daten:
>
> Welche Daten? Ihren Namen, Ihre E-Mail-Adresse, den gewählten Termin, die gewählte Leistung und die gewählte Person oder den gewählten Platz, auf Wunsch Ihre Telefonnummer und die Angaben, die Sie uns im Formular mitteilen. Außerdem speichern wir, wann Sie den Termin bestätigt haben, welche Fassung der Hinweise Sie bei der Buchung gesehen haben und welche Zustimmungen Sie erteilt haben, zum Beispiel ob Sie eine Erinnerung gewünscht haben.
>
> Wofür und auf welcher Grundlage? Wir brauchen die Daten, um Ihren Termin zu verwalten, Ihnen den Termin zu bestätigen und Sie über Änderungen oder Absagen zu informieren. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO (Vertrag über den gebuchten Termin, bei einer Terminanfrage vorvertragliche Maßnahmen auf Ihre Anfrage). Wenn Sie eine Erinnerung vor dem Termin wünschen, beruht diese auf Ihrer Einwilligung nach Art. 6 Abs. 1 lit. a DSGVO. Sie können sie jederzeit mit Wirkung für die Zukunft widerrufen, indem Sie uns eine kurze Nachricht senden oder den Termin über den Link in der E-Mail absagen. Widerrufen Sie einen Vertrag über die Schaltfläche „Vertrag widerrufen“, speichern wir Ihre Erklärung mit Datum und Uhrzeit und senden Ihnen eine Eingangsbestätigung (Art. 6 Abs. 1 lit. c DSGVO in Verbindung mit § 356a BGB). Zum Schutz vor Missbrauch (zum Beispiel automatisierten Buchungen) und um die Zahl der Anfragen zu begrenzen, merken wir uns Ihre IP-Adresse kurzzeitig nur im Arbeitsspeicher des Servers, nicht dauerhaft (Art. 6 Abs. 1 lit. f DSGVO; unser berechtigtes Interesse ist der sichere Betrieb der Terminbuchung).
>
> E-Mails. Sie erhalten eine E-Mail, in der Sie Ihre E-Mail-Adresse und den Termin per Link bestätigen, danach eine Bestätigung und, wenn Sie das angekreuzt haben, eine Erinnerung. Sagen wir den Termin ab oder verschieben ihn, informieren wir Sie ebenfalls per E-Mail. Die E-Mails enthalten keine Werbung und keine Tracking-Pixel. Absender ist {{Name des Betriebs}}, Ihre Antworten gehen an uns. Bis zum Versand liegt die E-Mail in einer Warteschlange; nach dem Versand wird dort die Empfängeradresse samt Inhalt gelöscht, nicht zustellbare E-Mails nach 48 Stunden.
>
> Wer verarbeitet die Daten in unserem Auftrag? Den Terminkalender betreibt die WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, als unser Auftragsverarbeiter nach Art. 28 DSGVO. Die Daten liegen auf Servern der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, in Deutschland (Rechenzentren Falkenstein und Nürnberg). Der E-Mail-Versand läuft ebenfalls über Hetzner. Eine Übermittlung in Drittländer findet nicht statt.
>
> Beim Aufruf der Terminbuchung. Beim Aufruf dieser Seite lädt Ihr Browser das Programm der Terminbuchung vom Server der WEBklar GbR (project.webklar.com), beim Öffnen der Terminbuchung zusätzlich die Termine und Texte. Dabei werden technisch bedingt Ihre IP-Adresse und Angaben zu Ihrem Browser übertragen. Der Server speichert sie kurzzeitig in Protokollen und löscht sie nach 14 Tagen (Art. 6 Abs. 1 lit. f DSGVO; berechtigtes Interesse ist der sichere und funktionsfähige Betrieb). Wir setzen dafür keine Cookies und speichern nichts auf Ihrem Gerät.
>
> Wie lange speichern wir? Nicht bestätigte Buchungen (der Bestätigungslink gilt 30 Minuten) löschen wir 48 Stunden nach Ablauf dieser Frist. Bestätigte Termine löschen wir 90 Tage nach dem Termin, abgesagte oder abgelehnte Termine 30 Tage nach der Absage oder Ablehnung. Terminanfragen, die wir bis zum Terminbeginn nicht beantworten, behandeln wir als abgelehnt. Wird die Online-Terminbuchung abgeschaltet, zum Beispiel weil unser Vertrag mit WEBklar endet, werden alle Termindaten 30 Tage nach dem Abschalten gelöscht. Gesetzliche Aufbewahrungspflichten für Geschäftsunterlagen (zum Beispiel Rechnungen) bleiben davon unberührt; sie erfüllen wir außerhalb des Terminkalenders.
>
> Pflicht zur Angabe. Ohne Name, E-Mail-Adresse und Terminwahl können wir keinen Termin online vergeben. Sie können uns auch telefonisch erreichen: {{Telefon}}. Eine automatisierte Entscheidungsfindung findet nicht statt. Bitte machen Sie bei der Buchung keine Angaben zu Ihrer Gesundheit.
>
> Ihre Rechte. Sie haben das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung und Datenübertragbarkeit sowie das Recht, der Verarbeitung auf Grundlage berechtigter Interessen zu widersprechen (Art. 15 bis 21 DSGVO). Eine erteilte Einwilligung können Sie jederzeit widerrufen. Sie können sich bei einer Datenschutzaufsichtsbehörde beschweren. Verantwortlich ist {{Name des Betriebs}}, {{Anschrift}}, E-Mail {{E-Mail}}.
### Du-Fassung
> Online-Terminbuchung
>
> Auf unserer Website kannst du Termine online buchen. Dafür verarbeiten wir die folgenden Daten:
>
> Welche Daten? Deinen Namen, deine E-Mail-Adresse, den gewählten Termin, die gewählte Leistung und die gewählte Person oder den gewählten Platz, auf Wunsch deine Telefonnummer und die Angaben, die du uns im Formular mitteilst. Außerdem speichern wir, wann du den Termin bestätigt hast, welche Fassung der Hinweise du bei der Buchung gesehen hast und welche Zustimmungen du erteilt hast, zum Beispiel ob du eine Erinnerung gewünscht hast.
>
> Wofür und auf welcher Grundlage? Wir brauchen die Daten, um deinen Termin zu verwalten, dir den Termin zu bestätigen und dich über Änderungen oder Absagen zu informieren. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO (Vertrag über den gebuchten Termin, bei einer Terminanfrage vorvertragliche Maßnahmen auf deine Anfrage). Wenn du eine Erinnerung vor dem Termin wünschst, beruht diese auf deiner Einwilligung nach Art. 6 Abs. 1 lit. a DSGVO. Du kannst sie jederzeit mit Wirkung für die Zukunft widerrufen, indem du uns eine kurze Nachricht sendest oder den Termin über den Link in der E-Mail absagst. Widerrufst du einen Vertrag über die Schaltfläche „Vertrag widerrufen“, speichern wir deine Erklärung mit Datum und Uhrzeit und senden dir eine Eingangsbestätigung (Art. 6 Abs. 1 lit. c DSGVO in Verbindung mit § 356a BGB). Zum Schutz vor Missbrauch (zum Beispiel automatisierten Buchungen) und um die Zahl der Anfragen zu begrenzen, merken wir uns deine IP-Adresse kurzzeitig nur im Arbeitsspeicher des Servers, nicht dauerhaft (Art. 6 Abs. 1 lit. f DSGVO; unser berechtigtes Interesse ist der sichere Betrieb der Terminbuchung).
>
> E-Mails. Du erhältst eine E-Mail, in der du deine E-Mail-Adresse und den Termin per Link bestätigst, danach eine Bestätigung und, wenn du das angekreuzt hast, eine Erinnerung. Sagen wir den Termin ab oder verschieben ihn, informieren wir dich ebenfalls per E-Mail. Die E-Mails enthalten keine Werbung und keine Tracking-Pixel. Absender ist {{Name des Betriebs}}, deine Antworten gehen an uns. Bis zum Versand liegt die E-Mail in einer Warteschlange; nach dem Versand wird dort die Empfängeradresse samt Inhalt gelöscht, nicht zustellbare E-Mails nach 48 Stunden.
>
> Wer verarbeitet die Daten in unserem Auftrag? Den Terminkalender betreibt die WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, als unser Auftragsverarbeiter nach Art. 28 DSGVO. Die Daten liegen auf Servern der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, in Deutschland (Rechenzentren Falkenstein und Nürnberg). Der E-Mail-Versand läuft ebenfalls über Hetzner. Eine Übermittlung in Drittländer findet nicht statt.
>
> Beim Aufruf der Terminbuchung. Beim Aufruf dieser Seite lädt dein Browser das Programm der Terminbuchung vom Server der WEBklar GbR (project.webklar.com), beim Öffnen der Terminbuchung zusätzlich die Termine und Texte. Dabei werden technisch bedingt deine IP-Adresse und Angaben zu deinem Browser übertragen. Der Server speichert sie kurzzeitig in Protokollen und löscht sie nach 14 Tagen (Art. 6 Abs. 1 lit. f DSGVO; berechtigtes Interesse ist der sichere und funktionsfähige Betrieb). Wir setzen dafür keine Cookies und speichern nichts auf deinem Gerät.
>
> Wie lange speichern wir? Nicht bestätigte Buchungen (der Bestätigungslink gilt 30 Minuten) löschen wir 48 Stunden nach Ablauf dieser Frist. Bestätigte Termine löschen wir 90 Tage nach dem Termin, abgesagte oder abgelehnte Termine 30 Tage nach der Absage oder Ablehnung. Terminanfragen, die wir bis zum Terminbeginn nicht beantworten, behandeln wir als abgelehnt. Wird die Online-Terminbuchung abgeschaltet, zum Beispiel weil unser Vertrag mit WEBklar endet, werden alle Termindaten 30 Tage nach dem Abschalten gelöscht. Gesetzliche Aufbewahrungspflichten für Geschäftsunterlagen (zum Beispiel Rechnungen) bleiben davon unberührt; sie erfüllen wir außerhalb des Terminkalenders.
>
> Pflicht zur Angabe. Ohne Name, E-Mail-Adresse und Terminwahl können wir keinen Termin online vergeben. Du kannst uns auch telefonisch erreichen: {{Telefon}}. Eine automatisierte Entscheidungsfindung findet nicht statt. Bitte mach bei der Buchung keine Angaben zu deiner Gesundheit.
>
> Deine Rechte. Du hast das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung und Datenübertragbarkeit sowie das Recht, der Verarbeitung auf Grundlage berechtigter Interessen zu widersprechen (Art. 15 bis 21 DSGVO). Eine erteilte Einwilligung kannst du jederzeit widerrufen. Du kannst dich bei einer Datenschutzaufsichtsbehörde beschweren. Verantwortlich ist {{Name des Betriebs}}, {{Anschrift}}, E-Mail {{E-Mail}}.
## 7. Nachweis
Der Nachweis speichert Hashes der Texte mit den Betriebsangaben zum Buchungszeitpunkt (⚖ Q26).
+145
View File
@@ -0,0 +1,145 @@
# Auftragsverarbeitungsvertrag Terminplaner (Art. 28 DSGVO)
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen
zwischen dem Kunden als Verantwortlichem („Kunde“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).
## Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen
(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „Terminplaner“ einschaltet und die dort angezeigte Zeile „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16)“ mit dem Klick auf „Einschalten“ bestätigt; diesem Vertrag und der Leistungsbeschreibung „Terminplaner“ („Leistungsbeschreibung“) in der angezeigten Fassung stimmt er damit zu. Das Portal speichert den Wortlaut der angezeigten Zeile, die Fassungen, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte in dieser Fassung als PDF enthält und auf sie verlinkt.
(2) Ein Mitarbeiter von WEBklar darf die Funktion nur einschalten, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. WEBklar speichert die Fassung, das Datum und den Weg dieser Erklärung, den Zeitpunkt des Einschaltens und den Namen des Mitarbeiters. Der Kunde erhält anschließend eine E-Mail mit diesen beiden Texten als PDF.
(3) Dieser Vertrag ist ein eigener Vertrag für den Terminplaner. Er ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für den Terminplaner geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung vor.
## 1. Gegenstand und Dauer
(1) WEBklar betreibt für die öffentliche Website des Kunden ein Buchungs-Widget mit einer Verwaltung im Kundenportal („Terminplaner“) und verarbeitet dabei personenbezogene Daten der Gäste und der Beschäftigten des Kunden im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung.
(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Abschalten, spätestens mit dem Ende des Wartungs-Abos; den Terminplaner schaltet WEBklar dann spätestens am folgenden Tag ab (Nr. 8 Abs. 1 der Leistungsbeschreibung). Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.
## 2. Art und Zweck der Verarbeitung
(1) Art: Erheben über das Widget auf der Website des Kunden, Übermitteln an den Server von WEBklar, Speichern, Verwalten und Anzeigen im Kundenportal, Versenden von E-Mails, Bereitstellen eines Kalender-Links und einer CSV-Datei und Löschen.
(2) Zwecke: Der Kunde soll Termine seiner Gäste verwalten. Dazu nimmt der Terminplaner Buchungen und Terminanfragen entgegen, verhindert Doppelbuchungen, bestätigt, ändert und sagt Termine ab, erinnert Gäste auf deren Wunsch und benachrichtigt den Kunden. WEBklar versendet die E-Mails im Auftrag des Kunden. Außerdem schützt WEBklar den Terminplaner für den Kunden vor Missbrauch (Mengenbegrenzung, Erkennen automatisierter Buchungen).
(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Gäste. Zählungen ohne Personenbezug, die WEBklar für den sicheren Betrieb braucht (etwa die Zahl der gelöschten Buchungen je Tag oder die Länge der Versandwarteschlange), sind keine eigenen Zwecke.
## 3. Datenarten und Betroffene
(1) Daten der Gäste: Name und E-Mail-Adresse; auf Einstellung des Kunden außerdem Telefonnummer, eine Nachricht und Zusatzfelder, die der Kunde im Formular festlegt; die gebuchte Leistung, die Person oder der Platz und der Termin; der Status der Buchung mit den Zeitpunkten (Anlegen, Bestätigung, Annahme, Absage, Widerruf); Absagegrund und Vermerke zu einer Absage oder einem Widerruf (Freitext, auch mit dem Namen aus einer Widerrufserklärung); Preis, Zahlart, Zahlungsstatus und die Angabe, ob als Verbraucher gebucht wurde; der Nachweis der Zustimmungen (Fassung der Gästetexte, Zeitpunkt, Text des Buchungsknopfs, Anrede und Hashes (Prüfsummen) der gezeigten Texte); der Wunsch nach einer Erinnerung; der Zustellstatus der E-Mails an den Gast.
(2) Daten der Beschäftigten des Kunden: Namen oder Bezeichnungen der Personen oder Plätze, deren Arbeitszeiten und Abwesenheiten sowie ein geheimer Kalender-Link je Person oder Platz.
(3) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht verarbeitet. Der Terminplaner ist nicht für den Gesundheitsbereich bestimmt; der Kunde fordert von seinen Gästen keine Gesundheitsangaben (Nummer 11 Abs. 4).
(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Widgets und jeder Anfrage. Die Mengenbegrenzung (Missbrauchsschutz) verwendet die IP-Adresse nur im Arbeitsspeicher des Servers; ein Neustart leert sie. Mit der Buchung wird keine IP-Adresse und kein Browser-Kennzeichen gespeichert. In den Server-Protokollen stehen sie 14 Tage (Anlage 2 der Abo-Bedingungen).
(5) Der Terminplaner setzt keine Cookies und speichert nichts im Browser des Gastes.
(6) Betroffene: Gäste, die über die Website des Kunden Termine buchen oder anfragen, und Beschäftigte des Kunden, die im Terminplaner geführt werden.
## 4. Weisungen
(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.
(2) Mit dem Einschalten weist der Kunde WEBklar an, den Terminplaner nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen im Kundenportal (Leistungen, Personen oder Plätze, Zeiten, Felder des Formulars, Aufbewahrungsdauer, Erinnerungen, Einschalten und Ausschalten). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Terminplaner abschalten lassen (Nr. 8 Abs. 6 der Leistungsbeschreibung). Die Löschung auf Weisung nach Nummer 9 Abs. 6 setzt WEBklar immer um.
(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.
## 5. Vertraulichkeit
WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.
## 6. Technische und organisatorische Maßnahmen
(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.
(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.
## 7. Unterauftragsverarbeiter
(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Terminplaner läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der Buchungs-E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt.
(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Terminplaner abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6).
(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).
(4) Die Zahlung der Einrichtung über Stripe betrifft nur den Vertrag zwischen dem Kunden und WEBklar. Daten von Gästen oder Beschäftigten des Kunden gehen nicht an Stripe. Stripe ist deshalb kein Unterauftragsverarbeiter dieses Vertrags.
## 8. Unterstützung des Kunden, Betroffenenrechte
(1) WEBklar leitet Anfragen von Gästen und Beschäftigten, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.
(2) Dafür bietet das Kundenportal die Aktion „Gastdaten löschen“ und den Export der Termine als CSV-Datei. Mit „Gastdaten löschen“ leert der Kunde Name, E-Mail-Adresse, Telefonnummer, Nachricht und Zusatzfelder einer Buchung; Status und Zeiten bleiben. Dabei löscht WEBklar die E-Mails zu dieser Buchung, die noch auf den Versand warten, und die Links des Gastes werden ungültig. E-Mails, deren Versand endgültig fehlgeschlagen ist, behalten Empfängeradresse und Inhalt, bis WEBklar sie nach Nummer 9 Abs. 4 löscht (48 Stunden nach dem Fehlschlag). Eine noch nicht bestätigte Buchung verfällt von selbst (Nummer 9 Abs. 1).
(3) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.
(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht.
(5) Die Buchung speichert als Nachweis der Zustimmungen die Fassung der Gästetexte, den Zeitpunkt, den Text des Buchungsknopfs, die Anrede und Hashes der übrigen Texte, die der Gast gesehen hat, nicht diese Texte selbst. Damit sich der Wortlaut nachweisen lässt, bewahrt WEBklar jede Fassung der Gästetexte (Hinweis am Formular, Texte der Haken, Datenschutz-Absatz und Widerrufsbelehrung für Gäste) mit dem Zeitraum ihrer Verwendung auf, bis drei Jahre nach dem Ende des Terminplaners. WEBklar stellt sie dem Kunden auf Verlangen zur Verfügung.
(6) Die Gäste informiert der Kunde (Art. 13 DSGVO; Nummer 11 Abs. 2).
## 9. Löschung und Rückgabe
(1) Nicht bestätigte Buchungen löscht WEBklar 48 Stunden nach Ablauf der Bestätigungsfrist von 30 Minuten.
(2) Abgesagte und abgelehnte Termine löscht WEBklar 30 Tage nach der Absage oder Ablehnung. Terminanfragen, die der Kunde bis zum angefragten Terminbeginn nicht beantwortet, behandelt der Terminplaner als abgelehnt; die 30 Tage laufen dann ab diesem Zeitpunkt.
(3) Bestätigte und erledigte Termine löscht WEBklar nach der Aufbewahrungsdauer, gerechnet ab dem Ende des Termins. Der Kunde stellt sie im Kundenportal ein; die Vorgabe sind 90 Tage, einstellbar sind 14 bis 365 Tage.
(4) In der Versandwarteschlange für E-Mails leert WEBklar die Empfängeradresse und den Inhalt einer E-Mail nach dem Versand und löscht den Eintrag 7 Tage nach dem Versand. E-Mails, die nicht zugestellt werden können, löscht WEBklar mit Empfängeradresse und Inhalt 48 Stunden nach dem endgültigen Fehlschlag.
(5) Der Löschlauf läuft täglich; gelöscht wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu löschen, als ein Lauf schafft, folgt der Rest im nächsten Lauf.
(6) Ist der Terminplaner abgeschaltet oder das Wartungs-Abo beendet, löscht WEBklar alle Termindaten und die Einstellungen 30 Tage danach. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Einstellungen bestehen (zum Beispiel Betriebsangaben, Namen der Leistungen, die ersten acht Namen der Personen oder Plätze und die Wünsche des Kunden zum Einbau) und enthält keine Daten von Gästen; dafür gilt die Datenschutzerklärung von WEBklar (Abschnitt 6). Bis zur Löschung kann der Kunde die Termine im Kundenportal als CSV-Datei exportieren. Beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schaltet WEBklar den Terminplaner mit den vorhandenen Daten und Einstellungen wieder ein (Nr. 8 Abs. 4 der Leistungsbeschreibung). Früher löscht der Kunde einzelne Gastdaten selbst mit der Aktion „Gastdaten löschen“ (Nummer 8 Abs. 2); alle Daten löscht WEBklar auf Weisung des Kunden in Textform unverzüglich.
(7) Entwürfe aus dem Fragebogen, aus denen die Funktion nicht eingeschaltet wurde, löscht WEBklar 90 Tage nach der letzten Änderung.
(8) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.
(9) Gesetzliche Aufbewahrungspflichten bleiben unberührt.
## 10. Nachweise und Kontrollen
WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.
## 11. Pflichten des Kunden
(1) Der Kunde ist Verantwortlicher für die Verarbeitung der Daten seiner Gäste und seiner Beschäftigten im Terminplaner.
(2) Der Kunde informiert seine Gäste nach Art. 13 DSGVO. Dazu nimmt er den Datenschutz-Absatz für die Online-Terminbuchung in seine Datenschutzerklärung auf (Nr. 4 Abs. 9 der Leistungsbeschreibung) und lässt den Datenschutz-Hinweis am Buchungsformular unverändert.
(3) Der Kunde informiert seine Beschäftigten darüber, dass ihre Namen oder Bezeichnungen, ihre Arbeitszeiten und Abwesenheiten im Terminplaner stehen und dass es einen geheimen Kalender-Link gibt (Nr. 4 Abs. 7 der Leistungsbeschreibung).
(4) Der Kunde setzt den Terminplaner nicht im Gesundheitsbereich ein und fordert von seinen Gästen keine Angaben zu ihrer Gesundheit, auch nicht über Zusatzfelder oder das Nachrichtenfeld (Nr. 4 Abs. 3 der Leistungsbeschreibung).
(5) Der Kunde verändert den Haken für die Erinnerung und die Texte der übrigen Haken nicht (Nr. 4 Abs. 6 der Leistungsbeschreibung). Erinnerungen versendet der Terminplaner nur, wenn der Gast den Haken gesetzt hat.
## 12. Haftung, Änderungen, Schlussbestimmungen
(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.
(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung, auch wegen geänderter Datenerhebung, teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.
(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar den Terminplaner für seine Website nach Nr. 9 Abs. 2 der Leistungsbeschreibung beenden oder nach Nr. 9 Abs. 3 der Leistungsbeschreibung aussetzen. Das Wartungs-Abo bleibt davon unberührt.
(4) Im Übrigen gilt § 16 der Abo-Bedingungen.
## Anlage: Technische und organisatorische Maßnahmen des Terminplaners (Art. 32 DSGVO)
| Bereich | Maßnahmen |
|---|---|
| Übertragung | Das Widget, die Seiten der Gäste und das Kundenportal werden nur über TLS (HTTPS) übertragen. Der Einbau-Code verweist auf eine Adresse mit https://. |
| Speicher im Browser | Es werden keine Cookies gesetzt, und der Terminplaner speichert nichts im Browser des Gastes (kein lokaler Speicher, kein Sitzungsspeicher). Das Widget sendet seine Anfragen ohne Cookies, liest weder den Referrer noch die Zeitzone des Browsers und ruft nur die Schnittstelle des Terminplaners auf. |
| Zugriffskontrolle | Das Kundenportal ist nur nach Anmeldung erreichbar; jeder Kunde sieht nur die Daten seiner eigenen Projekte. Die öffentliche Schnittstelle des Widgets liefert nur für einen eingeschalteten Terminplaner Daten und ist über den Website-Schlüssel (Site-Key) adressiert; Buchungen nimmt sie nur von Seiten des Kunden und des Portals an. Der Kalender-Link enthält ein geheimes Token, das mit einem Vergleich in konstanter Zeit geprüft wird. |
| Missbrauchsschutz | Ein verstecktes Feld (Honeypot), eine Mindestzeit bis zum Absenden des Formulars mit signiertem Formular-Token und eine Mindestantwortzeit schützen vor automatisierten Buchungen. Grenzen je IP-Adresse, je Website und je E-Mail-Adresse sowie das Mailbudget je Stunde (insgesamt und je Funktion) gelten nur im Arbeitsspeicher; ein Neustart leert sie. |
| Datenminimierung | Pflicht sind immer nur Name und E-Mail-Adresse; ob Telefonnummer, Nachricht und Zusatzfelder abgefragt werden, legt der Kunde fest. Mit der Buchung werden keine IP-Adresse und kein Browser-Kennzeichen gespeichert. Der Nachweis der Zustimmungen besteht aus der Fassung der Gästetexte, dem Zeitpunkt, dem Text des Buchungsknopfs, der Anrede und Hashes der übrigen gezeigten Texte, nicht aus diesen Texten. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert. |
| Trennung | Alle Buchungen, Belegungen und Warteschlangeneinträge tragen die Kennung der Funktion (eines Terminplaners je Website-Projekt); das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten der jeweiligen Funktion. Die Hintergrundläufe (Löschung, Versand der E-Mails, Erinnerungen) gehen die Dokumente aller Funktionen nacheinander durch und schreiben jeweils nur in das Dokument, um das es geht. Die Einstellungen liegen je Funktion in einem eigenen Dokument. |
| Löschung | Täglicher Löschlauf für Buchungen, Belegungen, Warteschlange und Entwürfe nach den Fristen der Nummer 9; Termindaten und Einstellungen abgeschalteter Funktionen 30 Tage nach dem Abschalten; das Einrichtungs-Ticket bleibt als Arbeitsnachweis mit einer Kurzfassung der Einstellungen bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6). Vor der Löschung einer Buchung, einer abgeschalteten Funktion oder eines nicht bestellten Fragebogens wird der Zustand frisch gelesen; bei einem Lesefehler wird nichts gelöscht. |
| Protokolle | Die Programmprotokolle des Terminplaners und der Tagesbericht enthalten nur Zählungen und Buchungsnummern, keine Namen, E-Mail-Adressen oder Nachrichten von Gästen. Server-Protokolle des Webservers werden nach 14 Tagen gelöscht (Anlage 2 der Abo-Bedingungen). |
| Datensicherung | Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). |
| Rechenzentrum | Hetzner Online GmbH, Rechenzentren Falkenstein und Nürnberg (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren. |
| Organisation | Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 3), Archiv der Fassungen der Gästetexte (Nummer 8 Abs. 5), Überprüfung der Maßnahmen mindestens einmal im Jahr. |
+145
View File
@@ -0,0 +1,145 @@
# Auftragsverarbeitungsvertrag Terminplaner (Art. 28 DSGVO)
**Fassung 2026-10-05**
zwischen dem Kunden als Verantwortlichem („Kunde“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).
## Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen
(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Einrichtung des Terminplaners bestellt und dabei per Haken diesem Vertrag und der Leistungsbeschreibung „Terminplaner“ („Leistungsbeschreibung“) in der angezeigten Fassung zustimmt. Das Portal speichert die Fassung, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte und die Widerrufsbelehrung in dieser Fassung als PDF enthält und auf sie verlinkt.
(2) Ein Mitarbeiter von WEBklar darf die Funktion nur einschalten, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. WEBklar speichert die Fassung, das Datum und den Weg dieser Erklärung, den Zeitpunkt des Einschaltens und den Namen des Mitarbeiters. Der Kunde erhält anschließend eine E-Mail mit diesen beiden Texten als PDF.
(3) Dieser Vertrag ist ein eigener Vertrag für den Terminplaner. Er ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für den Terminplaner geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung vor.
## 1. Gegenstand und Dauer
(1) WEBklar betreibt für die öffentliche Website des Kunden ein Buchungs-Widget mit einer Verwaltung im Kundenportal („Terminplaner“) und verarbeitet dabei personenbezogene Daten der Gäste und der Beschäftigten des Kunden im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung.
(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Abschalten, spätestens mit dem Ende des Wartungs-Abos; den Terminplaner schaltet WEBklar dann spätestens am folgenden Tag ab (Nr. 8 Abs. 1 der Leistungsbeschreibung). Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.
## 2. Art und Zweck der Verarbeitung
(1) Art: Erheben über das Widget auf der Website des Kunden, Übermitteln an den Server von WEBklar, Speichern, Verwalten und Anzeigen im Kundenportal, Versenden von E-Mails, Bereitstellen eines Kalender-Links und einer CSV-Datei und Löschen.
(2) Zwecke: Der Kunde soll Termine seiner Gäste verwalten. Dazu nimmt der Terminplaner Buchungen und Terminanfragen entgegen, verhindert Doppelbuchungen, bestätigt, ändert und sagt Termine ab, erinnert Gäste auf deren Wunsch und benachrichtigt den Kunden. WEBklar versendet die E-Mails im Auftrag des Kunden. Außerdem schützt WEBklar den Terminplaner für den Kunden vor Missbrauch (Mengenbegrenzung, Erkennen automatisierter Buchungen).
(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Gäste. Zählungen ohne Personenbezug, die WEBklar für den sicheren Betrieb braucht (etwa die Zahl der gelöschten Buchungen je Tag oder die Länge der Versandwarteschlange), sind keine eigenen Zwecke.
## 3. Datenarten und Betroffene
(1) Daten der Gäste: Name und E-Mail-Adresse; auf Einstellung des Kunden außerdem Telefonnummer, eine Nachricht und Zusatzfelder, die der Kunde im Formular festlegt; die gebuchte Leistung, die Person oder der Platz und der Termin; der Status der Buchung mit den Zeitpunkten (Anlegen, Bestätigung, Annahme, Absage, Widerruf); Absagegrund und Vermerke zu einer Absage oder einem Widerruf (Freitext, auch mit dem Namen aus einer Widerrufserklärung); Preis, Zahlart, Zahlungsstatus und die Angabe, ob als Verbraucher gebucht wurde; der Nachweis der Zustimmungen (Fassung der Gästetexte, Zeitpunkt, Text des Buchungsknopfs, Anrede und Hashes (Prüfsummen) der gezeigten Texte); der Wunsch nach einer Erinnerung; der Zustellstatus der E-Mails an den Gast.
(2) Daten der Beschäftigten des Kunden: Namen oder Bezeichnungen der Personen oder Plätze, deren Arbeitszeiten und Abwesenheiten sowie ein geheimer Kalender-Link je Person oder Platz.
(3) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht verarbeitet. Der Terminplaner ist nicht für den Gesundheitsbereich bestimmt; der Kunde fordert von seinen Gästen keine Gesundheitsangaben (Nummer 11 Abs. 4).
(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Widgets und jeder Anfrage. Die Mengenbegrenzung (Missbrauchsschutz) verwendet die IP-Adresse nur im Arbeitsspeicher des Servers; ein Neustart leert sie. Mit der Buchung wird keine IP-Adresse und kein Browser-Kennzeichen gespeichert. In den Server-Protokollen stehen sie 14 Tage (Anlage 2 der Abo-Bedingungen).
(5) Der Terminplaner setzt keine Cookies und speichert nichts im Browser des Gastes.
(6) Betroffene: Gäste, die über die Website des Kunden Termine buchen oder anfragen, und Beschäftigte des Kunden, die im Terminplaner geführt werden.
## 4. Weisungen
(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.
(2) Mit der Bestellung weist der Kunde WEBklar an, den Terminplaner nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen im Kundenportal (Leistungen, Personen oder Plätze, Zeiten, Felder des Formulars, Aufbewahrungsdauer, Erinnerungen, Einschalten und Ausschalten). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Terminplaner abschalten lassen (Nr. 8 Abs. 6 der Leistungsbeschreibung). Die Löschung auf Weisung nach Nummer 9 Abs. 6 setzt WEBklar immer um.
(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.
## 5. Vertraulichkeit
WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.
## 6. Technische und organisatorische Maßnahmen
(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.
(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.
## 7. Unterauftragsverarbeiter
(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Terminplaner läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der Buchungs-E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt.
(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Terminplaner abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6).
(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).
(4) Die Zahlung der Einrichtung über Stripe betrifft nur den Vertrag zwischen dem Kunden und WEBklar. Daten von Gästen oder Beschäftigten des Kunden gehen nicht an Stripe. Stripe ist deshalb kein Unterauftragsverarbeiter dieses Vertrags.
## 8. Unterstützung des Kunden, Betroffenenrechte
(1) WEBklar leitet Anfragen von Gästen und Beschäftigten, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.
(2) Dafür bietet das Kundenportal die Aktion „Gastdaten löschen“ und den Export der Termine als CSV-Datei. Mit „Gastdaten löschen“ leert der Kunde Name, E-Mail-Adresse, Telefonnummer, Nachricht und Zusatzfelder einer Buchung; Status und Zeiten bleiben. Dabei löscht WEBklar die E-Mails zu dieser Buchung, die noch auf den Versand warten, und die Links des Gastes werden ungültig. E-Mails, deren Versand endgültig fehlgeschlagen ist, behalten Empfängeradresse und Inhalt, bis WEBklar sie nach Nummer 9 Abs. 4 löscht (48 Stunden nach dem Fehlschlag). Eine noch nicht bestätigte Buchung verfällt von selbst (Nummer 9 Abs. 1).
(3) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.
(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht.
(5) Die Buchung speichert als Nachweis der Zustimmungen die Fassung der Gästetexte, den Zeitpunkt, den Text des Buchungsknopfs, die Anrede und Hashes der übrigen Texte, die der Gast gesehen hat, nicht diese Texte selbst. Damit sich der Wortlaut nachweisen lässt, bewahrt WEBklar jede Fassung der Gästetexte (Hinweis am Formular, Texte der Haken, Datenschutz-Absatz und Widerrufsbelehrung für Gäste) mit dem Zeitraum ihrer Verwendung auf, bis drei Jahre nach dem Ende des Terminplaners. WEBklar stellt sie dem Kunden auf Verlangen zur Verfügung.
(6) Die Gäste informiert der Kunde (Art. 13 DSGVO; Nummer 11 Abs. 2).
## 9. Löschung und Rückgabe
(1) Nicht bestätigte Buchungen löscht WEBklar 48 Stunden nach Ablauf der Bestätigungsfrist von 30 Minuten.
(2) Abgesagte und abgelehnte Termine löscht WEBklar 30 Tage nach der Absage oder Ablehnung. Terminanfragen, die der Kunde bis zum angefragten Terminbeginn nicht beantwortet, behandelt der Terminplaner als abgelehnt; die 30 Tage laufen dann ab diesem Zeitpunkt.
(3) Bestätigte und erledigte Termine löscht WEBklar nach der Aufbewahrungsdauer, gerechnet ab dem Ende des Termins. Der Kunde stellt sie im Kundenportal ein; die Vorgabe sind 90 Tage, einstellbar sind 14 bis 365 Tage.
(4) In der Versandwarteschlange für E-Mails leert WEBklar die Empfängeradresse und den Inhalt einer E-Mail nach dem Versand und löscht den Eintrag 7 Tage nach dem Versand. E-Mails, die nicht zugestellt werden können, löscht WEBklar mit Empfängeradresse und Inhalt 48 Stunden nach dem endgültigen Fehlschlag.
(5) Der Löschlauf läuft täglich; gelöscht wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu löschen, als ein Lauf schafft, folgt der Rest im nächsten Lauf.
(6) Ist der Terminplaner abgeschaltet oder das Wartungs-Abo beendet, löscht WEBklar alle Termindaten und die Einstellungen 30 Tage danach. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Einstellungen bestehen (zum Beispiel Betriebsangaben, Namen der Leistungen, die ersten acht Namen der Personen oder Plätze und die Wünsche des Kunden zum Einbau) und enthält keine Daten von Gästen; dafür gilt die Datenschutzerklärung von WEBklar (Abschnitt 6). Bis zur Löschung kann der Kunde die Termine im Kundenportal als CSV-Datei exportieren. Beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schaltet WEBklar den Terminplaner mit den vorhandenen Daten und Einstellungen wieder ein (Nr. 8 Abs. 4 der Leistungsbeschreibung). Früher löscht der Kunde einzelne Gastdaten selbst mit der Aktion „Gastdaten löschen“ (Nummer 8 Abs. 2); alle Daten löscht WEBklar auf Weisung des Kunden in Textform unverzüglich.
(7) Entwürfe aus dem Fragebogen, aus denen keine Bestellung geworden ist, löscht WEBklar 90 Tage nach der letzten Änderung.
(8) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.
(9) Gesetzliche Aufbewahrungspflichten bleiben unberührt.
## 10. Nachweise und Kontrollen
WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.
## 11. Pflichten des Kunden
(1) Der Kunde ist Verantwortlicher für die Verarbeitung der Daten seiner Gäste und seiner Beschäftigten im Terminplaner.
(2) Der Kunde informiert seine Gäste nach Art. 13 DSGVO. Dazu nimmt er den Datenschutz-Absatz für die Online-Terminbuchung in seine Datenschutzerklärung auf (Nr. 4 Abs. 9 der Leistungsbeschreibung) und lässt den Datenschutz-Hinweis am Buchungsformular unverändert.
(3) Der Kunde informiert seine Beschäftigten darüber, dass ihre Namen oder Bezeichnungen, ihre Arbeitszeiten und Abwesenheiten im Terminplaner stehen und dass es einen geheimen Kalender-Link gibt (Nr. 4 Abs. 7 der Leistungsbeschreibung).
(4) Der Kunde setzt den Terminplaner nicht im Gesundheitsbereich ein und fordert von seinen Gästen keine Angaben zu ihrer Gesundheit, auch nicht über Zusatzfelder oder das Nachrichtenfeld (Nr. 4 Abs. 3 der Leistungsbeschreibung).
(5) Der Kunde verändert den Haken für die Erinnerung und die Texte der übrigen Haken nicht (Nr. 4 Abs. 6 der Leistungsbeschreibung). Erinnerungen versendet der Terminplaner nur, wenn der Gast den Haken gesetzt hat.
## 12. Haftung, Änderungen, Schlussbestimmungen
(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.
(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung, auch wegen geänderter Datenerhebung, teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.
(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar den Terminplaner für seine Website nach Nr. 9 Abs. 2 der Leistungsbeschreibung beenden oder nach Nr. 9 Abs. 3 der Leistungsbeschreibung aussetzen. Das Wartungs-Abo bleibt davon unberührt.
(4) Im Übrigen gilt § 16 der Abo-Bedingungen.
## Anlage: Technische und organisatorische Maßnahmen des Terminplaners (Art. 32 DSGVO)
| Bereich | Maßnahmen |
|---|---|
| Übertragung | Das Widget, die Seiten der Gäste und das Kundenportal werden nur über TLS (HTTPS) übertragen. Der Einbau-Code verweist auf eine Adresse mit https://. |
| Speicher im Browser | Es werden keine Cookies gesetzt, und der Terminplaner speichert nichts im Browser des Gastes (kein lokaler Speicher, kein Sitzungsspeicher). Das Widget sendet seine Anfragen ohne Cookies, liest weder den Referrer noch die Zeitzone des Browsers und ruft nur die Schnittstelle des Terminplaners auf. |
| Zugriffskontrolle | Das Kundenportal ist nur nach Anmeldung erreichbar; jeder Kunde sieht nur die Daten seiner eigenen Projekte. Die öffentliche Schnittstelle des Widgets liefert nur für einen eingeschalteten Terminplaner Daten und ist über den Website-Schlüssel (Site-Key) adressiert; Buchungen nimmt sie nur von Seiten des Kunden und des Portals an. Der Kalender-Link enthält ein geheimes Token, das mit einem Vergleich in konstanter Zeit geprüft wird. |
| Missbrauchsschutz | Ein verstecktes Feld (Honeypot), eine Mindestzeit bis zum Absenden des Formulars mit signiertem Formular-Token und eine Mindestantwortzeit schützen vor automatisierten Buchungen. Grenzen je IP-Adresse, je Website und je E-Mail-Adresse sowie das Mailbudget je Stunde (insgesamt und je Funktion) gelten nur im Arbeitsspeicher; ein Neustart leert sie. |
| Datenminimierung | Pflicht sind immer nur Name und E-Mail-Adresse; ob Telefonnummer, Nachricht und Zusatzfelder abgefragt werden, legt der Kunde fest. Mit der Buchung werden keine IP-Adresse und kein Browser-Kennzeichen gespeichert. Der Nachweis der Zustimmungen besteht aus der Fassung der Gästetexte, dem Zeitpunkt, dem Text des Buchungsknopfs, der Anrede und Hashes der übrigen gezeigten Texte, nicht aus diesen Texten. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert. |
| Trennung | Alle Buchungen, Belegungen und Warteschlangeneinträge tragen die Kennung der Funktion (eines Terminplaners je Website-Projekt); das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten der jeweiligen Funktion. Die Hintergrundläufe (Löschung, Versand der E-Mails, Erinnerungen) gehen die Dokumente aller Funktionen nacheinander durch und schreiben jeweils nur in das Dokument, um das es geht. Die Einstellungen liegen je Funktion in einem eigenen Dokument. |
| Löschung | Täglicher Löschlauf für Buchungen, Belegungen, Warteschlange und Entwürfe nach den Fristen der Nummer 9; Termindaten und Einstellungen abgeschalteter Funktionen 30 Tage nach dem Abschalten; das Einrichtungs-Ticket bleibt als Arbeitsnachweis mit einer Kurzfassung der Einstellungen bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6). Vor der Löschung einer Buchung, einer abgeschalteten Funktion oder eines nicht bestellten Fragebogens wird der Zustand frisch gelesen; bei einem Lesefehler wird nichts gelöscht. |
| Protokolle | Die Programmprotokolle des Terminplaners und der Tagesbericht enthalten nur Zählungen und Buchungsnummern, keine Namen, E-Mail-Adressen oder Nachrichten von Gästen. Server-Protokolle des Webservers werden nach 14 Tagen gelöscht (Anlage 2 der Abo-Bedingungen). |
| Datensicherung | Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). |
| Rechenzentrum | Hetzner Online GmbH, Rechenzentren Falkenstein und Nürnberg (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren. |
| Organisation | Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 3), Archiv der Fassungen der Gästetexte (Nummer 8 Abs. 5), Überprüfung der Maßnahmen mindestens einmal im Jahr. |
+157
View File
@@ -0,0 +1,157 @@
# Auftragsverarbeitungsvertrag Buchungskalender (Art. 28 DSGVO)
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen
zwischen dem Kunden als Verantwortlichem („Kunde“; in der Leistungsbeschreibung „Vermieter“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).
## Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen
(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „Buchungskalender“ einschaltet und die dort angezeigte Zeile „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16)“ mit dem Klick auf „Einschalten“ bestätigt; diesem Vertrag und der Leistungsbeschreibung „Buchungskalender“ („Leistungsbeschreibung“) in der angezeigten Fassung stimmt er damit zu. Das Portal speichert den Wortlaut der angezeigten Zeile, die Fassungen, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte in dieser Fassung als PDF enthält und auf sie verlinkt.
(2) Ein Mitarbeiter von WEBklar darf die Funktion auch ohne Bestellung im Portal einschalten (zum Beispiel für einen Kunden, dem WEBklar sie als Teil seines Website-Projekts bereitstellt), aber nur, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. Das Portal verlangt dafür das Datum und den Weg der Erklärung; fehlt eine Angabe, schaltet es nicht ein. WEBklar speichert vor dem Einschalten einen Zustimmungsnachweis mit der Fassung der beiden Texte, dem Datum und dem Weg dieser Erklärung, dem Zeitpunkt des Einschaltens und dem Namen des Mitarbeiters (ohne IP-Adresse und Browser-Kennzeichen, weil der Kunde nicht im Portal zugestimmt hat). Der Kunde erhält anschließend eine E-Mail mit diesen beiden Texten als PDF.
(3) Dieser Vertrag ist ein eigener Vertrag für den Buchungskalender. Er ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für den Buchungskalender geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung vor.
(4) Die Rollen: Der Kunde ist Verantwortlicher für die personenbezogenen Daten seiner Gäste im Buchungskalender. WEBklar ist sein Auftragsverarbeiter. Stripe ist nicht Auftragsverarbeiter von WEBklar, sondern Vertragspartner des Kunden (Nummer 7 Abs. 4). Die Plattformen, deren Kalender der Kunde einbindet (zum Beispiel Booking.com oder Airbnb), sind weder Auftragsverarbeiter noch Unterauftragsverarbeiter von WEBklar.
## 1. Gegenstand und Dauer
(1) WEBklar betreibt für die öffentliche Website des Kunden ein Buchungs-Widget mit einer Buchungsseite und einer Verwaltung im Kundenportal („Buchungskalender“) und verarbeitet dabei personenbezogene Daten der Gäste des Kunden im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung.
(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Abschalten, spätestens mit dem Ende des Wartungs-Abos; den Buchungskalender schaltet WEBklar dann spätestens am folgenden Tag ab (Nr. 10 Abs. 1 der Leistungsbeschreibung). Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.
## 2. Art und Zweck der Verarbeitung
(1) Art: Übermitteln der Anfrage und der Buchung vom Widget und von der Buchungsseite an den Server von WEBklar, Speichern, Verwalten und Anzeigen im Kundenportal, Berechnen des Preises, Erzeugen von Rechnungen und Stornorechnungen im Namen des Kunden (Nr. 3 Abs. 6 der Leistungsbeschreibung), Abgleichen von Kalendern, Versenden von E-Mails, Übernehmen von Zahlungsstatus aus Stripe, Weitergeben von Erstattungen an Stripe im Auftrag des Kunden, Anonymisieren und Löschen.
(2) Zwecke: Der Kunde soll Anfragen und Buchungen seiner Gäste abwickeln. Dazu nimmt der Buchungskalender Anfragen entgegen, reserviert Zeiträume nach der Freigabe durch den Kunden, nimmt die verbindliche Buchung des Gastes entgegen, stellt die Rechnung aus, gleicht Online-Zahlungen mit Stripe ab, bildet Zahlungen vor Ort ab, bildet Stornierungen ab und benachrichtigt den Kunden. WEBklar versendet die E-Mails im Auftrag des Kunden. Außerdem schützt WEBklar den Buchungskalender für den Kunden vor Missbrauch (Mengenbegrenzung, Begrenzung der Anfragen je IP-Adresse, Erkennen automatisierter Anfragen).
(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Gäste. Zählungen ohne Personenbezug, die WEBklar für den sicheren Betrieb braucht (etwa die Zahl der anonymisierten Anfragen je Tag oder die Länge der Versandwarteschlange), sind keine eigenen Zwecke.
## 3. Datenarten und Betroffene
(1) Daten der Gäste, die WEBklar aus dem Widget, von der Buchungsseite und aus Stripe erhält und speichert:
- bei der Anfrage: Name, E-Mail-Adresse, auf Wunsch Firma, Telefonnummer und eine Nachricht, die gewünschte Unterkunft, der Zeitraum, die Zahl der Personen, die Anfragenummer, ein Hashwert der IP-Adresse (Absatz 4) und der Zeitpunkt;
- bei der Freigabe: die Posten und Summen des Preises (Text, Menge, Einzelbetrag, Steuersatz), die Reservierungsfrist, eine Nachricht des Kunden an den Gast, die Stornobedingungen, die galten, und die Fassung der Rechtstexte, der der Gast gesehen hat, jeweils als Schnappschuss;
- bei der Buchung: die Rechnungsadresse (Name, Firma, Straße, Postleitzahl, Ort, Land), die Zahlart, der Zeitpunkt der Buchung und die Rechnungsnummer;
- bei der Zahlung und der Erstattung: Status, Zeitpunkt, Art (online über Stripe oder vor Ort), die Kennungen der Zahlung bei Stripe, erstattete Beträge und der Grund einer Stornierung;
- die Rechnung und die Stornorechnung als unveränderlicher Schnappschuss (Angaben des Ausstellers, Empfänger, Unterkunft, Zeitraum, Posten, Summen, Steuerart, Bezug), aus dem WEBklar das PDF bei jedem Abruf erzeugt;
- ein Hashwert des geheimen Zugangs zur Buchungsseite (der Link in der E-Mail wird nur als Prüfsumme gespeichert);
- der Zustellstatus der E-Mails an den Gast.
**Kartendaten des Gastes erhält und speichert WEBklar nicht;** die gibt der Gast nur bei Stripe ein. Online-Zahlungen sind derzeit nur per Karte (einschließlich Apple Pay und Google Pay) möglich, nicht per SEPA-Lastschrift. Die Bankverbindung des Kunden selbst (Angabe auf der Rechnung) und seine Firmen- und Steuerangaben gehören nicht zu den Gästedaten.
(2) Von Kalendern anderer Plattformen übernimmt WEBklar nur die belegten Zeiträume (Datum von und bis), nicht Namen, Beschreibungen oder andere Angaben der dortigen Buchungen, auch wenn der Kalender sie enthält. Die iCal-Adresse, die WEBklar dem Kunden bereitstellt, enthält nur belegte Nächte, keine Angaben zu Gästen. Wer die geheime Adresse kennt, sieht die belegten Nächte; der Kunde kann sie jederzeit erneuern.
(3) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht verarbeitet. Der Kunde fordert von seinen Gästen keine entsprechenden Angaben und bittet sie, solche Angaben nicht in das Nachrichtenfeld zu schreiben (Nummer 11 Abs. 4). Meldedaten nach dem Bundesmeldegesetz (Meldeschein) und Daten zur Kurtaxe verarbeitet der Buchungskalender nicht.
(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Widgets und jeder Anfrage. Die Mengenbegrenzung (Missbrauchsschutz) verwendet die IP-Adresse im Arbeitsspeicher des Servers; ein Neustart leert sie. Mit der Anfrage werden weder eine IP-Adresse im Klartext noch ein Browser-Kennzeichen gespeichert, sondern nur ein **Hashwert der IP-Adresse („IP-Hash“)**: ein SHA-256-Wert, den WEBklar mit einem geheimen Schlüssel des Servers salzt. Der IP-Hash ist ein pseudonymes technisches Datum und bleibt ein personenbezogenes Datum, weil WEBklar mit dem Schlüssel für dieselbe IP-Adresse denselben Wert berechnen kann; ohne den Schlüssel lässt sich aus ihm keine IP-Adresse ableiten. WEBklar verwendet ihn ausschließlich zum Missbrauchsschutz. Er dient nicht dazu, Gäste zu identifizieren, wiederzuerkennen oder zu profilieren, wird dem Kunden weder angezeigt noch exportiert und steht nur im Datensatz der Anfrage. WEBklar entfernt ihn aus dem Datensatz, sobald der Kunde die Anfrage freigibt oder ablehnt, spätestens nach 30 Tagen (Nummer 9 Abs. 1). In den Server-Protokollen stehen IP-Adresse und Browser-Kennzeichen 14 Tage (Anlage 2 der Abo-Bedingungen).
(5) Der Buchungskalender setzt keine Cookies. Das Widget speichert nichts im Browser. Auch die Buchungsseite legt weder Cookies noch Daten im lokalen oder im Sitzungsspeicher des Browsers ab.
(6) Betroffene: Gäste, die über die Website des Kunden Anfragen stellen oder buchen, und Personen, deren Angaben der Gast in der Anfrage oder Buchung macht (zum Beispiel Mitreisende oder abweichende Rechnungsempfänger). Beschäftigte des Kunden sind von diesem Vertrag nicht betroffen.
## 4. Weisungen
(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.
(2) Mit dem Einschalten weist der Kunde WEBklar an, den Buchungskalender nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben, in seinem Namen Rechnungen auszustellen und Zahlungen, die nach einer Stornierung oder nach dem Ablauf einer Reservierung eingehen, sowie zweite Zahlungen auf eine bereits bezahlte Buchung ohne weitere Anweisung in voller Höhe über sein Stripe-Konto zu erstatten (dauerhafte Weisung; der Stand der Buchung ändert sich dabei nicht). Bei einer Stornierung schließt WEBklar eine noch offene Zahlungsseite bei Stripe. Weitere Weisungen ergeben sich aus den Einstellungen und Aktionen im Kundenportal (Unterkünfte, Preise, Sperren, Kalenderverbindungen, Freigabe und Ablehnung von Anfragen, Zahlungseingang vor Ort, Stornierungen und Erstattungen, Online-Zahlung, Öffnen und Schließen des Buchungskalenders). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Buchungskalender abschalten lassen (Nr. 10 Abs. 7 der Leistungsbeschreibung). Die Anonymisierung und Löschung auf Weisung nach Nummer 8 Abs. 2 und Nummer 9 Abs. 6 setzt WEBklar immer um, soweit Aufbewahrungspflichten dem nicht entgegenstehen (Nummer 9 Abs. 8).
(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.
## 5. Vertraulichkeit
WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.
## 6. Technische und organisatorische Maßnahmen
(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.
(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.
## 7. Unterauftragsverarbeiter
(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Buchungskalender läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt.
(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Buchungskalender abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6).
(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).
(4) **Stripe ist kein Unterauftragsverarbeiter von WEBklar.** Der Kunde verwendet sein eigenes Stripe-Konto und hat den Vertrag darüber unmittelbar mit Stripe geschlossen (Nr. 3 der Leistungsbeschreibung); die Zahlung des Gastes fließt direkt auf dieses Konto. Die Zahlungsseite (Stripe Checkout), die Erhebung von Zahlungs- und Kartendaten (derzeit nur Karte, einschließlich Apple Pay und Google Pay), die Prüfung von Zahlungen und die Auszahlungen verantworten der Kunde und Stripe nach ihrem Vertrag; WEBklar erhält diese Daten nicht. Auf Weisung des Kunden legt WEBklar bei Stripe die Zahlung an, ruft die Angaben zu Zahlungen ab, die der Buchungskalender braucht (Nummer 2 Abs. 1), schließt offene Zahlungsseiten und veranlasst Erstattungen (auch die automatischen nach Nummer 4 Abs. 2). An Stripe gehen dabei von WEBklar nur: die Bezeichnung des Aufenthalts mit Rechnungsnummer als Zahlungsposition, der Betrag, die Kennungen der Buchung und der Zahlung (Metadaten), die E-Mail-Adresse des Gastes zur Vorbelegung der Zahlungsseite, soweit WEBklar sie übergibt, die Erstattung und der **geheime Link zur Buchungsseite mit dem Token als Rücksprungadresse** (bei Erfolg und bei Abbruch). Dieser Link ist damit für Stripe und im Stripe-Dashboard des Kunden sichtbar; Der Link gilt bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage) und öffnet nur diese eine Buchungsseite. Die Weitergabe von Daten durch Stripe, auch an Stellen außerhalb der Europäischen Union, regeln der Kunde und Stripe; dafür ist WEBklar nicht verantwortlich.
(5) Plattformen, deren iCal-Kalender der Kunde einbindet oder denen er die iCal-Adresse des Buchungskalenders mitteilt, sind Empfänger nach seiner Entscheidung. WEBklar übermittelt ihnen keine Daten von Gästen.
## 8. Unterstützung des Kunden, Betroffenenrechte
(1) WEBklar leitet Anfragen von Gästen, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.
(2) Dafür bietet das Kundenportal die Anzeige der Anfragen und Buchungen, mit denen der Kunde Auskunft erteilen kann, und den Abruf der Rechnungen als PDF. Auf Weisung des Kunden in Textform (etwa im Chat des Kundenportals) anonymisiert WEBklar die Gästedaten einer Anfrage oder Buchung unverzüglich: Name, Firma, E-Mail-Adresse, Telefonnummer und die Freitexte werden gelöscht oder unkenntlich gemacht (ein IP-Hash ist zu diesem Zeitpunkt in der Regel längst entfernt); Anfragenummer, Zeitraum, Beträge und Status bleiben. Rechnungen und Rechnungsadresse einer gebuchten Anfrage bleiben, solange eine Aufbewahrungspflicht besteht (Nummer 9 Abs. 3). Dabei löscht WEBklar die E-Mails zu dieser Anfrage, die noch auf den Versand warten. E-Mails, deren Versand endgültig fehlgeschlagen ist, behalten Empfängeradresse und Inhalt, bis WEBklar sie nach Nummer 9 Abs. 5 löscht (48 Stunden nach dem Fehlschlag).
(3) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.
(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht.
(5) Die Gäste informiert der Kunde (Art. 13 DSGVO; Nummer 11 Abs. 2).
## 9. Löschung und Rückgabe
(1) Den IP-Hash entfernt WEBklar aus dem Datensatz einer Anfrage, sobald der Kunde sie freigibt oder ablehnt, spätestens 30 Tage nach dem Anlegen.
(2) Anfragen, aus denen keine Buchung wurde (abgelehnt, abgelaufen, oder neu und nicht innerhalb von 60 Tagen entschieden), anonymisiert WEBklar nach 6 Monaten: Name, Firma, E-Mail-Adresse, Telefonnummer und Nachricht werden gelöscht. Anfragenummer, Zeitraum, Status und, soweit vorhanden, der Preisvorschlag bleiben ohne Personenbezug erhalten.
(3) Bei gebuchten Aufenthalten anonymisiert WEBklar die Gästedaten (Name, Firma, E-Mail-Adresse, Telefonnummer und Nachricht) 3 Jahre nach dem Ende des Kalenderjahres, in dem der Gast abgereist ist (Abreisejahr; maßgeblich ist die Zeit in Berlin). **Rechnungen, Stornorechnungen und die Rechnungsadresse bleiben 8 Jahre erhalten**, gerechnet ab dem Ende des Kalenderjahres der Rechnung, solange der Buchungskalender läuft (Buchungsbelege, § 147 AO); sie sind für den Kunden als Aussteller bestimmt. Anfragenummer, Zeitraum, Beträge und Status bleiben ebenfalls.
(4) Die Anonymisierung nach den Absätzen 2 und 3 sowie die Entfernung des IP-Hash laufen im täglichen Löschlauf; anonymisiert oder entfernt wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu bearbeiten, als ein Lauf schafft, folgt der Rest im nächsten Lauf.
(5) In der Versandwarteschlange für E-Mails leert WEBklar die Empfängeradresse und den Inhalt einer E-Mail nach dem Versand und löscht den Eintrag 7 Tage nach dem Versand. E-Mails, die nicht zugestellt werden können, löscht WEBklar mit Empfängeradresse und Inhalt 48 Stunden nach dem endgültigen Fehlschlag.
(6) Ist der Buchungskalender abgeschaltet oder das Wartungs-Abo beendet, kann der Kunde die Anfragen, Buchungen und Rechnungen 90 Tage lang im Kundenportal lesen und die Rechnungen einzeln als PDF herunterladen; WEBklar weist ihn nach dem Abschalten per E-Mail mit dem Löschdatum darauf hin, seine Rechnungen zu sichern. Danach löscht WEBklar alle Daten des Buchungskalenders: Unterkünfte, Preise, Anfragen, Buchungen, Rechnungen, Kalenderverbindungen, Sperren und Einstellungen. Damit endet auch die Aufbewahrung der Rechnungen nach Absatz 3; sie bewahrt der Kunde dann selbst auf. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen (zum Beispiel Adresse der Website, Einbau-Code und die Angaben des Kunden aus der Anfrage) und enthält keine Daten von Gästen; dafür gilt die Datenschutzerklärung von WEBklar (Abschnitt 6). Beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schaltet WEBklar den Buchungskalender auf Wunsch des Kunden mit den vorhandenen Daten wieder ein (Nr. 10 Abs. 5 der Leistungsbeschreibung). Früher anonymisiert WEBklar einzelne Daten auf Weisung des Kunden (Nummer 8 Abs. 2); Wird das Kundenkonto gelöscht, schaltet WEBklar den Buchungskalender ab und löscht seine Daten wie nach Absatz 6 (bei Abschaltung 90 Tage später); einen Hinweis per E-Mail gibt es dann nicht, weil mit dem Konto auch die E-Mail-Adresse gelöscht wird. alle Daten des Buchungskalenders löscht WEBklar auf Weisung des Kunden in Textform unverzüglich.
(7) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte oder anonymisierte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen und Anonymisierungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.
(8) Gesetzliche Aufbewahrungspflichten bleiben unberührt. Der Kunde ist für seine eigenen Aufbewahrungspflichten (zum Beispiel für Rechnungen und Buchungsbelege) selbst verantwortlich; WEBklar ist kein Archiv im Sinne der Buchführung. Die Aufbewahrung der Rechnungen nach Absatz 3 geschieht als Dienst für den Kunden und für die Dauer des Buchungskalenders. Der Kunde sichert die Belege, die er braucht, rechtzeitig vor der Löschung nach Absatz 6 durch den Download. Soweit eine gesetzliche Aufbewahrungspflicht besteht, löscht oder anonymisiert WEBklar auch auf Weisung des Kunden nicht früher; stattdessen sperrt es die Daten für die weitere Verarbeitung.
## 10. Nachweise und Kontrollen
WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.
## 11. Pflichten des Kunden
(1) Der Kunde ist Verantwortlicher für die Verarbeitung der Daten seiner Gäste im Buchungskalender.
(2) Der Kunde informiert seine Gäste nach Art. 13 DSGVO. Dazu nimmt er den Datenschutz-Absatz für den Buchungskalender in seine Datenschutzerklärung auf (Nr. 5 Abs. 10 der Leistungsbeschreibung) und nennt in seinen Rechtstexten (Datenschutzerklärung, Impressum) die Angaben, die das Gesetz verlangt.
(3) Der Kunde schließt mit Stripe den Vertrag über sein Stripe-Konto selbst, wenn er die Online-Zahlung nutzt, und verantwortet die Verarbeitung von Daten durch Stripe, soweit sie nicht in diesem Vertrag geregelt ist (Nummer 7 Abs. 4).
(4) Der Kunde fordert von seinen Gästen keine Angaben zu ihrer Gesundheit oder zu anderen besonderen Kategorien personenbezogener Daten und weist sie auf der Website darauf hin, solche Angaben nicht in das Nachrichtenfeld zu schreiben.
(5) Der Kunde verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets und der Buchungsseite nicht (Nr. 5 Abs. 9 der Leistungsbeschreibung).
(6) Der Kunde erfüllt seine Pflichten zu Meldeschein, Kurtaxe und sonstigen Meldungen an Behörden selbst und verarbeitet die dafür nötigen Daten außerhalb des Buchungskalenders (Nr. 5 Abs. 6 der Leistungsbeschreibung).
## 12. Haftung, Änderungen, Schlussbestimmungen
(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.
(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung, auch wegen geänderter Datenerhebung, teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.
(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar den Buchungskalender für seine Website nach Nr. 12 Abs. 2 der Leistungsbeschreibung beenden oder nach Nr. 12 Abs. 3 der Leistungsbeschreibung aussetzen. Das Abo bleibt davon unberührt.
(4) Im Übrigen gilt § 16 der Abo-Bedingungen.
## Anlage: Technische und organisatorische Maßnahmen des Buchungskalenders (Art. 32 DSGVO)
| Bereich | Maßnahmen |
|---|---|
| Übertragung | Das Widget, die Buchungsseite und das Kundenportal werden nur über TLS (HTTPS) übertragen. Der Einbau-Code verweist auf eine Adresse mit https://. Die Zahlung geschieht auf der Zahlungsseite von Stripe. Kalender anderer Plattformen ruft WEBklar nur über https ab. |
| Speicher im Browser | Es werden keine Cookies gesetzt. Das Widget sendet seine Anfragen ohne Cookies und ruft nur die Schnittstelle des Buchungskalenders auf, keine Dienste Dritter. |
| Zugriffskontrolle | Das Kundenportal ist nur nach Anmeldung erreichbar; jeder Kunde sieht nur die Daten seiner eigenen Projekte. Die öffentliche Schnittstelle des Widgets liefert nur für einen vorhandenen Buchungskalender Daten und ist über den Website-Schlüssel (Site-Key) adressiert; die belegten Nächte nennt sie ohne Quelle und ohne Daten von Gästen. Die Buchungsseite ruft der Gast nur mit einem geheimen Token ab, das nur als Prüfsumme gespeichert und zeitkonstant verglichen wird. Der Token wird nur als Prüfsumme gespeichert; er wird nur als Teil der Rücksprungadresse an Stripe übergeben (Nummer 7 Abs. 4) und öffnet nur diese eine Buchungsseite; er gilt bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage), danach antwortet die Buchungsseite wie bei einem unbekannten Link. Benachrichtigungen von Stripe werden nur nach Prüfung ihrer Signatur verarbeitet; die Schlüssel für Stripe liegen nur auf dem Server. Die iCal-Adresse des Buchungskalenders ist ein geheimer, erneuerbarer Zufallswert. |
| Missbrauchsschutz | Ein verstecktes Feld (Honeypot), eine Mindestzeit bis zum Absenden des Formulars mit signiertem Formular-Token und Grenzen je IP-Adresse und je Buchungskalender schützen vor automatisierten Anfragen. Die Grenzen gelten im Arbeitsspeicher; ein Neustart leert sie. Mit der Anfrage wird nur ein gesalzener IP-Hash gespeichert (Nummer 3 Abs. 4). Beim Abruf von Kalendern anderer Plattformen sind nur Adressen mit https erlaubt, private und lokale Ziele sind gesperrt, und Größe und Dauer des Abrufs sind begrenzt. |
| Datenminimierung | Kartendaten erhält und speichert WEBklar nicht. Preise berechnet der Server aus den gespeicherten Preisen und Posten, nie aus Angaben des Browsers. Von anderen Kalendern werden nur Zeiträume übernommen. Mit der Anfrage werden keine IP-Adresse im Klartext und kein Browser-Kennzeichen gespeichert; der IP-Hash wird mit der Entscheidung des Kunden entfernt. Die Gästedaten werden nach den Fristen der Nummer 9 anonymisiert. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert. |
| Integrität der Rechnungen | Rechnungen und Stornorechnungen werden als unveränderlicher Schnappschuss gespeichert; das PDF entsteht bei jedem Abruf aus diesem Schnappschuss. Rechnungsnummern werden unter Sperre lückenlos je Kunde und Jahr vergeben. Freigabe und Buchung prüfen unter Sperre erneut, dass der Zeitraum frei ist. |
| Trennung | Alle Unterkünfte, Anfragen und Rechnungen tragen die Kennung des Kunden, des Projekts und der Funktion; das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten des jeweiligen Buchungskalenders. Die Hintergrundläufe (Ablauf von Reservierungen, Kalenderabgleich, Versand der E-Mails, Anonymisierung und Löschung) schreiben jeweils nur in das Dokument, um das es geht. |
| Löschung | Täglicher Löschlauf für Anfragen, Buchungen und Warteschlange nach den Fristen der Nummer 9; die Daten abgeschalteter Buchungskalender 90 Tage nach dem Abschalten; das Einrichtungs-Ticket bleibt als Arbeitsnachweis mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6). |
| Protokolle | Die Programmprotokolle des Buchungskalenders und der Tagesbericht enthalten nur Zählungen und Anfragenummern, keine Namen, E-Mail-Adressen oder Adressen von Gästen. Server-Protokolle des Webservers werden nach 14 Tagen gelöscht (Anlage 2 der Abo-Bedingungen). |
| Datensicherung | Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). |
| Rechenzentrum | Hetzner Online GmbH, Rechenzentren Falkenstein und Nürnberg (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren. |
| Organisation | Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 3), Überprüfung der Maßnahmen mindestens einmal im Jahr. |
+157
View File
@@ -0,0 +1,157 @@
# Auftragsverarbeitungsvertrag Buchungskalender (Art. 28 DSGVO)
**Fassung 2026-10-07** · Entwurf – Prüfung durch Kanzlei offen
zwischen dem Kunden als Verantwortlichem („Kunde“; in der Leistungsbeschreibung „Vermieter“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).
## Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen
(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Einrichtung des Buchungskalenders bestellt und dabei per Haken diesem Vertrag und der Leistungsbeschreibung „Buchungskalender“ („Leistungsbeschreibung“) in der angezeigten Fassung zustimmt. Das Portal speichert die Fassung, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte und die Widerrufsbelehrung in dieser Fassung als PDF enthält und auf sie verlinkt.
(2) Ein Mitarbeiter von WEBklar darf die Funktion auch ohne Bestellung im Portal einschalten (zum Beispiel für einen Kunden, dem WEBklar sie als Teil seines Website-Projekts bereitstellt), aber nur, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. Das Portal verlangt dafür das Datum und den Weg der Erklärung; fehlt eine Angabe, schaltet es nicht ein. WEBklar speichert vor dem Einschalten einen Zustimmungsnachweis mit der Fassung der beiden Texte, dem Datum und dem Weg dieser Erklärung, dem Zeitpunkt des Einschaltens und dem Namen des Mitarbeiters (ohne IP-Adresse und Browser-Kennzeichen, weil der Kunde nicht im Portal zugestimmt hat). Der Kunde erhält anschließend eine E-Mail mit diesen beiden Texten als PDF.
(3) Dieser Vertrag ist ein eigener Vertrag für den Buchungskalender. Er ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für den Buchungskalender geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung vor.
(4) Die Rollen: Der Kunde ist Verantwortlicher für die personenbezogenen Daten seiner Gäste im Buchungskalender. WEBklar ist sein Auftragsverarbeiter. Stripe ist nicht Auftragsverarbeiter von WEBklar, sondern Vertragspartner des Kunden (Nummer 7 Abs. 4). Die Plattformen, deren Kalender der Kunde einbindet (zum Beispiel Booking.com oder Airbnb), sind weder Auftragsverarbeiter noch Unterauftragsverarbeiter von WEBklar.
## 1. Gegenstand und Dauer
(1) WEBklar betreibt für die öffentliche Website des Kunden ein Buchungs-Widget mit einer Buchungsseite und einer Verwaltung im Kundenportal („Buchungskalender“) und verarbeitet dabei personenbezogene Daten der Gäste des Kunden im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung.
(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Abschalten, spätestens mit dem Ende des Vertrags, aufgrund dessen WEBklar die Website des Kunden betreibt; den Buchungskalender schaltet WEBklar dann zum Vertragsende ab (Nr. 10 Abs. 1 der Leistungsbeschreibung). Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.
## 2. Art und Zweck der Verarbeitung
(1) Art: Übermitteln der Anfrage und der Buchung vom Widget und von der Buchungsseite an den Server von WEBklar, Speichern, Verwalten und Anzeigen im Kundenportal, Berechnen des Preises, Erzeugen von Rechnungen und Stornorechnungen im Namen des Kunden (Nr. 3 Abs. 6 der Leistungsbeschreibung), Abgleichen von Kalendern, Versenden von E-Mails, Übernehmen von Zahlungsstatus aus Stripe, Weitergeben von Erstattungen an Stripe im Auftrag des Kunden, Anonymisieren und Löschen.
(2) Zwecke: Der Kunde soll Anfragen und Buchungen seiner Gäste abwickeln. Dazu nimmt der Buchungskalender Anfragen entgegen, reserviert Zeiträume nach der Freigabe durch den Kunden, nimmt die verbindliche Buchung des Gastes entgegen, stellt die Rechnung aus, gleicht Online-Zahlungen mit Stripe ab, bildet Zahlungen vor Ort ab, bildet Stornierungen ab und benachrichtigt den Kunden. WEBklar versendet die E-Mails im Auftrag des Kunden. Außerdem schützt WEBklar den Buchungskalender für den Kunden vor Missbrauch (Mengenbegrenzung, Begrenzung der Anfragen je IP-Adresse, Erkennen automatisierter Anfragen).
(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Gäste. Zählungen ohne Personenbezug, die WEBklar für den sicheren Betrieb braucht (etwa die Zahl der anonymisierten Anfragen je Tag oder die Länge der Versandwarteschlange), sind keine eigenen Zwecke.
## 3. Datenarten und Betroffene
(1) Daten der Gäste, die WEBklar aus dem Widget, von der Buchungsseite und aus Stripe erhält und speichert:
- bei der Anfrage: Name, E-Mail-Adresse, auf Wunsch Firma, Telefonnummer und eine Nachricht, die gewünschte Unterkunft, der Zeitraum, die Zahl der Personen, die Anfragenummer, ein Hashwert der IP-Adresse (Absatz 4) und der Zeitpunkt;
- bei der Freigabe: die Posten und Summen des Preises (Text, Menge, Einzelbetrag, Steuersatz), die Reservierungsfrist, eine Nachricht des Kunden an den Gast, die Stornobedingungen, die galten, und die Fassung der Rechtstexte, der der Gast gesehen hat, jeweils als Schnappschuss;
- bei der Buchung: die Rechnungsadresse (Name, Firma, Straße, Postleitzahl, Ort, Land), die Zahlart, der Zeitpunkt der Buchung und die Rechnungsnummer;
- bei der Zahlung und der Erstattung: Status, Zeitpunkt, Art (online über Stripe oder vor Ort), die Kennungen der Zahlung bei Stripe, erstattete Beträge und der Grund einer Stornierung;
- die Rechnung und die Stornorechnung als unveränderlicher Schnappschuss (Angaben des Ausstellers, Empfänger, Unterkunft, Zeitraum, Posten, Summen, Steuerart, Bezug), aus dem WEBklar das PDF bei jedem Abruf erzeugt;
- ein Hashwert des geheimen Zugangs zur Buchungsseite (der Link in der E-Mail wird nur als Prüfsumme gespeichert);
- der Zustellstatus der E-Mails an den Gast.
**Kartendaten des Gastes erhält und speichert WEBklar nicht;** die gibt der Gast nur bei Stripe ein. Online-Zahlungen sind derzeit nur per Karte (einschließlich Apple Pay und Google Pay) möglich, nicht per SEPA-Lastschrift. Die Bankverbindung des Kunden selbst (Angabe auf der Rechnung) und seine Firmen- und Steuerangaben gehören nicht zu den Gästedaten.
(2) Von Kalendern anderer Plattformen übernimmt WEBklar nur die belegten Zeiträume (Datum von und bis), nicht Namen, Beschreibungen oder andere Angaben der dortigen Buchungen, auch wenn der Kalender sie enthält. Die iCal-Adresse, die WEBklar dem Kunden bereitstellt, enthält nur belegte Nächte, keine Angaben zu Gästen. Wer die geheime Adresse kennt, sieht die belegten Nächte; der Kunde kann sie jederzeit erneuern.
(3) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht verarbeitet. Der Kunde fordert von seinen Gästen keine entsprechenden Angaben und bittet sie, solche Angaben nicht in das Nachrichtenfeld zu schreiben (Nummer 11 Abs. 4). Meldedaten nach dem Bundesmeldegesetz (Meldeschein) und Daten zur Kurtaxe verarbeitet der Buchungskalender nicht.
(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Widgets und jeder Anfrage. Die Mengenbegrenzung (Missbrauchsschutz) verwendet die IP-Adresse im Arbeitsspeicher des Servers; ein Neustart leert sie. Mit der Anfrage werden weder eine IP-Adresse im Klartext noch ein Browser-Kennzeichen gespeichert, sondern nur ein **Hashwert der IP-Adresse („IP-Hash“)**: ein SHA-256-Wert, den WEBklar mit einem geheimen Schlüssel des Servers salzt. Der IP-Hash ist ein pseudonymes technisches Datum und bleibt ein personenbezogenes Datum, weil WEBklar mit dem Schlüssel für dieselbe IP-Adresse denselben Wert berechnen kann; ohne den Schlüssel lässt sich aus ihm keine IP-Adresse ableiten. WEBklar verwendet ihn ausschließlich zum Missbrauchsschutz. Er dient nicht dazu, Gäste zu identifizieren, wiederzuerkennen oder zu profilieren, wird dem Kunden weder angezeigt noch exportiert und steht nur im Datensatz der Anfrage. WEBklar entfernt ihn aus dem Datensatz, sobald der Kunde die Anfrage freigibt oder ablehnt, spätestens nach 30 Tagen (Nummer 9 Abs. 1). In den Server-Protokollen stehen IP-Adresse und Browser-Kennzeichen 14 Tage (Anlage 2 der Abo-Bedingungen).
(5) Der Buchungskalender setzt keine Cookies. Das Widget speichert nichts im Browser. Auch die Buchungsseite legt weder Cookies noch Daten im lokalen oder im Sitzungsspeicher des Browsers ab.
(6) Betroffene: Gäste, die über die Website des Kunden Anfragen stellen oder buchen, und Personen, deren Angaben der Gast in der Anfrage oder Buchung macht (zum Beispiel Mitreisende oder abweichende Rechnungsempfänger). Beschäftigte des Kunden sind von diesem Vertrag nicht betroffen.
## 4. Weisungen
(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.
(2) Mit der Bestellung weist der Kunde WEBklar an, den Buchungskalender nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben, in seinem Namen Rechnungen auszustellen und Zahlungen, die nach einer Stornierung oder nach dem Ablauf einer Reservierung eingehen, sowie zweite Zahlungen auf eine bereits bezahlte Buchung ohne weitere Anweisung in voller Höhe über sein Stripe-Konto zu erstatten (dauerhafte Weisung; der Stand der Buchung ändert sich dabei nicht). Bei einer Stornierung schließt WEBklar eine noch offene Zahlungsseite bei Stripe. Weitere Weisungen ergeben sich aus den Einstellungen und Aktionen im Kundenportal (Unterkünfte, Preise, Sperren, Kalenderverbindungen, Freigabe und Ablehnung von Anfragen, Zahlungseingang vor Ort, Stornierungen und Erstattungen, Online-Zahlung, Öffnen und Schließen des Buchungskalenders). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Buchungskalender abschalten lassen (Nr. 10 Abs. 7 der Leistungsbeschreibung). Die Anonymisierung und Löschung auf Weisung nach Nummer 8 Abs. 2 und Nummer 9 Abs. 6 setzt WEBklar immer um, soweit Aufbewahrungspflichten dem nicht entgegenstehen (Nummer 9 Abs. 8).
(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.
## 5. Vertraulichkeit
WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.
## 6. Technische und organisatorische Maßnahmen
(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.
(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.
## 7. Unterauftragsverarbeiter
(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Buchungskalender läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt.
(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Buchungskalender abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6).
(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).
(4) **Stripe ist kein Unterauftragsverarbeiter von WEBklar.** Der Kunde verwendet sein eigenes Stripe-Konto und hat den Vertrag darüber unmittelbar mit Stripe geschlossen (Nr. 3 der Leistungsbeschreibung); die Zahlung des Gastes fließt direkt auf dieses Konto. Die Zahlungsseite (Stripe Checkout), die Erhebung von Zahlungs- und Kartendaten (derzeit nur Karte, einschließlich Apple Pay und Google Pay), die Prüfung von Zahlungen und die Auszahlungen verantworten der Kunde und Stripe nach ihrem Vertrag; WEBklar erhält diese Daten nicht. Auf Weisung des Kunden legt WEBklar bei Stripe die Zahlung an, ruft die Angaben zu Zahlungen ab, die der Buchungskalender braucht (Nummer 2 Abs. 1), schließt offene Zahlungsseiten und veranlasst Erstattungen (auch die automatischen nach Nummer 4 Abs. 2). An Stripe gehen dabei von WEBklar nur: die Bezeichnung des Aufenthalts mit Rechnungsnummer als Zahlungsposition, der Betrag, die Kennungen der Buchung und der Zahlung (Metadaten), die E-Mail-Adresse des Gastes zur Vorbelegung der Zahlungsseite, soweit WEBklar sie übergibt, die Erstattung und der **geheime Link zur Buchungsseite mit dem Token als Rücksprungadresse** (bei Erfolg und bei Abbruch). Dieser Link ist damit für Stripe und im Stripe-Dashboard des Kunden sichtbar; Der Link gilt bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage) und öffnet nur diese eine Buchungsseite. Die Weitergabe von Daten durch Stripe, auch an Stellen außerhalb der Europäischen Union, regeln der Kunde und Stripe; dafür ist WEBklar nicht verantwortlich.
(5) Plattformen, deren iCal-Kalender der Kunde einbindet oder denen er die iCal-Adresse des Buchungskalenders mitteilt, sind Empfänger nach seiner Entscheidung. WEBklar übermittelt ihnen keine Daten von Gästen.
## 8. Unterstützung des Kunden, Betroffenenrechte
(1) WEBklar leitet Anfragen von Gästen, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.
(2) Dafür bietet das Kundenportal die Anzeige der Anfragen und Buchungen, mit denen der Kunde Auskunft erteilen kann, und den Abruf der Rechnungen als PDF. Auf Weisung des Kunden in Textform (etwa im Chat des Kundenportals) anonymisiert WEBklar die Gästedaten einer Anfrage oder Buchung unverzüglich: Name, Firma, E-Mail-Adresse, Telefonnummer und die Freitexte werden gelöscht oder unkenntlich gemacht (ein IP-Hash ist zu diesem Zeitpunkt in der Regel längst entfernt); Anfragenummer, Zeitraum, Beträge und Status bleiben. Rechnungen und Rechnungsadresse einer gebuchten Anfrage bleiben, solange eine Aufbewahrungspflicht besteht (Nummer 9 Abs. 3). Dabei löscht WEBklar die E-Mails zu dieser Anfrage, die noch auf den Versand warten. E-Mails, deren Versand endgültig fehlgeschlagen ist, behalten Empfängeradresse und Inhalt, bis WEBklar sie nach Nummer 9 Abs. 5 löscht (48 Stunden nach dem Fehlschlag).
(3) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.
(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht.
(5) Die Gäste informiert der Kunde (Art. 13 DSGVO; Nummer 11 Abs. 2).
## 9. Löschung und Rückgabe
(1) Den IP-Hash entfernt WEBklar aus dem Datensatz einer Anfrage, sobald der Kunde sie freigibt oder ablehnt, spätestens 30 Tage nach dem Anlegen.
(2) Anfragen, aus denen keine Buchung wurde (abgelehnt, abgelaufen, oder neu und nicht innerhalb von 60 Tagen entschieden), anonymisiert WEBklar nach 6 Monaten: Name, Firma, E-Mail-Adresse, Telefonnummer und Nachricht werden gelöscht. Anfragenummer, Zeitraum, Status und, soweit vorhanden, der Preisvorschlag bleiben ohne Personenbezug erhalten.
(3) Bei gebuchten Aufenthalten anonymisiert WEBklar die Gästedaten (Name, Firma, E-Mail-Adresse, Telefonnummer und Nachricht) 3 Jahre nach dem Ende des Kalenderjahres, in dem der Gast abgereist ist (Abreisejahr; maßgeblich ist die Zeit in Berlin). **Rechnungen, Stornorechnungen und die Rechnungsadresse bleiben 8 Jahre erhalten**, gerechnet ab dem Ende des Kalenderjahres der Rechnung, solange der Buchungskalender läuft (Buchungsbelege, § 147 AO); sie sind für den Kunden als Aussteller bestimmt. Anfragenummer, Zeitraum, Beträge und Status bleiben ebenfalls.
(4) Die Anonymisierung nach den Absätzen 2 und 3 sowie die Entfernung des IP-Hash laufen im täglichen Löschlauf; anonymisiert oder entfernt wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu bearbeiten, als ein Lauf schafft, folgt der Rest im nächsten Lauf.
(5) In der Versandwarteschlange für E-Mails leert WEBklar die Empfängeradresse und den Inhalt einer E-Mail nach dem Versand und löscht den Eintrag 7 Tage nach dem Versand. E-Mails, die nicht zugestellt werden können, löscht WEBklar mit Empfängeradresse und Inhalt 48 Stunden nach dem endgültigen Fehlschlag.
(6) Ist der Buchungskalender abgeschaltet oder der Vertrag über den Betrieb der Website beendet, kann der Kunde die Anfragen, Buchungen und Rechnungen 90 Tage lang im Kundenportal lesen und die Rechnungen einzeln als PDF herunterladen; WEBklar weist ihn nach dem Abschalten per E-Mail mit dem Löschdatum darauf hin, seine Rechnungen zu sichern. Danach löscht WEBklar alle Daten des Buchungskalenders: Unterkünfte, Preise, Anfragen, Buchungen, Rechnungen, Kalenderverbindungen, Sperren und Einstellungen. Damit endet auch die Aufbewahrung der Rechnungen nach Absatz 3; sie bewahrt der Kunde dann selbst auf. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen (zum Beispiel Adresse der Website, Einbau-Code und die Angaben des Kunden aus der Anfrage) und enthält keine Daten von Gästen; dafür gilt die Datenschutzerklärung von WEBklar (Abschnitt 6). Beginnt innerhalb dieser Frist wieder ein Vertrag über den Betrieb der Website, schaltet WEBklar den Buchungskalender auf Wunsch des Kunden mit den vorhandenen Daten wieder ein (Nr. 10 Abs. 5 der Leistungsbeschreibung). Früher anonymisiert WEBklar einzelne Daten auf Weisung des Kunden (Nummer 8 Abs. 2); Wird das Kundenkonto gelöscht, schaltet WEBklar den Buchungskalender ab und löscht seine Daten wie nach Absatz 6 (bei Abschaltung 90 Tage später); einen Hinweis per E-Mail gibt es dann nicht, weil mit dem Konto auch die E-Mail-Adresse gelöscht wird. alle Daten des Buchungskalenders löscht WEBklar auf Weisung des Kunden in Textform unverzüglich.
(7) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte oder anonymisierte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen und Anonymisierungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.
(8) Gesetzliche Aufbewahrungspflichten bleiben unberührt. Der Kunde ist für seine eigenen Aufbewahrungspflichten (zum Beispiel für Rechnungen und Buchungsbelege) selbst verantwortlich; WEBklar ist kein Archiv im Sinne der Buchführung. Die Aufbewahrung der Rechnungen nach Absatz 3 geschieht als Dienst für den Kunden und für die Dauer des Buchungskalenders. Der Kunde sichert die Belege, die er braucht, rechtzeitig vor der Löschung nach Absatz 6 durch den Download. Soweit eine gesetzliche Aufbewahrungspflicht besteht, löscht oder anonymisiert WEBklar auch auf Weisung des Kunden nicht früher; stattdessen sperrt es die Daten für die weitere Verarbeitung.
## 10. Nachweise und Kontrollen
WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.
## 11. Pflichten des Kunden
(1) Der Kunde ist Verantwortlicher für die Verarbeitung der Daten seiner Gäste im Buchungskalender.
(2) Der Kunde informiert seine Gäste nach Art. 13 DSGVO. Dazu nimmt er den Datenschutz-Absatz für den Buchungskalender in seine Datenschutzerklärung auf (Nr. 5 Abs. 10 der Leistungsbeschreibung) und nennt in seinen Rechtstexten (Datenschutzerklärung, Impressum) die Angaben, die das Gesetz verlangt.
(3) Der Kunde schließt mit Stripe den Vertrag über sein Stripe-Konto selbst, wenn er die Online-Zahlung nutzt, und verantwortet die Verarbeitung von Daten durch Stripe, soweit sie nicht in diesem Vertrag geregelt ist (Nummer 7 Abs. 4).
(4) Der Kunde fordert von seinen Gästen keine Angaben zu ihrer Gesundheit oder zu anderen besonderen Kategorien personenbezogener Daten und weist sie auf der Website darauf hin, solche Angaben nicht in das Nachrichtenfeld zu schreiben.
(5) Der Kunde verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets und der Buchungsseite nicht (Nr. 5 Abs. 9 der Leistungsbeschreibung).
(6) Der Kunde erfüllt seine Pflichten zu Meldeschein, Kurtaxe und sonstigen Meldungen an Behörden selbst und verarbeitet die dafür nötigen Daten außerhalb des Buchungskalenders (Nr. 5 Abs. 6 der Leistungsbeschreibung).
## 12. Haftung, Änderungen, Schlussbestimmungen
(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.
(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung, auch wegen geänderter Datenerhebung, teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.
(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar den Buchungskalender für seine Website nach Nr. 12 Abs. 2 der Leistungsbeschreibung beenden oder nach Nr. 12 Abs. 3 der Leistungsbeschreibung aussetzen. Das Abo bleibt davon unberührt.
(4) Im Übrigen gilt § 16 der Abo-Bedingungen.
## Anlage: Technische und organisatorische Maßnahmen des Buchungskalenders (Art. 32 DSGVO)
| Bereich | Maßnahmen |
|---|---|
| Übertragung | Das Widget, die Buchungsseite und das Kundenportal werden nur über TLS (HTTPS) übertragen. Der Einbau-Code verweist auf eine Adresse mit https://. Die Zahlung geschieht auf der Zahlungsseite von Stripe. Kalender anderer Plattformen ruft WEBklar nur über https ab. |
| Speicher im Browser | Es werden keine Cookies gesetzt. Das Widget sendet seine Anfragen ohne Cookies und ruft nur die Schnittstelle des Buchungskalenders auf, keine Dienste Dritter. |
| Zugriffskontrolle | Das Kundenportal ist nur nach Anmeldung erreichbar; jeder Kunde sieht nur die Daten seiner eigenen Projekte. Die öffentliche Schnittstelle des Widgets liefert nur für einen vorhandenen Buchungskalender Daten und ist über den Website-Schlüssel (Site-Key) adressiert; die belegten Nächte nennt sie ohne Quelle und ohne Daten von Gästen. Die Buchungsseite ruft der Gast nur mit einem geheimen Token ab, das nur als Prüfsumme gespeichert und zeitkonstant verglichen wird. Der Token wird nur als Prüfsumme gespeichert; er wird nur als Teil der Rücksprungadresse an Stripe übergeben (Nummer 7 Abs. 4) und öffnet nur diese eine Buchungsseite; er gilt bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage), danach antwortet die Buchungsseite wie bei einem unbekannten Link. Benachrichtigungen von Stripe werden nur nach Prüfung ihrer Signatur verarbeitet; die Schlüssel für Stripe liegen nur auf dem Server. Die iCal-Adresse des Buchungskalenders ist ein geheimer, erneuerbarer Zufallswert. |
| Missbrauchsschutz | Ein verstecktes Feld (Honeypot), eine Mindestzeit bis zum Absenden des Formulars mit signiertem Formular-Token und Grenzen je IP-Adresse und je Buchungskalender schützen vor automatisierten Anfragen. Die Grenzen gelten im Arbeitsspeicher; ein Neustart leert sie. Mit der Anfrage wird nur ein gesalzener IP-Hash gespeichert (Nummer 3 Abs. 4). Beim Abruf von Kalendern anderer Plattformen sind nur Adressen mit https erlaubt, private und lokale Ziele sind gesperrt, und Größe und Dauer des Abrufs sind begrenzt. |
| Datenminimierung | Kartendaten erhält und speichert WEBklar nicht. Preise berechnet der Server aus den gespeicherten Preisen und Posten, nie aus Angaben des Browsers. Von anderen Kalendern werden nur Zeiträume übernommen. Mit der Anfrage werden keine IP-Adresse im Klartext und kein Browser-Kennzeichen gespeichert; der IP-Hash wird mit der Entscheidung des Kunden entfernt. Die Gästedaten werden nach den Fristen der Nummer 9 anonymisiert. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert. |
| Integrität der Rechnungen | Rechnungen und Stornorechnungen werden als unveränderlicher Schnappschuss gespeichert; das PDF entsteht bei jedem Abruf aus diesem Schnappschuss. Rechnungsnummern werden unter Sperre lückenlos je Kunde und Jahr vergeben. Freigabe und Buchung prüfen unter Sperre erneut, dass der Zeitraum frei ist. |
| Trennung | Alle Unterkünfte, Anfragen und Rechnungen tragen die Kennung des Kunden, des Projekts und der Funktion; das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten des jeweiligen Buchungskalenders. Die Hintergrundläufe (Ablauf von Reservierungen, Kalenderabgleich, Versand der E-Mails, Anonymisierung und Löschung) schreiben jeweils nur in das Dokument, um das es geht. |
| Löschung | Täglicher Löschlauf für Anfragen, Buchungen und Warteschlange nach den Fristen der Nummer 9; die Daten abgeschalteter Buchungskalender 90 Tage nach dem Abschalten; das Einrichtungs-Ticket bleibt als Arbeitsnachweis mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6). |
| Protokolle | Die Programmprotokolle des Buchungskalenders und der Tagesbericht enthalten nur Zählungen und Anfragenummern, keine Namen, E-Mail-Adressen oder Adressen von Gästen. Server-Protokolle des Webservers werden nach 14 Tagen gelöscht (Anlage 2 der Abo-Bedingungen). |
| Datensicherung | Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). |
| Rechenzentrum | Hetzner Online GmbH, Rechenzentren Falkenstein und Nürnberg (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren. |
| Organisation | Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 3), Überprüfung der Maßnahmen mindestens einmal im Jahr. |
+145
View File
@@ -0,0 +1,145 @@
# Auftragsverarbeitungsvertrag Webshop (Art. 28 DSGVO)
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen
zwischen dem Kunden als Verantwortlichem („Kunde“; in der Leistungsbeschreibung „Händler“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).
## Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen
(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „Webshop“ einschaltet und die dort angezeigte Zeile „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16)“ mit dem Klick auf „Einschalten“ bestätigt; diesem Vertrag und der Leistungsbeschreibung „Webshop“ („Leistungsbeschreibung“) in der angezeigten Fassung stimmt er damit zu. Das Portal speichert den Wortlaut der angezeigten Zeile, die Fassungen, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte in dieser Fassung als PDF enthält und auf sie verlinkt.
(2) Ein Mitarbeiter von WEBklar darf die Funktion nur einschalten, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. Das Portal verlangt dafür das Datum und den Weg der Erklärung; fehlt eine Angabe, schaltet es nicht ein. WEBklar speichert vor dem Einschalten einen Zustimmungsnachweis mit der Fassung der beiden Texte, dem Datum und dem Weg dieser Erklärung, dem Zeitpunkt des Einschaltens und dem Namen des Mitarbeiters (ohne IP-Adresse und Browser-Kennzeichen, weil der Kunde nicht im Portal zugestimmt hat). Der Kunde erhält anschließend eine E-Mail mit diesen beiden Texten als PDF.
(3) Dieser Vertrag ist ein eigener Vertrag für den Webshop. Er ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für den Webshop geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung vor.
(4) Die Rollen: Der Kunde ist Verantwortlicher für die personenbezogenen Daten seiner Käufer im Webshop. WEBklar ist sein Auftragsverarbeiter. Stripe ist nicht Auftragsverarbeiter von WEBklar, sondern Vertragspartner des Kunden (Nummer 7 Abs. 4).
## 1. Gegenstand und Dauer
(1) WEBklar betreibt für die öffentliche Website des Kunden ein Shop-Widget mit einer Bestellverwaltung im Kundenportal („Webshop“) und verarbeitet dabei personenbezogene Daten der Käufer des Kunden im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung.
(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Abschalten, spätestens mit dem Ende des Wartungs-Abos; den Webshop schaltet WEBklar dann spätestens am folgenden Tag ab (Nr. 10 Abs. 1 der Leistungsbeschreibung). Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.
## 2. Art und Zweck der Verarbeitung
(1) Art: Übermitteln der Bestellung vom Widget an den Server von WEBklar, Übernehmen der Zahlungs- und Adressangaben aus der Checkout-Sitzung bei Stripe, Speichern, Verwalten und Anzeigen im Kundenportal, Versenden von E-Mails, Bereitstellen einer CSV-Datei, Weitergeben von Erstattungen an Stripe im Auftrag des Kunden, Anonymisieren und Löschen.
(2) Zwecke: Der Kunde soll Bestellungen seiner Käufer abwickeln. Dazu nimmt der Webshop Bestellungen entgegen, reserviert den Bestand, gleicht Zahlungen mit Stripe ab, bestätigt Bestellungen (bei einer Zahlung, die noch aussteht, etwa einer SEPA-Lastschrift, zunächst nur mit einer Eingangsmail ohne Vertragsschluss), bildet Versand und Abholung ab, nimmt Widerrufe entgegen, erstattet Zahlungen im Auftrag des Kunden und benachrichtigt den Kunden. WEBklar versendet die E-Mails im Auftrag des Kunden. Außerdem schützt WEBklar den Webshop für den Kunden vor Missbrauch (Mengenbegrenzung, Begrenzung der offenen Bestellungen je IP-Adresse, Erkennen automatisierter Bestellungen und Widerrufe).
(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Käufer. Zählungen ohne Personenbezug, die WEBklar für den sicheren Betrieb braucht (etwa die Zahl der anonymisierten Bestellungen je Tag oder die Länge der Versandwarteschlange), sind keine eigenen Zwecke.
## 3. Datenarten und Betroffene
(1) Daten der Käufer, die WEBklar aus der Checkout-Sitzung bei Stripe und aus dem Widget erhält und speichert: Name, E-Mail-Adresse, Lieferadresse und Rechnungsadresse (die Rechnungsadresse nur, soweit die Zahlungsseite von Stripe sie für die gewählte Zahlungsart abfragt); die Bestellung (Bestellnummer, Positionen mit Namen, Mengen und Preisen zum Zeitpunkt der Bestellung, Versandkosten, Lieferart, Steuerausweis, Gesamtbetrag); der Status der Bestellung mit den Zeitpunkten (bestellt, bezahlt, versendet, abholbereit, abgeschlossen, storniert, erstattet); Zahlungsstatus, Zahlungsart (zum Beispiel Karte oder SEPA-Lastschrift) und Kennungen der Zahlung bei Stripe; Versanddienst und Sendungsnummer, wenn der Kunde sie einträgt; Erstattungen mit Betrag, Zeitpunkt und Grund; die Prüfsumme (Hash) der Rechtstexte des Kunden, die der Käufer bei der Bestellung gesehen hat (den Wortlaut der Fassung verwahrt das Archiv nach Nummer 8 Abs. 5); ein Hashwert der IP-Adresse (Absatz 4); der Zustellstatus der E-Mails an den Käufer, auch der Eingangsmail bei ausstehender Zahlung. **Kartendaten und Kontodaten (IBAN) erhält und speichert WEBklar nicht;** die gibt der Käufer nur bei Stripe ein. **Eine Telefonnummer des Käufers erhebt und speichert WEBklar nicht:** Das Widget hat kein Telefonfeld, die Zahlungsseite von Stripe ist so eingestellt, dass sie keine Telefonnummer verlangt, und eine Telefonnummer, die Stripe in den Kundendaten der Sitzung dennoch übermitteln sollte, übernimmt WEBklar nicht in die Bestellung. Die Telefonnummer des Kunden selbst (Anbieterangaben in seinen Einstellungen) gehört nicht zu den Käuferdaten.
(2) Daten von Personen, die über die Widerrufsfunktion einen Vertrag widerrufen (Käufer und Interessenten, auch ohne passende Bestellung): Name, E-Mail-Adresse, Bestellnummer, die Erklärung als Freitext und der Zeitpunkt des Eingangs, außerdem der Zeitpunkt der Eingangsbestätigung und der Bearbeitung durch den Kunden.
(3) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht verarbeitet. Waren, aus deren Kauf sich besondere Kategorien ergeben können, etwa Arzneimittel, sind nach Nr. 3 der Leistungsbeschreibung ausgeschlossen; der Kunde fordert von seinen Käufern keine entsprechenden Angaben (Nummer 11 Abs. 4).
(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Widgets und jeder Anfrage. Die Mengenbegrenzung (Missbrauchsschutz) verwendet die IP-Adresse im Arbeitsspeicher des Servers; ein Neustart leert sie. Mit dem Widerruf wird keine IP-Adresse und kein Browser-Kennzeichen gespeichert. Mit der Bestellung werden ebenfalls weder eine IP-Adresse im Klartext noch ein Browser-Kennzeichen gespeichert, sondern nur ein **Hashwert der IP-Adresse („IP-Hash“)**: die ersten 16 Hexadezimalzeichen eines SHA-256-Werts, den WEBklar mit einem geheimen Schlüssel des Servers salzt. Der IP-Hash ist ein pseudonymes technisches Datum und bleibt ein personenbezogenes Datum, weil WEBklar mit dem Schlüssel für dieselbe IP-Adresse denselben Wert berechnen kann; ohne den Schlüssel lässt sich aus ihm keine IP-Adresse ableiten. WEBklar verwendet ihn ausschließlich zum Missbrauchsschutz: Je IP-Hash und Webshop sind höchstens drei offene (noch nicht bezahlte) Bestellungen innerhalb von 30 Minuten zulässig. Er dient nicht dazu, Käufer zu identifizieren, wiederzuerkennen oder zu profilieren, wird dem Kunden weder angezeigt noch exportiert und steht nur im Datensatz der Bestellung. WEBklar löscht ihn aus dem Datensatz der Bestellung, sobald die Bestellung bezahlt oder storniert wird, spätestens mit der Löschung nicht bezahlter Bestellungen nach 30 Tagen (Nummer 9 Abs. 1); bei bezahlten Bestellungen bleibt er nicht bis zur Anonymisierung (Nummer 9 Abs. 2) erhalten. In den Server-Protokollen stehen IP-Adresse und Browser-Kennzeichen 14 Tage (Anlage 2 der Abo-Bedingungen).
(5) Der Webshop setzt keine Cookies. Das Widget speichert im Browser des Besuchers nur den Warenkorb (Kennungen der Artikel und Mengen, ohne Personenbezug) im Sitzungsspeicher (sessionStorage) des Tabs, solange der Tab geöffnet ist; das ist für den Warenkorb technisch erforderlich. Andere Daten legt das Widget im Browser nicht ab.
(6) Betroffene: Käufer, die über die Website des Kunden Waren bestellen, und Personen, die über die Widerrufsfunktion einen Vertrag widerrufen. Beschäftigte des Kunden sind von diesem Vertrag nicht betroffen.
## 4. Weisungen
(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.
(2) Mit dem Einschalten weist der Kunde WEBklar an, den Webshop nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen und Aktionen im Kundenportal (Artikel, Versand, Abholung, Zahlungsarten, Rechtstexte, Bestellstatus, Erstattungen, Öffnen und Schließen des Webshops, Export). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Webshop abschalten lassen (Nr. 10 Abs. 7 der Leistungsbeschreibung). Die Anonymisierung und Löschung auf Weisung nach Nummer 8 Abs. 2 und Nummer 9 Abs. 6 setzt WEBklar immer um.
(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.
## 5. Vertraulichkeit
WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.
## 6. Technische und organisatorische Maßnahmen
(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.
(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.
## 7. Unterauftragsverarbeiter
(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Webshop läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der Bestell-E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt.
(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Webshop abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6).
(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).
(4) **Stripe ist kein Unterauftragsverarbeiter von WEBklar.** Der Kunde verwendet sein eigenes Stripe-Konto und hat den Vertrag darüber unmittelbar mit Stripe geschlossen (Nr. 4 der Leistungsbeschreibung). Die Zahlungsseite (Stripe Checkout), die Erhebung von Zahlungs-, Karten- und Kontodaten, die Prüfung von Zahlungen und die Auszahlungen verantworten der Kunde und Stripe nach ihrem Vertrag; WEBklar erhält diese Daten nicht. Auf Weisung des Kunden ruft WEBklar bei Stripe die Angaben zu Zahlungen und Bestellungen ab, die der Webshop braucht (Nummer 2 Abs. 1), und veranlasst Erstattungen. Daten von Käufern gehen dabei nicht von WEBklar an Stripe, außer der Erstattung selbst und der Kennung der Zahlung. Die Weitergabe von Daten durch Stripe, auch an Stellen außerhalb der Europäischen Union, regeln der Kunde und Stripe; dafür ist WEBklar nicht verantwortlich.
## 8. Unterstützung des Kunden, Betroffenenrechte
(1) WEBklar leitet Anfragen von Käufern, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.
(2) Dafür bietet das Kundenportal den Export der Bestellungen als CSV-Datei und die Anzeige der Bestellungen und Widerrufe, mit denen der Kunde Auskunft erteilen kann. Auf Weisung des Kunden in Textform (etwa im Chat des Kundenportals) anonymisiert WEBklar die Käuferdaten einer Bestellung oder eines Widerrufs unverzüglich: Name, E-Mail-Adresse, Liefer- und Rechnungsadresse und die Freitexte werden gelöscht oder unkenntlich gemacht (ein IP-Hash ist zu diesem Zeitpunkt längst gelöscht); Bestellnummer, Positionen, Beträge und Status bleiben. Dabei löscht WEBklar die E-Mails zu dieser Bestellung, die noch auf den Versand warten. E-Mails, deren Versand endgültig fehlgeschlagen ist, behalten Empfängeradresse und Inhalt, bis WEBklar sie nach Nummer 9 Abs. 4 löscht (48 Stunden nach dem Fehlschlag). Gesetzliche Aufbewahrungspflichten des Kunden (Nummer 9 Abs. 8) prüft der Kunde vor seiner Weisung selbst.
(3) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.
(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht.
(5) Bestätigt der Kunde seine Rechtstexte im Kundenportal (Allgemeine Geschäftsbedingungen, Widerrufsbelehrung, Muster-Widerrufsformular, Versand und Zahlung, Gewährleistungshinweis sowie die Adressen von Impressum und Datenschutzerklärung), legt WEBklar jede bestätigte Fassung einmal im Archiv der Rechtstexte ab: mit ihrer Prüfsumme (Hash), dem Zeitpunkt und der Angabe, welche angemeldete Person des Kunden (Name oder E-Mail-Adresse) sie bestätigt hat. Die Bestellung speichert die Prüfsumme der Fassung, die der Käufer bei der Bestellung gesehen hat. Die Bestellbestätigung (G09) zeigt die archivierte Fassung zu dieser Prüfsumme, nicht die Fassung, die beim Versand der E-Mail gerade gilt; fehlt die archivierte Fassung ausnahmsweise, verwendet WEBklar die aktuell bestätigte Fassung. Das Archiv enthält keine Daten von Käufern. WEBklar löscht es mit den übrigen Daten des Webshops (Nummer 9 Abs. 6), nicht früher. Das Archiv hilft dem Kunden, die Fassung nachzuweisen, die ein Käufer erhalten hat; es ersetzt nicht seine eigene Aufbewahrung (Nummer 9 Abs. 8). Die Bestellbestätigung enthält diese Texte; WEBklar löscht ihren Inhalt nach dem Versand aus der Versandwarteschlange (Nummer 9 Abs. 4).
(6) Die Käufer informiert der Kunde (Art. 13 DSGVO; Nummer 11 Abs. 2).
## 9. Löschung und Rückgabe
(1) Eine Bestellung, die nicht bezahlt wird, verfällt 30 Minuten nach dem Anlegen und gilt als storniert. WEBklar löscht nicht bezahlte Bestellungen (offen oder storniert) 30 Tage nach dem Anlegen. Den IP-Hash löscht WEBklar aus dem Datensatz, sobald die Bestellung bezahlt oder storniert wird, spätestens mit dieser Löschung.
(2) Bei bezahlten Bestellungen anonymisiert WEBklar die Käuferdaten 3 Jahre nach dem Ende des Kalenderjahres, in dem die Bestellung angelegt wurde (Bestelljahr; maßgeblich ist die Zeit in Berlin): Name, E-Mail-Adresse sowie Liefer- und Rechnungsadresse werden gelöscht (der IP-Hash ist mit der Zahlung bereits gelöscht; eine Telefonnummer des Käufers wird nicht gespeichert). Bestellnummer, Positionen, Beträge, Steuerausweis, Status, Versanddienst und Sendungsnummer bleiben für die Buchhaltung des Kunden erhalten.
(3) Widerrufe nach Nummer 3 Abs. 2 behandelt WEBklar wie bezahlte Bestellungen: Name, E-Mail-Adresse und Erklärung werden 3 Jahre nach dem Ende des Kalenderjahres des Eingangs gelöscht; die Bestellnummer, die die Person angegeben hat, und der Zeitpunkt des Eingangs bleiben.
(4) In der Versandwarteschlange für E-Mails leert WEBklar die Empfängeradresse und den Inhalt einer E-Mail nach dem Versand und löscht den Eintrag 7 Tage nach dem Versand. E-Mails, die nicht zugestellt werden können, löscht WEBklar mit Empfängeradresse und Inhalt 48 Stunden nach dem endgültigen Fehlschlag.
(5) Der Löschlauf läuft täglich; gelöscht oder anonymisiert wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu bearbeiten, als ein Lauf schafft, folgt der Rest im nächsten Lauf.
(6) Ist der Webshop abgeschaltet oder das Wartungs-Abo beendet, kann der Kunde die Bestellungen und Widerrufe 90 Tage lang im Kundenportal lesen und als CSV-Datei exportieren. Danach löscht WEBklar alle Daten des Webshops: Artikel, Bilder, Bestellungen, Widerrufe, Einstellungen, Rechtstexte und das Archiv der bestätigten Fassungen der Rechtstexte (Nummer 8 Abs. 5). Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen (zum Beispiel Adresse der Website, Einbau-Code und die Angaben des Kunden aus der Anfrage) und enthält keine Daten von Käufern; dafür gilt die Datenschutzerklärung von WEBklar (Abschnitt 6). Beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schaltet WEBklar den Webshop auf Wunsch des Kunden mit den vorhandenen Daten wieder ein (Nr. 10 Abs. 5 der Leistungsbeschreibung). Früher anonymisiert WEBklar einzelne Daten auf Weisung des Kunden (Nummer 8 Abs. 2); alle Daten des Webshops löscht WEBklar auf Weisung des Kunden in Textform unverzüglich.
(7) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte oder anonymisierte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen und Anonymisierungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.
(8) Gesetzliche Aufbewahrungspflichten bleiben unberührt. Der Kunde ist für seine eigenen Aufbewahrungspflichten (zum Beispiel für Buchungsbelege) selbst verantwortlich; WEBklar ist kein Archiv im Sinne der Buchführung, und das Archiv der Rechtstexte dient nur dem Nachweis der Texte. Der Kunde sichert die Belege, die er braucht, rechtzeitig vor der Anonymisierung oder Löschung durch den Export und die Belege von Stripe.
## 10. Nachweise und Kontrollen
WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.
## 11. Pflichten des Kunden
(1) Der Kunde ist Verantwortlicher für die Verarbeitung der Daten seiner Käufer im Webshop.
(2) Der Kunde informiert seine Käufer nach Art. 13 DSGVO. Dazu nimmt er den Datenschutz-Absatz für den Webshop in seine Datenschutzerklärung auf (Nr. 5 Abs. 10 der Leistungsbeschreibung) und nennt in seinen Rechtstexten (Datenschutzerklärung, Impressum) die Angaben, die das Gesetz verlangt.
(3) Der Kunde schließt mit Stripe den Vertrag über sein Stripe-Konto selbst und verantwortet die Verarbeitung von Daten durch Stripe, soweit sie nicht in diesem Vertrag geregelt ist (Nummer 7 Abs. 4).
(4) Der Kunde verkauft keine ausgeschlossenen Waren (Nr. 3 der Leistungsbeschreibung) und fordert von seinen Käufern keine Angaben zu ihrer Gesundheit oder zu anderen besonderen Kategorien personenbezogener Daten, auch nicht im Freitext der Widerrufserklärung.
(5) Der Kunde verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets nicht und entfernt den Link „Vertrag widerrufen“ nicht (Nr. 5 Abs. 6 und 9 der Leistungsbeschreibung).
## 12. Haftung, Änderungen, Schlussbestimmungen
(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.
(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung, auch wegen geänderter Datenerhebung, teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.
(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar den Webshop für seine Website nach Nr. 11 Abs. 2 der Leistungsbeschreibung beenden oder nach Nr. 11 Abs. 3 der Leistungsbeschreibung aussetzen. Das Abo bleibt davon unberührt.
(4) Im Übrigen gilt § 16 der Abo-Bedingungen.
## Anlage: Technische und organisatorische Maßnahmen des Webshops (Art. 32 DSGVO)
| Bereich | Maßnahmen |
|---|---|
| Übertragung | Das Widget, die Seiten der Käufer und das Kundenportal werden nur über TLS (HTTPS) übertragen. Der Einbau-Code verweist auf eine Adresse mit https://. Die Zahlung geschieht auf der Zahlungsseite von Stripe. |
| Speicher im Browser | Es werden keine Cookies gesetzt. Das Widget speichert nur den Warenkorb (Kennungen der Artikel und Mengen) im Sitzungsspeicher des Tabs. Es sendet seine Anfragen ohne Cookies und ruft nur die Schnittstelle des Webshops auf, keine Dienste Dritter. |
| Zugriffskontrolle | Das Kundenportal ist nur nach Anmeldung erreichbar; jeder Kunde sieht nur die Daten seiner eigenen Projekte. Die öffentliche Schnittstelle des Widgets liefert nur für einen vorhandenen Webshop Daten und ist über den Website-Schlüssel (Site-Key) adressiert; Bestellungen und Widerrufe nimmt sie nur von Seiten des Kunden und des Portals an. Den Stand einer Bestellung ruft der Käufer nur mit einem geheimen Token ab, das nur als Prüfsumme gespeichert wird. Benachrichtigungen von Stripe werden nur nach Prüfung ihrer Signatur verarbeitet; die Schlüssel für Stripe liegen nur auf dem Server. |
| Missbrauchsschutz | Ein verstecktes Feld (Honeypot), eine Mindestzeit bis zum Absenden des Formulars mit signiertem Formular-Token und eine Mindestantwortzeit schützen vor automatisierten Bestellungen und Widerrufen. Grenzen je IP-Adresse und je Webshop gelten im Arbeitsspeicher; ein Neustart leert sie. Die Obergrenze offener Bestellungen je IP-Adresse (drei in 30 Minuten) arbeitet mit einem gesalzenen Hashwert der IP-Adresse in der Bestellung (Nummer 3 Abs. 4). |
| Datenminimierung | Kartendaten und Kontodaten erhält und speichert WEBklar nicht. Preise berechnet der Server aus den gespeicherten Artikeln, nie aus Angaben des Browsers. Eine Telefonnummer wird weder abgefragt noch gespeichert, die Rechnungsadresse nur, soweit Stripe sie für die Zahlungsart braucht. Mit der Bestellung und mit dem Widerruf werden keine IP-Adresse im Klartext und kein Browser-Kennzeichen gespeichert, bei der Bestellung nur der gesalzene IP-Hash, und der nur, solange die Bestellung offen ist (gelöscht mit Zahlung oder Stornierung). Die Käuferdaten werden nach den Fristen der Nummer 9 anonymisiert. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert. |
| Trennung | Alle Artikel, Bestellungen und Widerrufe tragen die Kennung des Webshops und des Kunden; das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten des jeweiligen Webshops. Artikelbilder liegen in einem eigenen Ordner je Webshop. Die Hintergrundläufe (Abgleich offener Bestellungen, Versand der E-Mails, Anonymisierung und Löschung) schreiben jeweils nur in das Dokument, um das es geht. |
| Löschung | Täglicher Löschlauf für Bestellungen, Widerrufe und Warteschlange nach den Fristen der Nummer 9; die Daten abgeschalteter Webshops einschließlich des Archivs der bestätigten Rechtstexte 90 Tage nach dem Abschalten; das Einrichtungs-Ticket bleibt als Arbeitsnachweis mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Käufern (Nummer 9 Abs. 6). |
| Protokolle | Die Programmprotokolle des Webshops und der Tagesbericht enthalten nur Zählungen und Bestellnummern, keine Namen, E-Mail-Adressen oder Adressen von Käufern. Server-Protokolle des Webservers werden nach 14 Tagen gelöscht (Anlage 2 der Abo-Bedingungen). |
| Datensicherung | Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). |
| Rechenzentrum | Hetzner Online GmbH, Rechenzentren Falkenstein und Nürnberg (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren. |
| Organisation | Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 3), Überprüfung der Maßnahmen mindestens einmal im Jahr. |
+145
View File
@@ -0,0 +1,145 @@
# Auftragsverarbeitungsvertrag Webshop (Art. 28 DSGVO)
**Fassung 2026-10-05**
zwischen dem Kunden als Verantwortlichem („Kunde“; in der Leistungsbeschreibung „Händler“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).
## Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen
(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Einrichtung des Webshops bestellt und dabei per Haken diesem Vertrag und der Leistungsbeschreibung „Webshop“ („Leistungsbeschreibung“) in der angezeigten Fassung zustimmt. Das Portal speichert die Fassung, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte und die Widerrufsbelehrung in dieser Fassung als PDF enthält und auf sie verlinkt.
(2) Ein Mitarbeiter von WEBklar darf die Funktion nur einschalten, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. Das Portal verlangt dafür das Datum und den Weg der Erklärung; fehlt eine Angabe, schaltet es nicht ein. WEBklar speichert vor dem Einschalten einen Zustimmungsnachweis mit der Fassung der beiden Texte, dem Datum und dem Weg dieser Erklärung, dem Zeitpunkt des Einschaltens und dem Namen des Mitarbeiters (ohne IP-Adresse und Browser-Kennzeichen, weil der Kunde nicht im Portal zugestimmt hat). Der Kunde erhält anschließend eine E-Mail mit diesen beiden Texten als PDF.
(3) Dieser Vertrag ist ein eigener Vertrag für den Webshop. Er ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für den Webshop geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung vor.
(4) Die Rollen: Der Kunde ist Verantwortlicher für die personenbezogenen Daten seiner Käufer im Webshop. WEBklar ist sein Auftragsverarbeiter. Stripe ist nicht Auftragsverarbeiter von WEBklar, sondern Vertragspartner des Kunden (Nummer 7 Abs. 4).
## 1. Gegenstand und Dauer
(1) WEBklar betreibt für die öffentliche Website des Kunden ein Shop-Widget mit einer Bestellverwaltung im Kundenportal („Webshop“) und verarbeitet dabei personenbezogene Daten der Käufer des Kunden im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung.
(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Abschalten, spätestens mit dem Ende des Vertrags, aufgrund dessen WEBklar die Website des Kunden betreibt; den Webshop schaltet WEBklar dann zum Vertragsende ab (Nr. 10 Abs. 1 der Leistungsbeschreibung). Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.
## 2. Art und Zweck der Verarbeitung
(1) Art: Übermitteln der Bestellung vom Widget an den Server von WEBklar, Übernehmen der Zahlungs- und Adressangaben aus der Checkout-Sitzung bei Stripe, Speichern, Verwalten und Anzeigen im Kundenportal, Versenden von E-Mails, Bereitstellen einer CSV-Datei, Weitergeben von Erstattungen an Stripe im Auftrag des Kunden, Anonymisieren und Löschen.
(2) Zwecke: Der Kunde soll Bestellungen seiner Käufer abwickeln. Dazu nimmt der Webshop Bestellungen entgegen, reserviert den Bestand, gleicht Zahlungen mit Stripe ab, bestätigt Bestellungen (bei einer Zahlung, die noch aussteht, etwa einer SEPA-Lastschrift, zunächst nur mit einer Eingangsmail ohne Vertragsschluss), bildet Versand und Abholung ab, nimmt Widerrufe entgegen, erstattet Zahlungen im Auftrag des Kunden und benachrichtigt den Kunden. WEBklar versendet die E-Mails im Auftrag des Kunden. Außerdem schützt WEBklar den Webshop für den Kunden vor Missbrauch (Mengenbegrenzung, Begrenzung der offenen Bestellungen je IP-Adresse, Erkennen automatisierter Bestellungen und Widerrufe).
(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Käufer. Zählungen ohne Personenbezug, die WEBklar für den sicheren Betrieb braucht (etwa die Zahl der anonymisierten Bestellungen je Tag oder die Länge der Versandwarteschlange), sind keine eigenen Zwecke.
## 3. Datenarten und Betroffene
(1) Daten der Käufer, die WEBklar aus der Checkout-Sitzung bei Stripe und aus dem Widget erhält und speichert: Name, E-Mail-Adresse, Lieferadresse und Rechnungsadresse (die Rechnungsadresse nur, soweit die Zahlungsseite von Stripe sie für die gewählte Zahlungsart abfragt); die Bestellung (Bestellnummer, Positionen mit Namen, Mengen und Preisen zum Zeitpunkt der Bestellung, Versandkosten, Lieferart, Steuerausweis, Gesamtbetrag); der Status der Bestellung mit den Zeitpunkten (bestellt, bezahlt, versendet, abholbereit, abgeschlossen, storniert, erstattet); Zahlungsstatus, Zahlungsart (zum Beispiel Karte oder SEPA-Lastschrift) und Kennungen der Zahlung bei Stripe; Versanddienst und Sendungsnummer, wenn der Kunde sie einträgt; Erstattungen mit Betrag, Zeitpunkt und Grund; die Prüfsumme (Hash) der Rechtstexte des Kunden, die der Käufer bei der Bestellung gesehen hat (den Wortlaut der Fassung verwahrt das Archiv nach Nummer 8 Abs. 5); ein Hashwert der IP-Adresse (Absatz 4); der Zustellstatus der E-Mails an den Käufer, auch der Eingangsmail bei ausstehender Zahlung. **Kartendaten und Kontodaten (IBAN) erhält und speichert WEBklar nicht;** die gibt der Käufer nur bei Stripe ein. **Eine Telefonnummer des Käufers erhebt und speichert WEBklar nicht:** Das Widget hat kein Telefonfeld, die Zahlungsseite von Stripe ist so eingestellt, dass sie keine Telefonnummer verlangt, und eine Telefonnummer, die Stripe in den Kundendaten der Sitzung dennoch übermitteln sollte, übernimmt WEBklar nicht in die Bestellung. Die Telefonnummer des Kunden selbst (Anbieterangaben in seinen Einstellungen) gehört nicht zu den Käuferdaten.
(2) Daten von Personen, die über die Widerrufsfunktion einen Vertrag widerrufen (Käufer und Interessenten, auch ohne passende Bestellung): Name, E-Mail-Adresse, Bestellnummer, die Erklärung als Freitext und der Zeitpunkt des Eingangs, außerdem der Zeitpunkt der Eingangsbestätigung und der Bearbeitung durch den Kunden.
(3) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht verarbeitet. Waren, aus deren Kauf sich besondere Kategorien ergeben können, etwa Arzneimittel, sind nach Nr. 3 der Leistungsbeschreibung ausgeschlossen; der Kunde fordert von seinen Käufern keine entsprechenden Angaben (Nummer 11 Abs. 4).
(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Widgets und jeder Anfrage. Die Mengenbegrenzung (Missbrauchsschutz) verwendet die IP-Adresse im Arbeitsspeicher des Servers; ein Neustart leert sie. Mit dem Widerruf wird keine IP-Adresse und kein Browser-Kennzeichen gespeichert. Mit der Bestellung werden ebenfalls weder eine IP-Adresse im Klartext noch ein Browser-Kennzeichen gespeichert, sondern nur ein **Hashwert der IP-Adresse („IP-Hash“)**: die ersten 16 Hexadezimalzeichen eines SHA-256-Werts, den WEBklar mit einem geheimen Schlüssel des Servers salzt. Der IP-Hash ist ein pseudonymes technisches Datum und bleibt ein personenbezogenes Datum, weil WEBklar mit dem Schlüssel für dieselbe IP-Adresse denselben Wert berechnen kann; ohne den Schlüssel lässt sich aus ihm keine IP-Adresse ableiten. WEBklar verwendet ihn ausschließlich zum Missbrauchsschutz: Je IP-Hash und Webshop sind höchstens drei offene (noch nicht bezahlte) Bestellungen innerhalb von 30 Minuten zulässig. Er dient nicht dazu, Käufer zu identifizieren, wiederzuerkennen oder zu profilieren, wird dem Kunden weder angezeigt noch exportiert und steht nur im Datensatz der Bestellung. WEBklar löscht ihn aus dem Datensatz der Bestellung, sobald die Bestellung bezahlt oder storniert wird, spätestens mit der Löschung nicht bezahlter Bestellungen nach 30 Tagen (Nummer 9 Abs. 1); bei bezahlten Bestellungen bleibt er nicht bis zur Anonymisierung (Nummer 9 Abs. 2) erhalten. In den Server-Protokollen stehen IP-Adresse und Browser-Kennzeichen 14 Tage (Anlage 2 der Abo-Bedingungen).
(5) Der Webshop setzt keine Cookies. Das Widget speichert im Browser des Besuchers nur den Warenkorb (Kennungen der Artikel und Mengen, ohne Personenbezug) im Sitzungsspeicher (sessionStorage) des Tabs, solange der Tab geöffnet ist; das ist für den Warenkorb technisch erforderlich. Andere Daten legt das Widget im Browser nicht ab.
(6) Betroffene: Käufer, die über die Website des Kunden Waren bestellen, und Personen, die über die Widerrufsfunktion einen Vertrag widerrufen. Beschäftigte des Kunden sind von diesem Vertrag nicht betroffen.
## 4. Weisungen
(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.
(2) Mit der Bestellung weist der Kunde WEBklar an, den Webshop nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen und Aktionen im Kundenportal (Artikel, Versand, Abholung, Zahlungsarten, Rechtstexte, Bestellstatus, Erstattungen, Öffnen und Schließen des Webshops, Export). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Webshop abschalten lassen (Nr. 10 Abs. 7 der Leistungsbeschreibung). Die Anonymisierung und Löschung auf Weisung nach Nummer 8 Abs. 2 und Nummer 9 Abs. 6 setzt WEBklar immer um.
(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.
## 5. Vertraulichkeit
WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.
## 6. Technische und organisatorische Maßnahmen
(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.
(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.
## 7. Unterauftragsverarbeiter
(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Webshop läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der Bestell-E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt.
(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Webshop abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6).
(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).
(4) **Stripe ist kein Unterauftragsverarbeiter von WEBklar.** Der Kunde verwendet sein eigenes Stripe-Konto und hat den Vertrag darüber unmittelbar mit Stripe geschlossen (Nr. 4 der Leistungsbeschreibung). Die Zahlungsseite (Stripe Checkout), die Erhebung von Zahlungs-, Karten- und Kontodaten, die Prüfung von Zahlungen und die Auszahlungen verantworten der Kunde und Stripe nach ihrem Vertrag; WEBklar erhält diese Daten nicht. Auf Weisung des Kunden ruft WEBklar bei Stripe die Angaben zu Zahlungen und Bestellungen ab, die der Webshop braucht (Nummer 2 Abs. 1), und veranlasst Erstattungen. Daten von Käufern gehen dabei nicht von WEBklar an Stripe, außer der Erstattung selbst und der Kennung der Zahlung. Die Weitergabe von Daten durch Stripe, auch an Stellen außerhalb der Europäischen Union, regeln der Kunde und Stripe; dafür ist WEBklar nicht verantwortlich.
## 8. Unterstützung des Kunden, Betroffenenrechte
(1) WEBklar leitet Anfragen von Käufern, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.
(2) Dafür bietet das Kundenportal den Export der Bestellungen als CSV-Datei und die Anzeige der Bestellungen und Widerrufe, mit denen der Kunde Auskunft erteilen kann. Auf Weisung des Kunden in Textform (etwa im Chat des Kundenportals) anonymisiert WEBklar die Käuferdaten einer Bestellung oder eines Widerrufs unverzüglich: Name, E-Mail-Adresse, Liefer- und Rechnungsadresse und die Freitexte werden gelöscht oder unkenntlich gemacht (ein IP-Hash ist zu diesem Zeitpunkt längst gelöscht); Bestellnummer, Positionen, Beträge und Status bleiben. Dabei löscht WEBklar die E-Mails zu dieser Bestellung, die noch auf den Versand warten. E-Mails, deren Versand endgültig fehlgeschlagen ist, behalten Empfängeradresse und Inhalt, bis WEBklar sie nach Nummer 9 Abs. 4 löscht (48 Stunden nach dem Fehlschlag). Gesetzliche Aufbewahrungspflichten des Kunden (Nummer 9 Abs. 8) prüft der Kunde vor seiner Weisung selbst.
(3) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.
(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht.
(5) Bestätigt der Kunde seine Rechtstexte im Kundenportal (Allgemeine Geschäftsbedingungen, Widerrufsbelehrung, Muster-Widerrufsformular, Versand und Zahlung, Gewährleistungshinweis sowie die Adressen von Impressum und Datenschutzerklärung), legt WEBklar jede bestätigte Fassung einmal im Archiv der Rechtstexte ab: mit ihrer Prüfsumme (Hash), dem Zeitpunkt und der Angabe, welche angemeldete Person des Kunden (Name oder E-Mail-Adresse) sie bestätigt hat. Die Bestellung speichert die Prüfsumme der Fassung, die der Käufer bei der Bestellung gesehen hat. Die Bestellbestätigung (G09) zeigt die archivierte Fassung zu dieser Prüfsumme, nicht die Fassung, die beim Versand der E-Mail gerade gilt; fehlt die archivierte Fassung ausnahmsweise, verwendet WEBklar die aktuell bestätigte Fassung. Das Archiv enthält keine Daten von Käufern. WEBklar löscht es mit den übrigen Daten des Webshops (Nummer 9 Abs. 6), nicht früher. Das Archiv hilft dem Kunden, die Fassung nachzuweisen, die ein Käufer erhalten hat; es ersetzt nicht seine eigene Aufbewahrung (Nummer 9 Abs. 8). Die Bestellbestätigung enthält diese Texte; WEBklar löscht ihren Inhalt nach dem Versand aus der Versandwarteschlange (Nummer 9 Abs. 4).
(6) Die Käufer informiert der Kunde (Art. 13 DSGVO; Nummer 11 Abs. 2).
## 9. Löschung und Rückgabe
(1) Eine Bestellung, die nicht bezahlt wird, verfällt 30 Minuten nach dem Anlegen und gilt als storniert. WEBklar löscht nicht bezahlte Bestellungen (offen oder storniert) 30 Tage nach dem Anlegen. Den IP-Hash löscht WEBklar aus dem Datensatz, sobald die Bestellung bezahlt oder storniert wird, spätestens mit dieser Löschung.
(2) Bei bezahlten Bestellungen anonymisiert WEBklar die Käuferdaten 3 Jahre nach dem Ende des Kalenderjahres, in dem die Bestellung angelegt wurde (Bestelljahr; maßgeblich ist die Zeit in Berlin): Name, E-Mail-Adresse sowie Liefer- und Rechnungsadresse werden gelöscht (der IP-Hash ist mit der Zahlung bereits gelöscht; eine Telefonnummer des Käufers wird nicht gespeichert). Bestellnummer, Positionen, Beträge, Steuerausweis, Status, Versanddienst und Sendungsnummer bleiben für die Buchhaltung des Kunden erhalten.
(3) Widerrufe nach Nummer 3 Abs. 2 behandelt WEBklar wie bezahlte Bestellungen: Name, E-Mail-Adresse und Erklärung werden 3 Jahre nach dem Ende des Kalenderjahres des Eingangs gelöscht; die Bestellnummer, die die Person angegeben hat, und der Zeitpunkt des Eingangs bleiben.
(4) In der Versandwarteschlange für E-Mails leert WEBklar die Empfängeradresse und den Inhalt einer E-Mail nach dem Versand und löscht den Eintrag 7 Tage nach dem Versand. E-Mails, die nicht zugestellt werden können, löscht WEBklar mit Empfängeradresse und Inhalt 48 Stunden nach dem endgültigen Fehlschlag.
(5) Der Löschlauf läuft täglich; gelöscht oder anonymisiert wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu bearbeiten, als ein Lauf schafft, folgt der Rest im nächsten Lauf.
(6) Ist der Webshop abgeschaltet oder der Vertrag über den Betrieb der Website beendet, kann der Kunde die Bestellungen und Widerrufe 90 Tage lang im Kundenportal lesen und als CSV-Datei exportieren. Danach löscht WEBklar alle Daten des Webshops: Artikel, Bilder, Bestellungen, Widerrufe, Einstellungen, Rechtstexte und das Archiv der bestätigten Fassungen der Rechtstexte (Nummer 8 Abs. 5). Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen (zum Beispiel Adresse der Website, Einbau-Code und die Angaben des Kunden aus der Anfrage) und enthält keine Daten von Käufern; dafür gilt die Datenschutzerklärung von WEBklar (Abschnitt 6). Beginnt innerhalb dieser Frist wieder ein Vertrag über den Betrieb der Website, schaltet WEBklar den Webshop auf Wunsch des Kunden mit den vorhandenen Daten wieder ein (Nr. 10 Abs. 5 der Leistungsbeschreibung). Früher anonymisiert WEBklar einzelne Daten auf Weisung des Kunden (Nummer 8 Abs. 2); alle Daten des Webshops löscht WEBklar auf Weisung des Kunden in Textform unverzüglich.
(7) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte oder anonymisierte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen und Anonymisierungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.
(8) Gesetzliche Aufbewahrungspflichten bleiben unberührt. Der Kunde ist für seine eigenen Aufbewahrungspflichten (zum Beispiel für Buchungsbelege) selbst verantwortlich; WEBklar ist kein Archiv im Sinne der Buchführung, und das Archiv der Rechtstexte dient nur dem Nachweis der Texte. Der Kunde sichert die Belege, die er braucht, rechtzeitig vor der Anonymisierung oder Löschung durch den Export und die Belege von Stripe.
## 10. Nachweise und Kontrollen
WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.
## 11. Pflichten des Kunden
(1) Der Kunde ist Verantwortlicher für die Verarbeitung der Daten seiner Käufer im Webshop.
(2) Der Kunde informiert seine Käufer nach Art. 13 DSGVO. Dazu nimmt er den Datenschutz-Absatz für den Webshop in seine Datenschutzerklärung auf (Nr. 5 Abs. 10 der Leistungsbeschreibung) und nennt in seinen Rechtstexten (Datenschutzerklärung, Impressum) die Angaben, die das Gesetz verlangt.
(3) Der Kunde schließt mit Stripe den Vertrag über sein Stripe-Konto selbst und verantwortet die Verarbeitung von Daten durch Stripe, soweit sie nicht in diesem Vertrag geregelt ist (Nummer 7 Abs. 4).
(4) Der Kunde verkauft keine ausgeschlossenen Waren (Nr. 3 der Leistungsbeschreibung) und fordert von seinen Käufern keine Angaben zu ihrer Gesundheit oder zu anderen besonderen Kategorien personenbezogener Daten, auch nicht im Freitext der Widerrufserklärung.
(5) Der Kunde verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets nicht und entfernt den Link „Vertrag widerrufen“ nicht (Nr. 5 Abs. 6 und 9 der Leistungsbeschreibung).
## 12. Haftung, Änderungen, Schlussbestimmungen
(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.
(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung, auch wegen geänderter Datenerhebung, teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.
(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar den Webshop für seine Website nach Nr. 11 Abs. 2 der Leistungsbeschreibung beenden oder nach Nr. 11 Abs. 3 der Leistungsbeschreibung aussetzen. Das Abo bleibt davon unberührt.
(4) Im Übrigen gilt § 16 der Abo-Bedingungen.
## Anlage: Technische und organisatorische Maßnahmen des Webshops (Art. 32 DSGVO)
| Bereich | Maßnahmen |
|---|---|
| Übertragung | Das Widget, die Seiten der Käufer und das Kundenportal werden nur über TLS (HTTPS) übertragen. Der Einbau-Code verweist auf eine Adresse mit https://. Die Zahlung geschieht auf der Zahlungsseite von Stripe. |
| Speicher im Browser | Es werden keine Cookies gesetzt. Das Widget speichert nur den Warenkorb (Kennungen der Artikel und Mengen) im Sitzungsspeicher des Tabs. Es sendet seine Anfragen ohne Cookies und ruft nur die Schnittstelle des Webshops auf, keine Dienste Dritter. |
| Zugriffskontrolle | Das Kundenportal ist nur nach Anmeldung erreichbar; jeder Kunde sieht nur die Daten seiner eigenen Projekte. Die öffentliche Schnittstelle des Widgets liefert nur für einen vorhandenen Webshop Daten und ist über den Website-Schlüssel (Site-Key) adressiert; Bestellungen und Widerrufe nimmt sie nur von Seiten des Kunden und des Portals an. Den Stand einer Bestellung ruft der Käufer nur mit einem geheimen Token ab, das nur als Prüfsumme gespeichert wird. Benachrichtigungen von Stripe werden nur nach Prüfung ihrer Signatur verarbeitet; die Schlüssel für Stripe liegen nur auf dem Server. |
| Missbrauchsschutz | Ein verstecktes Feld (Honeypot), eine Mindestzeit bis zum Absenden des Formulars mit signiertem Formular-Token und eine Mindestantwortzeit schützen vor automatisierten Bestellungen und Widerrufen. Grenzen je IP-Adresse und je Webshop gelten im Arbeitsspeicher; ein Neustart leert sie. Die Obergrenze offener Bestellungen je IP-Adresse (drei in 30 Minuten) arbeitet mit einem gesalzenen Hashwert der IP-Adresse in der Bestellung (Nummer 3 Abs. 4). |
| Datenminimierung | Kartendaten und Kontodaten erhält und speichert WEBklar nicht. Preise berechnet der Server aus den gespeicherten Artikeln, nie aus Angaben des Browsers. Eine Telefonnummer wird weder abgefragt noch gespeichert, die Rechnungsadresse nur, soweit Stripe sie für die Zahlungsart braucht. Mit der Bestellung und mit dem Widerruf werden keine IP-Adresse im Klartext und kein Browser-Kennzeichen gespeichert, bei der Bestellung nur der gesalzene IP-Hash, und der nur, solange die Bestellung offen ist (gelöscht mit Zahlung oder Stornierung). Die Käuferdaten werden nach den Fristen der Nummer 9 anonymisiert. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert. |
| Trennung | Alle Artikel, Bestellungen und Widerrufe tragen die Kennung des Webshops und des Kunden; das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten des jeweiligen Webshops. Artikelbilder liegen in einem eigenen Ordner je Webshop. Die Hintergrundläufe (Abgleich offener Bestellungen, Versand der E-Mails, Anonymisierung und Löschung) schreiben jeweils nur in das Dokument, um das es geht. |
| Löschung | Täglicher Löschlauf für Bestellungen, Widerrufe und Warteschlange nach den Fristen der Nummer 9; die Daten abgeschalteter Webshops einschließlich des Archivs der bestätigten Rechtstexte 90 Tage nach dem Abschalten; das Einrichtungs-Ticket bleibt als Arbeitsnachweis mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Käufern (Nummer 9 Abs. 6). |
| Protokolle | Die Programmprotokolle des Webshops und der Tagesbericht enthalten nur Zählungen und Bestellnummern, keine Namen, E-Mail-Adressen oder Adressen von Käufern. Server-Protokolle des Webservers werden nach 14 Tagen gelöscht (Anlage 2 der Abo-Bedingungen). |
| Datensicherung | Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). |
| Rechenzentrum | Hetzner Online GmbH, Rechenzentren Falkenstein und Nürnberg (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren. |
| Organisation | Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 3), Überprüfung der Maßnahmen mindestens einmal im Jahr. |
+138
View File
@@ -0,0 +1,138 @@
# Auftragsverarbeitungsvertrag Website-Analyse (Art. 28 DSGVO)
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen
zwischen dem Kunden als Verantwortlichem („Kunde“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).
## Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen
(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „Website-Analyse“ einschaltet und die dort angezeigte Zeile „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16)“ mit dem Klick auf „Einschalten“ bestätigt; diesem Vertrag und der Leistungsbeschreibung „Website-Analyse“ („Leistungsbeschreibung“) in der angezeigten Fassung stimmt er damit zu. Das Portal speichert den Wortlaut der angezeigten Zeile, die Fassungen, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte in dieser Fassung als PDF enthält und auf sie verlinkt.
(2) Ein Mitarbeiter von WEBklar darf die Funktion nur einschalten, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. WEBklar speichert die Fassung, das Datum und den Weg dieser Erklärung, den Zeitpunkt des Einschaltens und den Namen des Mitarbeiters. Der Kunde erhält die E-Mail nach Absatz 1.
(3) Dieser Vertrag ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für die Website-Analyse geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung „Website-Analyse“ vor.
## 1. Gegenstand und Dauer
(1) WEBklar betreibt für die öffentliche Website des Kunden eine Reichweitenmessung mit Einwilligungsbanner („Website-Analyse“) und verarbeitet dabei personenbezogene Daten der Besucher der Website im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung „Website-Analyse“.
(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Ausschalten, spätestens mit dem Ende des Abos, das die Website-Analyse enthält (Hosting-Abo oder Wartungs-Abo); endet nur das Wartungs-Abo und läuft ein Hosting-Abo weiter, läuft dieser Vertrag weiter. Den Messdienst schaltet WEBklar bei Vertragsende spätestens am folgenden Tag ab. Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.
## 2. Art und Zweck der Verarbeitung
(1) Art: Erheben über ein Skript, das WEBklar beim Veröffentlichen in die Website des Kunden einfügt, Übermitteln an den Messdienst von WEBklar, Speichern, Verdichten zu Tageswerten, Auswerten im Kundenportal (Kennzahlen, Heatmap) und Löschen.
(2) Zwecke: Der Kunde soll erkennen, wie seine Website genutzt wird, um sie zu verbessern. Außerdem schützt WEBklar den Messdienst für den Kunden vor Missbrauch und verfälschten Zahlen (Mengenbegrenzung, Erkennen von Bots).
(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Besucher. Die Besucher-Kennung dient nur dazu, die Besucher eines Tages zu zählen.
## 3. Datenarten und Betroffene
(1) Solange der Besucher im Banner keine Wahl getroffen hat und sein Browser kein Signal nach Absatz 2 Satz 2 sendet: Pfad der aufgerufenen Seite (ohne Suchparameter und Anker), auch bei einem Seitenwechsel ohne Neuladen; Land (aus der IP-Adresse abgeleitet); Geräteklasse (Handy, Tablet oder Computer; aus dem Browser-Kennzeichen abgeleitet); Zeitpunkt (auf die volle Stunde gerundet); ein Zähler dafür, dass das Banner angezeigt wurde.
(2) Nach „Ablehnen“ sendet das Skript einmalig einen Zähler „abgelehnt“ und danach nichts mehr. Sendet der Browser die Signale „Global Privacy Control“ oder „Do Not Track“, zeigt das Skript kein Banner und sendet nichts.
(3) Nach „Annehmen“ zusätzlich: Domain der Website, von der der Besucher kam; Größenklasse des Browserfensters; eine täglich wechselnde pseudonyme Besucher-Kennung; Verweildauer (ab dem Laden der Seite) und Scrolltiefe je Seite; Klickpositionen mit einem Elementpfad aus Tag-Namen und Position (ohne Texte, Kennungen und Klassennamen); die Art von Kontakt-Klicks (Telefon, E-Mail, WhatsApp oder Link auf eine andere Website, dann mit deren Domain); ein Zähler „angenommen“.
(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Skripts und bei jeder Messanfrage, auch nach „Ablehnen“. Der Messdienst verarbeitet sie nur kurz im Arbeitsspeicher, um Land, Geräteklasse, Bots, die Mengenbegrenzung und, nur nach Einwilligung, die Besucher-Kennung zu bestimmen. Er speichert und protokolliert sie nicht. Nicht erhoben werden Cookies, Formularinhalte, Seitentexte, Mausbewegungen, Tastatureingaben und Sitzungsaufzeichnungen.
(5) Tageswerte sind Summen je Tag und Merkmal (etwa Aufrufe je Seite, je Land oder je Geräteklasse), ohne Besucher-Kennung. Enthalten die Pfade der Website personenbezogene Angaben, können auch Tageswerte personenbezogen sein (Nummer 11 Abs. 2).
(6) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht gezielt verarbeitet. Je nach Inhalt der Website können sich aus Pfaden oder Domains aber Rückschlüsse ergeben (Nummer 11 Abs. 3).
(7) Betroffene: Besucher der Website des Kunden.
## 4. Weisungen
(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.
(2) Mit dem Einschalten weist der Kunde WEBklar an, die Website-Analyse in der Standardkonfiguration nach der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen im Kundenportal (Einschalten, Ausschalten, Akzentfarbe des Banners). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die von der Standardkonfiguration abweichen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde die Funktion ausschalten.
(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.
## 5. Vertraulichkeit
WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.
## 6. Technische und organisatorische Maßnahmen
(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.
(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.
## 7. Unterauftragsverarbeiter
(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Messdienst läuft, und für dessen Sicherung. Ort der Verarbeitung ist das Rechenzentrum Falkenstein (Deutschland). Eine Übermittlung in Drittländer findet nicht statt.
(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde die Website-Analyse ausschalten und WEBklar sie für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Verlangen des Kunden sofort.
(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).
(4) Die Länderzuordnung nutzt eine Datenbank, die WEBklar auf dem eigenen Server speichert („IP to Country Lite“ von DB-IP.com, Lizenz CC BY 4.0). Daten der Besucher werden dabei nicht an DB-IP übermittelt. DB-IP ist kein Unterauftragsverarbeiter.
## 8. Unterstützung des Kunden, Betroffenenrechte
(1) WEBklar leitet Anfragen betroffener Personen, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.
(2) Die gespeicherten Messdaten enthalten keine IP-Adresse, keinen Namen und keine Kennung, die länger als einen Tag gilt; Zeitpunkte sind auf die volle Stunde gerundet. Einzelne Besucher kann WEBklar daher in der Regel nicht identifizieren (Art. 11 DSGVO). Ein Abgleich kann aber möglich sein, solange die Zugriffsprotokolle der Website (Anlage 1 und 2 der Abo-Bedingungen, 14 Tage) vorhanden sind. Nennt eine betroffene Person zusätzliche Angaben, etwa Datum, Uhrzeit, besuchte Seiten oder ihre IP-Adresse, prüft WEBklar auf Weisung des Kunden, auch anhand dieser Protokolle, ob eine Zuordnung möglich ist, und teilt ihm das Ergebnis mit.
(3) Der Messung können Besucher technisch widersprechen (Art. 21 DSGVO): Nach „Ablehnen“ und bei den Browser-Signalen „Global Privacy Control“ und „Do Not Track“ sendet das Skript nichts mehr.
(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht, und eine kurze Interessenabwägung für die Zählung nach Art. 6 Abs. 1 lit. f DSGVO, die WEBklar mit jeder Fassung dieses Vertrags bereitstellt.
(5) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.
(6) Zum Nachweis der Einwilligungen bewahrt WEBklar jede Fassung des Banner-Textes und der Zusatzseite mit dem Zeitraum ihrer Verwendung auf, bis drei Jahre nach dem Ende der Website-Analyse. WEBklar stellt sie dem Kunden auf Verlangen zur Verfügung. Ein Protokoll je Besucher führt WEBklar nicht.
(7) Die Besucher informiert der Kunde (Art. 13 DSGVO). WEBklar stellt ihm dafür die Zusatzseite „/datenschutz-analyse.html“ als Muster bereit. Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar dort einen Hinweis auf die Zusatzseite ein; sonst erhält der Kunde den Hinweis als Textbaustein (Leistungsbeschreibung Nr. 4 Abs. 4).
## 9. Löschung und Rückgabe
(1) Einzelne Ereignisse und Klickpositionen löscht WEBklar automatisch nach 30 Tagen. Der Löschlauf läuft täglich, die Löschung erfolgt also in der Regel spätestens am 31. Tag; fällt ein Lauf aus, holt der nächste ihn nach.
(2) Tageswerte löscht WEBklar 25 Monate nach dem jeweiligen Tag. Nach dem Ende der Website-Analyse (Ausschalten oder Ende des Abos, das sie enthält) löscht WEBklar alle Daten der Website am 61. Tag. Bis zum 46. Tag nach dem Ende kann der Kunde eine Kopie der Tageswerte und der noch vorhandenen Einzelereignisse verlangen; WEBklar stellt sie innerhalb von zwei Wochen als CSV-Datei bereit. Auf Weisung des Kunden löscht WEBklar die Daten früher.
(3) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.
(4) Gesetzliche Aufbewahrungspflichten bleiben unberührt.
## 10. Nachweise und Kontrollen
WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.
## 11. Pflichten des Kunden
(1) Der Kunde ist Verantwortlicher für die Messung. Er entfernt, verdeckt oder umgeht das Banner nicht. Er bindet ohne Abstimmung mit WEBklar keine weiteren Mess- oder Tracking-Werkzeuge ein.
(2) Enthalten die Pfade seiner Website personenbezogene Angaben, etwa Namen, Kundennummern oder E-Mail-Adressen, teilt der Kunde das WEBklar vor dem Einschalten mit.
(3) Richtet sich die Website auch gezielt an Kinder unter 16 Jahren oder lassen ihre Inhalte auf besondere Kategorien personenbezogener Daten schließen (etwa Gesundheit, Religion oder politische Meinung), gilt Nr. 3 Abs. 4 der Leistungsbeschreibung.
## 12. Haftung, Änderungen, Schlussbestimmungen
(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.
(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.
(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar die Website-Analyse für seine Website nach Nr. 8 Abs. 2 der Leistungsbeschreibung beenden oder aussetzen. Das Abo bleibt davon unberührt.
(4) Im Übrigen gilt § 16 der Abo-Bedingungen.
## Anlage: Technische und organisatorische Maßnahmen der Website-Analyse (Art. 32 DSGVO)
| Bereich | Maßnahmen |
|---|---|
| Übertragung | Skript und Messdaten werden nur über TLS (HTTPS) übertragen. |
| Speicher im Browser | Es werden keine Cookies gesetzt, und der Messdienst wertet keine Cookies aus. Messanfragen sendet das Skript ohne Cookies. Im lokalen Speicher des Browsers liegt nur die Banner-Wahl (Eintrag „wk-consent“ mit Wahl, Zeitpunkt und Fassung des Banner-Textes). Sie gilt 183 Tage; danach und bei einer neuen Fassung des Banner-Textes fragt das Banner erneut. |
| Einwilligung | Solange keine Wahl getroffen ist und kein Browser-Signal vorliegt, sendet das Skript nur den Pfad der Seite, auch beim Seitenwechsel ohne Neuladen, und den Zähler „Banner angezeigt“. Nach „Ablehnen“ sendet es einmalig den Zähler „abgelehnt“ und danach nichts mehr, auch nicht aus anderen geöffneten Tabs. Bei „Global Privacy Control“ oder „Do Not Track“ zeigt es kein Banner und sendet nichts. Klick-, Scroll- und Verweildauer-Ereignisse registriert es erst nach „Annehmen“. Die Schaltflächen „Ablehnen“ und „Annehmen“ sind gleich gestaltet. |
| Datenminimierung | IP-Adresse und Browser-Kennzeichen werden weder gespeichert noch protokolliert. Der Messdienst verwendet sie nur im Arbeitsspeicher für Land, Geräteklasse, Bot-Erkennung, Mengenbegrenzung und, nur nach Einwilligung, die Besucher-Kennung. Zeitpunkte werden auf die volle Stunde gerundet gespeichert. Pfade werden ohne Suchparameter und Anker gespeichert; Abschnitte, die wie Kennungen aussehen (lange Ziffernfolgen, UUIDs, E-Mail-Adressen), werden vorher ersetzt. Elementpfade bestehen nur aus Tag-Namen und Position. Formulareingaben und Seitentexte werden nicht erfasst. |
| Pseudonymisierung | Die Besucher-Kennung (nur nach Einwilligung) ist ein auf 64 Bit gekürzter HMAC-SHA256 aus Website-Schlüssel, IP-Adresse und Browser-Kennzeichen mit einem Tagesschlüssel. Auch der Schlüssel der Mengenbegrenzung wird mit dem Tagesschlüssel gebildet. Der Tagesschlüssel liegt nur im Arbeitsspeicher; die Auslagerung auf die Festplatte ist für den Messdienst gesperrt. Er wird beim ersten Ereignis eines neuen Kalendertags (Europe/Berlin) und bei jedem Neustart neu erzeugt und nie gesichert. Ältere Kennungen lassen sich danach nicht mehr zuordnen. |
| Zugriffskontrolle | Die Verwaltungsschnittstelle des Messdienstes (Auswertung, Website-Verwaltung) läuft auf einem internen Port, der nicht öffentlich erreichbar ist, und ist durch ein Zugriffstoken geschützt. Zugriff haben nur das Kundenportal und die Verwaltung von WEBklar. Das Portal zeigt jedem Kunden nur die Daten seiner eigenen Websites. |
| Heatmap | Die Heatmap öffnet ein signierter Link, den nur der angemeldete Kunde im Kundenportal erzeugen kann. Der Link gilt 30 Minuten und liefert nur Klickpunkte der gewählten Seite und Ansicht, ohne Zeitpunkte und Kennungen. |
| Eingangsprüfung | Messdaten werden nur angenommen, wenn die Website registriert und eingeschaltet ist und Origin bzw. Referer zu ihren Domains passen. Bots werden verworfen. Größe und Menge der Anfragen sind je Website und Absender begrenzt (Zähler nur im Arbeitsspeicher). Für ausgeschaltete Websites liefert der Messdienst ein leeres Skript aus. |
| Löschung | Täglicher Löschlauf um 02:30 Uhr (Europe/Berlin): Ereignisse und Klicks nach 30 Tagen, Tageswerte nach 25 Monaten, alle Daten einer Website 60 Tage nach dem Ausschalten. Gelöschte Datensätze werden in der Datenbankdatei überschrieben und aus dem Schreibprotokoll (WAL) entfernt. |
| Trennung | Der Messdienst läuft in einem eigenen Container mit eigenem Datenverzeichnis und hat keinen Zugriff auf andere Kundendaten. Alle Datensätze tragen den Website-Schlüssel; Abfragen erfolgen nur je Website. |
| Protokolle | Der Reverse-Proxy führt kein Zugriffsprotokoll, und der Messdienst protokolliert keine Anfragen. Wird ein Zugriffsprotokoll eingeschaltet, dann nicht für die Adresse des Messdienstes. Meldungen der Container werden nach 14 Tagen gelöscht. |
| Datensicherung | Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). Einen gesonderten Datenbank-Export des Messdienstes gibt es nicht. |
| Rechenzentrum | Hetzner Online GmbH, Rechenzentrum Falkenstein (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren. |
| Organisation | Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 5), Archiv der Fassungen von Banner-Text und Zusatzseite (Nummer 8 Abs. 6), Überprüfung der Maßnahmen mindestens einmal im Jahr. |
+138
View File
@@ -0,0 +1,138 @@
# Auftragsverarbeitungsvertrag Website-Analyse (Art. 28 DSGVO)
**Fassung 2026-10-02**
zwischen dem Kunden als Verantwortlichem („Kunde“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).
## Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen
(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „Website-Analyse“ einschaltet und dabei per Haken diesem Vertrag und der Leistungsbeschreibung „Website-Analyse“ in der angezeigten Fassung zustimmt. Das Portal speichert die Fassung, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte in dieser Fassung als PDF enthält und auf sie verlinkt.
(2) Ein Mitarbeiter von WEBklar darf die Funktion nur einschalten, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. WEBklar speichert die Fassung, das Datum und den Weg dieser Erklärung, den Zeitpunkt des Einschaltens und den Namen des Mitarbeiters. Der Kunde erhält die E-Mail nach Absatz 1.
(3) Dieser Vertrag ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für die Website-Analyse geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung „Website-Analyse“ vor.
## 1. Gegenstand und Dauer
(1) WEBklar betreibt für die öffentliche Website des Kunden eine Reichweitenmessung mit Einwilligungsbanner („Website-Analyse“) und verarbeitet dabei personenbezogene Daten der Besucher der Website im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung „Website-Analyse“.
(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Ausschalten, spätestens mit dem Ende des Wartungs-Abos; den Messdienst schaltet WEBklar dann spätestens am folgenden Tag ab. Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.
## 2. Art und Zweck der Verarbeitung
(1) Art: Erheben über ein Skript, das WEBklar beim Veröffentlichen in die Website des Kunden einfügt, Übermitteln an den Messdienst von WEBklar, Speichern, Verdichten zu Tageswerten, Auswerten im Kundenportal (Kennzahlen, Heatmap) und Löschen.
(2) Zwecke: Der Kunde soll erkennen, wie seine Website genutzt wird, um sie zu verbessern. Außerdem schützt WEBklar den Messdienst für den Kunden vor Missbrauch und verfälschten Zahlen (Mengenbegrenzung, Erkennen von Bots).
(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Besucher. Die Besucher-Kennung dient nur dazu, die Besucher eines Tages zu zählen.
## 3. Datenarten und Betroffene
(1) Solange der Besucher im Banner keine Wahl getroffen hat und sein Browser kein Signal nach Absatz 2 Satz 2 sendet: Pfad der aufgerufenen Seite (ohne Suchparameter und Anker), auch bei einem Seitenwechsel ohne Neuladen; Land (aus der IP-Adresse abgeleitet); Geräteklasse (Handy, Tablet oder Computer; aus dem Browser-Kennzeichen abgeleitet); Zeitpunkt (auf die volle Stunde gerundet); ein Zähler dafür, dass das Banner angezeigt wurde.
(2) Nach „Ablehnen“ sendet das Skript einmalig einen Zähler „abgelehnt“ und danach nichts mehr. Sendet der Browser die Signale „Global Privacy Control“ oder „Do Not Track“, zeigt das Skript kein Banner und sendet nichts.
(3) Nach „Annehmen“ zusätzlich: Domain der Website, von der der Besucher kam; Größenklasse des Browserfensters; eine täglich wechselnde pseudonyme Besucher-Kennung; Verweildauer (ab dem Laden der Seite) und Scrolltiefe je Seite; Klickpositionen mit einem Elementpfad aus Tag-Namen und Position (ohne Texte, Kennungen und Klassennamen); die Art von Kontakt-Klicks (Telefon, E-Mail, WhatsApp oder Link auf eine andere Website, dann mit deren Domain); ein Zähler „angenommen“.
(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Skripts und bei jeder Messanfrage, auch nach „Ablehnen“. Der Messdienst verarbeitet sie nur kurz im Arbeitsspeicher, um Land, Geräteklasse, Bots, die Mengenbegrenzung und, nur nach Einwilligung, die Besucher-Kennung zu bestimmen. Er speichert und protokolliert sie nicht. Nicht erhoben werden Cookies, Formularinhalte, Seitentexte, Mausbewegungen, Tastatureingaben und Sitzungsaufzeichnungen.
(5) Tageswerte sind Summen je Tag und Merkmal (etwa Aufrufe je Seite, je Land oder je Geräteklasse), ohne Besucher-Kennung. Enthalten die Pfade der Website personenbezogene Angaben, können auch Tageswerte personenbezogen sein (Nummer 11 Abs. 2).
(6) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht gezielt verarbeitet. Je nach Inhalt der Website können sich aus Pfaden oder Domains aber Rückschlüsse ergeben (Nummer 11 Abs. 3).
(7) Betroffene: Besucher der Website des Kunden.
## 4. Weisungen
(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.
(2) Mit dem Einschalten weist der Kunde WEBklar an, die Website-Analyse in der Standardkonfiguration nach der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen im Kundenportal (Einschalten, Ausschalten, Akzentfarbe des Banners). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die von der Standardkonfiguration abweichen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde die Funktion ausschalten.
(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.
## 5. Vertraulichkeit
WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.
## 6. Technische und organisatorische Maßnahmen
(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.
(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.
## 7. Unterauftragsverarbeiter
(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Messdienst läuft, und für dessen Sicherung. Ort der Verarbeitung ist das Rechenzentrum Falkenstein (Deutschland). Eine Übermittlung in Drittländer findet nicht statt.
(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde die Website-Analyse ausschalten und WEBklar sie für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Verlangen des Kunden sofort.
(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).
(4) Die Länderzuordnung nutzt eine Datenbank, die WEBklar auf dem eigenen Server speichert („IP to Country Lite“ von DB-IP.com, Lizenz CC BY 4.0). Daten der Besucher werden dabei nicht an DB-IP übermittelt. DB-IP ist kein Unterauftragsverarbeiter.
## 8. Unterstützung des Kunden, Betroffenenrechte
(1) WEBklar leitet Anfragen betroffener Personen, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.
(2) Die gespeicherten Messdaten enthalten keine IP-Adresse, keinen Namen und keine Kennung, die länger als einen Tag gilt; Zeitpunkte sind auf die volle Stunde gerundet. Einzelne Besucher kann WEBklar daher in der Regel nicht identifizieren (Art. 11 DSGVO). Ein Abgleich kann aber möglich sein, solange die Zugriffsprotokolle der Website (Anlage 1 und 2 der Abo-Bedingungen, 14 Tage) vorhanden sind. Nennt eine betroffene Person zusätzliche Angaben, etwa Datum, Uhrzeit, besuchte Seiten oder ihre IP-Adresse, prüft WEBklar auf Weisung des Kunden, auch anhand dieser Protokolle, ob eine Zuordnung möglich ist, und teilt ihm das Ergebnis mit.
(3) Der Messung können Besucher technisch widersprechen (Art. 21 DSGVO): Nach „Ablehnen“ und bei den Browser-Signalen „Global Privacy Control“ und „Do Not Track“ sendet das Skript nichts mehr.
(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht, und eine kurze Interessenabwägung für die Zählung nach Art. 6 Abs. 1 lit. f DSGVO, die WEBklar mit jeder Fassung dieses Vertrags bereitstellt.
(5) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.
(6) Zum Nachweis der Einwilligungen bewahrt WEBklar jede Fassung des Banner-Textes und der Zusatzseite mit dem Zeitraum ihrer Verwendung auf, bis drei Jahre nach dem Ende der Website-Analyse. WEBklar stellt sie dem Kunden auf Verlangen zur Verfügung. Ein Protokoll je Besucher führt WEBklar nicht.
(7) Die Besucher informiert der Kunde (Art. 13 DSGVO). WEBklar stellt ihm dafür die Zusatzseite „/datenschutz-analyse.html“ als Muster bereit. Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar dort einen Hinweis auf die Zusatzseite ein; sonst erhält der Kunde den Hinweis als Textbaustein (Leistungsbeschreibung Nr. 4 Abs. 4).
## 9. Löschung und Rückgabe
(1) Einzelne Ereignisse und Klickpositionen löscht WEBklar automatisch nach 30 Tagen. Der Löschlauf läuft täglich, die Löschung erfolgt also in der Regel spätestens am 31. Tag; fällt ein Lauf aus, holt der nächste ihn nach.
(2) Tageswerte löscht WEBklar 25 Monate nach dem jeweiligen Tag. Nach dem Ende der Website-Analyse (Ausschalten oder Ende des Wartungs-Abos) löscht WEBklar alle Daten der Website am 61. Tag. Bis zum 46. Tag nach dem Ende kann der Kunde eine Kopie der Tageswerte und der noch vorhandenen Einzelereignisse verlangen; WEBklar stellt sie innerhalb von zwei Wochen als CSV-Datei bereit. Auf Weisung des Kunden löscht WEBklar die Daten früher.
(3) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.
(4) Gesetzliche Aufbewahrungspflichten bleiben unberührt.
## 10. Nachweise und Kontrollen
WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.
## 11. Pflichten des Kunden
(1) Der Kunde ist Verantwortlicher für die Messung. Er entfernt, verdeckt oder umgeht das Banner nicht. Er bindet ohne Abstimmung mit WEBklar keine weiteren Mess- oder Tracking-Werkzeuge ein.
(2) Enthalten die Pfade seiner Website personenbezogene Angaben, etwa Namen, Kundennummern oder E-Mail-Adressen, teilt der Kunde das WEBklar vor dem Einschalten mit.
(3) Richtet sich die Website auch gezielt an Kinder unter 16 Jahren oder lassen ihre Inhalte auf besondere Kategorien personenbezogener Daten schließen (etwa Gesundheit, Religion oder politische Meinung), gilt Nr. 3 Abs. 4 der Leistungsbeschreibung.
## 12. Haftung, Änderungen, Schlussbestimmungen
(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.
(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.
(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar die Website-Analyse für seine Website nach Nr. 8 Abs. 2 der Leistungsbeschreibung beenden oder aussetzen. Das Wartungs-Abo bleibt davon unberührt.
(4) Im Übrigen gilt § 16 der Abo-Bedingungen.
## Anlage: Technische und organisatorische Maßnahmen der Website-Analyse (Art. 32 DSGVO)
| Bereich | Maßnahmen |
|---|---|
| Übertragung | Skript und Messdaten werden nur über TLS (HTTPS) übertragen. |
| Speicher im Browser | Es werden keine Cookies gesetzt, und der Messdienst wertet keine Cookies aus. Messanfragen sendet das Skript ohne Cookies. Im lokalen Speicher des Browsers liegt nur die Banner-Wahl (Eintrag „wk-consent“ mit Wahl, Zeitpunkt und Fassung des Banner-Textes). Sie gilt 183 Tage; danach und bei einer neuen Fassung des Banner-Textes fragt das Banner erneut. |
| Einwilligung | Solange keine Wahl getroffen ist und kein Browser-Signal vorliegt, sendet das Skript nur den Pfad der Seite, auch beim Seitenwechsel ohne Neuladen, und den Zähler „Banner angezeigt“. Nach „Ablehnen“ sendet es einmalig den Zähler „abgelehnt“ und danach nichts mehr, auch nicht aus anderen geöffneten Tabs. Bei „Global Privacy Control“ oder „Do Not Track“ zeigt es kein Banner und sendet nichts. Klick-, Scroll- und Verweildauer-Ereignisse registriert es erst nach „Annehmen“. Die Schaltflächen „Ablehnen“ und „Annehmen“ sind gleich gestaltet. |
| Datenminimierung | IP-Adresse und Browser-Kennzeichen werden weder gespeichert noch protokolliert. Der Messdienst verwendet sie nur im Arbeitsspeicher für Land, Geräteklasse, Bot-Erkennung, Mengenbegrenzung und, nur nach Einwilligung, die Besucher-Kennung. Zeitpunkte werden auf die volle Stunde gerundet gespeichert. Pfade werden ohne Suchparameter und Anker gespeichert; Abschnitte, die wie Kennungen aussehen (lange Ziffernfolgen, UUIDs, E-Mail-Adressen), werden vorher ersetzt. Elementpfade bestehen nur aus Tag-Namen und Position. Formulareingaben und Seitentexte werden nicht erfasst. |
| Pseudonymisierung | Die Besucher-Kennung (nur nach Einwilligung) ist ein auf 64 Bit gekürzter HMAC-SHA256 aus Website-Schlüssel, IP-Adresse und Browser-Kennzeichen mit einem Tagesschlüssel. Auch der Schlüssel der Mengenbegrenzung wird mit dem Tagesschlüssel gebildet. Der Tagesschlüssel liegt nur im Arbeitsspeicher; die Auslagerung auf die Festplatte ist für den Messdienst gesperrt. Er wird beim ersten Ereignis eines neuen Kalendertags (Europe/Berlin) und bei jedem Neustart neu erzeugt und nie gesichert. Ältere Kennungen lassen sich danach nicht mehr zuordnen. |
| Zugriffskontrolle | Die Verwaltungsschnittstelle des Messdienstes (Auswertung, Website-Verwaltung) läuft auf einem internen Port, der nicht öffentlich erreichbar ist, und ist durch ein Zugriffstoken geschützt. Zugriff haben nur das Kundenportal und die Verwaltung von WEBklar. Das Portal zeigt jedem Kunden nur die Daten seiner eigenen Websites. |
| Heatmap | Die Heatmap öffnet ein signierter Link, den nur der angemeldete Kunde im Kundenportal erzeugen kann. Der Link gilt 30 Minuten und liefert nur Klickpunkte der gewählten Seite und Ansicht, ohne Zeitpunkte und Kennungen. |
| Eingangsprüfung | Messdaten werden nur angenommen, wenn die Website registriert und eingeschaltet ist und Origin bzw. Referer zu ihren Domains passen. Bots werden verworfen. Größe und Menge der Anfragen sind je Website und Absender begrenzt (Zähler nur im Arbeitsspeicher). Für ausgeschaltete Websites liefert der Messdienst ein leeres Skript aus. |
| Löschung | Täglicher Löschlauf um 02:30 Uhr (Europe/Berlin): Ereignisse und Klicks nach 30 Tagen, Tageswerte nach 25 Monaten, alle Daten einer Website 60 Tage nach dem Ausschalten. Gelöschte Datensätze werden in der Datenbankdatei überschrieben und aus dem Schreibprotokoll (WAL) entfernt. |
| Trennung | Der Messdienst läuft in einem eigenen Container mit eigenem Datenverzeichnis und hat keinen Zugriff auf andere Kundendaten. Alle Datensätze tragen den Website-Schlüssel; Abfragen erfolgen nur je Website. |
| Protokolle | Der Reverse-Proxy führt kein Zugriffsprotokoll, und der Messdienst protokolliert keine Anfragen. Wird ein Zugriffsprotokoll eingeschaltet, dann nicht für die Adresse des Messdienstes. Meldungen der Container werden nach 14 Tagen gelöscht. |
| Datensicherung | Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). Einen gesonderten Datenbank-Export des Messdienstes gibt es nicht. |
| Rechenzentrum | Hetzner Online GmbH, Rechenzentrum Falkenstein (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren. |
| Organisation | Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 5), Archiv der Fassungen von Banner-Text und Zusatzseite (Nummer 8 Abs. 6), Überprüfung der Maßnahmen mindestens einmal im Jahr. |
@@ -0,0 +1,9 @@
# Prüfpunkte zur Vorlage datenschutz-analyse.html (intern, nicht veröffentlichen)
> Entwurf – vor Verwendung anwaltlich prüfen lassen.
- **P1** Zählung ohne Einwilligung (Seitenaufruf, Referrer-Domain, Land, Gerätegröße): kein Zugriff auf das Endgerät nach § 25 TDDDG? Das Skript liest Referrer und Fensterbreite aus; Einordnung nach strenger DSK-Lesart offen.
- **P2** Tages-Kennung nach Einwilligung: pseudonym oder faktisch anonym?
- **P3** Rechtsgrundlage des anonymen Banner-Zählers (angezeigt, angenommen, abgelehnt).
- **P5** Speicherung der Banner-Wahl (auch der Ablehnung) im lokalen Speicher nach § 25 Abs. 2 Nr. 2 TDDDG; GPC/DNT wie Ablehnung.
- **P18** Betroffenenrechte bei nicht identifizierbaren Daten (Art. 11, Art. 12 Abs. 2 DSGVO), Formulierung unter "Deine Rechte".
@@ -0,0 +1,23 @@
# Datenschutzerklärung: Prüfpunkte für die Kundenansicht neu
Stand 16.10.2026 · Entwurf – Prüfung durch Kanzlei offen · Entwurf zur Freigabe, keine Rechtsberatung
Diese Liste ändert die Datenschutzerklärung nicht. `public/datenschutz.html` bleibt unverändert (eingefroren); die Stellen unten sind die, die mit der neuen Kundenansicht und den Texten der Fassung 6 geprüft werden müssen. Die Änderung der Datenschutzerklärung erfolgt erst mit dem Umschalten (Entscheidung R14 in `docs/rechtliches/kundenansicht-entscheidungen.md`), als eigene Fassung, sonst würde sie sofort öffentlich. Zeilenangaben beziehen sich auf den Stand von `public/datenschutz.html` am 16.10.2026.
Je Prüfpunkt: **Fundstelle**, was dort steht, was sich ändert, und die Frage an den Inhaber oder die Kanzlei.
**D1** · Fundstelle `public/datenschutz.html:127`. Nachweis bei der Bestellung einer Funktion. Dort steht der „Klick auf ‚Zahlungspflichtig bestellen‘“, die Stripe-Sitzung und eine Funktionsliste nur mit News-Pinnwand und Terminplaner. Künftig löst das Einschalten einer Funktion im Abo keine Zahlung aus und hat keine Stripe-Sitzung; gespeichert werden der Wortlaut der angezeigten Zeilen, die Fassungen, Zeitpunkt des Klicks auf „Einschalten“, IP-Adresse, Browserkennung, Konto, angemeldete Person und die Kundenart (Fassungen: Abo-Bedingungen 2026-10-16-v6, Funktionsunterlagen 2026-10-16). Die Funktionsliste muss alle Funktionen nennen (News-Pinnwand, Terminplaner, Webshop, Buchungskalender, Öffnungszeiten live, Website-Analyse). Frage: Neuer Absatz „Funktion einschalten“ neben dem Absatz für Bestellungen, damit der Bestand (Funktionen, die einzeln gekauft wurden) unverändert beschrieben bleibt?
**D2** · Fundstelle `public/datenschutz.html:126`. Nachweis bei Abo, Wechsel und Zusatzspeicher. Dort stehen „gegebenenfalls Ihre Angaben zum Leistungsbeginn“ sowie Zeitpunkt, Version, IP-Adresse und Browserkennung. Künftig kommen der Wortlaut der angezeigten Zeilen (Linkzeile, Häkchen für Privatkunden) und die Kundenart zum Klickzeitpunkt hinzu (§ 2 Abs. 6 der Abo-Bedingungen Fassung 6). Frage: Genügt es, die Kundenart in den Aufzählungssatz aufzunehmen, oder braucht sie wegen D3 einen eigenen Hinweis?
**D3** · Fundstelle `public/datenschutz.html:88`. Abschnitt 6 „Kundenportal“, Kundenkonto-Absatz in der Zeile darunter. Die Kundenart („Betrieb“ oder „privat“) ist ein neues Datum im Kundenkonto. Das Team legt sie nach den Angaben des Kunden fest (Entscheidung E3); ist sie leer, gilt der Kunde als privat (E4). Zu ergänzen sind Zweck (Bestellseiten richten sich danach, Nachweis), Rechtsgrundlage (Art. 6 Abs. 1 lit. b und f DSGVO), Quelle (Angaben des Kunden an das Team) und Dauer (wie das Kundenkonto; im Nachweis wie in D1). Frage: Eigener Satz im Kundenkonto-Absatz oder in Abschnitt 7?
**D4** · Fundstelle `public/datenschutz.html:128`, ergänzend `public/datenschutz.html:115` und `public/datenschutz.html:121`. Der Stripe-Absatz beschreibt Zahlungen für Abos und Zusatzspeicher; das Einschalten einer Funktion im Abo löst keine Zahlung und keine Übermittlung an Stripe aus. Die Abschnitte zu Stripe Connect beim Webshop und beim Buchungskalender (Zahlungen der Käufer und Gäste auf das eigene Stripe-Konto des Kunden) bleiben inhaltlich wie sie sind. Frage: Genügt ein Satz, dass Funktionen im Abo ohne Zahlung eingeschaltet werden, oder sollen die Abschnitte nur an der Einleitung („bestellen“ statt „einschalten“) angepasst werden?
**D5** · Fundstelle `public/datenschutz.html:102`. Abschnitt zur Website-Analyse: „Kunden mit Wartungs-Abo“ können die Reichweitenmessung einschalten. Künftig steckt die Website-Analyse im Hosting-Abo und im Wartungs-Abo (Leistungsbeschreibung Website-Analyse Nr. 1 Abs. 1). Der Absatz muss beide Abos nennen und beschreiben, dass die Funktion mit dem Abo endet (Löschfrist 60 Tage, AVV Website-Analyse Nr. 9). Frage: Reicht eine Anpassung der Einleitung?
**D6** · Fundstellen `public/datenschutz.html:105` (Terminplaner), `public/datenschutz.html:113` (Webshop), `public/datenschutz.html:119` (Buchungskalender), außerdem der Abschnitt zur News-Pinnwand und zu Öffnungszeiten live. Die Funktionsabschnitte sprechen von „Kunden mit Wartungs-Abo“, vom „Freischalten“ und vom Ende mit dem Vertrag. Künftig gilt: Funktionen sind Teil des Wartungs-Abos, enden mit dem Abo (Terminplaner, News-Pinnwand, Webshop und Buchungskalender spätestens am Folgetag, Öffnungszeiten live nach 30 Tagen Weiterlauf) und werden nach den Fristen der Leistungsbeschreibungen gelöscht (30 Tage Terminplaner, News-Pinnwand und Öffnungszeiten live; 90 Tage Webshop und Buchungskalender). Frage: Die Fristen in den Abschnitten und in Abschnitt 7 (Speicherdauer) müssen übereinstimmen; soll Abschnitt 7 die Fristen künftig als Tabelle führen?
**D7** · Fundstelle `public/datenschutz.html:138`, ergänzend die Beschreibung des Stundenkontingents im Kundenkonto-Absatz. Das Stundenkonto und der Vorschuss für die Einrichtung (§ 4 Abs. 8 der Abo-Bedingungen Fassung 6) sind Abrechnungsdaten: Arbeitszeit, Verbrauch, Vorschuss, Ablaufdaten. Sie stehen heute unter „gegebenenfalls Ihr Stundenkontingent“. Frage: Ist der Vorschuss als Abrechnungsdatum mit demselben Zweck, derselben Rechtsgrundlage (Art. 6 Abs. 1 lit. b DSGVO) und derselben Dauer ausreichend beschrieben, oder braucht er einen eigenen Satz?
**D8** · Fundstelle `public/datenschutz.html:130`. Abschnitt zu den Seiten Kündigen und Widerrufen. Die Seiten erscheinen in der neuen Kundenansicht in der du-Form (Entscheidung R16) und zeigen angemeldeten Kunden die laufenden Widerrufsfristen aus bereits vorhandenen Daten (Sammelabfrage der Fristen im Kundenportal, `GET /api/fristen`). Das ist ein Lesen vorhandener Daten ohne neues Datum und ohne neuen Zweck; der Absatz bleibt inhaltlich unverändert. Frage: Genügt die Feststellung, dass kein neuer Absatz nötig ist, oder soll die Anzeige der Fristen im Konto erwähnt werden?
@@ -0,0 +1,144 @@
# Rechtsrunde Fassung 6: Entscheidungen, Kanzlei-Fragen und Ablauf
Stand 16.10.2026 · Entwurf – Prüfung durch Kanzlei offen · Entwurf zur Freigabe, keine Rechtsberatung
Dieses Blatt ist die Grundlage für die Freigabe der Rechtstexte der Kundenansicht neu durch den Inhaber (Entscheidung E20). Die Kanzlei-Fragen halten den Livegang nicht auf; die Fragen aus den früheren Rechtsrunden stehen in `/root/reports/kundenansicht-neu-2026-10-15/bestand/recht.md` § 4. Die Entwürfe liegen unter `docs/rechtliches/*-v6.md`, die Seiten unter `public/neu/rechtliches/` (Abo-Bedingungen: `abo-bedingungen.html`, Funktionsunterlagen: `<slug>-2026-10-16.html`). Datenschutz-Prüfpunkte D1 bis D8 stehen in `docs/rechtliches/datenschutz-kundenansicht-pruefpunkte.md`.
## Zu entscheiden
| Nr. | Punkt | Vorschlag | Alternative | Kanzlei-Frage |
|---|---|---|---|---|
| R1 | Fassungsnummer und Datum | Abo `2026-10-16-v6`, Funktionen `2026-10-16`; bei Freigabe auf den Freigabetag setzen | Entwurfstag bleibt (keine Umbenennung der Dateien und Fassungen) | – |
| R2 | Geltung für laufende Abos v3 bis v5 (C11) | nur neue Abschlüsse (§ 15 Abs. 2); Funktionen gelten über Linkzeile und Klick, auch mit der Stundenkonto- und Vorschuss-Regel; keine Pflicht-Info-Mail, eine freiwillige Mail später | Info-Mail an laufende Abos mit Bitte um Zustimmung nach § 15 | KQ1 |
| R3 | Vorschuss-Regel (E17) | § 4 Abs. 8 und jede Leistungsbeschreibung außer der Analyse: echte Arbeitszeit, nie Mehraufwand, ein Rest bei Vertragsende verfällt zugunsten des Kunden | Obergrenze (zum Beispiel 3 Stunden) mit Rückfrage darüber | KQ2 |
| R4 | Einrichtung beim Projektstart (E9, E21) | § 4 Abs. 9 und Baustein „projekt“ der Leistungsbeschreibungen: Abrechnung über die Projektkosten, die Funktion läuft nur mit dem passenden Abo | nur in den Leistungsbeschreibungen, nicht in den Abo-Bedingungen | KQ8 |
| R5 | Kundenart als Grundlage (E3, E4) | § 1 Abs. 2: hinterlegt nach Angabe des Kunden, leer gilt als privat, maßgeblich ist der Zweck des Vertrags | Selbstauskunft des Kunden in den Bedingungen | KQ4 |
| R6 | Häkchen-Wortlaut für Abo, Wechsel und Einrichtung | Texte aus `WORTLAUT_V6.beginn` ohne Paragrafenzitate; Betriebe setzen nichts | Zitate §§ 356 Abs. 5 Nr. 2, 357a Abs. 2 BGB im Häkchen | KQ5 |
| R7 | Einrichtung in der Widerrufsfrist | Teil des Abos, kein eigener Vertrag; Widerruf beendet die Funktion; Wertersatz nach aufgewendeter Einrichtungszeit zum Stundensatz des Abos | WEBklar verzichtet auf Wertersatz für die Einrichtung (wie beim Zusatzspeicher), das Häkchen für die Einrichtung entfällt | KQ3 |
| R8 | Ende mit dem Abo, Fristen, kein „Behalten“ (C12); mit P4/P6 abgestimmt | Terminplaner, News, Webshop und Buchungskalender schalten spätestens am Folgetag ab (Tagesjob), die Analyse wie bisher; Öffnungszeiten live läuft nach dem Ende noch 30 Tage unverändert weiter und wird dann abgeschaltet (kein Behalten gegen 30 €); Eingaben danach: Terminplaner, News und Öffnungszeiten 30 Tage, Webshop und Buchungskalender 90, Analyse 60; ein neues Abo in der Frist holt die Funktion zurück | Öffnungszeiten live schalten wie die übrigen spätestens am Folgetag ab (P4 Text Z2, P6 Mail K40 und der Zeiten-Abgleich müssten dafür geändert werden) | KQ8 |
| R9 | Analyse im Hosting-Abo (C10) | in beiden Abos, Einrichtung kostenlos (C6): § 3 Abs. 8, § 6 Abs. 6, Leistungsbeschreibung A | nur im Wartungs-Abo, wie heute | KQ8 |
| R10 | AVV-Struktur | Option A: die AVV je Funktion bleiben eigene Verträge mit neuer Vorbemerkung, Anlage 1 bleibt der Rahmen (§ 14 Abs. 2) | B: Anlagen 4 bis 7 zu den Abo-Bedingungen; C: Rahmen-AVV mit einer Anlage je Funktion | KQ7 |
| R11 | Widerrufsbelehrung der Funktionen | bleibt als Datei für den Bestand, die neuen Texte verlinken sie nicht | bleibt Anlage der Leistungsbeschreibungen | KQ6 |
| R12 | Anrede der Rechtsdokumente (E15, Spec 5.7) | bleiben im bisherigen Stil („der Kunde“, in der Übersichtsbox „Ihr“); nur Oberflächentexte stehen in „du“ | Übersichtsbox und Leistungsbeschreibungen auf „du“ | – |
| R13 | Webshop nur für Betriebe, Terminplaner nicht im Gesundheitsbereich (C15) | steht in den Leistungsbeschreibungen, das Team prüft bei der Freigabe | Webshop auch für Privatkunden mit Hinweis | KQ4, KQ9 |
| R14 | Datenschutzerklärung | Prüfpunkte D1 bis D8; `public/datenschutz.html` wird erst mit dem Umschalten als eigene Fassung geändert (sonst sofort öffentlich) | sofort ändern | – |
| R15 | Zusatzspeicher und Übernahmegutschrift | Zusatzspeicher: Linkzeile ohne Häkchen (§ 10 Abs. 3 und Hinweise zum Widerruf Nr. 4: kein Wertersatz); Gutschrift: Satz über dem Knopf (P7) | das Häkchen bleibt | KQ5 |
| R16 | Du-Anrede der Seiten Kündigen und Widerrufen (N38) | beide Seiten sprechen ab Fassung 6 „du“; sie speichern keinen Wortlaut und haben keine eigene Version (C14); Belehrung und Muster-Formular folgen R12 | Sie-Form bleibt bis zur Freigabe von Belehrung und Formular | KQ6 |
| R17 | Zeilen „Anbieter“ und „Abrechnung“ im Block der Bestellseiten (N37) | beide Zeilen entfallen im Block; der Anbieter steht im Impressum im Konto und in § 1 der Bedingungen, die Abrechnung (monatlich im Voraus per Stripe) in den Zeilen zu Preis und Fälligkeit und in § 7 | beide Zeilen bleiben im Block über dem Knopf | KQ6 |
| R18 | Länge des Blocks „Was du bestellst“ am Handy (Abschlussprüfung F1) | Der Block über dem Knopf ist knapp: Leistung als eine Zeile mit Link „Leistungen ansehen“ auf die Leistungsbeschreibung, Preis, Fällig, Laufzeit, Beginn und Zahlung in je ein bis drei Zeilen; die ausführlichen Sätze (Leistung, Laufzeit mit Kündigungsfrist, Zahlungsweg, Gutschrift und Abbuchungstag beim Wechsel) stehen unter dem Knopf („Im Einzelnen“), ohne Klappe. So stehen Kerninfo und Knopf auch auf 375 × 667 zusammen im nutzbaren Bereich (zwischen Kopf und Leiste) | voller Wortlaut im Block über dem Knopf: am Handy dann nicht mehr auf einem Bildschirm mit dem Knopf | KQ6 |
## Fragen an die Kanzlei
Jede Frage hat vier Teile wie in `docs/rechtliches/zeiten-kanzlei-fragen.md`: **Frage** (was wir wissen möchten), **Fundstelle im Text** (wo der Wortlaut steht), **Unsere Annahme** (der Stand, mit dem wir arbeiten; bitte bestätigen oder korrigieren), **Alternative** (was wir stattdessen täten). Annahmen sind keine Rechtsurteile.
### KQ1: Geltung der Fassung 6 und Information an laufende Abos
**Frage.** Fassung 6 gilt nur für neue Abschlüsse. Wer ein laufendes Abo nach Fassung 3 bis 5 hat und eine Funktion einschaltet, stimmt Leistungsbeschreibung und Auftragsverarbeitung der Funktion in der angezeigten Fassung durch den Klick auf „Einschalten“ zu; dazu gehören die Regeln zur Einrichtung (Stundenkontingent) und zum Vorschuss. Reicht der Klick, um diese Regeln in das laufende Abo einzubeziehen, oder braucht es eine Zustimmung nach dem bisherigen § 15 (Mitteilung mit sechs Wochen Frist, Zustimmung)? Muss WEBklar laufende Kunden über Fassung 6 informieren, obwohl sie für sie nur vorteilhaft ist?
**Fundstelle im Text.** § 15 Abs. 2 der Abo-Bedingungen Fassung 6; § 3 Abs. 8; § 4 Abs. 8; Nr. 1 Abs. 1 der Leistungsbeschreibungen (Baustein „geltung“).
**Unsere Annahme.** Der Klick genügt, weil jede Funktion ein zusätzliches Angebot ohne Gebühr ist und die Änderung des Abos nur für neue Abschlüsse gilt. Eine Info-Mail ist keine Pflicht; wir würden später freiwillig informieren.
**Alternative.** Info-Mail an alle laufenden Abos mit Bitte um Zustimmung nach § 15; Funktionen für Altkunden erst nach dieser Zustimmung.
### KQ2: Vorschuss-Regel für die Einrichtung
**Frage.** Die Einrichtung einer Funktion geht mit der tatsächlich aufgewendeten Zeit vom Stundenkontingent ab. Reicht es nicht, entsteht ein Vorschuss, den die folgenden Monatskontingente ausgleichen; ein Rest bei Vertragsende verfällt zugunsten des Kunden, und es wird nie Mehraufwand berechnet. Ist das transparent und wirksam, auch gegenüber Verbrauchern (Überraschungsklausel, Kontingent als Vorleistung)? Braucht es eine Obergrenze oder eine Rückfrage?
**Fundstelle im Text.** § 4 Abs. 2 und 8 der Abo-Bedingungen Fassung 6; Nr. 5 Abs. 2 (Terminplaner), Nr. 3 Abs. 1 (News-Pinnwand), Nr. 4 Abs. 1 (Öffnungszeiten live), Nr. 9 Abs. 1 (Webshop, Buchungskalender).
**Unsere Annahme.** Die Regel ist für den Kunden günstig (keine zusätzliche Rechnung, Verfall zu seinen Gunsten) und wird im Kundenportal mit „Vorschuss für Einrichtung“ angezeigt.
**Alternative.** Obergrenze (zum Beispiel 3 Stunden) mit Rückfrage darüber; oder Einrichtung nur, soweit das Kontingent reicht.
### KQ3: Einrichtung in der Widerrufsfrist
**Frage.** Das Einschalten einer Funktion ist nach unserem Text Teil des Abos und kein eigener Vertrag; es verlängert die Widerrufsfrist nicht. Ist das richtig, oder ist die Einrichtung eine eigene Leistung mit eigenem Widerrufsrecht? Erlischt das Widerrufsrecht nach § 356 Abs. 5 Nr. 2 BGB bei einem Dauerschuldverhältnis überhaupt durch „vollständige Erbringung“, und wie bemisst sich der Wertersatz nach § 357a Abs. 2 BGB für Einrichtungszeit innerhalb der Frist?
**Fundstelle im Text.** Hinweise zum Widerruf Nr. 5 und § 2 Abs. 2 c der Abo-Bedingungen Fassung 6; Baustein „widerruf“ der Leistungsbeschreibungen; `WORTLAUT_V6.beginn.einrichtung` in `server/services/products.js`.
**Unsere Annahme.** Teil des Abos; Wertersatz nach aufgewendeter Einrichtungszeit zum Stundensatz des Abos; das Häkchen gibt es nur für Privatkunden in der Frist.
**Alternative.** WEBklar verzichtet auf Wertersatz für die Einrichtung (wie beim Zusatzspeicher nach § 10 Abs. 3) und das Häkchen für die Einrichtung entfällt.
### KQ4: Belastbarkeit der Kundenart
**Frage.** Das Team hinterlegt die Kundenart („Betrieb“ oder „privat“) nach den Angaben des Kunden; ist keine hinterlegt, gilt der Kunde als privat; maßgeblich bleibt der Zweck des Vertrags (§ 1 Abs. 2). Wie belastbar ist das, wenn sich später ein Verbraucher als Betrieb geführt herausstellt (Existenzgründer, Vereine, Freiberufler)? Wer trägt das Risiko einer fehlenden Belehrung? Das ist die Frage 5 aus `recht.md` § 4.
**Fundstelle im Text.** § 1 Abs. 2 und § 2 Abs. 2 c der Abo-Bedingungen Fassung 6; `WORTLAUT_V6.beginn`; Nr. 1 Abs. 1 und Nr. 5 Abs. 1 der Leistungsbeschreibung Webshop.
**Unsere Annahme.** Unsere Kunden sind fast nur Betriebe (E2). Widerrufsbelehrung, Muster-Formular und die Funktion „Vertrag widerrufen“ bleiben für alle erreichbar; nur das Häkchen und der Beginn nach der Frist hängen an der Kundenart.
**Alternative.** Selbstauskunft des Kunden bei der Bestellung („Ich bestelle als Unternehmer“) oder das Häkchen für alle Kunden.
### KQ5: Häkchentexte ohne Normzitat, Wechsel nur mit Häkchen
**Frage.** Dürfen die Häkchen für den Sofortbeginn auf zwei bis drei Sätze ohne Paragrafenzitate gekürzt werden, ohne die Information nach Art. 246a § 1 Abs. 3 EGBGB über das Erlöschen und den Wertersatz zu verlieren (Frage 2 aus `recht.md` § 4)? Ist es zulässig, dass der Wechsel ins Wartungs-Abo für Privatkunden nur mit Häkchen möglich ist, weil er sofort wirkt? Genügt beim Zusatzspeicher und bei der Übernahmegutschrift eine Linkzeile ohne Häkchen, weil WEBklar auf Wertersatz verzichtet?
**Fundstelle im Text.** `WORTLAUT_V6.beginn` (Abo, Wechsel, Einrichtung); § 2 Abs. 2 c und § 9 Abs. 6 der Abo-Bedingungen Fassung 6; § 10 Abs. 3 und Hinweise zum Widerruf Nr. 4.
**Unsere Annahme.** Die Wortlaute der Konstante genügen; für Privatkunden ist der Wechsel ohne Häkchen gesperrt; Zusatzspeicher und Gutschrift brauchen kein Häkchen.
**Alternative.** Zitate §§ 356 Abs. 5 Nr. 2, 357a Abs. 2 BGB im Häkchen; Wechsel für Privatkunden mit Beginn nach Ablauf der Frist.
### KQ6: Belehrung und Pflichtinformationen per Link und PDF, Anrede der Seiten Kündigen und Widerrufen
**Frage.** Genügt der Link auf Widerrufsbelehrung und Muster-Formular vor der Bestellung, wenn der Volltext erst im Bestätigungs-PDF steht (Frage 4 aus `recht.md` § 4)? Passen Belehrung und Formular zum Gestaltungshinweis in Anlage 1 EGBGB zu § 356a? Dürfen die Zeilen „Anbieter“ und „Abrechnung“ im Block über dem Knopf entfallen, solange der Anbieter im Impressum und in § 1 steht und die Abrechnung in den Zeilen zu Preis und Fälligkeit? Ändert die Du-Anrede der Seiten Kündigen und Widerrufen etwas an der Wirksamkeit von Kündigung (§ 312k BGB) und Widerruf (§ 356a BGB)?
**Fundstelle im Text.** § 2 Abs. 2 c, Widerrufsbelehrung, Muster-Widerrufsformular und Hinweise zum Widerruf der Abo-Bedingungen Fassung 6; Seiten `kuendigen.html` und `widerrufen.html` in der neuen Ansicht; Block „Was du bestellst“ der Bestellseiten.
**Unsere Annahme.** Der Link genügt, das PDF kommt vor Leistungsbeginn per E-Mail (§ 312f BGB); Anbieter und Abrechnung sind an anderer Stelle erreichbar; die Du-Anrede ändert die Wirksamkeit nicht.
**Alternative.** Volltext der Belehrung auf der Bestellseite; Zeilen „Anbieter“ und „Abrechnung“ bleiben im Block; Sie-Form der beiden Seiten.
### KQ7: AVV per Klick auf die Linkzeile, Struktur A, B oder C
**Frage.** Genügt der Klick auf „Einschalten“ unter der Linkzeile als Abschluss des Auftragsverarbeitungsvertrags nach Art. 28 Abs. 3 und 9 DSGVO („schriftlich, auch elektronisch“), ohne eigenes Häkchen (Frage 3 aus `recht.md` § 4)? Welche Struktur ist am sichersten: A (eigene Verträge je Funktion), B (Anlagen zu den Abo-Bedingungen) oder C (Rahmen-AVV mit Anlage je Funktion)?
**Fundstelle im Text.** § 14 Abs. 2 der Abo-Bedingungen Fassung 6; Vorbemerkung Abs. 1 jedes AVV in `docs/rechtliches/avv-*-v6.md`; Abschnitt „AVV-Struktur“ unten.
**Unsere Annahme.** Der Klick genügt, weil Wortlaut, Fassung, Zeit, Konto und angemeldete Person gespeichert werden; wir bleiben bei Option A.
**Alternative.** Ein eigenes Häkchen für den AVV; Option B oder C.
### KQ8: Funktionen als Teil des Abos, Kopplung an das Abo-Ende, Rückkehr in der Frist
**Frage.** Sind die Funktionen als Teil der Abo-Leistung wirksam geregelt (§ 3 Abs. 8), einschließlich der Kopplung an das Abo (§ 11 Abs. 4: Ende mit dem Abo, Weiterlauf von Öffnungszeiten live für 30 Tage, Löschfristen von 30, 60 und 90 Tagen)? Ist es zulässig, dass ein neues Abo in der Frist die Funktion mit den vorhandenen Daten ohne neue Zustimmung zurückholt? Tragen die Analyse im Hosting-Abo (C10) und die Abrechnung der Einrichtung beim Projektstart über die Projektkosten (E9, E21) ohne weitere Hinweise?
**Fundstelle im Text.** § 3 Abs. 8, § 4 Abs. 9 und § 11 Abs. 4 der Abo-Bedingungen Fassung 6; Abschnitt „Ende der Leistung“ jeder Leistungsbeschreibung; Nr. 10 Abs. 5 der Leistungsbeschreibungen Webshop und Buchungskalender („auf Wunsch in Textform“).
**Unsere Annahme.** Die Regeln sind als Leistungsbeschreibung wirksam; die Rückkehr in der Frist holt die Funktion zurück. Offen ist für uns, ob die Rückkehr bei Webshop und Buchungskalender wie heute „auf Wunsch in Textform“ oder automatisch geschehen soll.
**Alternative.** Öffnungszeiten live wie die übrigen Funktionen am Folgetag abschalten (R8); Rückkehr nur auf Wunsch in Textform; einheitliche Löschfrist von 30 Tagen.
### KQ9: Ausschlüsse als Vertragsklausel
**Frage.** Sind die Ausschlüsse in den Leistungsbeschreibungen als Leistungsgrenzen wirksam: Terminplaner nicht im Gesundheitsbereich, Webshop nur für Betriebe und ohne die ausgeschlossenen Waren, Buchungskalender nur für Unternehmer? Gilt das auch, wenn die Prüfung beim Einschalten nur durch das Team oder durch eine Selbstangabe im Fragebogen erfolgt? Darf WEBklar bei Verstößen abschalten?
**Fundstelle im Text.** Nr. 4 Abs. 3 der Leistungsbeschreibung Terminplaner; Nr. 1 Abs. 1, Nr. 3 und Nr. 5 Abs. 1 der Leistungsbeschreibung Webshop; Nr. 1 Abs. 1 der Leistungsbeschreibung Buchungskalender.
**Unsere Annahme.** Die Ausschlüsse sind als Leistungsbeschreibung wirksam; das Team prüft bei der Freigabe (R13).
**Alternative.** Webshop auch für Privatkunden mit Hinweis (C15 verschoben); keine automatische Abschaltung, nur ein Hinweis in Textform.
## AVV-Struktur
Die Auftragsverarbeitung der Funktionen ist in Fassung 6 so geregelt, dass der Kunde sie mit dem Klick auf „Einschalten“ unter der Linkzeile abschließt. Drei Aufbauten sind möglich; empfohlen wird Option A.
**Option A, empfohlen: eigene Verträge je Funktion, Anlage 1 bleibt der Rahmen.** Die AVV Terminplaner, Webshop, Buchungskalender und Website-Analyse bleiben eigene Dokumente mit einer neuen Vorbemerkung (Abschluss per Klick) und gelten neben Anlage 1 der Abo-Bedingungen (§ 14 Abs. 2). Vorteil: jede Funktion hat ihre eigenen Datenarten, Fristen und Unterauftragsverarbeiter, und die bestehenden Verträge ändern sich kaum. Nachteil: vier Einzelverträge, deren Gleichlauf mit Anlage 1 bei jeder Änderung geprüft werden muss.
**Option B: Anlagen 4 bis 7 zu den Abo-Bedingungen.** Die AVV der Funktionen werden Anlagen der Abo-Bedingungen. Vorteil: ein Dokument mit einer Fassung, der Kunde stimmt den Bedingungen einmal zu. Nachteil: die Abo-Bedingungen werden lang und müssen bei jeder Änderung einer Funktion neu versioniert werden; Funktionen, die ein Kunde nie nutzt, stehen trotzdem in seinem Vertrag.
**Option C: Rahmen-AVV mit einer Anlage je Funktion.** Ein neuer Rahmenvertrag regelt das Gemeinsame (Weisungen, Sicherheit, Löschung), die Anlagen je Funktion nennen nur Daten und Unterauftragsverarbeiter. Vorteil: einheitliche Regeln, bei Änderungen ändert sich nur die Anlage. Nachteil: ein neuer Vertrag neben Anlage 1, abzustimmen mit der Kanzlei, und mehr Arbeit als jetzt nötig.
## Ablauf nach der Freigabe
Die Reihenfolge ist fest; jeder Schritt erst, wenn der vorige grün ist. Die Skripte laufen nur auf ausdrückliche Anweisung des Controllers nach der Freigabe.
1. Freigabe des Inhabers je Punkt (R1 bis R17). Die Antworten auf die Kanzlei-Fragen KQ1 bis KQ9 halten den Livegang nicht auf (E20). Entschiedene Änderungen werden zuerst in den Texten und Tests nachgezogen.
2. Datum der Fassungen auf den Freigabetag setzen: `config.abo.termsVersionNeu` (Abo), `FUNKTIONEN_FASSUNG_V6` in `server/services/products.js`, die Datei- und Testnamen mit dem Datum (`<slug>-2026-10-16`), die Fassungszeilen der Markdown-Dateien, `Version 6 · Stand …` und den Druckkopf in der Abo-Seite.
3. Entwurfsmarke aus allen Dateien entfernen (Fassungszeilen der Markdown-Dateien, der Absatz unter der Kopfzeile und das meta robots in der Abo-Seite), `ENTWURF_V6 = false` in `scripts/build-legal-html.mjs`, und `ENTWURF = false` in `test/aboBedingungenV6.test.js`, `test/funktionenRechtV6.test.js` und `test/kundenansichtEntscheidungen.test.js`. Auch der Vermerk im Kopf dieser Datei und der Prüfpunkte-Datei fällt weg.
4. `node --max-old-space-size=256 scripts/build-legal-html.mjs funktionen6` erzeugt die zehn Seiten neu; `scripts/build-legal-html.mjs funktionen6 --check` meldet danach nichts.
5. `free -m` (mindestens 2000 MB verfügbar), dann `nice -n 10 scripts/build-legal-pdfs.sh abo6` und `nice -n 10 scripts/build-legal-pdfs.sh funktionen6`, einzeln und nie parallel (`FORCE=1` nur nach einer Textänderung). Die PDFs und die `.sha256`-Dateien werden mit committet.
6. `node scripts/rechtliches-hochladen.mjs --probe`. Meldet der Vorflug `STOPP: Karte fehlt: …` (laut `setup-legal.mjs` des WOMS fehlen drei der elf Karten: News, Website-Analyse und AVV Website-Analyse), zuerst `node scripts/rechtliches-hochladen.mjs --karten-anlegen`, danach den Vorflug wiederholen. Ist er grün, folgt ein Aufruf je Dokument (elf Aufrufe): `node scripts/rechtliches-hochladen.mjs <documentKey> --freigabe=JJJJ-MM-TT`. Zuletzt `node scripts/rechtliches-hochladen.mjs --pruefen`: Exit 0 heißt, alle elf Dokumente haben eine aktuelle Version mit passender Quelle; sonst weiter bei Schritt 6.
7. Erst danach `KUNDENANSICHT_NEU` auf `on` (P10); P10 prüft vorher, dass `WORTLAUT_STATUS` auf `'freigegeben'` steht und die Gleichheitstests grün sind (Nachtrag N48). Die Verdrahtung der Analyse (`config.analyse.avvVersion`, `legalPaths`, die Weiche der Mail K18) folgt der Freigabe (Nachtrag N33); P10 verschiebt Seite und PDF der Abo-Bedingungen nach `public/`.
@@ -0,0 +1,45 @@
# Leistungsbeschreibung News-Pinnwand
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo
## 1. Geltung
(1) Funktion im Sinne dieser Leistungsbeschreibung ist die News-Pinnwand. Diese Leistungsbeschreibung gilt, wenn der Kunde die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Kunde die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht.
(2) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für die News-Pinnwand diese Leistungsbeschreibung vor.
## 2. Leistung
(1) WEBklar baut die News-Pinnwand in die Website ein, die der Kunde für das gewählte Projekt bei WEBklar im Kundenportal führt. Die Pinnwand zeigt die Beiträge des Kunden als Zettel mit Titel, Text und Foto; pro Seite erscheinen sechs Beiträge, die neuesten zuerst.
(2) Die Beiträge schreibt, ändert und löscht der Kunde selbst im Kundenportal im Bereich „Funktionen“. Er wählt je Beitrag Zettelfarbe und Anzeigedauer; danach verschwindet der Beitrag von seiner Website. Hochgeladene Fotos werden verkleinert, alle Metadaten einschließlich Aufnahmeort (GPS) werden entfernt.
(3) Änderungen an der Website, die über den Einbau der Pinnwand hinausgehen, sind nicht Teil dieser Leistung.
## 3. Einrichtung
(1) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Kunden.
(2) Der Einbau ist erfahrungsgemäß meist innerhalb von 1–2 Wochen abgeschlossen. Das ist eine Einschätzung und keine verbindliche Zusage eines Termins. Nach Abschluss des Einbaus schaltet WEBklar die Funktion frei; ab dann erscheinen die Beiträge auf der Website.
(3) Hat WEBklar die Funktion auf Wunsch des Kunden schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.
(4) Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Kunden in Textform aus; es gilt § 4 der Abo-Bedingungen.
## 4. Pflichten des Kunden
(1) Der Kunde ist für die Inhalte seiner Beiträge und Fotos verantwortlich. Er versichert, dass er die nötigen Rechte an Texten und Fotos hat und dass die Inhalte keine Rechte Dritter verletzen, etwa Urheberrechte, Persönlichkeitsrechte oder das Recht am eigenen Bild abgebildeter Personen.
## 5. Ende der Leistung
(1) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo oder durch den Widerruf eines Wechsels, schaltet WEBklar die Funktion spätestens am folgenden Tag ab. WEBklar informiert den Kunden per E-Mail.
(2) Auf Wunsch des Kunden schaltet WEBklar die Pinnwand jederzeit ab; für den Widerruf gilt Nr. 6.
(3) Beiträge und Fotos bleiben nach dem Abschalten 30 Tage gespeichert und werden danach gelöscht; beginnt in dieser Zeit wieder ein Wartungs-Abo, schaltet WEBklar die Pinnwand mit den vorhandenen Beiträgen ohne weitere Kosten wieder ein. Der Kunde kann seine Beiträge und Fotos jederzeit selbst löschen; andernfalls gelten die Fristen der Datenschutzerklärung (Abschnitt 6a).
## 6. Widerruf
(1) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion.
(2) Hat der Kunde die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat.
@@ -0,0 +1,125 @@
# Leistungsbeschreibung Terminplaner
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo
## 1. Geltung
(1) Funktion im Sinne dieser Leistungsbeschreibung ist der Terminplaner. Diese Leistungsbeschreibung gilt, wenn der Kunde die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Kunde die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Terminplaner.
(2) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für den Terminplaner diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Terminplaner.
(3) Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen („Rechtstexte … nicht enthalten“) gilt nicht für den Datenschutz-Absatz nach Nr. 4 Abs. 9 und die Gästetexte nach Nr. 2 Abs. 6. Beides ist Teil des Terminplaners.
## 2. Leistung
(1) Der Terminplaner ist ein Buchungs-Widget für die Website des Kunden mit einer Verwaltung im Kundenportal. WEBklar baut je Website-Projekt ein Widget an einer Stelle der Website ein, entweder eingebettet in die Seite oder hinter einem Knopf. Die Gestaltung richtet sich nach dem Fragebogen, den der Kunde vor dem Einschalten im Kundenportal beantwortet.
(2) Der Kunde legt seine Leistungen, seine Personen oder Plätze, seine Zeiten und seine Regeln für Buchungen fest. Gäste buchen im Widget einen Termin; je nach Einstellung ist die Buchung verbindlich oder eine Anfrage.
(3) Der Terminplaner versendet im Auftrag des Kunden E-Mails an die Gäste: den Bestätigungslink, die Bestätigung, Änderungen, Absagen, auf Wunsch des Gastes eine Erinnerung (nur mit seinem Haken) und die Eingangsbestätigung eines Widerrufs. Den Kunden benachrichtigt er über neue und geänderte Buchungen.
(4) Der Kunde sieht und verwaltet seine Termine im Kundenportal und kann sie als CSV-Datei exportieren. Dazu erhält er einen geheimen Kalender-Link (ICS), mit dem er seine Termine in einem Kalenderprogramm abonnieren kann.
(5) Der Terminplaner enthält einen Missbrauchsschutz gegen automatisierte Buchungen. Die Löschfristen für Buchungsdaten nennt Nummer 9 des AVV Terminplaner.
(6) Bezahlt wird nur vor Ort. Der Hinweis am Buchungsformular, die Texte der Haken, der Datenschutz-Absatz für die Website des Kunden und die Widerrufsbelehrung für Gäste sind Mustertexte der Standardkonfiguration (Nr. 11 Abs. 1).
## 3. Nicht enthalten
(1) Nicht enthalten sind SMS, Online-Zahlung, ein Kalender-Abgleich (es gibt nur das Abonnement des Kalender-Links nach Nr. 2 Abs. 4), Konten für Gäste, Marketing-E-Mails, Gruppentermine, Mehrsprachigkeit und der Einsatz im Gesundheitsbereich (Nr. 4 Abs. 3).
(2) Zusätzliche Wünsche, etwa ein zweites Widget, Knöpfe je Leistung oder ein Sonderdesign, gehören nicht zur Einrichtung nach Nr. 5. WEBklar führt sie nach Stunden aus (§ 4 der Abo-Bedingungen).
## 4. Voraussetzungen und Pflichten des Kunden
(1) Der Kunde hat ein laufendes Wartungs-Abo.
(2) Der Kunde trägt vor dem Einschalten seine Betriebsangaben ein, dazu die Adresse seines Impressums und die Adresse seiner Datenschutzerklärung.
(3) Der Terminplaner ist nicht für Angebote im Gesundheitsbereich bestimmt, weil dort besondere Kategorien personenbezogener Daten anfallen können (Art. 9 DSGVO). Der Kunde setzt ihn dort nicht ein und fordert von seinen Gästen keine Gesundheitsangaben. WEBklar darf die Einrichtung für eine solche Website ablehnen oder den Terminplaner dort abschalten. Der Fragebogen fragt die Branche ab; wählt der Kunde den Gesundheitsbereich, ist der Terminplaner für ihn nicht verfügbar.
(4) Richtet sich das Angebot des Kunden gezielt an Kinder unter 16 Jahren, nutzt der Kunde den Terminplaner nicht.
(5) Der Kunde nennt eine Telefonnummer, unter der Gäste Termine vereinbaren können, wenn das Widget nicht erreichbar ist (Nr. 6 Abs. 2).
(6) Er lässt die Texte der Haken und Hinweise im Widget unverändert.
(7) Er informiert seine Beschäftigten darüber, dass ihre Namen oder Bezeichnungen, ihre Arbeitszeiten und Abwesenheiten im Terminplaner stehen und dass es einen geheimen Kalender-Link gibt.
(8) Die Pflichten gegenüber seinen Gästen trägt der Kunde: Preisangaben, Allgemeine Geschäftsbedingungen und Stornobedingungen sowie die Einstufung seiner Leistungen als Leistung für Verbraucher oder Unternehmer und als Freizeitleistung.
(9) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz für die Online-Terminbuchung ein; das gehört zur Einrichtung. Liegt sie woanders, etwa bei einem externen Generator, erhält der Kunde den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt der Kunde verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Kunden darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
## 5. Einrichtung
(1) Die Einrichtung umfasst die Einrichtung nach dem Fragebogen, den Einbau des Widgets in die Website, den Datenschutz-Absatz nach Nr. 4 Abs. 9, die Abnahme durch den Kunden und die Freischaltung durch WEBklar.
(2) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Kunden.
(3) Die voraussichtliche Dauer der Einrichtung beträgt meist 1–2 Wochen. Sie ist eine Einschätzung und keine verbindliche Zusage eines Termins.
(4) Hat WEBklar die Funktion auf Wunsch des Kunden schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.
(5) Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Kunden in Textform aus; es gilt § 4 der Abo-Bedingungen. Für den Betrieb des Terminplaners berechnet WEBklar neben dem Wartungs-Abo nichts weiter; er läuft, solange das Wartungs-Abo läuft (Nr. 8).
## 6. Verfügbarkeit und Störungen
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für den Terminplaner. Störungen des Terminplaners beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen).
(2) Ist der Terminplaner nicht erreichbar, zeigt das Widget einen Ausweichtext mit der Telefonnummer des Kunden (Nr. 4 Abs. 5).
(3) Alle Zeiten gelten in der Zeitzone Europe/Berlin.
(4) WEBklar übernimmt keine Gewähr dafür, dass E-Mails zugestellt und gelesen werden. Das hängt vom Postfach des Empfängers und von den Mailgrenzen seines Anbieters ab.
(5) Die gesetzlichen Rechte des Kunden sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.
## 7. Haftung für verpasste Termine
(1) Der Terminplaner ist ein Hilfsmittel. Die Einhaltung der Termine bleibt Sache des Kunden.
(2) WEBklar übernimmt keine Gewähr für die Zustellung der E-Mails (Nr. 6 Abs. 4).
(3) Für entgangenen Gewinn und Folgeschäden aus verpassten Terminen haftet WEBklar nur im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen.
(4) Der Haftungsrahmen des Kunden gegenüber seinen Gästen bleibt unberührt.
## 8. Ende der Leistung
(1) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo oder durch den Widerruf eines Wechsels, schaltet WEBklar die Funktion spätestens am folgenden Tag ab. WEBklar informiert den Kunden per E-Mail.
(2) Mit dem Abschalten nimmt der Terminplaner keine neuen Buchungen mehr an, und das Widget zeigt den Ausweichtext. Die Termine bleiben im Kundenportal sichtbar, und der Kunde kann sie als CSV-Datei exportieren. Gäste können über ihren Link absagen. Bereits angeforderte Erinnerungen laufen weiter. Der Kunde informiert seine Gäste.
(3) WEBklar löscht alle Termindaten und die Einstellungen 30 Tage nach dem Abschalten. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Einstellungen bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6 des AVV Terminplaner).
(4) Beginnt innerhalb dieser 30 Tage wieder ein Wartungs-Abo, schaltet WEBklar den Terminplaner mit den vorhandenen Daten und Einstellungen ohne weitere Kosten wieder ein. Nach der Löschung ist eine neue Einrichtung nötig (neuer Fragebogen, Einrichtung nach Nr. 5).
(5) Endet das Wartungs-Abo, bevor WEBklar den Terminplaner freigeschaltet hat, bricht WEBklar die Einrichtung ab; ein Vorschuss nach Nr. 5 Abs. 2 verfällt.
(6) Auf Wunsch des Kunden in Textform schaltet WEBklar den Terminplaner jederzeit ab; für den Widerruf gilt Nr. 10.
## 9. Änderungen und Einstellung
(1) WEBklar darf die Technik des Terminplaners verbessern, solange der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Terminplaner.
(2) WEBklar darf den Terminplaner für eine Website mit einer Frist von vier Wochen beenden oder für alle Kunden mit einer Frist von drei Monaten einstellen, wenn
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Kunde einer dafür nötigen neuen Fassung nicht zustimmt (AVV Terminplaner Nummer 12), oder
- eine Sicherheitslücke sich nicht anders beheben lässt.
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar den Terminplaner ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Kunden darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.
(4) Eine Einstellung ohne einen Grund nach Absatz 2 ist ausgeschlossen. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
## 10. Widerruf
(1) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion.
(2) Hat der Kunde die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat.
## 11. Hinweise
(1) **Keine Rechtsberatung.** Der Hinweis am Buchungsformular, die Texte der Haken, der Datenschutz-Absatz und die Widerrufsbelehrung für Gäste sind Mustertexte für die Standardkonfiguration. WEBklar sorgt dafür, dass sie die Technik richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Die Einstufung seiner Leistungen als Leistung für Verbraucher oder Unternehmer und als Freizeitleistung trägt der Kunde. Eine rechtliche Beratung im Einzelfall schuldet WEBklar nicht.
(2) **Barrierefreiheit.** Das Widget ist für die Bedienung mit Tastatur und Hilfstechniken gestaltet (Beschriftungen, Fokusführung, Fehlermeldungen als Live-Region). Eine Konformität nach WCAG 2.1 AA oder EN 301 549 sagt WEBklar nicht zu. Ob der Kunde als Dienstleistungserbringer unter das BFSG fällt (Ausnahme für Kleinstunternehmen, § 3 Abs. 3 BFSG), entscheidet und prüft der Kunde.
@@ -0,0 +1,127 @@
# Leistungsbeschreibung Terminplaner
**Fassung 2026-10-05** · Zusatzbedingungen zum Wartungs-Abo
## 1. Geltung
(1) Diese Leistungsbeschreibung regelt die Funktion „Terminplaner“ im Wartungs-Abo. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht. Sie wird vereinbart, wenn der Kunde die Einrichtung des Terminplaners im Kundenportal bestellt und dabei dieser Leistungsbeschreibung, dem Auftragsverarbeitungsvertrag Terminplaner („AVV Terminplaner“) und der Widerrufsbelehrung zustimmt. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Terminplaner.
(2) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für den Terminplaner diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Terminplaner.
(3) Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen („Rechtstexte … nicht enthalten“) gilt nicht für den Datenschutz-Absatz nach Nr. 4 Abs. 9 und die Gästetexte nach Nr. 2 Abs. 6. Beides ist Teil des Terminplaners.
## 2. Leistung
(1) Der Terminplaner ist ein Buchungs-Widget für die Website des Kunden mit einer Verwaltung im Kundenportal. WEBklar baut je Website-Projekt ein Widget an einer Stelle der Website ein, entweder eingebettet in die Seite oder hinter einem Knopf. Die Gestaltung richtet sich nach dem Fragebogen, den der Kunde vor der Bestellung im Kundenportal beantwortet.
(2) Der Kunde legt seine Leistungen, seine Personen oder Plätze, seine Zeiten und seine Regeln für Buchungen fest. Gäste buchen im Widget einen Termin; je nach Einstellung ist die Buchung verbindlich oder eine Anfrage.
(3) Der Terminplaner versendet im Auftrag des Kunden E-Mails an die Gäste: den Bestätigungslink, die Bestätigung, Änderungen, Absagen, auf Wunsch des Gastes eine Erinnerung (nur mit seinem Haken) und die Eingangsbestätigung eines Widerrufs. Den Kunden benachrichtigt er über neue und geänderte Buchungen.
(4) Der Kunde sieht und verwaltet seine Termine im Kundenportal und kann sie als CSV-Datei exportieren. Dazu erhält er einen geheimen Kalender-Link (ICS), mit dem er seine Termine in einem Kalenderprogramm abonnieren kann.
(5) Der Terminplaner enthält einen Missbrauchsschutz gegen automatisierte Buchungen. Die Löschfristen für Buchungsdaten nennt Nummer 9 des AVV Terminplaner.
(6) Bezahlt wird nur vor Ort. Der Hinweis am Buchungsformular, die Texte der Haken, der Datenschutz-Absatz für die Website des Kunden und die Widerrufsbelehrung für Gäste sind Mustertexte der Standardkonfiguration (Nr. 11 Abs. 1).
## 3. Nicht enthalten
(1) Nicht enthalten sind SMS, Online-Zahlung, ein Kalender-Abgleich (es gibt nur das Abonnement des Kalender-Links nach Nr. 2 Abs. 4), Konten für Gäste, Marketing-E-Mails, Gruppentermine, Mehrsprachigkeit und der Einsatz im Gesundheitsbereich (Nr. 4 Abs. 3).
(2) Zusätzliche Wünsche, etwa ein zweites Widget, Knöpfe je Leistung oder ein Sonderdesign, gehören nicht zur Einrichtung nach Nr. 5. WEBklar führt sie nach Stunden aus (§ 4 der Abo-Bedingungen).
## 4. Voraussetzungen und Pflichten des Kunden
(1) Der Kunde hat ein laufendes Wartungs-Abo.
(2) Der Kunde trägt vor der Bestellung seine Betriebsangaben ein, dazu die Adresse seines Impressums und die Adresse seiner Datenschutzerklärung.
(3) Der Terminplaner ist nicht für Angebote im Gesundheitsbereich bestimmt, weil dort besondere Kategorien personenbezogener Daten anfallen können (Art. 9 DSGVO). Der Kunde setzt ihn dort nicht ein und fordert von seinen Gästen keine Gesundheitsangaben. WEBklar darf die Einrichtung für eine solche Website ablehnen oder den Terminplaner dort abschalten.
(4) Richtet sich das Angebot des Kunden gezielt an Kinder unter 16 Jahren, nutzt der Kunde den Terminplaner nicht.
(5) Der Kunde nennt eine Telefonnummer, unter der Gäste Termine vereinbaren können, wenn das Widget nicht erreichbar ist (Nr. 6 Abs. 2).
(6) Er lässt die Texte der Haken und Hinweise im Widget unverändert.
(7) Er informiert seine Beschäftigten darüber, dass ihre Namen oder Bezeichnungen, ihre Arbeitszeiten und Abwesenheiten im Terminplaner stehen und dass es einen geheimen Kalender-Link gibt.
(8) Die Pflichten gegenüber seinen Gästen trägt der Kunde: Preisangaben, Allgemeine Geschäftsbedingungen und Stornobedingungen sowie die Einstufung seiner Leistungen als Leistung für Verbraucher oder Unternehmer und als Freizeitleistung.
(9) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz für die Online-Terminbuchung ein; das gehört zur Einrichtung und wird nicht auf das Stundenkontingent angerechnet. Liegt sie woanders, etwa bei einem externen Generator, erhält der Kunde den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt der Kunde verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Kunden darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
## 5. Einrichtung und Preis
(1) Die Einrichtung umfasst die Einrichtung nach dem Fragebogen, den Einbau des Widgets in die Website, den Datenschutz-Absatz nach Nr. 4 Abs. 9, die Abnahme durch den Kunden und die Freischaltung durch WEBklar.
(2) Die Einrichtung kostet 149 € einmalig je Website-Projekt, ohne Umsatzsteuer gemäß § 19 UStG.
(3) Die voraussichtliche Dauer der Einrichtung nennt die Bestellseite vor der Bestellung. Sie ist eine Einschätzung und keine verbindliche Zusage eines Termins.
(4) Der Kunde bezahlt bei der Bestellung im Kundenportal über Stripe und erhält eine Rechnung.
(5) Der Preis steht neben dem Stundenkontingent des Wartungs-Abos: Die Einrichtung wird weder auf das Kontingent angerechnet noch damit bezahlt. Für den Betrieb des Terminplaners berechnet WEBklar neben dem Wartungs-Abo nichts weiter; er läuft, solange das Wartungs-Abo läuft (Nr. 8).
## 6. Verfügbarkeit und Störungen
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für den Terminplaner. Störungen des Terminplaners beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen).
(2) Ist der Terminplaner nicht erreichbar, zeigt das Widget einen Ausweichtext mit der Telefonnummer des Kunden (Nr. 4 Abs. 5).
(3) Alle Zeiten gelten in der Zeitzone Europe/Berlin.
(4) WEBklar übernimmt keine Gewähr dafür, dass E-Mails zugestellt und gelesen werden. Das hängt vom Postfach des Empfängers und von den Mailgrenzen seines Anbieters ab.
(5) Die gesetzlichen Rechte des Kunden sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.
## 7. Haftung für verpasste Termine
(1) Der Terminplaner ist ein Hilfsmittel. Die Einhaltung der Termine bleibt Sache des Kunden.
(2) WEBklar übernimmt keine Gewähr für die Zustellung der E-Mails (Nr. 6 Abs. 4).
(3) Für entgangenen Gewinn und Folgeschäden aus verpassten Terminen haftet WEBklar nur im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen.
(4) Der Haftungsrahmen des Kunden gegenüber seinen Gästen bleibt unberührt.
## 8. Ende der Leistung
(1) Mit dem Ende des Wartungs-Abos endet der Terminplaner. WEBklar schaltet ihn spätestens am folgenden Tag ab und informiert den Kunden per E-Mail. Das gilt auch bei einem Wechsel ins Hosting-Abo und beim Widerruf des Abschlusses oder eines Wechsels ins Wartungs-Abo.
(2) Mit dem Abschalten nimmt der Terminplaner keine neuen Buchungen mehr an, und das Widget zeigt den Ausweichtext. Die Termine bleiben im Kundenportal sichtbar, und der Kunde kann sie als CSV-Datei exportieren. Gäste können über ihren Link absagen. Bereits angeforderte Erinnerungen laufen weiter. Der Kunde informiert seine Gäste.
(3) WEBklar löscht alle Termindaten und die Einstellungen 30 Tage nach dem Abschalten. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Einstellungen bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6 des AVV Terminplaner).
(4) Beginnt innerhalb dieser 30 Tage wieder ein Wartungs-Abo, schaltet WEBklar den Terminplaner mit den vorhandenen Daten und Einstellungen ohne weitere Kosten wieder ein. Nach der Löschung ist eine neue Einrichtung nötig (neuer Fragebogen, neuer Preis nach Nr. 5).
(5) Endet das Wartungs-Abo, bevor WEBklar den Terminplaner freigeschaltet hat, erstattet WEBklar den Einrichtungspreis vollständig.
(6) Auf Wunsch des Kunden in Textform schaltet WEBklar den Terminplaner jederzeit ab. Eine Erstattung des Einrichtungspreises folgt daraus nicht; für den Widerruf gilt Nr. 10.
## 9. Änderungen und Einstellung
(1) WEBklar darf die Technik des Terminplaners verbessern, solange der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Terminplaner.
(2) WEBklar darf den Terminplaner für eine Website mit einer Frist von vier Wochen beenden oder für alle Kunden mit einer Frist von drei Monaten einstellen, wenn
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Kunde einer dafür nötigen neuen Fassung nicht zustimmt (AVV Terminplaner Nummer 12), oder
- eine Sicherheitslücke sich nicht anders beheben lässt.
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar den Terminplaner ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Kunden darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.
(4) Eine Einstellung ohne einen Grund nach Absatz 2 ist ausgeschlossen. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
## 10. Widerruf
(1) Verbraucher können die Bestellung der Einrichtung nach der Widerrufsbelehrung widerrufen. Belehrung und Muster-Widerrufsformular stehen in der Anlage „Widerrufsbelehrung und Muster-Widerrufsformular (Einrichtung von Funktionen)“.
(2) Ob das Widerrufsrecht erlischt und ob der Kunde einen angemessenen Betrag für die bis dahin erbrachte Leistung schuldet, richtet sich nach der Zustimmung, die der Kunde bei der Bestellung gibt, und nach den Hinweisen zum Widerruf in der Anlage.
(3) Mit dem Widerruf endet der Terminplaner.
## 11. Hinweise
(1) **Keine Rechtsberatung.** Der Hinweis am Buchungsformular, die Texte der Haken, der Datenschutz-Absatz und die Widerrufsbelehrung für Gäste sind Mustertexte für die Standardkonfiguration. WEBklar sorgt dafür, dass sie die Technik richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Die Einstufung seiner Leistungen als Leistung für Verbraucher oder Unternehmer und als Freizeitleistung trägt der Kunde. Eine rechtliche Beratung im Einzelfall schuldet WEBklar nicht.
(2) **Barrierefreiheit.** Das Widget ist für die Bedienung mit Tastatur und Hilfstechniken gestaltet (Beschriftungen, Fokusführung, Fehlermeldungen als Live-Region). Eine Konformität nach WCAG 2.1 AA oder EN 301 549 sagt WEBklar nicht zu. Ob der Kunde als Dienstleistungserbringer unter das BFSG fällt (Ausnahme für Kleinstunternehmen, § 3 Abs. 3 BFSG), entscheidet und prüft der Kunde.
@@ -0,0 +1,192 @@
# Leistungsbeschreibung Buchungskalender
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo
## 1. Geltung
(1) Diese Leistungsbeschreibung regelt die Funktion „Buchungskalender“. Mit dem Buchungskalender nimmt der Kunde über seine Website Anfragen für die Vermietung von Unterkünften entgegen (Ferienwohnungen, Monteurzimmer, kleine Pensionen) und rechnet die Aufenthalte ab („Vermieter“); Gäste sind die Besucher seiner Website, die im Buchungskalender anfragen und buchen („Gäste“). Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht. Sie richtet sich nur an Unternehmer, die in eigenem Namen und für eigene Rechnung vermieten.
(2) Diese Leistungsbeschreibung gilt, wenn der Vermieter die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Vermieter die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Buchungskalender. Einschalten kann der Vermieter erst, nachdem WEBklar seine Anfrage für den Buchungskalender geprüft und angenommen hat (Nr. 2 Abs. 1). Für einen Vermieter, dem WEBklar die Funktion als Teil seines Website-Projekts bereitstellt, schaltet WEBklar sie nach seiner Zustimmung in Textform frei; die Bedingungen dieser Leistungsbeschreibung gelten dann ebenso, die Einrichtung rechnet WEBklar nach Nr. 9 Abs. 3 ab.
(3) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für den Buchungskalender diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Buchungskalender.
(4) Leistungsbeschreibung A Nr. 2 Abs. 3 und Nr. 4 sowie Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen (Formulare mit Server, „Rechtstexte … nicht enthalten“) stehen dem Buchungskalender nicht entgegen. Das Widget des Buchungskalenders läuft auf dem Server von WEBklar, nicht auf dem Server der Website, und der Muster-Text zur Stornierung nach Nr. 6 ist Teil des Buchungskalenders.
## 2. Leistung
(1) Der Vermieter fragt den Buchungskalender im Kundenportal an und beschreibt seine Unterkünfte (Art der Unterkunft, Zahl der Einheiten, ob er Booking.com oder Airbnb nutzt). WEBklar prüft die Anfrage und nimmt sie an oder lehnt sie mit einer Begründung ab. Ein Anspruch auf Annahme besteht nicht. Erst nach der Annahme kann der Vermieter den Buchungskalender einschalten.
(2) Die Einrichtung umfasst den Einbau eines Kalender-Widgets an einer Stelle der Website des Vermieters, die Bereitstellung der Verwaltung im Kundenportal, den Einbau des Datenschutz-Absatzes für den Buchungskalender (Nr. 5 Abs. 10) und die Freischaltung durch WEBklar. Für jede Website wird ein Buchungskalender eingerichtet; er kann mehrere Unterkünfte desselben Vermieters enthalten. Zusätzlich gibt es für jeden freigegebenen Aufenthalt eine Buchungsseite bei WEBklar, die der Gast über den Link in seiner E-Mail erreicht.
(3) So läuft eine Buchung ab:
- Das Widget zeigt dem Gast die belegten Nächte. Er wählt Anreise und Abreise, gibt Personenzahl, Namen, E-Mail-Adresse und auf Wunsch Firma, Telefonnummer und Nachricht an und sendet eine **unverbindliche Anfrage**. Er erhält eine Eingangsbestätigung, der Vermieter eine Benachrichtigung.
- Der Vermieter prüft die Anfrage im Kundenportal, sieht den vorgerechneten Preis, kann die Posten ändern und gibt die Anfrage mit einer Reservierungsfrist frei oder lehnt sie ab. Gibt er sie frei, reserviert der Buchungskalender die Nächte bis zum Ablauf der Frist, und der Gast erhält eine E-Mail mit Preis, Stornobedingungen und dem Link zur Buchungsseite.
- Auf der Buchungsseite sieht der Gast alle Angaben zum Aufenthalt, trägt seine Rechnungsadresse ein, wählt die Zahlart (online über Stripe, wenn der Vermieter sie eingeschaltet hat, oder vor Ort) und bucht mit der Schaltfläche zur zahlungspflichtigen Buchung. Erst damit kommt der Vertrag zwischen Vermieter und Gast zustande, nicht schon mit der Anfrage oder der Freigabe.
- Mit der Buchung erzeugt der Buchungskalender eine Rechnung mit lückenloser Nummer und sendet dem Gast eine Buchungsbestätigung mit der Rechnung als PDF; der Vermieter wird benachrichtigt.
- Verstreicht die Frist ohne Buchung, läuft die Reservierung ab und die Nächte werden frei; der Gast erhält darüber eine E-Mail.
- Der Vermieter kann einen Aufenthalt stornieren. Dann erhält der Gast eine Stornorechnung mit eigener Nummer und, soweit der Vermieter eine Online-Zahlung erstattet, eine Mitteilung über den Erstattungsbetrag. Eine Stornierung durch den Gast gibt es im Widget nicht; er wendet sich an den Vermieter.
(4) Im Kundenportal verwaltet der Vermieter:
- Unterkünfte mit Beschreibung, Höchstzahl der Personen, Mindestnächten und Check-in- und Check-out-Zeiten,
- Preise (Staffel nach Aufenthaltsdauer, Endreinigung, Zusatzpersonen, Direktbuchungsrabatt, Steuerart und Steuersätze) und seine Stornobedingungen,
- Anfragen und Buchungen mit Status, die Eintragung „bezahlt (vor Ort)“ und Stornierungen,
- Kalender mit manuellen Sperren,
- die Verbindungen zu anderen Kalendern (Nr. 4),
- seine Rechnungen als PDF,
- seine Angaben als Aussteller der Rechnungen (Name, Anschrift, Steuernummer oder Umsatzsteuer-Identifikationsnummer, Bankverbindung, Rechnungspräfix) und die Verbindung mit seinem Stripe-Konto (Nr. 3).
(5) Der Buchungskalender versendet im Namen des Vermieters E-Mails an die Gäste: Eingang der Anfrage, Freigabe mit Buchungslink, Ablehnung, Buchungsbestätigung mit Rechnung, Zahlungseingang, Ablauf der Reservierung und Stornierung. Absender ist die Versandadresse von WEBklar mit dem Namen des Vermieters, als Antwortadresse dient seine E-Mail-Adresse. Den Vermieter benachrichtigt der Buchungskalender über neue Anfragen und neue Buchungen sowie, wenn der Abgleich mit einem anderen Kalender seit mindestens sechs Stunden fehlschlägt.
(6) Der Buchungskalender enthält einen Missbrauchsschutz gegen automatisierte Anfragen (verstecktes Feld, Mindest-Ausfüllzeit, Obergrenzen je IP-Adresse). Dafür speichert WEBklar mit der Anfrage nur einen Hashwert der IP-Adresse, nicht die IP-Adresse selbst; er wird entfernt, sobald der Vermieter die Anfrage freigibt oder ablehnt, spätestens nach 30 Tagen (Nummer 3 Abs. 4 des AVV Buchungskalender).
(7) Nicht enthalten sind:
- der Meldeschein nach dem Bundesmeldegesetz und die Verwaltung der Meldedaten,
- Kurtaxe, Gästebeitrag und Beherbergungsabgaben,
- Anzahlungen und Teilzahlungen (der Gast zahlt den Gesamtbetrag in einer Zahlung, online oder vor Ort),
- Sofortbuchung ohne Freigabe,
- saisonale Preise (die Preise richten sich nur nach der Aufenthaltsdauer),
- Gutscheine, Channel-Manager-Anbindungen außer dem Kalenderabgleich nach Nr. 4 und Mehrsprachigkeit (der Buchungskalender ist deutschsprachig),
- die Buchführung des Vermieters.
Zusätzliche Wünsche, etwa ein Sonderdesign, ein zweiter Buchungskalender oder eine über das übliche Maß hinausgehende Zahl von Unterkünften, gehören nicht zur Einrichtung nach Nr. 9. WEBklar führt sie nach Absprache nach Stunden aus (Nr. 9 Abs. 4).
## 3. Zahlungen über Stripe und Rechnungen im Namen des Vermieters
(1) Online-Zahlungen im Buchungskalender sind optional und laufen über Stripe Connect. Der Vermieter verbindet dazu sein eigenes Stripe-Konto mit dem Buchungskalender. Hat er für den Webshop von WEBklar bereits ein Stripe-Konto verbunden, kann er es auch für den Buchungskalender verwenden. Den Vertrag über dieses Konto schließt der Vermieter unmittelbar mit Stripe; WEBklar ist daran nicht beteiligt. Für das Konto, die Prüfung des Vermieters durch Stripe und die Auszahlungen gelten die Bedingungen von Stripe. Ohne verbundenes und freigeschaltetes Stripe-Konto ist nur die Zahlart „vor Ort“ möglich.
(2) Das Geld der Gäste fließt direkt an den Vermieter (Zahlung unmittelbar auf dem Konto des Vermieters, sogenannte Direct Charge). WEBklar erhält und verwahrt kein Geld der Gäste, leitet keines weiter und beteiligt sich nicht am Umsatz des Vermieters. WEBklar ist kein Zahlungsdienstleister. Die Gebühren von Stripe trägt der Vermieter; sie fallen nur zwischen dem Vermieter und Stripe an.
(3) Der Vermieter prüft selbst, ob sein Stripe-Konto Zahlungen annehmen und Auszahlungen leisten kann. WEBklar zeigt ihm im Kundenportal den Stand, den Stripe meldet, hat aber keinen Einfluss darauf, ob und wann Stripe ein Konto freischaltet, einschränkt oder sperrt, und haftet dafür nicht. Rückbuchungen, Streitfälle mit Gästen und Zahlungsausfälle regeln der Vermieter und Stripe.
(4) Erstattungen bei einer Stornierung veranlasst der Vermieter im Kundenportal; WEBklar führt die Erstattung über das Stripe-Konto des Vermieters aus. Er bestimmt den Betrag bis zur Höhe der Zahlung des Gastes. Storniert der Vermieter einen Aufenthalt, schließt WEBklar eine noch offene Zahlungsseite bei Stripe. **Automatische Erstattung:** Geht eine Zahlung ein, nachdem die Buchung storniert oder die Reservierung abgelaufen ist, oder zahlt der Gast eine bereits bezahlte Buchung ein zweites Mal, erstattet WEBklar den Betrag ohne weitere Anweisung im Namen des Vermieters in voller Höhe über dessen Stripe-Konto. Dazu erteilt der Vermieter WEBklar mit der Zustimmung zum Buchungskalender eine dauerhafte Weisung; der Stand der Buchung ändert sich dadurch nicht. Erstattungen aus dem Stripe-Dashboard trägt WEBklar in die Buchung nach, soweit Stripe sie meldet.
(5) Die Zahlungsseite selbst (Stripe Checkout) betreibt Stripe. Online-Zahlungen sind derzeit nur per Karte möglich, einschließlich Apple Pay und Google Pay; eine SEPA-Lastschrift bietet der Buchungskalender nicht an. Kartendaten gibt der Gast nur dort ein; WEBklar erhält sie nicht. Beim Start der Zahlung übergibt WEBklar Stripe als Rücksprungadresse (bei Erfolg und bei Abbruch) den geheimen Link zur Buchungsseite des Gastes samt Token. Dieser Link ist damit für Stripe und im Stripe-Dashboard des Vermieters sichtbar; Der Link gilt bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage) und öffnet nur diese eine Buchungsseite.
(6) **Rechnungen im Namen des Vermieters.** Der Vermieter beauftragt WEBklar mit dem Rechnungsauftrag in den Einstellungen des Buchungskalenders, in seinem Namen und für seine Rechnung Rechnungen und Stornorechnungen an seine Gäste auszustellen (Rechnung durch einen Dritten im Namen und im Auftrag des Leistenden, § 14 Abs. 2 Satz 4 UStG). Aussteller der Rechnung ist der Vermieter; sie trägt seinen Namen, seine Anschrift, seine Steuernummer oder Umsatzsteuer-Identifikationsnummer und keinen Hinweis auf WEBklar. WEBklar erzeugt die Rechnung aus den Angaben, die der Vermieter hinterlegt und bei der Freigabe bestätigt hat. Er verantwortet die Richtigkeit seiner Angaben, der Preise, der Steuerart und der Steuersätze; WEBklar prüft sie nicht steuerrechtlich. Die Rechnung wird nach ihrer Ausstellung nicht mehr verändert; Fehler korrigiert der Vermieter durch Stornierung und neue Buchung nach den Möglichkeiten des Kundenportals. Ohne diesen Auftrag stellt WEBklar keine Rechnungen aus. Widerruft er den Auftrag in Textform, stellt der Buchungskalender keine Rechnungen mehr aus und ist für ihn nicht mehr nutzbar.
(7) Die Rechnungsnummern sind je Vermieter und Jahr lückenlos fortlaufend; sie bestehen aus dem Präfix, das der Vermieter vor der ersten Rechnung festlegt, dem Jahr und einer laufenden Nummer.
## 4. Verfügbarkeit und Kalenderabgleich (iCal)
(1) Der Vermieter kann Kalender anderer Plattformen (zum Beispiel Booking.com oder Airbnb) über eine iCal-Adresse einlesen lassen; die eingelesenen Zeiträume blockiert der Buchungskalender. Umgekehrt stellt der Buchungskalender dem Vermieter eine geheime iCal-Adresse bereit, die er bei anderen Plattformen einträgt; sie enthält die belegten Nächte ohne Daten der Gäste. Der Vermieter kann diese Adresse jederzeit erneuern; die bisherige Adresse funktioniert dann nicht mehr.
(2) Der Abgleich läuft regelmäßig, aber zeitversetzt: WEBklar liest die Kalender anderer Plattformen etwa alle 15 Minuten ein, und die anderen Plattformen lesen die Adresse des Buchungskalenders nach ihrem eigenen Takt, oft nur alle paar Stunden. **Zwischen einer Buchung hier und ihrem Eingang dort können deshalb Minuten bis Stunden vergehen, und umgekehrt.** WEBklar sagt keine Echtzeit-Verfügbarkeit zu und übernimmt keine Gewähr dafür, dass eine Unterkunft nicht doppelt gebucht wird.
(3) Dieses Risiko mindert die Freigabe: Jede Anfrage im Buchungskalender braucht die Freigabe des Vermieters. Er prüft vor der Freigabe seine anderen Plattformen und seinen Kalender. Beim Freigeben und beim Buchen prüft der Buchungskalender zusätzlich, ob die Nächte nach seinem Stand frei sind. Eine Doppelbuchung entsteht trotzdem, wenn eine andere Plattform eine Buchung noch nicht gemeldet hat oder der Abgleich ausfällt. Der Vermieter löst eine Doppelbuchung selbst (etwa durch Stornierung) und trägt die Folgen gegenüber seinen Gästen und den Plattformen.
(4) Fehlerhafte oder nicht erreichbare iCal-Adressen kann WEBklar nicht beheben. Schlägt der Abruf fehl, bleibt der zuletzt gelesene Stand erhalten, und der Vermieter wird benachrichtigt, wenn der Abruf seit mindestens sechs Stunden nicht gelungen ist (höchstens einmal je 24 Stunden und Kalender). Eingelesen werden nur Zeiträume von gestern bis 18 Monate in die Zukunft, höchstens 1 Megabyte je Kalender; nur Adressen mit https werden abgerufen.
## 5. Pflichten des Vermieters
(1) Der Vermieter hat ein laufendes Wartungs-Abo und ist Unternehmer. Will er Online-Zahlungen nutzen, stellt er sein eigenes Stripe-Konto bereit und schließt die Anmeldung bei Stripe selbst ab. Der Vermieter vermietet in eigenem Namen; für Preise, Stornobedingungen, Rechnungen und Meldepflichten ist er verantwortlich. Online-Zahlungen seiner Gäste laufen, sofern er sie nutzt, über sein eigenes Stripe-Konto zu den Bedingungen von Stripe; WEBklar erhält und verwahrt kein Geld seiner Gäste und erhebt keine Umsatzbeteiligung. Rechnungen an Gäste stellt WEBklar nur aus, wenn der Vermieter in den Einstellungen des Buchungskalenders den Rechnungsauftrag erteilt hat (§ 14 Abs. 2 Satz 4 UStG); für die Richtigkeit von Preisen und Steuerangaben bleibt er verantwortlich.
(2) Er verantwortet seine Unterkünfte, Preise und Angaben zum Angebot. Dazu gehören richtige Preise einschließlich Steuerausweis (Umsatzsteuer, für die Beherbergung und die Endreinigung nach den dafür geltenden Steuersätzen, oder Hinweis auf § 19 UStG), Check-in- und Check-out-Zeiten, Höchstbelegung, Mindestnächte und alle Angaben, die er seinen Gästen vor der Buchung geben muss. Die Steuerart und die Steuersätze trägt er selbst im Kundenportal ein; WEBklar berät dazu nicht.
(3) Er legt vor der Freischaltung seine Stornobedingungen fest und verantwortet sie. Der Muster-Text nach Nr. 6 ist nur ein Vorschlag.
(4) Er verantwortet die Verfügbarkeit seiner Unterkünfte, pflegt Sperren und Kalenderverbindungen im Kundenportal und prüft regelmäßig auf neue Anfragen und Buchungen. Die Folgen von Angaben, die er setzt (zum Beispiel einer fälschlich freien oder gesperrten Zeit), trägt er.
(5) Er gibt Anfragen nur frei, wenn er die Unterkunft für den Zeitraum zu den angegebenen Bedingungen bereitstellen kann, und erfüllt die gebuchten Aufenthalte. Er trägt alle Pflichten eines Vermieters gegenüber seinen Gästen, auch Gewährleistung, Kundenservice und Beschwerden.
(6) Er erfüllt seine gesetzlichen Pflichten rund um die Beherbergung selbst. Das betrifft insbesondere den Meldeschein nach dem Bundesmeldegesetz, die Kurtaxe und andere kommunale Abgaben, Anzeige- und Genehmigungspflichten für die Vermietung (zum Beispiel Zweckentfremdungs- oder Gewerberecht) sowie die Pflichten aus seinem Steuerrecht. Der Buchungskalender nimmt ihm davon nichts ab.
(7) Anzahlungen und Teilzahlungen bietet der Buchungskalender nicht an. Vereinbart der Vermieter außerhalb des Buchungskalenders etwas anderes mit seinem Gast, ist das nicht Gegenstand dieser Leistungsbeschreibung.
(8) Er erfüllt seine Aufbewahrungspflichten selbst, etwa nach der Abgabenordnung und dem Handelsgesetzbuch. WEBklar ist kein Archiv für die Buchführung und kein Buchführungssystem. WEBklar bewahrt die Rechnungen des Vermieters im Buchungskalender 8 Jahre auf, solange der Buchungskalender läuft und nach Nr. 10 bis zur Löschung, hält sie ihm zum Abruf als PDF bereit und anonymisiert personenbezogene Daten der Gäste nach Nummer 9 des AVV Buchungskalender. Der Vermieter lädt seine Rechnungen deshalb rechtzeitig vor der Löschung nach Nr. 10 Abs. 3 herunter und bewahrt sie selbst auf.
(9) Er hält die Vorgaben dieser Leistungsbeschreibung ein und verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets und der Buchungsseite nicht.
(10) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz für den Buchungskalender ein; das gehört zur Einrichtung. Liegt sie woanders, erhält der Vermieter den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt er verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Vermieter darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
(11) Der Vermieter informiert seine Gäste nach Art. 13 DSGVO. Die Rollen im Datenschutz regelt der AVV Buchungskalender: Der Vermieter ist Verantwortlicher, WEBklar Auftragsverarbeiter, Stripe Vertragspartner des Vermieters.
## 6. Muster-Text zur Stornierung, keine Rechtsberatung
(1) WEBklar stellt dem Vermieter einen Muster-Text für Stornobedingungen bereit. Er ist eine **Vorlage ohne Gewähr** und keine Rechtsberatung (kein Rechtsrat im Sinne des Rechtsdienstleistungsgesetzes). Er passt zur Technik des Buchungskalenders, kann aber auf den Betrieb des Vermieters nicht zugeschnitten sein.
(2) WEBklar empfiehlt, die Stornobedingungen und die Angaben zum Angebot vor der Freischaltung von einer Rechtsanwältin oder einem Rechtsanwalt prüfen zu lassen.
(3) Der Vermieter bleibt für seine Stornobedingungen und alle seine Texte verantwortlich, auch wenn er den Muster-Text unverändert übernimmt (Nr. 5 Abs. 3). Die Stornobedingungen, die bei der Freigabe galten, speichert der Buchungskalender mit der Anfrage; die Buchungsseite und die Buchungsbestätigung zeigen dem Gast diese Fassung.
## 7. Kein Widerrufsrecht des Gastes, Hinweise auf der Buchungsseite
(1) Für die Buchung von Beherbergungsleistungen zu einem bestimmten Zeitpunkt oder in einem bestimmten Zeitraum besteht nach § 312g Abs. 2 Satz 1 Nr. 9 BGB kein gesetzliches Widerrufsrecht. Die Buchungsseite weist den Gast darauf hin; der Vermieter kann ihm vertraglich ein weitergehendes Recht einräumen, der Buchungskalender bietet dafür aber keine Funktion.
(2) Die Buchungsseite zeigt dem Gast vor der Schaltfläche zur zahlungspflichtigen Buchung die wesentlichen Angaben nach Art. 246a EGBGB, soweit der Buchungskalender sie kennt (Anbieter, Unterkunft, Zeitraum, Leistung, Preis insgesamt, Zahlart, Stornobedingungen, Hinweis auf das fehlende Widerrufsrecht). Der Vermieter ergänzt, was darüber hinausgeht, in seinen Texten selbst (Nr. 5 Abs. 2).
(3) Die Buchungsbestätigung erhält der Gast als E-Mail mit der Rechnung als PDF; sie enthält die Angaben zum Vertrag und die Stornobedingungen auf einem dauerhaften Datenträger.
## 8. Verfügbarkeit und Störungen
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für den Buchungskalender. WEBklar sagt für den Buchungskalender keine bestimmte Verfügbarkeit zu, die über den Betrieb der Website im Abo hinausgeht. Störungen des Buchungskalenders beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen).
(2) Angekündigte Wartungsfenster (§ 5 der Abo-Bedingungen) können den Buchungskalender kurzzeitig unterbrechen. Während einer Unterbrechung sind Anfragen und Buchungen nicht möglich; das Widget zeigt dann einen Ausweichtext.
(3) Zahlungen übernimmt der Buchungskalender über Benachrichtigungen von Stripe und einen Abgleich beim Aufruf der Buchungsseite. Fällt die Verbindung zu Stripe oder zum Server aus, kann sich die Bestätigung einer Zahlung verzögern. Eine Zahlung, die nach einer Stornierung oder nach dem Ablauf der Reservierung eingeht, und eine zweite Zahlung auf eine bereits bezahlte Buchung erstattet WEBklar automatisch und in voller Höhe (Nr. 3 Abs. 4); die Buchung bleibt dabei unverändert, und die Erstattung braucht einige Tage, bis sie beim Gast ankommt. Ausfälle von Stripe, von Zahlungsarten und von Banken liegen nicht im Einflussbereich von WEBklar.
(4) Alle Zeiten gelten in der Zeitzone Europe/Berlin.
(5) WEBklar übernimmt keine Gewähr dafür, dass E-Mails zugestellt und gelesen werden. Das hängt vom Postfach des Empfängers und von den Mailgrenzen seines Anbieters ab.
(6) Die gesetzlichen Rechte des Vermieters sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.
## 9. Einrichtung, Abrechnung und Widerruf
(1) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Vermieters. Für den Betrieb des Buchungskalenders berechnet WEBklar nichts weiter; es gibt keine laufende Gebühr und keine Beteiligung am Umsatz oder an den Buchungen.
(2) Die Einrichtung dauert nach der Erfahrung von WEBklar meist 1 bis 2 Wochen ab dem Einschalten. Sie hängt davon ab, wie schnell der Vermieter Unterkünfte, Preise und gegebenenfalls das Stripe-Konto bereitstellt und wie umfangreich der Einbau ist. Die Dauer ist eine Einschätzung und keine verbindliche Zusage eines Termins.
(3) Hat WEBklar die Funktion auf Wunsch des Vermieters schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.
(4) Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Vermieters in Textform aus; es gilt § 4 der Abo-Bedingungen. Welche Wünsche nicht zur Einrichtung gehören, beschreibt Nr. 2 Abs. 7.
(5) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion. Hat der Vermieter die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat. Das Widerrufsrecht gegenüber WEBklar ist vom Widerrufsrecht der Gäste gegenüber dem Vermieter (Nr. 7) zu unterscheiden.
## 10. Laufzeit und Ende
(1) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo oder durch den Widerruf eines Wechsels, schaltet WEBklar die Funktion spätestens am folgenden Tag ab. WEBklar informiert den Vermieter per E-Mail und weist ihn darauf hin, seine Rechnungen als PDF herunterzuladen.
(2) Mit dem Abschalten nimmt der Buchungskalender keine neuen Anfragen und Buchungen mehr an, und das Widget zeigt den Ausweichtext. Reservierungen laufen aus. Bereits gebuchte Aufenthalte bleiben Verträge zwischen dem Vermieter und seinen Gästen; der Vermieter erfüllt sie und informiert seine Gäste, wenn er den Buchungskalender nicht mehr anbietet. Anfragen, Buchungen und Rechnungen bleiben im Kundenportal 90 Tage lesbar; Rechnungen kann er einzeln als PDF herunterladen.
(3) WEBklar löscht alle Daten des Buchungskalenders, also Unterkünfte, Preise, Anfragen, Buchungen, Rechnungen, Kalenderverbindungen, Sperren und Einstellungen, 90 Tage nach dem Abschalten. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6 des AVV Buchungskalender). Die Aufbewahrungsfrist der Rechnungen (Nr. 5 Abs. 8) endet damit mit der Löschung; der Vermieter bewahrt sie danach selbst auf. Nach dem Abschalten weist WEBklar den Vermieter per E-Mail mit dem Löschdatum darauf hin, seine Rechnungen zu sichern; bis dahin sind sie im Kundenportal abrufbar. Wird das Kundenkonto gelöscht, schaltet WEBklar den Buchungskalender ab und löscht seine Daten wie nach Absatz 3 (90 Tage nach dem Abschalten); einen Hinweis per E-Mail gibt es dann nicht, weil mit dem Konto auch die E-Mail-Adresse gelöscht wird.
(4) Das Stripe-Konto bleibt beim Vermieter. Das Ende des Buchungskalenders berührt weder das Konto noch die Auszahlungen noch den Vertrag mit Stripe. Der Vermieter kann die Verbindung zwischen dem Konto und dem Buchungskalender im Stripe-Dashboard beenden.
(5) Beginnt innerhalb der 90 Tage wieder ein Wartungs-Abo, schaltet WEBklar den Buchungskalender auf Wunsch des Vermieters in Textform mit den vorhandenen Daten und Einstellungen ohne weitere Kosten wieder ein. Nach der Löschung ist eine neue Anfrage und eine neue Einrichtung nötig (Einrichtung nach Nr. 9).
(6) Endet das Wartungs-Abo, bevor WEBklar den Buchungskalender freigeschaltet hat, bricht WEBklar die Einrichtung ab; ein Vorschuss nach Nr. 9 Abs. 1 verfällt.
(7) Auf Wunsch des Vermieters in Textform schaltet WEBklar den Buchungskalender jederzeit ab; für den Widerruf gilt Nr. 9 Abs. 5.
## 11. Haftung
(1) Der Buchungskalender ist ein Hilfsmittel für die Vermietung des Vermieters. Der Vertrag über die Unterkunft besteht allein zwischen dem Vermieter und dem Gast; WEBklar ist daran nicht beteiligt.
(2) Für entgangenen Gewinn und Folgeschäden aus Störungen des Buchungskalenders, aus Verzögerungen oder Fehlern beim Kalenderabgleich (Nr. 4), aus Doppelbuchungen und aus nicht zugestellten E-Mails haftet WEBklar nur im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen.
(3) WEBklar haftet nicht für Inhalte des Vermieters, insbesondere nicht für seine Unterkünfte, Preise, Steuerangaben, Stornobedingungen und Rechtstexte, auch nicht für den Muster-Text, den er übernommen hat (Nr. 6), und nicht für die Erfüllung seiner Pflichten gegenüber den Gästen und den Behörden (Nr. 5).
(4) WEBklar haftet nicht für Ausfälle, Sperren oder Entscheidungen von Stripe und der Zahlungsarten sowie für Rückbuchungen und Zahlungsausfälle (Nr. 3 Abs. 3), und nicht für Ausfälle, Änderungen und Entscheidungen anderer Plattformen, deren Kalender der Vermieter einbindet.
(5) Die Haftung nach § 13 Abs. 1 der Abo-Bedingungen (unter anderem bei Vorsatz, grober Fahrlässigkeit, Verletzung von Leben, Körper oder Gesundheit und nach dem Produkthaftungsgesetz) bleibt unberührt. Der Haftungsrahmen des Vermieters gegenüber seinen Gästen bleibt unberührt.
## 12. Änderungen und Einstellung
(1) WEBklar darf die Technik des Buchungskalenders verbessern, solange der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Buchungskalender.
(2) WEBklar darf den Buchungskalender für eine Website mit einer Frist von vier Wochen beenden oder für alle Vermieter mit einer Frist von drei Monaten einstellen, wenn
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Vermieter einer dafür nötigen neuen Fassung nicht zustimmt (AVV Buchungskalender Nummer 12),
- Stripe die Anbindung nach Nr. 3 beendet oder wesentlich ändert, so dass die Online-Zahlung in der bisherigen Form nicht weiterbetrieben werden kann (dann entfällt nur die Online-Zahlung, soweit der Buchungskalender ohne sie weiterläuft), oder
- eine Sicherheitslücke sich nicht anders beheben lässt.
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar den Buchungskalender ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Vermieter darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.
(4) Eine Einstellung ohne einen Grund nach Absatz 2 ist ausgeschlossen. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
## 13. Hinweise
(1) **Keine Rechtsberatung.** Der Buchungskalender und der Muster-Text sind technische Hilfsmittel, keine Rechtsberatung (Nr. 6). Die rechtliche und steuerliche Einordnung seiner Vermietung, seiner Preisangaben und seines Angebots trägt der Vermieter.
(2) **Barrierefreiheit.** Das Widget ist für die Bedienung mit Tastatur und Hilfstechniken gestaltet (Beschriftungen der Tage, Bedienung mit Pfeiltasten, Fehlermeldungen als Live-Region). Eine Konformität nach WCAG 2.1 AA oder EN 301 549 sagt WEBklar nicht zu. Ob der Vermieter als Anbieter von Dienstleistungen im elektronischen Geschäftsverkehr unter das BFSG fällt (Ausnahme für Kleinstunternehmen, § 3 Abs. 3 BFSG), entscheidet und prüft der Vermieter.
@@ -0,0 +1,194 @@
# Leistungsbeschreibung Buchungskalender
**Fassung 2026-10-07** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Vertrag über den Betrieb der Website
## 1. Geltung
(1) Diese Leistungsbeschreibung regelt die Funktion „Buchungskalender“. Mit dem Buchungskalender nimmt der Kunde über seine Website Anfragen für die Vermietung von Unterkünften entgegen (Ferienwohnungen, Monteurzimmer, kleine Pensionen) und rechnet die Aufenthalte ab („Vermieter“); Gäste sind die Besucher seiner Website, die im Buchungskalender anfragen und buchen („Gäste“). Die Leistungsbeschreibung ist eine Zusatzleistung zum Vertrag über den Betrieb der Website: Sie setzt voraus, dass WEBklar die Website des Vermieters aufgrund eines Vertrags betreibt, etwa im Hosting-Abo, im Wartungs-Abo oder aufgrund eines anderen Vertrags über den Betrieb der Website. Sie richtet sich nur an Unternehmer, die in eigenem Namen und für eigene Rechnung vermieten.
(2) Sie wird vereinbart, wenn der Vermieter die Einrichtung des Buchungskalenders im Kundenportal bestellt und dabei dieser Leistungsbeschreibung, dem Auftragsverarbeitungsvertrag Buchungskalender („AVV Buchungskalender“) und der Widerrufsbelehrung zustimmt. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Buchungskalender. Bestellen kann der Vermieter erst, nachdem WEBklar seine Anfrage für den Buchungskalender geprüft und angenommen hat (Nr. 2 Abs. 1). Für einen Vermieter, dem WEBklar die Funktion als Teil seines Website-Projekts bereitstellt, schaltet WEBklar sie nach seiner Zustimmung in Textform frei; die Bedingungen dieser Leistungsbeschreibung gelten dann ebenso, ein Einrichtungspreis fällt nur an, wenn er vereinbart ist.
(3) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für den Buchungskalender diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Buchungskalender.
(4) Leistungsbeschreibung A Nr. 2 Abs. 3 und Nr. 4 sowie Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen (Formulare mit Server, „Rechtstexte … nicht enthalten“) stehen dem Buchungskalender nicht entgegen. Das Widget des Buchungskalenders läuft auf dem Server von WEBklar, nicht auf dem Server der Website, und der Muster-Text zur Stornierung nach Nr. 6 ist Teil des Buchungskalenders. Das gilt auch im Hosting-Abo.
## 2. Leistung
(1) Der Vermieter fragt den Buchungskalender im Kundenportal an und beschreibt seine Unterkünfte (Art der Unterkunft, Zahl der Einheiten, ob er Booking.com oder Airbnb nutzt). WEBklar prüft die Anfrage und nimmt sie an oder lehnt sie mit einer Begründung ab. Ein Anspruch auf Annahme besteht nicht. Erst nach der Annahme kann der Vermieter die Einrichtung bestellen.
(2) Die Einrichtung umfasst den Einbau eines Kalender-Widgets an einer Stelle der Website des Vermieters, die Bereitstellung der Verwaltung im Kundenportal, den Einbau des Datenschutz-Absatzes für den Buchungskalender (Nr. 5 Abs. 10) und die Freischaltung durch WEBklar. Für jede Website wird ein Buchungskalender eingerichtet; er kann mehrere Unterkünfte desselben Vermieters enthalten. Zusätzlich gibt es für jeden freigegebenen Aufenthalt eine Buchungsseite bei WEBklar, die der Gast über den Link in seiner E-Mail erreicht.
(3) So läuft eine Buchung ab:
- Das Widget zeigt dem Gast die belegten Nächte. Er wählt Anreise und Abreise, gibt Personenzahl, Namen, E-Mail-Adresse und auf Wunsch Firma, Telefonnummer und Nachricht an und sendet eine **unverbindliche Anfrage**. Er erhält eine Eingangsbestätigung, der Vermieter eine Benachrichtigung.
- Der Vermieter prüft die Anfrage im Kundenportal, sieht den vorgerechneten Preis, kann die Posten ändern und gibt die Anfrage mit einer Reservierungsfrist frei oder lehnt sie ab. Gibt er sie frei, reserviert der Buchungskalender die Nächte bis zum Ablauf der Frist, und der Gast erhält eine E-Mail mit Preis, Stornobedingungen und dem Link zur Buchungsseite.
- Auf der Buchungsseite sieht der Gast alle Angaben zum Aufenthalt, trägt seine Rechnungsadresse ein, wählt die Zahlart (online über Stripe, wenn der Vermieter sie eingeschaltet hat, oder vor Ort) und bucht mit der Schaltfläche „Zahlungspflichtig buchen“. Erst damit kommt der Vertrag zwischen Vermieter und Gast zustande, nicht schon mit der Anfrage oder der Freigabe.
- Mit der Buchung erzeugt der Buchungskalender eine Rechnung mit lückenloser Nummer und sendet dem Gast eine Buchungsbestätigung mit der Rechnung als PDF; der Vermieter wird benachrichtigt.
- Verstreicht die Frist ohne Buchung, läuft die Reservierung ab und die Nächte werden frei; der Gast erhält darüber eine E-Mail.
- Der Vermieter kann einen Aufenthalt stornieren. Dann erhält der Gast eine Stornorechnung mit eigener Nummer und, soweit der Vermieter eine Online-Zahlung erstattet, eine Mitteilung über den Erstattungsbetrag. Eine Stornierung durch den Gast gibt es im Widget nicht; er wendet sich an den Vermieter.
(4) Im Kundenportal verwaltet der Vermieter:
- Unterkünfte mit Beschreibung, Höchstzahl der Personen, Mindestnächten und Check-in- und Check-out-Zeiten,
- Preise (Staffel nach Aufenthaltsdauer, Endreinigung, Zusatzpersonen, Direktbuchungsrabatt, Steuerart und Steuersätze) und seine Stornobedingungen,
- Anfragen und Buchungen mit Status, die Eintragung „bezahlt (vor Ort)“ und Stornierungen,
- Kalender mit manuellen Sperren,
- die Verbindungen zu anderen Kalendern (Nr. 4),
- seine Rechnungen als PDF,
- seine Angaben als Aussteller der Rechnungen (Name, Anschrift, Steuernummer oder Umsatzsteuer-Identifikationsnummer, Bankverbindung, Rechnungspräfix) und die Verbindung mit seinem Stripe-Konto (Nr. 3).
(5) Der Buchungskalender versendet im Namen des Vermieters E-Mails an die Gäste: Eingang der Anfrage, Freigabe mit Buchungslink, Ablehnung, Buchungsbestätigung mit Rechnung, Zahlungseingang, Ablauf der Reservierung und Stornierung. Absender ist die Versandadresse von WEBklar mit dem Namen des Vermieters, als Antwortadresse dient seine E-Mail-Adresse. Den Vermieter benachrichtigt der Buchungskalender über neue Anfragen und neue Buchungen sowie, wenn der Abgleich mit einem anderen Kalender seit mindestens sechs Stunden fehlschlägt.
(6) Der Buchungskalender enthält einen Missbrauchsschutz gegen automatisierte Anfragen (verstecktes Feld, Mindest-Ausfüllzeit, Obergrenzen je IP-Adresse). Dafür speichert WEBklar mit der Anfrage nur einen Hashwert der IP-Adresse, nicht die IP-Adresse selbst; er wird entfernt, sobald der Vermieter die Anfrage freigibt oder ablehnt, spätestens nach 30 Tagen (Nummer 3 Abs. 4 des AVV Buchungskalender).
(7) Nicht enthalten sind:
- der Meldeschein nach dem Bundesmeldegesetz und die Verwaltung der Meldedaten,
- Kurtaxe, Gästebeitrag und Beherbergungsabgaben,
- Anzahlungen und Teilzahlungen (der Gast zahlt den Gesamtbetrag in einer Zahlung, online oder vor Ort),
- Sofortbuchung ohne Freigabe,
- saisonale Preise (die Preise richten sich nur nach der Aufenthaltsdauer),
- Gutscheine, Channel-Manager-Anbindungen außer dem Kalenderabgleich nach Nr. 4 und Mehrsprachigkeit (der Buchungskalender ist deutschsprachig),
- die Buchführung des Vermieters.
Zusätzliche Wünsche, etwa ein Sonderdesign, ein zweiter Buchungskalender oder eine über das übliche Maß hinausgehende Zahl von Unterkünften, gehören nicht zur Einrichtung nach Nr. 9. WEBklar führt sie nach Absprache nach Stunden aus (Nr. 9 Abs. 5).
## 3. Zahlungen über Stripe und Rechnungen im Namen des Vermieters
(1) Online-Zahlungen im Buchungskalender sind optional und laufen über Stripe Connect. Der Vermieter verbindet dazu sein eigenes Stripe-Konto mit dem Buchungskalender. Hat er für den Webshop von WEBklar bereits ein Stripe-Konto verbunden, kann er es auch für den Buchungskalender verwenden. Den Vertrag über dieses Konto schließt der Vermieter unmittelbar mit Stripe; WEBklar ist daran nicht beteiligt. Für das Konto, die Prüfung des Vermieters durch Stripe und die Auszahlungen gelten die Bedingungen von Stripe. Ohne verbundenes und freigeschaltetes Stripe-Konto ist nur die Zahlart „vor Ort“ möglich.
(2) Das Geld der Gäste fließt direkt an den Vermieter (Zahlung unmittelbar auf dem Konto des Vermieters, sogenannte Direct Charge). WEBklar erhält und verwahrt kein Geld der Gäste, leitet keines weiter und beteiligt sich nicht am Umsatz des Vermieters. WEBklar ist kein Zahlungsdienstleister. Die Gebühren von Stripe trägt der Vermieter; sie fallen nur zwischen dem Vermieter und Stripe an.
(3) Der Vermieter prüft selbst, ob sein Stripe-Konto Zahlungen annehmen und Auszahlungen leisten kann. WEBklar zeigt ihm im Kundenportal den Stand, den Stripe meldet, hat aber keinen Einfluss darauf, ob und wann Stripe ein Konto freischaltet, einschränkt oder sperrt, und haftet dafür nicht. Rückbuchungen, Streitfälle mit Gästen und Zahlungsausfälle regeln der Vermieter und Stripe.
(4) Erstattungen bei einer Stornierung veranlasst der Vermieter im Kundenportal; WEBklar führt die Erstattung über das Stripe-Konto des Vermieters aus. Er bestimmt den Betrag bis zur Höhe der Zahlung des Gastes. Storniert der Vermieter einen Aufenthalt, schließt WEBklar eine noch offene Zahlungsseite bei Stripe. **Automatische Erstattung:** Geht eine Zahlung ein, nachdem die Buchung storniert oder die Reservierung abgelaufen ist, oder zahlt der Gast eine bereits bezahlte Buchung ein zweites Mal, erstattet WEBklar den Betrag ohne weitere Anweisung im Namen des Vermieters in voller Höhe über dessen Stripe-Konto. Dazu erteilt der Vermieter WEBklar mit der Zustimmung zum Buchungskalender eine dauerhafte Weisung; der Stand der Buchung ändert sich dadurch nicht. Erstattungen aus dem Stripe-Dashboard trägt WEBklar in die Buchung nach, soweit Stripe sie meldet.
(5) Die Zahlungsseite selbst (Stripe Checkout) betreibt Stripe. Online-Zahlungen sind derzeit nur per Karte möglich, einschließlich Apple Pay und Google Pay; eine SEPA-Lastschrift bietet der Buchungskalender nicht an. Kartendaten gibt der Gast nur dort ein; WEBklar erhält sie nicht. Beim Start der Zahlung übergibt WEBklar Stripe als Rücksprungadresse (bei Erfolg und bei Abbruch) den geheimen Link zur Buchungsseite des Gastes samt Token. Dieser Link ist damit für Stripe und im Stripe-Dashboard des Vermieters sichtbar; Der Link gilt bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage) und öffnet nur diese eine Buchungsseite.
(6) **Rechnungen im Namen des Vermieters.** Der Vermieter beauftragt WEBklar mit der Zustimmung bei der Bestellung, in seinem Namen und für seine Rechnung Rechnungen und Stornorechnungen an seine Gäste auszustellen (Rechnung durch einen Dritten im Namen und im Auftrag des Leistenden, § 14 Abs. 2 Satz 4 UStG). Aussteller der Rechnung ist der Vermieter; sie trägt seinen Namen, seine Anschrift, seine Steuernummer oder Umsatzsteuer-Identifikationsnummer und keinen Hinweis auf WEBklar. WEBklar erzeugt die Rechnung aus den Angaben, die der Vermieter hinterlegt und bei der Freigabe bestätigt hat. Er verantwortet die Richtigkeit seiner Angaben, der Preise, der Steuerart und der Steuersätze; WEBklar prüft sie nicht steuerrechtlich. Die Rechnung wird nach ihrer Ausstellung nicht mehr verändert; Fehler korrigiert der Vermieter durch Stornierung und neue Buchung nach den Möglichkeiten des Kundenportals. Die Zustimmung zu diesem Auftrag gibt der Vermieter bei der Bestellung und bestätigt sie in den Einstellungen des Buchungskalenders. Widerruft er den Auftrag in Textform, stellt der Buchungskalender keine Rechnungen mehr aus und ist für ihn nicht mehr nutzbar.
(7) Die Rechnungsnummern sind je Vermieter und Jahr lückenlos fortlaufend; sie bestehen aus dem Präfix, das der Vermieter vor der ersten Rechnung festlegt, dem Jahr und einer laufenden Nummer.
## 4. Verfügbarkeit und Kalenderabgleich (iCal)
(1) Der Vermieter kann Kalender anderer Plattformen (zum Beispiel Booking.com oder Airbnb) über eine iCal-Adresse einlesen lassen; die eingelesenen Zeiträume blockiert der Buchungskalender. Umgekehrt stellt der Buchungskalender dem Vermieter eine geheime iCal-Adresse bereit, die er bei anderen Plattformen einträgt; sie enthält die belegten Nächte ohne Daten der Gäste. Der Vermieter kann diese Adresse jederzeit erneuern; die bisherige Adresse funktioniert dann nicht mehr.
(2) Der Abgleich läuft regelmäßig, aber zeitversetzt: WEBklar liest die Kalender anderer Plattformen etwa alle 15 Minuten ein, und die anderen Plattformen lesen die Adresse des Buchungskalenders nach ihrem eigenen Takt, oft nur alle paar Stunden. **Zwischen einer Buchung hier und ihrem Eingang dort können deshalb Minuten bis Stunden vergehen, und umgekehrt.** WEBklar sagt keine Echtzeit-Verfügbarkeit zu und übernimmt keine Gewähr dafür, dass eine Unterkunft nicht doppelt gebucht wird.
(3) Dieses Risiko mindert die Freigabe: Jede Anfrage im Buchungskalender braucht die Freigabe des Vermieters. Er prüft vor der Freigabe seine anderen Plattformen und seinen Kalender. Beim Freigeben und beim Buchen prüft der Buchungskalender zusätzlich, ob die Nächte nach seinem Stand frei sind. Eine Doppelbuchung entsteht trotzdem, wenn eine andere Plattform eine Buchung noch nicht gemeldet hat oder der Abgleich ausfällt. Der Vermieter löst eine Doppelbuchung selbst (etwa durch Stornierung) und trägt die Folgen gegenüber seinen Gästen und den Plattformen.
(4) Fehlerhafte oder nicht erreichbare iCal-Adressen kann WEBklar nicht beheben. Schlägt der Abruf fehl, bleibt der zuletzt gelesene Stand erhalten, und der Vermieter wird benachrichtigt, wenn der Abruf seit mindestens sechs Stunden nicht gelungen ist (höchstens einmal je 24 Stunden und Kalender). Eingelesen werden nur Zeiträume von gestern bis 18 Monate in die Zukunft, höchstens 1 Megabyte je Kalender; nur Adressen mit https werden abgerufen.
## 5. Pflichten des Vermieters
(1) Der Vermieter hat einen laufenden Vertrag mit WEBklar über den Betrieb seiner Website (Hosting-Abo, Wartungs-Abo oder ein anderer Vertrag über den Betrieb der Website), ist Unternehmer und vermietet in eigenem Namen. Will er Online-Zahlungen nutzen, stellt er sein eigenes Stripe-Konto bereit und schließt die Anmeldung bei Stripe selbst ab.
(2) Er verantwortet seine Unterkünfte, Preise und Angaben zum Angebot. Dazu gehören richtige Preise einschließlich Steuerausweis (Umsatzsteuer, für die Beherbergung und die Endreinigung nach den dafür geltenden Steuersätzen, oder Hinweis auf § 19 UStG), Check-in- und Check-out-Zeiten, Höchstbelegung, Mindestnächte und alle Angaben, die er seinen Gästen vor der Buchung geben muss. Die Steuerart und die Steuersätze trägt er selbst im Kundenportal ein; WEBklar berät dazu nicht.
(3) Er legt vor der Freischaltung seine Stornobedingungen fest und verantwortet sie. Der Muster-Text nach Nr. 6 ist nur ein Vorschlag.
(4) Er verantwortet die Verfügbarkeit seiner Unterkünfte, pflegt Sperren und Kalenderverbindungen im Kundenportal und prüft regelmäßig auf neue Anfragen und Buchungen. Die Folgen von Angaben, die er setzt (zum Beispiel einer fälschlich freien oder gesperrten Zeit), trägt er.
(5) Er gibt Anfragen nur frei, wenn er die Unterkunft für den Zeitraum zu den angegebenen Bedingungen bereitstellen kann, und erfüllt die gebuchten Aufenthalte. Er trägt alle Pflichten eines Vermieters gegenüber seinen Gästen, auch Gewährleistung, Kundenservice und Beschwerden.
(6) Er erfüllt seine gesetzlichen Pflichten rund um die Beherbergung selbst. Das betrifft insbesondere den Meldeschein nach dem Bundesmeldegesetz, die Kurtaxe und andere kommunale Abgaben, Anzeige- und Genehmigungspflichten für die Vermietung (zum Beispiel Zweckentfremdungs- oder Gewerberecht) sowie die Pflichten aus seinem Steuerrecht. Der Buchungskalender nimmt ihm davon nichts ab.
(7) Anzahlungen und Teilzahlungen bietet der Buchungskalender nicht an. Vereinbart der Vermieter außerhalb des Buchungskalenders etwas anderes mit seinem Gast, ist das nicht Gegenstand dieser Leistungsbeschreibung.
(8) Er erfüllt seine Aufbewahrungspflichten selbst, etwa nach der Abgabenordnung und dem Handelsgesetzbuch. WEBklar ist kein Archiv für die Buchführung und kein Buchführungssystem. WEBklar bewahrt die Rechnungen des Vermieters im Buchungskalender 8 Jahre auf, solange der Buchungskalender läuft und nach Nr. 10 bis zur Löschung, hält sie ihm zum Abruf als PDF bereit und anonymisiert personenbezogene Daten der Gäste nach Nummer 9 des AVV Buchungskalender. Der Vermieter lädt seine Rechnungen deshalb rechtzeitig vor der Löschung nach Nr. 10 Abs. 3 herunter und bewahrt sie selbst auf.
(9) Er hält die Vorgaben dieser Leistungsbeschreibung ein und verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets und der Buchungsseite nicht.
(10) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz für den Buchungskalender ein; das gehört zur Einrichtung. Liegt sie woanders, erhält der Vermieter den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt er verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Vermieter darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
(11) Der Vermieter informiert seine Gäste nach Art. 13 DSGVO. Die Rollen im Datenschutz regelt der AVV Buchungskalender: Der Vermieter ist Verantwortlicher, WEBklar Auftragsverarbeiter, Stripe Vertragspartner des Vermieters.
## 6. Muster-Text zur Stornierung, keine Rechtsberatung
(1) WEBklar stellt dem Vermieter einen Muster-Text für Stornobedingungen bereit. Er ist eine **Vorlage ohne Gewähr** und keine Rechtsberatung (kein Rechtsrat im Sinne des Rechtsdienstleistungsgesetzes). Er passt zur Technik des Buchungskalenders, kann aber auf den Betrieb des Vermieters nicht zugeschnitten sein.
(2) WEBklar empfiehlt, die Stornobedingungen und die Angaben zum Angebot vor der Freischaltung von einer Rechtsanwältin oder einem Rechtsanwalt prüfen zu lassen.
(3) Der Vermieter bleibt für seine Stornobedingungen und alle seine Texte verantwortlich, auch wenn er den Muster-Text unverändert übernimmt (Nr. 5 Abs. 3). Die Stornobedingungen, die bei der Freigabe galten, speichert der Buchungskalender mit der Anfrage; die Buchungsseite und die Buchungsbestätigung zeigen dem Gast diese Fassung.
## 7. Kein Widerrufsrecht des Gastes, Hinweise auf der Buchungsseite
(1) Für die Buchung von Beherbergungsleistungen zu einem bestimmten Zeitpunkt oder in einem bestimmten Zeitraum besteht nach § 312g Abs. 2 Satz 1 Nr. 9 BGB kein gesetzliches Widerrufsrecht. Die Buchungsseite weist den Gast darauf hin; der Vermieter kann ihm vertraglich ein weitergehendes Recht einräumen, der Buchungskalender bietet dafür aber keine Funktion.
(2) Die Buchungsseite zeigt dem Gast vor der Schaltfläche „Zahlungspflichtig buchen“ die wesentlichen Angaben nach Art. 246a EGBGB, soweit der Buchungskalender sie kennt (Anbieter, Unterkunft, Zeitraum, Leistung, Gesamtpreis, Zahlart, Stornobedingungen, Hinweis auf das fehlende Widerrufsrecht). Der Vermieter ergänzt, was darüber hinausgeht, in seinen Texten selbst (Nr. 5 Abs. 2).
(3) Die Buchungsbestätigung erhält der Gast als E-Mail mit der Rechnung als PDF; sie enthält die Angaben zum Vertrag und die Stornobedingungen auf einem dauerhaften Datenträger.
## 8. Verfügbarkeit und Störungen
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für den Buchungskalender. WEBklar sagt für den Buchungskalender keine bestimmte Verfügbarkeit zu, die über den Betrieb der Website im Abo hinausgeht. Störungen des Buchungskalenders beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen).
(2) Angekündigte Wartungsfenster (§ 5 der Abo-Bedingungen) können den Buchungskalender kurzzeitig unterbrechen. Während einer Unterbrechung sind Anfragen und Buchungen nicht möglich; das Widget zeigt dann einen Ausweichtext.
(3) Zahlungen übernimmt der Buchungskalender über Benachrichtigungen von Stripe und einen Abgleich beim Aufruf der Buchungsseite. Fällt die Verbindung zu Stripe oder zum Server aus, kann sich die Bestätigung einer Zahlung verzögern. Eine Zahlung, die nach einer Stornierung oder nach dem Ablauf der Reservierung eingeht, und eine zweite Zahlung auf eine bereits bezahlte Buchung erstattet WEBklar automatisch und in voller Höhe (Nr. 3 Abs. 4); die Buchung bleibt dabei unverändert, und die Erstattung braucht einige Tage, bis sie beim Gast ankommt. Ausfälle von Stripe, von Zahlungsarten und von Banken liegen nicht im Einflussbereich von WEBklar.
(4) Alle Zeiten gelten in der Zeitzone Europe/Berlin.
(5) WEBklar übernimmt keine Gewähr dafür, dass E-Mails zugestellt und gelesen werden. Das hängt vom Postfach des Empfängers und von den Mailgrenzen seines Anbieters ab.
(6) Die gesetzlichen Rechte des Vermieters sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.
## 9. Preis, Abrechnung und Widerruf
(1) Die Einrichtung kostet 199 € einmalig je Website-Projekt, ohne Umsatzsteuer gemäß § 19 UStG. Für den Betrieb des Buchungskalenders berechnet WEBklar nichts weiter; es gibt keine laufende Gebühr und keine Beteiligung am Umsatz oder an den Buchungen. Der Buchungskalender läuft, solange WEBklar die Website betreibt (Nr. 10).
(2) Die Einrichtung dauert nach der Erfahrung von WEBklar meist 1 bis 2 Wochen ab der Bestellung. Sie hängt davon ab, wie schnell der Vermieter Unterkünfte, Preise und gegebenenfalls das Stripe-Konto bereitstellt und wie umfangreich der Einbau ist. Die Dauer ist eine Einschätzung und keine verbindliche Zusage eines Termins.
(3) Der Vermieter bezahlt bei der Bestellung im Kundenportal über Stripe und erhält eine Rechnung. Für einen Vermieter, dem WEBklar die Funktion als Teil seines Website-Projekts bereitstellt (Nr. 1 Abs. 2), gilt die dafür getroffene Vereinbarung.
(4) Der Preis steht neben den Leistungen eines Abos: Die Einrichtung wird weder auf ein Stundenkontingent angerechnet noch damit bezahlt.
(5) Mehraufwand, der nicht zur Einrichtung gehört (Nr. 2 Abs. 7), führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Vermieters in Textform aus. WEBklar nennt vorher den geschätzten Aufwand und berechnet 100 € je Stunde in Schritten von 15 Minuten, ohne Umsatzsteuer gemäß § 19 UStG (§ 4 der Abo-Bedingungen). Der Mehraufwand wird gesondert berechnet und nicht auf ein Stundenkontingent angerechnet.
(6) Verbraucher können die Bestellung der Einrichtung nach der Widerrufsbelehrung widerrufen. Belehrung und Muster-Widerrufsformular stehen in der Anlage „Widerrufsbelehrung und Muster-Widerrufsformular (Einrichtung von Funktionen)“. Ob das Widerrufsrecht erlischt und ob der Vermieter einen angemessenen Betrag für die bis dahin erbrachte Leistung schuldet, richtet sich nach der Zustimmung, die er bei der Bestellung gibt, und nach den Hinweisen zum Widerruf in der Anlage. Mit dem Widerruf endet der Buchungskalender. Dieses Widerrufsrecht des Vermieters gegenüber WEBklar ist vom Widerrufsrecht der Gäste gegenüber dem Vermieter (Nr. 7) zu unterscheiden.
## 10. Laufzeit und Ende
(1) Der Buchungskalender läuft, solange WEBklar die Website des Vermieters betreibt. Endet der Vertrag über den Betrieb der Website, endet auch der Buchungskalender; WEBklar schaltet ihn zum Vertragsende ab und informiert den Vermieter vorher per E-Mail und weist ihn darauf hin, seine Rechnungen als PDF herunterzuladen. Das gilt auch beim Widerruf dieses Vertrags.
(2) Mit dem Abschalten nimmt der Buchungskalender keine neuen Anfragen und Buchungen mehr an, und das Widget zeigt den Ausweichtext. Reservierungen laufen aus. Bereits gebuchte Aufenthalte bleiben Verträge zwischen dem Vermieter und seinen Gästen; der Vermieter erfüllt sie und informiert seine Gäste, wenn er den Buchungskalender nicht mehr anbietet. Anfragen, Buchungen und Rechnungen bleiben im Kundenportal 90 Tage lesbar; Rechnungen kann er einzeln als PDF herunterladen.
(3) WEBklar löscht alle Daten des Buchungskalenders, also Unterkünfte, Preise, Anfragen, Buchungen, Rechnungen, Kalenderverbindungen, Sperren und Einstellungen, 90 Tage nach dem Abschalten. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6 des AVV Buchungskalender). Die Aufbewahrungsfrist der Rechnungen (Nr. 5 Abs. 8) endet damit mit der Löschung; der Vermieter bewahrt sie danach selbst auf. Nach dem Abschalten weist WEBklar den Vermieter per E-Mail mit dem Löschdatum darauf hin, seine Rechnungen zu sichern; bis dahin sind sie im Kundenportal abrufbar. Wird das Kundenkonto gelöscht, schaltet WEBklar den Buchungskalender ab und löscht seine Daten wie nach Absatz 3 (90 Tage nach dem Abschalten); einen Hinweis per E-Mail gibt es dann nicht, weil mit dem Konto auch die E-Mail-Adresse gelöscht wird.
(4) Das Stripe-Konto bleibt beim Vermieter. Das Ende des Buchungskalenders berührt weder das Konto noch die Auszahlungen noch den Vertrag mit Stripe. Der Vermieter kann die Verbindung zwischen dem Konto und dem Buchungskalender im Stripe-Dashboard beenden.
(5) Beginnt innerhalb der 90 Tage wieder ein Vertrag über den Betrieb der Website, schaltet WEBklar den Buchungskalender auf Wunsch des Vermieters in Textform mit den vorhandenen Daten und Einstellungen ohne weitere Kosten wieder ein. Nach der Löschung ist eine neue Anfrage und eine neue Einrichtung nötig (neuer Preis nach Nr. 9).
(6) Endet der Vertrag über den Betrieb der Website, bevor WEBklar den Buchungskalender freigeschaltet hat, erstattet WEBklar den gezahlten Einrichtungspreis vollständig.
(7) Auf Wunsch des Vermieters in Textform schaltet WEBklar den Buchungskalender jederzeit ab. Eine Erstattung des Einrichtungspreises folgt daraus nicht; für den Widerruf gilt Nr. 9 Abs. 6.
## 11. Haftung
(1) Der Buchungskalender ist ein Hilfsmittel für die Vermietung des Vermieters. Der Vertrag über die Unterkunft besteht allein zwischen dem Vermieter und dem Gast; WEBklar ist daran nicht beteiligt.
(2) Für entgangenen Gewinn und Folgeschäden aus Störungen des Buchungskalenders, aus Verzögerungen oder Fehlern beim Kalenderabgleich (Nr. 4), aus Doppelbuchungen und aus nicht zugestellten E-Mails haftet WEBklar nur im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen.
(3) WEBklar haftet nicht für Inhalte des Vermieters, insbesondere nicht für seine Unterkünfte, Preise, Steuerangaben, Stornobedingungen und Rechtstexte, auch nicht für den Muster-Text, den er übernommen hat (Nr. 6), und nicht für die Erfüllung seiner Pflichten gegenüber den Gästen und den Behörden (Nr. 5).
(4) WEBklar haftet nicht für Ausfälle, Sperren oder Entscheidungen von Stripe und der Zahlungsarten sowie für Rückbuchungen und Zahlungsausfälle (Nr. 3 Abs. 3), und nicht für Ausfälle, Änderungen und Entscheidungen anderer Plattformen, deren Kalender der Vermieter einbindet.
(5) Die Haftung nach § 13 Abs. 1 der Abo-Bedingungen (unter anderem bei Vorsatz, grober Fahrlässigkeit, Verletzung von Leben, Körper oder Gesundheit und nach dem Produkthaftungsgesetz) bleibt unberührt. Der Haftungsrahmen des Vermieters gegenüber seinen Gästen bleibt unberührt.
## 12. Änderungen und Einstellung
(1) WEBklar darf die Technik des Buchungskalenders verbessern, solange der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Buchungskalender.
(2) WEBklar darf den Buchungskalender für eine Website mit einer Frist von vier Wochen beenden oder für alle Vermieter mit einer Frist von drei Monaten einstellen, wenn
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Vermieter einer dafür nötigen neuen Fassung nicht zustimmt (AVV Buchungskalender Nummer 12),
- Stripe die Anbindung nach Nr. 3 beendet oder wesentlich ändert, so dass die Online-Zahlung in der bisherigen Form nicht weiterbetrieben werden kann (dann entfällt nur die Online-Zahlung, soweit der Buchungskalender ohne sie weiterläuft), oder
- eine Sicherheitslücke sich nicht anders beheben lässt.
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar den Buchungskalender ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Vermieter darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.
(4) Eine Einstellung ohne einen Grund nach Absatz 2 ist ausgeschlossen. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
## 13. Hinweise
(1) **Keine Rechtsberatung.** Der Buchungskalender und der Muster-Text sind technische Hilfsmittel, keine Rechtsberatung (Nr. 6). Die rechtliche und steuerliche Einordnung seiner Vermietung, seiner Preisangaben und seines Angebots trägt der Vermieter.
(2) **Barrierefreiheit.** Das Widget ist für die Bedienung mit Tastatur und Hilfstechniken gestaltet (Beschriftungen der Tage, Bedienung mit Pfeiltasten, Fehlermeldungen als Live-Region). Eine Konformität nach WCAG 2.1 AA oder EN 301 549 sagt WEBklar nicht zu. Ob der Vermieter als Anbieter von Dienstleistungen im elektronischen Geschäftsverkehr unter das BFSG fällt (Ausnahme für Kleinstunternehmen, § 3 Abs. 3 BFSG), entscheidet und prüft der Vermieter.
@@ -0,0 +1,179 @@
# Leistungsbeschreibung Webshop
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo
## 1. Geltung
(1) Diese Leistungsbeschreibung regelt die Funktion „Webshop“. Mit dem Webshop verkauft der Kunde Waren über seine Website („Händler“); Käufer sind die Besucher seiner Website, die im Webshop bestellen („Käufer“). Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht. Sie richtet sich nur an Unternehmer, die in eigenem Namen und für eigene Rechnung verkaufen.
(2) Diese Leistungsbeschreibung gilt, wenn der Händler die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Händler die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Webshop. Einschalten kann der Händler erst, nachdem WEBklar seine Anfrage für den Webshop geprüft und freigegeben hat (Nr. 2 Abs. 1).
(3) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für den Webshop diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Webshop.
(4) Leistungsbeschreibung A Nr. 2 Abs. 3 und Nr. 4 sowie Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen (Formulare mit Server, „Rechtstexte … nicht enthalten“) stehen dem Webshop nicht entgegen. Das Widget des Webshops läuft auf dem Server von WEBklar, nicht auf dem Server der Website, und die Muster-Rechtstexte nach Nr. 6 sind Teil des Webshops.
## 2. Leistung
(1) Der Händler fragt den Webshop im Kundenportal an und beschreibt, was er verkauft. WEBklar prüft die Anfrage, vor allem nach Nr. 3, und gibt sie frei oder lehnt sie mit einer Begründung ab. Ein Anspruch auf Freigabe besteht nicht. Erst nach der Freigabe kann der Händler den Webshop einschalten.
(2) Die Einrichtung umfasst den Einbau eines Shop-Widgets an einer Stelle der Website des Händlers, die Seite des Webshops mit einem Verweis im Menü und den Link „Vertrag widerrufen“ in der Fußzeile der Seiten der Website, außerdem die Freischaltung durch WEBklar. Für jede Website wird ein Webshop eingerichtet. Zusätzlich steht dem Händler eine eigenständige Seite des Webshops bei WEBklar zur Verfügung (Standalone-Seite), etwa als Vorschau.
(3) Im Kundenportal verwaltet der Händler:
- Artikel mit Beschreibung, Bildern, Preis, Varianten, Bestand und den Pflichtangaben zum Artikel,
- Versand (nur nach Deutschland und Österreich) und Abholung mit Versandkosten und Lieferzeiten,
- Bestellungen mit Status (zum Beispiel bezahlt, versendet, abholbereit, abgeschlossen), die er als CSV-Datei exportieren kann,
- Erstattungen, ganz oder teilweise, über sein Stripe-Konto (Nr. 4),
- die Widerrufe, die Käufer über die Widerrufsfunktion erklärt haben,
- die Verbindung mit seinem Stripe-Konto, seine Einstellungen und seine Rechtstexte, deren bestätigte Fassungen WEBklar archiviert (Nr. 6 Abs. 5).
(4) Der Webshop enthält die Widerrufsfunktion nach § 356a BGB: Das Widget zeigt in seiner Fußzeile und die Standalone-Seite zeigt unter einer eigenen Adresse die Schaltfläche „Vertrag widerrufen“. Sie führt zu einem Formular und danach zur Bestätigungsschaltfläche „Widerruf bestätigen“. WEBklar speichert den Eingang, sendet dem Erklärenden sofort eine Eingangsbestätigung mit Inhalt, Datum und Uhrzeit und benachrichtigt den Händler. Die Funktion ist ohne Anmeldung erreichbar, auch wenn zur Erklärung keine Bestellung gefunden wird. Der Händler bearbeitet jeden Widerruf selbst (Nr. 5 Abs. 6).
(5) Der Webshop versendet im Namen des Händlers E-Mails an die Käufer: Eingang der Bestellung (bei einer Zahlung, die noch aussteht, etwa einer SEPA-Lastschrift, vor dem Vertragsschluss), Bestellbestätigung, Versand, Abholbereitschaft, Eingangsbestätigung eines Widerrufs, Erstattung und Hinweis auf eine fehlgeschlagene Zahlung. Absender ist die Versandadresse von WEBklar mit dem Namen des Shops, als Antwortadresse dient die E-Mail-Adresse des Händlers. Den Händler benachrichtigt der Webshop über neue Bestellungen und eingegangene Widerrufe.
(6) Der Webshop enthält einen Missbrauchsschutz gegen automatisierte Bestellungen und Widerrufe, darunter eine Obergrenze von drei offenen Bestellungen je IP-Adresse in 30 Minuten. Dafür speichert WEBklar mit der offenen Bestellung nur einen Hashwert der IP-Adresse, nicht die IP-Adresse selbst; er wird gelöscht, sobald die Bestellung bezahlt oder storniert wird (Nummer 3 Abs. 4 des AVV Webshop). Eine Telefonnummer des Käufers wird nicht erhoben und nicht gespeichert. Die Löschfristen für Bestelldaten nennt Nummer 9 des AVV Webshop.
(7) Nicht enthalten sind der Verkauf an Verbraucher außerhalb von Deutschland und Österreich, Rechnungen (Nr. 5 Abs. 7), die Verwaltung mehrerer Lager, Rabattcodes, Marktplatzfunktionen, die Anbindung an Warenwirtschaft oder Versanddienstleister, Mehrsprachigkeit und die Zahlung per PayPal (Nr. 4 Abs. 5). Zusätzliche Wünsche, etwa ein Sonderdesign, ein zweiter Webshop oder eine größere Zahl von Artikeln als vorgesehen, gehören nicht zur Einrichtung nach Nr. 9. WEBklar führt sie nach Absprache nach Stunden aus (Nr. 9 Abs. 4).
## 3. Ausgeschlossene Waren und Leistungen
(1) Über den Webshop dürfen nicht verkauft werden:
- alkoholische Getränke,
- Tabakwaren, E-Zigaretten und Zubehör,
- Arzneimittel und Medizinprodukte,
- Waffen, Munition, Feuerwerk und Sprengstoffe,
- Waren mit erotischem oder pornografischem Inhalt,
- lebende Tiere,
- digitale Inhalte und Downloads,
- Gutscheine, Geschenkkarten und Guthaben,
- Abonnements und andere wiederkehrende Zahlungen,
- Waren und Leistungen, die nach der Liste der verbotenen und eingeschränkten Geschäfte von Stripe nicht erlaubt sind, und
- Waren, deren Verkauf oder Versand nach deutschem, österreichischem oder europäischem Recht verboten ist.
(2) Der Händler liefert nur nach Deutschland und Österreich. Eine Lieferung in andere Länder bietet der Webshop nicht an.
(3) Der Händler bestätigt bei der Anfrage und beim Einschalten, dass er keine dieser Waren und Leistungen verkauft. WEBklar darf eine Anfrage ablehnen, wenn der Händler Waren nach Absatz 1 verkaufen will oder die Angaben dazu nicht ausreichen. Verkauft der Händler später Waren nach Absatz 1, darf WEBklar den Webshop nach einem Hinweis in Textform abschalten; bei einem Verstoß, der Rechte Dritter oder Käufer gefährdet, darf WEBklar ihn auch ohne vorherigen Hinweis abschalten und informiert den Händler unverzüglich. Für das Abschalten gilt Nr. 10 Abs. 2 und 3. Die bereits auf das Stundenkontingent angerechnete Einrichtungszeit wird daraus nicht gutgeschrieben, wenn der Händler den Verstoß zu vertreten hat.
## 4. Zahlungen über Stripe
(1) Zahlungen im Webshop laufen über Stripe Connect. Der Händler verbindet dazu sein eigenes Stripe-Konto mit dem Webshop. Den Vertrag über dieses Konto schließt der Händler unmittelbar mit Stripe; WEBklar ist daran nicht beteiligt. Für das Konto, die Prüfung des Händlers durch Stripe (zum Beispiel zur Identität) und die Auszahlungen gelten die Bedingungen von Stripe.
(2) Das Geld der Käufer fließt direkt an den Händler. WEBklar erhält und verwahrt kein Geld der Käufer, leitet keines weiter und beteiligt sich nicht am Umsatz des Händlers. WEBklar ist kein Zahlungsdienstleister. Die Gebühren von Stripe trägt der Händler; sie fallen nur zwischen dem Händler und Stripe an.
(3) Der Händler prüft selbst, ob sein Stripe-Konto Zahlungen annehmen und Auszahlungen leisten kann. WEBklar zeigt dem Händler im Kundenportal den Stand, den Stripe meldet, hat aber keinen Einfluss darauf, ob und wann Stripe ein Konto freischaltet, einschränkt oder sperrt, und haftet dafür nicht. Rückbuchungen, Streitfälle mit Käufern und Zahlungsausfälle regeln der Händler und Stripe.
(4) Der Händler erstattet Zahlungen im Kundenportal oder im Stripe-Dashboard. WEBklar trägt Erstattungen aus dem Stripe-Dashboard in die Bestellung nach. Eine Erstattung führt der Händler rechtzeitig aus (Nr. 5 Abs. 6).
(5) Der Webshop bietet die Zahlungsarten an, die Stripe im Checkout für das Konto des Händlers bereitstellt und der Händler in seinen Einstellungen wählt, zum Beispiel Karte und SEPA-Lastschrift. PayPal ist über Stripe für diese Art der Anbindung nicht verfügbar und deshalb nicht Teil des Webshops.
(6) Die Zahlungsseite selbst (Stripe Checkout) betreibt Stripe. Kartendaten und Kontodaten gibt der Käufer nur dort ein; WEBklar erhält sie nicht.
## 5. Pflichten des Händlers
(1) Der Händler hat ein laufendes Wartungs-Abo, ist Unternehmer und stellt sein eigenes Stripe-Konto bereit; die Anmeldung bei Stripe schließt er selbst ab. Der Webshop steht nur Kunden zur Verfügung, deren Kundenart „Betrieb“ ist. Der Händler verkauft in eigenem Namen. Für seine Produkte, Preise, Produktangaben und Rechtstexte (Allgemeine Geschäftsbedingungen, Widerrufsbelehrung, Impressum, Datenschutzerklärung) ist er selbst verantwortlich; WEBklar stellt Muster bereit und berät nicht rechtlich. Er verkauft keine der in Nr. 3 ausgeschlossenen Waren. Die Zahlungen seiner Käufer laufen über sein eigenes Stripe-Konto zu den Bedingungen von Stripe; WEBklar erhält und verwahrt kein Geld seiner Käufer und erhebt keine Umsatzbeteiligung.
(2) Er verantwortet seine Rechtstexte: Impressum, Datenschutzerklärung, Allgemeine Geschäftsbedingungen, Widerrufsbelehrung, Muster-Widerrufsformular sowie die Angaben zu Versand und Zahlung. Die Mustertexte nach Nr. 6 prüft er, passt sie an seinen Betrieb an und bestätigt sie im Kundenportal, bevor er den Webshop öffnet. Er trägt vor dem Einschalten die Adressen von Impressum und Datenschutzerklärung seiner Website ein.
(3) Er verantwortet seine Artikel, Preise und Produktangaben. Dazu gehören richtige Preise einschließlich Steuerausweis (Umsatzsteuer oder Hinweis auf § 19 UStG), Grundpreise, Versandkosten und Lieferzeiten, die Pflichtangaben für Lebensmittel (zum Beispiel Zutaten, Allergene, Füllmenge, verantwortlicher Lebensmittelunternehmer), die Angaben zur Produktsicherheit (zum Beispiel Hersteller, Warn- und Sicherheitshinweise), Widerrufsausschlüsse bei einzelnen Artikeln und die Rechte an seinen Texten und Bildern.
(4) Er liefert die Waren, wie bestellt und beschrieben, und trägt alle Pflichten eines Verkäufers gegenüber seinen Käufern, auch Gewährleistung, Kundenservice und Beschwerden.
(5) Er pflegt Bestand, Bestellstatus und Versandstatus im Kundenportal. Die Folgen falscher Angaben oder Statusmeldungen, die der Händler setzt, trägt er.
(6) Er bearbeitet Widerrufe und Erstattungen selbst, fristgerecht und nach dem Gesetz. Er entfernt den Link „Vertrag widerrufen“ nicht und verändert die Widerrufsfunktion nicht. Er prüft das Kundenportal regelmäßig auf neue Bestellungen und Widerrufe und lässt sich über die Benachrichtigungen informieren.
(7) Rechnungen und Belege an die Käufer erstellt der Händler selbst; WEBklar stellt sie nicht aus. Die Quittung von Stripe und die Bestellbestätigung des Webshops ersetzen keine Rechnung nach dem Umsatzsteuergesetz, soweit der Händler eine ausstellen muss.
(8) Er erfüllt seine Aufbewahrungspflichten selbst, etwa nach der Abgabenordnung und dem Handelsgesetzbuch. WEBklar ist kein Archiv für die Buchführung und kein Buchführungssystem (das Archiv der Rechtstexte nach Nr. 6 Abs. 5 dient nur dem Nachweis der Texte): Personenbezogene Käuferdaten werden nach Nummer 9 des AVV Webshop anonymisiert und nach dem Ende des Webshops gelöscht. Der Händler exportiert seine Bestellungen deshalb rechtzeitig als CSV-Datei und bewahrt sie mit den Belegen von Stripe auf.
(9) Er hält die Vorgaben nach Nr. 3 ein und verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets nicht.
(10) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz für den Webshop ein; das gehört zur Einrichtung. Liegt sie woanders, erhält der Händler den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt er verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Händler darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
(11) Der Händler informiert seine Käufer nach Art. 13 DSGVO. Die Rollen im Datenschutz regelt der AVV Webshop: Der Händler ist Verantwortlicher, WEBklar Auftragsverarbeiter, Stripe Vertragspartner des Händlers.
## 6. Muster-Rechtstexte, keine Rechtsberatung
(1) WEBklar stellt dem Händler Mustertexte bereit: eine Muster-Widerrufsbelehrung, ein Muster-Widerrufsformular, Allgemeine Geschäftsbedingungen und den Text zu Versand und Zahlung. Das Widget zeigt außerdem die Hinweise, die für die Kasse und die Bestellbestätigung vorgesehen sind, darunter den Hinweis auf die gesetzliche Gewährleistung.
(2) Die Mustertexte sind **Muster ohne Gewähr**. Sie passen zur Standardkonfiguration des Webshops und können auf den Betrieb des Händlers nicht zugeschnitten sein. WEBklar sorgt dafür, dass sie die Technik des Webshops richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Eine rechtliche Beratung im Einzelfall schuldet WEBklar nicht (kein Rechtsrat im Sinne des Rechtsdienstleistungsgesetzes).
(3) WEBklar empfiehlt, die Rechtstexte vor der Öffnung des Webshops durch eine Rechtsanwältin oder einen Rechtsanwalt prüfen zu lassen, besonders bei besonderen Waren (zum Beispiel Lebensmitteln, Kosmetik, Textilien mit Pflegekennzeichnung, Elektrogeräten).
(4) Der Händler bleibt für seine Rechtstexte verantwortlich, auch wenn er Mustertexte unverändert übernimmt (Nr. 5 Abs. 2).
(5) Bestätigt der Händler seine Rechtstexte im Kundenportal, legt WEBklar jede bestätigte Fassung mit Prüfsumme, Zeitpunkt und der bestätigenden Person im Archiv ab. Die Bestellbestätigung zeigt dem Käufer die archivierte Fassung, die zum Zeitpunkt seiner Bestellung galt. WEBklar löscht das Archiv mit den übrigen Daten des Webshops (Nr. 10 Abs. 3; Nummer 8 Abs. 5 des AVV Webshop). Der Händler bewahrt die Fassungen, die er für eigene Aufbewahrungspflichten braucht, selbst auf (Nr. 5 Abs. 8).
## 7. Verfügbarkeit und Störungen
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für den Webshop. WEBklar sagt für den Webshop keine bestimmte Verfügbarkeit zu, die über den Betrieb der Website im Abo hinausgeht. Störungen des Webshops beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen).
(2) Angekündigte Wartungsfenster (§ 5 der Abo-Bedingungen) können den Webshop kurzzeitig unterbrechen. Während einer Unterbrechung sind Bestellungen im Widget nicht möglich; das Widget zeigt dann einen Ausweichtext.
(3) Zahlungen übernimmt der Webshop über Benachrichtigungen von Stripe und einen regelmäßigen Abgleich in die Bestellung. Fällt die Verbindung zu Stripe oder zum Server aus, kann sich die Bestätigung einer Bestellung verzögern. Ausfälle von Stripe, von Zahlungsarten und von Banken liegen nicht im Einflussbereich von WEBklar.
(4) Alle Zeiten gelten in der Zeitzone Europe/Berlin.
(5) WEBklar übernimmt keine Gewähr dafür, dass E-Mails zugestellt und gelesen werden. Das hängt vom Postfach des Empfängers und von den Mailgrenzen seines Anbieters ab.
(6) Die gesetzlichen Rechte des Händlers sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.
## 8. Haftung
(1) Der Webshop ist ein Hilfsmittel für den Verkauf des Händlers. Der Vertrag über die Ware besteht allein zwischen dem Händler und dem Käufer; WEBklar ist daran nicht beteiligt.
(2) Für entgangenen Gewinn und Folgeschäden aus Störungen des Webshops, aus Fehlern bei Bestand oder Bestellstatus und aus nicht zugestellten E-Mails haftet WEBklar nur im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen.
(3) WEBklar haftet nicht für Inhalte des Händlers, insbesondere nicht für seine Artikel, Preise, Produktangaben und Rechtstexte, auch nicht für Mustertexte, die er übernommen hat (Nr. 6), und nicht für die Erfüllung seiner Pflichten gegenüber den Käufern (Nr. 5).
(4) WEBklar haftet nicht für Ausfälle, Sperren oder Entscheidungen von Stripe und der Zahlungsarten sowie für Rückbuchungen und Zahlungsausfälle (Nr. 4 Abs. 3).
(5) Die Haftung nach § 13 Abs. 1 der Abo-Bedingungen (unter anderem bei Vorsatz, grober Fahrlässigkeit, Verletzung von Leben, Körper oder Gesundheit und nach dem Produkthaftungsgesetz) bleibt unberührt. Der Haftungsrahmen des Händlers gegenüber seinen Käufern bleibt unberührt.
## 9. Einrichtung, Abrechnung und Widerruf
(1) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Händlers. Für den Betrieb des Webshops berechnet WEBklar nichts weiter; es gibt keine laufende Gebühr und keine Beteiligung am Umsatz.
(2) Die Einrichtung dauert nach der Erfahrung von WEBklar meist 2 bis 3 Wochen ab dem Einschalten. Sie hängt davon ab, wie schnell der Händler Stripe-Konto, Artikel und Rechtstexte bereitstellt und wie umfangreich der Einbau ist. Die Dauer ist eine Einschätzung und keine verbindliche Zusage eines Termins.
(3) Hat WEBklar die Funktion auf Wunsch des Händlers schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.
(4) Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Händlers in Textform aus; es gilt § 4 der Abo-Bedingungen. Welche Wünsche nicht zur Einrichtung gehören, beschreibt Nr. 2 Abs. 7.
(5) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion. Hat der Händler die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat.
## 10. Laufzeit und Ende
(1) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo oder durch den Widerruf eines Wechsels, schaltet WEBklar die Funktion spätestens am folgenden Tag ab. WEBklar informiert den Händler per E-Mail.
(2) Mit dem Abschalten nimmt der Webshop keine neuen Bestellungen mehr an, und das Widget zeigt den Ausweichtext. Bestellungen, die noch nicht bezahlt sind, laufen aus. Die Bestellungen und Widerrufe bleiben im Kundenportal 90 Tage lesbar, und der Händler kann sie als CSV-Datei exportieren. Die Widerrufsfunktion bleibt bis zur Löschung nach Absatz 3 erreichbar, soweit das Widget noch eingebunden ist. Der Händler erfüllt seine Pflichten gegenüber den Käufern weiter (Nr. 5) und informiert sie, wenn er den Webshop nicht mehr anbietet.
(3) WEBklar löscht alle Daten des Webshops, also Artikel, Bilder, Bestellungen, Widerrufe, Einstellungen, Rechtstexte und das Archiv der bestätigten Fassungen, 90 Tage nach dem Abschalten. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Käufern (Nummer 9 Abs. 6 des AVV Webshop).
(4) Das Stripe-Konto bleibt beim Händler. Das Ende des Webshops berührt weder das Konto noch die Auszahlungen noch den Vertrag mit Stripe. Der Händler kann die Verbindung zwischen dem Konto und dem Webshop im Stripe-Dashboard beenden.
(5) Beginnt innerhalb der 90 Tage wieder ein Wartungs-Abo, schaltet WEBklar den Webshop auf Wunsch des Händlers in Textform mit den vorhandenen Daten und Einstellungen ohne weitere Kosten wieder ein. Nach der Löschung ist eine neue Anfrage und eine neue Einrichtung nötig (Einrichtung nach Nr. 9).
(6) Endet das Wartungs-Abo, bevor WEBklar den Webshop freigeschaltet hat, bricht WEBklar die Einrichtung ab; ein Vorschuss nach Nr. 9 Abs. 1 verfällt.
(7) Auf Wunsch des Händlers in Textform schaltet WEBklar den Webshop jederzeit ab; für den Widerruf gilt Nr. 9 Abs. 5.
## 11. Änderungen und Einstellung
(1) WEBklar darf die Technik des Webshops verbessern, solange der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Webshop.
(2) WEBklar darf den Webshop für eine Website mit einer Frist von vier Wochen beenden oder für alle Händler mit einer Frist von drei Monaten einstellen, wenn
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Händler einer dafür nötigen neuen Fassung nicht zustimmt (AVV Webshop Nummer 12),
- Stripe die Anbindung nach Nr. 4 beendet oder wesentlich ändert, so dass der Webshop in der bisherigen Form nicht weiterbetrieben werden kann, oder
- eine Sicherheitslücke sich nicht anders beheben lässt.
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar den Webshop ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Händler darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.
(4) Eine Einstellung ohne einen Grund nach Absatz 2 ist ausgeschlossen. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
## 12. Hinweise
(1) **Keine Rechtsberatung.** Der Webshop und die Mustertexte sind technische Hilfsmittel, keine Rechtsberatung (Nr. 6). Die rechtliche Einordnung seiner Waren, seiner Preisangaben und seines Angebots trägt der Händler.
(2) **Barrierefreiheit.** Das Widget ist für die Bedienung mit Tastatur und Hilfstechniken gestaltet (Beschriftungen, Fokusführung, Fehlermeldungen als Live-Region). Eine Konformität nach WCAG 2.1 AA oder EN 301 549 sagt WEBklar nicht zu. Ob der Händler als Anbieter von Dienstleistungen im elektronischen Geschäftsverkehr unter das BFSG fällt (Ausnahme für Kleinstunternehmen, § 3 Abs. 3 BFSG), entscheidet und prüft der Händler.
@@ -0,0 +1,181 @@
# Leistungsbeschreibung Webshop
**Fassung 2026-10-05** · Zusatzbedingungen zum Vertrag über den Betrieb der Website
## 1. Geltung
(1) Diese Leistungsbeschreibung regelt die Funktion „Webshop“. Mit dem Webshop verkauft der Kunde Waren über seine Website („Händler“); Käufer sind die Besucher seiner Website, die im Webshop bestellen („Käufer“). Die Leistungsbeschreibung ist eine Zusatzleistung zum Vertrag über den Betrieb der Website: Sie setzt voraus, dass WEBklar die Website des Händlers aufgrund eines Vertrags betreibt, etwa im Hosting-Abo, im Wartungs-Abo oder aufgrund eines anderen Vertrags über den Betrieb der Website. Sie richtet sich nur an Unternehmer, die in eigenem Namen und für eigene Rechnung verkaufen.
(2) Sie wird vereinbart, wenn der Händler die Einrichtung des Webshops im Kundenportal bestellt und dabei dieser Leistungsbeschreibung, dem Auftragsverarbeitungsvertrag Webshop („AVV Webshop“) und der Widerrufsbelehrung zustimmt. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Webshop. Bestellen kann der Händler erst, nachdem WEBklar seine Anfrage für den Webshop geprüft und freigegeben hat (Nr. 2 Abs. 1).
(3) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für den Webshop diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Webshop.
(4) Leistungsbeschreibung A Nr. 2 Abs. 3 und Nr. 4 sowie Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen (Formulare mit Server, „Rechtstexte … nicht enthalten“) stehen dem Webshop nicht entgegen. Das Widget des Webshops läuft auf dem Server von WEBklar, nicht auf dem Server der Website, und die Muster-Rechtstexte nach Nr. 6 sind Teil des Webshops. Das gilt auch im Hosting-Abo.
## 2. Leistung
(1) Der Händler fragt den Webshop im Kundenportal an und beschreibt, was er verkauft. WEBklar prüft die Anfrage, vor allem nach Nr. 3, und gibt sie frei oder lehnt sie mit einer Begründung ab. Ein Anspruch auf Freigabe besteht nicht. Erst nach der Freigabe kann der Händler die Einrichtung bestellen.
(2) Die Einrichtung umfasst den Einbau eines Shop-Widgets an einer Stelle der Website des Händlers, die Seite des Webshops mit einem Verweis im Menü und den Link „Vertrag widerrufen“ in der Fußzeile der Seiten der Website, außerdem die Freischaltung durch WEBklar. Für jede Website wird ein Webshop eingerichtet. Zusätzlich steht dem Händler eine eigenständige Seite des Webshops bei WEBklar zur Verfügung (Standalone-Seite), etwa als Vorschau.
(3) Im Kundenportal verwaltet der Händler:
- Artikel mit Beschreibung, Bildern, Preis, Varianten, Bestand und den Pflichtangaben zum Artikel,
- Versand (nur nach Deutschland und Österreich) und Abholung mit Versandkosten und Lieferzeiten,
- Bestellungen mit Status (zum Beispiel bezahlt, versendet, abholbereit, abgeschlossen), die er als CSV-Datei exportieren kann,
- Erstattungen, ganz oder teilweise, über sein Stripe-Konto (Nr. 4),
- die Widerrufe, die Käufer über die Widerrufsfunktion erklärt haben,
- die Verbindung mit seinem Stripe-Konto, seine Einstellungen und seine Rechtstexte, deren bestätigte Fassungen WEBklar archiviert (Nr. 6 Abs. 5).
(4) Der Webshop enthält die Widerrufsfunktion nach § 356a BGB: Das Widget zeigt in seiner Fußzeile und die Standalone-Seite zeigt unter einer eigenen Adresse die Schaltfläche „Vertrag widerrufen“. Sie führt zu einem Formular und danach zur Bestätigungsschaltfläche „Widerruf bestätigen“. WEBklar speichert den Eingang, sendet dem Erklärenden sofort eine Eingangsbestätigung mit Inhalt, Datum und Uhrzeit und benachrichtigt den Händler. Die Funktion ist ohne Anmeldung erreichbar, auch wenn zur Erklärung keine Bestellung gefunden wird. Der Händler bearbeitet jeden Widerruf selbst (Nr. 5 Abs. 6).
(5) Der Webshop versendet im Namen des Händlers E-Mails an die Käufer: Eingang der Bestellung (bei einer Zahlung, die noch aussteht, etwa einer SEPA-Lastschrift, vor dem Vertragsschluss), Bestellbestätigung, Versand, Abholbereitschaft, Eingangsbestätigung eines Widerrufs, Erstattung und Hinweis auf eine fehlgeschlagene Zahlung. Absender ist die Versandadresse von WEBklar mit dem Namen des Shops, als Antwortadresse dient die E-Mail-Adresse des Händlers. Den Händler benachrichtigt der Webshop über neue Bestellungen und eingegangene Widerrufe.
(6) Der Webshop enthält einen Missbrauchsschutz gegen automatisierte Bestellungen und Widerrufe, darunter eine Obergrenze von drei offenen Bestellungen je IP-Adresse in 30 Minuten. Dafür speichert WEBklar mit der offenen Bestellung nur einen Hashwert der IP-Adresse, nicht die IP-Adresse selbst; er wird gelöscht, sobald die Bestellung bezahlt oder storniert wird (Nummer 3 Abs. 4 des AVV Webshop). Eine Telefonnummer des Käufers wird nicht erhoben und nicht gespeichert. Die Löschfristen für Bestelldaten nennt Nummer 9 des AVV Webshop.
(7) Nicht enthalten sind der Verkauf an Verbraucher außerhalb von Deutschland und Österreich, Rechnungen (Nr. 5 Abs. 7), die Verwaltung mehrerer Lager, Rabattcodes, Marktplatzfunktionen, die Anbindung an Warenwirtschaft oder Versanddienstleister, Mehrsprachigkeit und die Zahlung per PayPal (Nr. 4 Abs. 5). Zusätzliche Wünsche, etwa ein Sonderdesign, ein zweiter Webshop oder eine größere Zahl von Artikeln als vorgesehen, gehören nicht zur Einrichtung nach Nr. 9. WEBklar führt sie nach Absprache nach Stunden aus (Nr. 9 Abs. 5).
## 3. Ausgeschlossene Waren und Leistungen
(1) Über den Webshop dürfen nicht verkauft werden:
- alkoholische Getränke,
- Tabakwaren, E-Zigaretten und Zubehör,
- Arzneimittel und Medizinprodukte,
- Waffen, Munition, Feuerwerk und Sprengstoffe,
- Waren mit erotischem oder pornografischem Inhalt,
- lebende Tiere,
- digitale Inhalte und Downloads,
- Gutscheine, Geschenkkarten und Guthaben,
- Abonnements und andere wiederkehrende Zahlungen,
- Waren und Leistungen, die nach der Liste der verbotenen und eingeschränkten Geschäfte von Stripe nicht erlaubt sind, und
- Waren, deren Verkauf oder Versand nach deutschem, österreichischem oder europäischem Recht verboten ist.
(2) Der Händler liefert nur nach Deutschland und Österreich. Eine Lieferung in andere Länder bietet der Webshop nicht an.
(3) Der Händler bestätigt bei der Anfrage und bei der Bestellung, dass er keine dieser Waren und Leistungen verkauft. WEBklar darf eine Anfrage ablehnen, wenn der Händler Waren nach Absatz 1 verkaufen will oder die Angaben dazu nicht ausreichen. Verkauft der Händler später Waren nach Absatz 1, darf WEBklar den Webshop nach einem Hinweis in Textform abschalten; bei einem Verstoß, der Rechte Dritter oder Käufer gefährdet, darf WEBklar ihn auch ohne vorherigen Hinweis abschalten und informiert den Händler unverzüglich. Für das Abschalten gilt Nr. 10 Abs. 2 und 3. Eine Erstattung des Einrichtungspreises folgt daraus nicht, wenn der Händler den Verstoß zu vertreten hat.
## 4. Zahlungen über Stripe
(1) Zahlungen im Webshop laufen über Stripe Connect. Der Händler verbindet dazu sein eigenes Stripe-Konto mit dem Webshop. Den Vertrag über dieses Konto schließt der Händler unmittelbar mit Stripe; WEBklar ist daran nicht beteiligt. Für das Konto, die Prüfung des Händlers durch Stripe (zum Beispiel zur Identität) und die Auszahlungen gelten die Bedingungen von Stripe.
(2) Das Geld der Käufer fließt direkt an den Händler. WEBklar erhält und verwahrt kein Geld der Käufer, leitet keines weiter und beteiligt sich nicht am Umsatz des Händlers. WEBklar ist kein Zahlungsdienstleister. Die Gebühren von Stripe trägt der Händler; sie fallen nur zwischen dem Händler und Stripe an.
(3) Der Händler prüft selbst, ob sein Stripe-Konto Zahlungen annehmen und Auszahlungen leisten kann. WEBklar zeigt dem Händler im Kundenportal den Stand, den Stripe meldet, hat aber keinen Einfluss darauf, ob und wann Stripe ein Konto freischaltet, einschränkt oder sperrt, und haftet dafür nicht. Rückbuchungen, Streitfälle mit Käufern und Zahlungsausfälle regeln der Händler und Stripe.
(4) Der Händler erstattet Zahlungen im Kundenportal oder im Stripe-Dashboard. WEBklar trägt Erstattungen aus dem Stripe-Dashboard in die Bestellung nach. Eine Erstattung führt der Händler rechtzeitig aus (Nr. 5 Abs. 6).
(5) Der Webshop bietet die Zahlungsarten an, die Stripe im Checkout für das Konto des Händlers bereitstellt und der Händler in seinen Einstellungen wählt, zum Beispiel Karte und SEPA-Lastschrift. PayPal ist über Stripe für diese Art der Anbindung nicht verfügbar und deshalb nicht Teil des Webshops.
(6) Die Zahlungsseite selbst (Stripe Checkout) betreibt Stripe. Kartendaten und Kontodaten gibt der Käufer nur dort ein; WEBklar erhält sie nicht.
## 5. Pflichten des Händlers
(1) Der Händler hat einen laufenden Vertrag mit WEBklar über den Betrieb seiner Website (Hosting-Abo, Wartungs-Abo oder ein anderer Vertrag über den Betrieb der Website), ist Unternehmer und verkauft in eigenem Namen. Er stellt sein eigenes Stripe-Konto bereit und schließt die Anmeldung bei Stripe selbst ab.
(2) Er verantwortet seine Rechtstexte: Impressum, Datenschutzerklärung, Allgemeine Geschäftsbedingungen, Widerrufsbelehrung, Muster-Widerrufsformular sowie die Angaben zu Versand und Zahlung. Die Mustertexte nach Nr. 6 prüft er, passt sie an seinen Betrieb an und bestätigt sie im Kundenportal, bevor er den Webshop öffnet. Er trägt vor der Bestellung die Adressen von Impressum und Datenschutzerklärung seiner Website ein.
(3) Er verantwortet seine Artikel, Preise und Produktangaben. Dazu gehören richtige Preise einschließlich Steuerausweis (Umsatzsteuer oder Hinweis auf § 19 UStG), Grundpreise, Versandkosten und Lieferzeiten, die Pflichtangaben für Lebensmittel (zum Beispiel Zutaten, Allergene, Füllmenge, verantwortlicher Lebensmittelunternehmer), die Angaben zur Produktsicherheit (zum Beispiel Hersteller, Warn- und Sicherheitshinweise), Widerrufsausschlüsse bei einzelnen Artikeln und die Rechte an seinen Texten und Bildern.
(4) Er liefert die Waren, wie bestellt und beschrieben, und trägt alle Pflichten eines Verkäufers gegenüber seinen Käufern, auch Gewährleistung, Kundenservice und Beschwerden.
(5) Er pflegt Bestand, Bestellstatus und Versandstatus im Kundenportal. Die Folgen falscher Angaben oder Statusmeldungen, die der Händler setzt, trägt er.
(6) Er bearbeitet Widerrufe und Erstattungen selbst, fristgerecht und nach dem Gesetz. Er entfernt den Link „Vertrag widerrufen“ nicht und verändert die Widerrufsfunktion nicht. Er prüft das Kundenportal regelmäßig auf neue Bestellungen und Widerrufe und lässt sich über die Benachrichtigungen informieren.
(7) Rechnungen und Belege an die Käufer erstellt der Händler selbst; WEBklar stellt sie nicht aus. Die Quittung von Stripe und die Bestellbestätigung des Webshops ersetzen keine Rechnung nach dem Umsatzsteuergesetz, soweit der Händler eine ausstellen muss.
(8) Er erfüllt seine Aufbewahrungspflichten selbst, etwa nach der Abgabenordnung und dem Handelsgesetzbuch. WEBklar ist kein Archiv für die Buchführung und kein Buchführungssystem (das Archiv der Rechtstexte nach Nr. 6 Abs. 5 dient nur dem Nachweis der Texte): Personenbezogene Käuferdaten werden nach Nummer 9 des AVV Webshop anonymisiert und nach dem Ende des Webshops gelöscht. Der Händler exportiert seine Bestellungen deshalb rechtzeitig als CSV-Datei und bewahrt sie mit den Belegen von Stripe auf.
(9) Er hält die Vorgaben nach Nr. 3 ein und verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets nicht.
(10) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz für den Webshop ein; das gehört zur Einrichtung. Liegt sie woanders, erhält der Händler den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt er verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Händler darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
(11) Der Händler informiert seine Käufer nach Art. 13 DSGVO. Die Rollen im Datenschutz regelt der AVV Webshop: Der Händler ist Verantwortlicher, WEBklar Auftragsverarbeiter, Stripe Vertragspartner des Händlers.
## 6. Muster-Rechtstexte, keine Rechtsberatung
(1) WEBklar stellt dem Händler Mustertexte bereit: eine Muster-Widerrufsbelehrung, ein Muster-Widerrufsformular, Allgemeine Geschäftsbedingungen und den Text zu Versand und Zahlung. Das Widget zeigt außerdem die Hinweise, die für die Kasse und die Bestellbestätigung vorgesehen sind, darunter den Hinweis auf die gesetzliche Gewährleistung.
(2) Die Mustertexte sind **Muster ohne Gewähr**. Sie passen zur Standardkonfiguration des Webshops und können auf den Betrieb des Händlers nicht zugeschnitten sein. WEBklar sorgt dafür, dass sie die Technik des Webshops richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Eine rechtliche Beratung im Einzelfall schuldet WEBklar nicht (kein Rechtsrat im Sinne des Rechtsdienstleistungsgesetzes).
(3) WEBklar empfiehlt, die Rechtstexte vor der Öffnung des Webshops durch eine Rechtsanwältin oder einen Rechtsanwalt prüfen zu lassen, besonders bei besonderen Waren (zum Beispiel Lebensmitteln, Kosmetik, Textilien mit Pflegekennzeichnung, Elektrogeräten).
(4) Der Händler bleibt für seine Rechtstexte verantwortlich, auch wenn er Mustertexte unverändert übernimmt (Nr. 5 Abs. 2).
(5) Bestätigt der Händler seine Rechtstexte im Kundenportal, legt WEBklar jede bestätigte Fassung mit Prüfsumme, Zeitpunkt und der bestätigenden Person im Archiv ab. Die Bestellbestätigung zeigt dem Käufer die archivierte Fassung, die zum Zeitpunkt seiner Bestellung galt. WEBklar löscht das Archiv mit den übrigen Daten des Webshops (Nr. 10 Abs. 3; Nummer 8 Abs. 5 des AVV Webshop). Der Händler bewahrt die Fassungen, die er für eigene Aufbewahrungspflichten braucht, selbst auf (Nr. 5 Abs. 8).
## 7. Verfügbarkeit und Störungen
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für den Webshop. WEBklar sagt für den Webshop keine bestimmte Verfügbarkeit zu, die über den Betrieb der Website im Abo hinausgeht. Störungen des Webshops beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen).
(2) Angekündigte Wartungsfenster (§ 5 der Abo-Bedingungen) können den Webshop kurzzeitig unterbrechen. Während einer Unterbrechung sind Bestellungen im Widget nicht möglich; das Widget zeigt dann einen Ausweichtext.
(3) Zahlungen übernimmt der Webshop über Benachrichtigungen von Stripe und einen regelmäßigen Abgleich in die Bestellung. Fällt die Verbindung zu Stripe oder zum Server aus, kann sich die Bestätigung einer Bestellung verzögern. Ausfälle von Stripe, von Zahlungsarten und von Banken liegen nicht im Einflussbereich von WEBklar.
(4) Alle Zeiten gelten in der Zeitzone Europe/Berlin.
(5) WEBklar übernimmt keine Gewähr dafür, dass E-Mails zugestellt und gelesen werden. Das hängt vom Postfach des Empfängers und von den Mailgrenzen seines Anbieters ab.
(6) Die gesetzlichen Rechte des Händlers sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.
## 8. Haftung
(1) Der Webshop ist ein Hilfsmittel für den Verkauf des Händlers. Der Vertrag über die Ware besteht allein zwischen dem Händler und dem Käufer; WEBklar ist daran nicht beteiligt.
(2) Für entgangenen Gewinn und Folgeschäden aus Störungen des Webshops, aus Fehlern bei Bestand oder Bestellstatus und aus nicht zugestellten E-Mails haftet WEBklar nur im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen.
(3) WEBklar haftet nicht für Inhalte des Händlers, insbesondere nicht für seine Artikel, Preise, Produktangaben und Rechtstexte, auch nicht für Mustertexte, die er übernommen hat (Nr. 6), und nicht für die Erfüllung seiner Pflichten gegenüber den Käufern (Nr. 5).
(4) WEBklar haftet nicht für Ausfälle, Sperren oder Entscheidungen von Stripe und der Zahlungsarten sowie für Rückbuchungen und Zahlungsausfälle (Nr. 4 Abs. 3).
(5) Die Haftung nach § 13 Abs. 1 der Abo-Bedingungen (unter anderem bei Vorsatz, grober Fahrlässigkeit, Verletzung von Leben, Körper oder Gesundheit und nach dem Produkthaftungsgesetz) bleibt unberührt. Der Haftungsrahmen des Händlers gegenüber seinen Käufern bleibt unberührt.
## 9. Preis, Abrechnung und Widerruf
(1) Die Einrichtung kostet 299 € einmalig je Website-Projekt, ohne Umsatzsteuer gemäß § 19 UStG. Für den Betrieb des Webshops berechnet WEBklar nichts weiter; es gibt keine laufende Gebühr und keine Beteiligung am Umsatz. Der Webshop läuft, solange WEBklar die Website betreibt (Nr. 10).
(2) Die Einrichtung dauert nach der Erfahrung von WEBklar meist 2 bis 3 Wochen ab der Bestellung. Sie hängt davon ab, wie schnell der Händler Stripe-Konto, Artikel und Rechtstexte bereitstellt und wie umfangreich der Einbau ist. Die Dauer ist eine Einschätzung und keine verbindliche Zusage eines Termins.
(3) Der Händler bezahlt bei der Bestellung im Kundenportal über Stripe und erhält eine Rechnung.
(4) Der Preis steht neben den Leistungen eines Abos: Die Einrichtung wird weder auf ein Stundenkontingent angerechnet noch damit bezahlt.
(5) Mehraufwand, der nicht zur Einrichtung gehört (Nr. 2 Abs. 7), führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Händlers in Textform aus. WEBklar nennt vorher den geschätzten Aufwand und berechnet 100 € je Stunde in Schritten von 15 Minuten, ohne Umsatzsteuer gemäß § 19 UStG (§ 4 der Abo-Bedingungen). Der Mehraufwand wird gesondert berechnet und nicht auf ein Stundenkontingent angerechnet.
(6) Verbraucher können die Bestellung der Einrichtung nach der Widerrufsbelehrung widerrufen. Belehrung und Muster-Widerrufsformular stehen in der Anlage „Widerrufsbelehrung und Muster-Widerrufsformular (Einrichtung von Funktionen)“. Ob das Widerrufsrecht erlischt und ob der Händler einen angemessenen Betrag für die bis dahin erbrachte Leistung schuldet, richtet sich nach der Zustimmung, die er bei der Bestellung gibt, und nach den Hinweisen zum Widerruf in der Anlage. Mit dem Widerruf endet der Webshop.
## 10. Laufzeit und Ende
(1) Der Webshop läuft, solange WEBklar die Website des Händlers betreibt. Endet der Vertrag über den Betrieb der Website, endet auch der Webshop; WEBklar schaltet ihn zum Vertragsende ab und informiert den Händler vorher per E-Mail. Das gilt auch beim Widerruf dieses Vertrags.
(2) Mit dem Abschalten nimmt der Webshop keine neuen Bestellungen mehr an, und das Widget zeigt den Ausweichtext. Bestellungen, die noch nicht bezahlt sind, laufen aus. Die Bestellungen und Widerrufe bleiben im Kundenportal 90 Tage lesbar, und der Händler kann sie als CSV-Datei exportieren. Die Widerrufsfunktion bleibt bis zur Löschung nach Absatz 3 erreichbar, soweit das Widget noch eingebunden ist. Der Händler erfüllt seine Pflichten gegenüber den Käufern weiter (Nr. 5) und informiert sie, wenn er den Webshop nicht mehr anbietet.
(3) WEBklar löscht alle Daten des Webshops, also Artikel, Bilder, Bestellungen, Widerrufe, Einstellungen, Rechtstexte und das Archiv der bestätigten Fassungen, 90 Tage nach dem Abschalten. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Käufern (Nummer 9 Abs. 6 des AVV Webshop).
(4) Das Stripe-Konto bleibt beim Händler. Das Ende des Webshops berührt weder das Konto noch die Auszahlungen noch den Vertrag mit Stripe. Der Händler kann die Verbindung zwischen dem Konto und dem Webshop im Stripe-Dashboard beenden.
(5) Beginnt innerhalb der 90 Tage wieder ein Vertrag über den Betrieb der Website, schaltet WEBklar den Webshop auf Wunsch des Händlers in Textform mit den vorhandenen Daten und Einstellungen ohne weitere Kosten wieder ein. Nach der Löschung ist eine neue Anfrage und eine neue Einrichtung nötig (neuer Preis nach Nr. 9).
(6) Endet der Vertrag über den Betrieb der Website, bevor WEBklar den Webshop freigeschaltet hat, erstattet WEBklar den Einrichtungspreis vollständig.
(7) Auf Wunsch des Händlers in Textform schaltet WEBklar den Webshop jederzeit ab. Eine Erstattung des Einrichtungspreises folgt daraus nicht; für den Widerruf gilt Nr. 9 Abs. 6.
## 11. Änderungen und Einstellung
(1) WEBklar darf die Technik des Webshops verbessern, solange der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Webshop.
(2) WEBklar darf den Webshop für eine Website mit einer Frist von vier Wochen beenden oder für alle Händler mit einer Frist von drei Monaten einstellen, wenn
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Händler einer dafür nötigen neuen Fassung nicht zustimmt (AVV Webshop Nummer 12),
- Stripe die Anbindung nach Nr. 4 beendet oder wesentlich ändert, so dass der Webshop in der bisherigen Form nicht weiterbetrieben werden kann, oder
- eine Sicherheitslücke sich nicht anders beheben lässt.
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar den Webshop ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Händler darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.
(4) Eine Einstellung ohne einen Grund nach Absatz 2 ist ausgeschlossen. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
## 12. Hinweise
(1) **Keine Rechtsberatung.** Der Webshop und die Mustertexte sind technische Hilfsmittel, keine Rechtsberatung (Nr. 6). Die rechtliche Einordnung seiner Waren, seiner Preisangaben und seines Angebots trägt der Händler.
(2) **Barrierefreiheit.** Das Widget ist für die Bedienung mit Tastatur und Hilfstechniken gestaltet (Beschriftungen, Fokusführung, Fehlermeldungen als Live-Region). Eine Konformität nach WCAG 2.1 AA oder EN 301 549 sagt WEBklar nicht zu. Ob der Händler als Anbieter von Dienstleistungen im elektronischen Geschäftsverkehr unter das BFSG fällt (Ausnahme für Kleinstunternehmen, § 3 Abs. 3 BFSG), entscheidet und prüft der Händler.
@@ -0,0 +1,109 @@
# Leistungsbeschreibung Website-Analyse
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Hosting-Abo und zum Wartungs-Abo
## 1. Geltung
(1) Funktion im Sinne dieser Leistungsbeschreibung ist die Website-Analyse. Diese Leistungsbeschreibung gilt, wenn der Kunde die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Kunde die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Die Funktion ist in beiden Abos enthalten. Sie setzt ein laufendes Hosting-Abo oder Wartungs-Abo voraus. Sie wird zusammen mit dem Auftragsverarbeitungsvertrag Website-Analyse („AVV Website-Analyse“) vereinbart. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Website-Analyse.
(2) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für die Website-Analyse diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Website-Analyse.
(3) Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen („Rechtstexte … nicht enthalten“) gilt nicht für die Zusatzseite nach Nr. 2 Abs. 2 und den Hinweis in der Datenschutzerklärung nach Nr. 4 Abs. 4. Beides ist Teil der Website-Analyse.
(4) Das Einschalten der Website-Analyse ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen.
## 2. Leistung
(1) Die Website-Analyse misst, wie die öffentliche Website des Kunden genutzt wird. Sie ist im Hosting-Abo und im Wartungs-Abo ohne Aufpreis enthalten. Die Einrichtung der Website-Analyse ist für den Kunden kostenlos. WEBklar rechnet sie weder auf ein Stundenkontingent an noch berechnet WEBklar sie gesondert.
(2) Beim Veröffentlichen der Website fügt WEBklar Folgendes ein:
- ein Messskript in jede HTML-Datei der Website,
- ein Einwilligungsbanner am unteren Rand mit den gleich gestalteten Schaltflächen „Ablehnen“ und „Annehmen“ in der Akzentfarbe, die der Kunde beim Einschalten wählt (ist sie für weiße Schrift zu hell, nimmt das Banner Schwarz),
- eine Schaltfläche „Datenschutz-Einstellungen“ unten rechts, über die Besucher ihre Wahl jederzeit ändern können (hat die Website ein eigenes Element mit dem Attribut `data-webklar-consent`, übernimmt dieses Element die Aufgabe),
- eine Zusatzseite „/datenschutz-analyse.html“ mit den Datenschutzhinweisen zur Messung (Verantwortlicher: der Kunde).
(3) Gemessen wird:
- **solange der Besucher keine Wahl getroffen hat und sein Browser kein Signal nach dem nächsten Spiegelstrich sendet:** Seitenaufrufe mit Pfad (ohne Suchparameter), Land (aus der IP-Adresse, die nicht gespeichert wird), Geräteklasse und ein Zähler dafür, dass das Banner angezeigt wurde;
- **bei den Browser-Signalen „Global Privacy Control“ und „Do Not Track“:** nichts, und es erscheint kein Banner;
- **nach „Ablehnen“:** nichts mehr, außer einmal dem Zähler „abgelehnt“;
- **nach „Annehmen“ zusätzlich:** die Domain der Website, von der der Besucher kam, die Größenklasse des Browserfensters, eine täglich wechselnde pseudonyme Besucher-Kennung, Verweildauer, Scrolltiefe, Klickpositionen (Heatmap), die Art von Kontakt-Klicks (Telefon, E-Mail, WhatsApp, Link auf eine andere Website mit deren Domain) und ein Zähler „angenommen“.
Die einzelnen Datenarten nennt der AVV Website-Analyse in Nummer 3.
(4) Es werden keine Cookies gesetzt. Gespeichert wird auf dem Gerät des Besuchers nur seine Banner-Wahl im lokalen Speicher des Browsers, für 183 Tage.
(5) Die Auswertung steht im Kundenportal zur Verfügung: Kennzahlen und Diagramme für die letzten 7, 30 oder 90 Tage und eine Heatmap je Seite und Ansicht (Handy, Tablet, Computer) aus den bis zu 5.000 jüngsten Klicks der letzten 30 Tage. Die Daten liegen auf dem Server von WEBklar bei der Hetzner Online GmbH in Deutschland; sie werden nicht in Drittländer übermittelt. Einzelne Ereignisse und Klicks werden nach 30 Tagen gelöscht, Tageswerte nach 25 Monaten, alle Daten rund 60 Tage nach dem Ende der Website-Analyse (AVV Website-Analyse Nummer 9).
(6) Die Zahlen sind Näherungswerte. Besucher mit Blockern, abgeschaltetem JavaScript, Browser-Signal oder Ablehnung werden nur eingeschränkt oder gar nicht erfasst. Besucherzahlen, Herkunftsseiten, Verweildauer, Scrolltiefe, Klicks und Kontakt-Klicks stammen nur von Besuchern, die „Annehmen“ gewählt haben.
(7) Nicht enthalten sind Sitzungsaufzeichnungen, die Auswertung von Formularinhalten, die Messung von Ladezeiten und Fehlern, Kampagnen- und Trichteranalysen sowie ein Datenexport außer nach Nr. 7 Abs. 3.
(8) Das Banner fragt nur nach der Einwilligung in die Website-Analyse von WEBklar. Andere einwilligungspflichtige Inhalte der Website, etwa eingebettete Karten, Videos oder Schriften von Drittanbietern, deckt es nicht ab.
## 3. Voraussetzungen
(1) Der Kunde hat ein laufendes Hosting-Abo oder Wartungs-Abo.
(2) Die Website ist öffentlich erreichbar und wird bei WEBklar gehostet (Adresse unter project.webklar.com oder eigene Domain, die auf den Server von WEBklar zeigt). Nicht öffentliche Websites und Vorschauen, die nur nach einer Anmeldung erreichbar sind, werden nicht gemessen, ebenso wenig Websites, die im Dev-Modus auf einem Entwicklungsserver laufen. Enthält eine öffentliche Website Bereiche, die erst nach einer Anmeldung zugänglich sind (etwa eine Web-Anwendung mit Kundenkonto), werden auch dort Seitenaufrufe gezählt; dafür gilt Nr. 4 Abs. 5.
(3) Auf der Zusatzseite erscheinen als Verantwortlicher die Firma (sonst der Name), die Anschrift, soweit sie im Kundenkonto hinterlegt ist, und die E-Mail-Adresse aus dem Kundenkonto. Das Portal zeigt diese Angaben vor dem Einschalten an. Der Kunde prüft sie und teilt Änderungen mit. Fehlen Firma und Name oder fehlt eine gültige E-Mail-Adresse, lässt sich die Funktion nicht einschalten.
(4) Richtet sich die Website auch gezielt an Kinder unter 16 Jahren, schaltet der Kunde die Website-Analyse nicht ein, weil eine Einwilligung über das Banner dann nicht genügt (Art. 8 DSGVO). Lassen Inhalte oder Adressen der Website auf besondere Kategorien personenbezogener Daten schließen, etwa Gesundheit, Religion oder politische Meinung, teilt der Kunde das WEBklar vor dem Einschalten mit. WEBklar darf das Einschalten für eine solche Website ablehnen oder die Website-Analyse dort ausschalten.
## 4. Pflichten des Kunden
(1) Der Kunde ist für die Messung auf seiner Website Verantwortlicher im Sinne der DSGVO.
(2) Er entfernt, verdeckt oder umgeht das Banner nicht und verändert das Skript nicht. Text und Gestaltung des Banners legt WEBklar fest.
(3) Er bindet ohne Abstimmung mit WEBklar keine weiteren Mess-, Tracking- oder Marketing-Werkzeuge ein (zum Beispiel Google Analytics oder Meta Pixel), weil sie eine eigene Einwilligung erfordern können.
(4) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einschalten dort einen kurzen Hinweis mit Link auf die Zusatzseite ein und entfernt ihn beim Ausschalten wieder; das wird nicht auf das Stundenkontingent angerechnet. Liegt sie woanders, etwa bei einem externen Generator, erhält der Kunde den Hinweis als Textbaustein und fügt ihn selbst ein und entfernt ihn nach dem Ausschalten selbst. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt der Kunde verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf (etwa „Wir setzen keine Analyse-Werkzeuge ein“), weist WEBklar den Kunden darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
(5) Enthalten die Pfade seiner Website personenbezogene Angaben, etwa Namen, Kundennummern oder E-Mail-Adressen, teilt der Kunde das WEBklar vor dem Einschalten mit.
(6) Er nutzt die Auswertung nur für eigene Zwecke, versucht nicht, einzelne Besucher zu identifizieren, und gibt Heatmap-Links nicht weiter.
## 5. Verfügbarkeit und Störungen
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für die Messung. Störungen der Messung beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen). Daten, die während einer Störung nicht erfasst wurden, lassen sich nicht nachträglich erheben.
(2) Fällt der Messdienst aus, wird die Website weiter ausgeliefert, nur ohne Banner und Messung.
(3) Die gesetzlichen Rechte des Kunden sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.
## 6. Hinweis zur Rechtslage
Für die Zählung, solange der Besucher keine Wahl getroffen hat (Nr. 2 Abs. 3), übermittelt das Skript nur die Adresse der aufgerufenen Seite und die Angabe, dass das Banner angezeigt wurde. Land und Geräteklasse bestimmt der Messdienst aus den Daten, die jeder Browser bei einer Anfrage ohnehin mitsendet. Ob auch dafür eine Einwilligung nach § 25 TDDDG nötig ist, haben Gerichte und Aufsichtsbehörden nicht entschieden; die Aufsichtsbehörden legen den Begriff des Zugriffs auf das Endgerät eher weit aus. Ändert sich die Rechtslage, passt WEBklar die Messung an (Nr. 8).
## 7. Ende der Leistung
(1) Der Kunde kann die Website-Analyse jederzeit im Kundenportal ausschalten; das Portal bestätigt die Abschaltung. Die Website-Analyse läuft, solange das Abo läuft, das sie enthält. Endet es, schaltet WEBklar sie spätestens am folgenden Tag ab. Wechselt der Kunde vom Wartungs- ins Hosting-Abo, bleibt die Website-Analyse; das Gleiche gilt, wenn er den Wechsel ins Wartungs-Abo widerruft. WEBklar informiert den Kunden per E-Mail.
(2) Mit dem Ausschalten speichert der Messdienst keine Messdaten mehr, und die Heatmap ist nicht mehr abrufbar. Das Messskript liefert dann kein Banner mehr aus, sobald die Website einmal mit der aktuellen Fassung des Skripts veröffentlicht wurde. Skript, Zusatzseite und, soweit WEBklar ihn eingefügt hat, den Hinweis in der Datenschutzerklärung entfernt WEBklar mit der nächsten Veröffentlichung der Website. WEBklar löst sie in der Regel sofort aus, spätestens innerhalb von fünf Werktagen.
(3) Die gespeicherten Daten löscht WEBklar nach Nummer 9 des AVV Website-Analyse. Bis zum 46. Tag nach dem Ende kann der Kunde eine Kopie der Tageswerte und der noch vorhandenen Einzelereignisse als CSV-Datei verlangen.
## 8. Änderungen und Einstellung
(1) WEBklar darf die Technik der Website-Analyse verbessern, wenn der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Website-Analyse.
(2) WEBklar darf die Website-Analyse für eine Website mit einer Frist von vier Wochen beenden oder für alle Kunden mit einer Frist von drei Monaten einstellen, wenn
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Kunde einer dafür nötigen neuen Fassung nicht zustimmt (AVV Website-Analyse Nummer 12 Abs. 3), oder
- eine Sicherheitslücke sich nicht anders beheben lässt.
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar die Messung ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Kunden darüber unverzüglich in Textform mit dem Grund und nimmt die Messung wieder auf, sobald das zulässig und technisch möglich ist.
(4) Gegenüber Unternehmern darf WEBklar die Website-Analyse außerdem ohne besonderen Grund mit einer Frist von drei Monaten in Textform für alle Kunden einstellen. Der Kunde kann das Abo dann innerhalb von 30 Tagen nach der Mitteilung zum Zeitpunkt der Einstellung kündigen; die Mindestlaufzeit steht dem nicht entgegen.
(5) Weil für die Website-Analyse kein Aufpreis berechnet wird, ändert ihre Einstellung den Preis des Abos nicht. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
## 9. Hinweise
(1) **Keine Rechtsberatung.** Die Zusatzseite und der Hinweis in der Datenschutzerklärung sind Mustertexte für die Standardkonfiguration. WEBklar sorgt dafür, dass sie die Technik richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Eine Prüfung, ob die Website des Kunden insgesamt datenschutzkonform ist, und eine rechtliche Beratung im Einzelfall schuldet WEBklar nicht. Abstimmungen nach dieser Leistungsbeschreibung betreffen nur die Technik.
(2) **Nachweis der Einwilligung.** Ein Protokoll je Besucher wird nicht geführt. Als Nachweis dienen die Dokumentation der Technik und das Archiv der Fassungen von Banner-Text und Zusatzseite (AVV Website-Analyse Nummer 8 Abs. 6).
(3) Die Länderzuordnung nutzt die Datenbank „IP to Country Lite“ von DB-IP.com (Lizenz CC BY 4.0). Im Kundenportal steht dazu unter der Auswertung der Hinweis „Länderdaten: IP-Geolocation by DB-IP“ mit Link auf db-ip.com.
@@ -0,0 +1,107 @@
# Leistungsbeschreibung Website-Analyse
**Fassung 2026-10-02** · Zusatzbedingungen zum Wartungs-Abo
## 1. Geltung
(1) Diese Leistungsbeschreibung regelt die Funktion „Website-Analyse“ im Wartungs-Abo. Sie wird vereinbart, wenn der Kunde die Funktion einschaltet und ihr zusammen mit dem Auftragsverarbeitungsvertrag Website-Analyse („AVV Website-Analyse“) zustimmt. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Website-Analyse.
(2) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für die Website-Analyse diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Website-Analyse.
(3) Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen („Rechtstexte … nicht enthalten“) gilt nicht für die Zusatzseite nach Nr. 2 Abs. 2 und den Hinweis in der Datenschutzerklärung nach Nr. 4 Abs. 4. Beides ist Teil der Website-Analyse.
## 2. Leistung
(1) Die Website-Analyse misst, wie die öffentliche Website des Kunden genutzt wird. Sie ist im Wartungs-Abo ohne Aufpreis enthalten.
(2) Beim Veröffentlichen der Website fügt WEBklar Folgendes ein:
- ein Messskript in jede HTML-Datei der Website,
- ein Einwilligungsbanner am unteren Rand mit den gleich gestalteten Schaltflächen „Ablehnen“ und „Annehmen“ in der Akzentfarbe, die der Kunde beim Einschalten wählt (ist sie für weiße Schrift zu hell, nimmt das Banner Schwarz),
- eine Schaltfläche „Datenschutz-Einstellungen“ unten rechts, über die Besucher ihre Wahl jederzeit ändern können (hat die Website ein eigenes Element mit dem Attribut `data-webklar-consent`, übernimmt dieses Element die Aufgabe),
- eine Zusatzseite „/datenschutz-analyse.html“ mit den Datenschutzhinweisen zur Messung (Verantwortlicher: der Kunde).
(3) Gemessen wird:
- **solange der Besucher keine Wahl getroffen hat und sein Browser kein Signal nach dem nächsten Spiegelstrich sendet:** Seitenaufrufe mit Pfad (ohne Suchparameter), Land (aus der IP-Adresse, die nicht gespeichert wird), Geräteklasse und ein Zähler dafür, dass das Banner angezeigt wurde;
- **bei den Browser-Signalen „Global Privacy Control“ und „Do Not Track“:** nichts, und es erscheint kein Banner;
- **nach „Ablehnen“:** nichts mehr, außer einmal dem Zähler „abgelehnt“;
- **nach „Annehmen“ zusätzlich:** die Domain der Website, von der der Besucher kam, die Größenklasse des Browserfensters, eine täglich wechselnde pseudonyme Besucher-Kennung, Verweildauer, Scrolltiefe, Klickpositionen (Heatmap), die Art von Kontakt-Klicks (Telefon, E-Mail, WhatsApp, Link auf eine andere Website mit deren Domain) und ein Zähler „angenommen“.
Die einzelnen Datenarten nennt der AVV Website-Analyse in Nummer 3.
(4) Es werden keine Cookies gesetzt. Gespeichert wird auf dem Gerät des Besuchers nur seine Banner-Wahl im lokalen Speicher des Browsers, für 183 Tage.
(5) Die Auswertung steht im Kundenportal zur Verfügung: Kennzahlen und Diagramme für die letzten 7, 30 oder 90 Tage und eine Heatmap je Seite und Ansicht (Handy, Tablet, Computer) aus den bis zu 5.000 jüngsten Klicks der letzten 30 Tage. Die Daten liegen auf dem Server von WEBklar bei der Hetzner Online GmbH in Deutschland; sie werden nicht in Drittländer übermittelt. Einzelne Ereignisse und Klicks werden nach 30 Tagen gelöscht, Tageswerte nach 25 Monaten, alle Daten rund 60 Tage nach dem Ende der Website-Analyse (AVV Website-Analyse Nummer 9).
(6) Die Zahlen sind Näherungswerte. Besucher mit Blockern, abgeschaltetem JavaScript, Browser-Signal oder Ablehnung werden nur eingeschränkt oder gar nicht erfasst. Besucherzahlen, Herkunftsseiten, Verweildauer, Scrolltiefe, Klicks und Kontakt-Klicks stammen nur von Besuchern, die „Annehmen“ gewählt haben.
(7) Nicht enthalten sind Sitzungsaufzeichnungen, die Auswertung von Formularinhalten, die Messung von Ladezeiten und Fehlern, Kampagnen- und Trichteranalysen sowie ein Datenexport außer nach Nr. 7 Abs. 3.
(8) Das Banner fragt nur nach der Einwilligung in die Website-Analyse von WEBklar. Andere einwilligungspflichtige Inhalte der Website, etwa eingebettete Karten, Videos oder Schriften von Drittanbietern, deckt es nicht ab.
## 3. Voraussetzungen
(1) Der Kunde hat ein laufendes Wartungs-Abo. Im Hosting-Abo ist die Website-Analyse nicht enthalten.
(2) Die Website ist öffentlich erreichbar und wird bei WEBklar gehostet (Adresse unter project.webklar.com oder eigene Domain, die auf den Server von WEBklar zeigt). Nicht öffentliche Websites und Vorschauen, die nur nach einer Anmeldung erreichbar sind, werden nicht gemessen, ebenso wenig Websites, die im Dev-Modus auf einem Entwicklungsserver laufen. Enthält eine öffentliche Website Bereiche, die erst nach einer Anmeldung zugänglich sind (etwa eine Web-Anwendung mit Kundenkonto), werden auch dort Seitenaufrufe gezählt; dafür gilt Nr. 4 Abs. 5.
(3) Auf der Zusatzseite erscheinen als Verantwortlicher die Firma (sonst der Name), die Anschrift, soweit sie im Kundenkonto hinterlegt ist, und die E-Mail-Adresse aus dem Kundenkonto. Das Portal zeigt diese Angaben vor dem Einschalten an. Der Kunde prüft sie und teilt Änderungen mit. Fehlen Firma und Name oder fehlt eine gültige E-Mail-Adresse, lässt sich die Funktion nicht einschalten.
(4) Richtet sich die Website auch gezielt an Kinder unter 16 Jahren, schaltet der Kunde die Website-Analyse nicht ein, weil eine Einwilligung über das Banner dann nicht genügt (Art. 8 DSGVO). Lassen Inhalte oder Adressen der Website auf besondere Kategorien personenbezogener Daten schließen, etwa Gesundheit, Religion oder politische Meinung, teilt der Kunde das WEBklar vor dem Einschalten mit. WEBklar darf das Einschalten für eine solche Website ablehnen oder die Website-Analyse dort ausschalten.
## 4. Pflichten des Kunden
(1) Der Kunde ist für die Messung auf seiner Website Verantwortlicher im Sinne der DSGVO.
(2) Er entfernt, verdeckt oder umgeht das Banner nicht und verändert das Skript nicht. Text und Gestaltung des Banners legt WEBklar fest.
(3) Er bindet ohne Abstimmung mit WEBklar keine weiteren Mess-, Tracking- oder Marketing-Werkzeuge ein (zum Beispiel Google Analytics oder Meta Pixel), weil sie eine eigene Einwilligung erfordern können.
(4) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einschalten dort einen kurzen Hinweis mit Link auf die Zusatzseite ein und entfernt ihn beim Ausschalten wieder; das wird nicht auf das Stundenkontingent angerechnet. Liegt sie woanders, etwa bei einem externen Generator, erhält der Kunde den Hinweis als Textbaustein und fügt ihn selbst ein und entfernt ihn nach dem Ausschalten selbst. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt der Kunde verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf (etwa „Wir setzen keine Analyse-Werkzeuge ein“), weist WEBklar den Kunden darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
(5) Enthalten die Pfade seiner Website personenbezogene Angaben, etwa Namen, Kundennummern oder E-Mail-Adressen, teilt der Kunde das WEBklar vor dem Einschalten mit.
(6) Er nutzt die Auswertung nur für eigene Zwecke, versucht nicht, einzelne Besucher zu identifizieren, und gibt Heatmap-Links nicht weiter.
## 5. Verfügbarkeit und Störungen
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für die Messung. Störungen der Messung beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen). Daten, die während einer Störung nicht erfasst wurden, lassen sich nicht nachträglich erheben.
(2) Fällt der Messdienst aus, wird die Website weiter ausgeliefert, nur ohne Banner und Messung.
(3) Die gesetzlichen Rechte des Kunden sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.
## 6. Hinweis zur Rechtslage
Für die Zählung, solange der Besucher keine Wahl getroffen hat (Nr. 2 Abs. 3), übermittelt das Skript nur die Adresse der aufgerufenen Seite und die Angabe, dass das Banner angezeigt wurde. Land und Geräteklasse bestimmt der Messdienst aus den Daten, die jeder Browser bei einer Anfrage ohnehin mitsendet. Ob auch dafür eine Einwilligung nach § 25 TDDDG nötig ist, haben Gerichte und Aufsichtsbehörden nicht entschieden; die Aufsichtsbehörden legen den Begriff des Zugriffs auf das Endgerät eher weit aus. Ändert sich die Rechtslage, passt WEBklar die Messung an (Nr. 8).
## 7. Ende der Leistung
(1) Der Kunde kann die Website-Analyse jederzeit im Kundenportal ausschalten; das Portal bestätigt die Abschaltung. Mit dem Ende des Wartungs-Abos endet die Website-Analyse; WEBklar schaltet sie spätestens am folgenden Tag ab und informiert den Kunden per E-Mail. Das gilt auch bei einem Wechsel ins Hosting-Abo und beim Widerruf eines Wechsels ins Wartungs-Abo.
(2) Mit dem Ausschalten speichert der Messdienst keine Messdaten mehr, und die Heatmap ist nicht mehr abrufbar. Das Messskript liefert dann kein Banner mehr aus, sobald die Website einmal mit der aktuellen Fassung des Skripts veröffentlicht wurde. Skript, Zusatzseite und, soweit WEBklar ihn eingefügt hat, den Hinweis in der Datenschutzerklärung entfernt WEBklar mit der nächsten Veröffentlichung der Website. WEBklar löst sie in der Regel sofort aus, spätestens innerhalb von fünf Werktagen.
(3) Die gespeicherten Daten löscht WEBklar nach Nummer 9 des AVV Website-Analyse. Bis zum 46. Tag nach dem Ende kann der Kunde eine Kopie der Tageswerte und der noch vorhandenen Einzelereignisse als CSV-Datei verlangen.
## 8. Änderungen und Einstellung
(1) WEBklar darf die Technik der Website-Analyse verbessern, wenn der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Website-Analyse.
(2) WEBklar darf die Website-Analyse für eine Website mit einer Frist von vier Wochen beenden oder für alle Kunden mit einer Frist von drei Monaten einstellen, wenn
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Kunde einer dafür nötigen neuen Fassung nicht zustimmt (AVV Website-Analyse Nummer 12 Abs. 3), oder
- eine Sicherheitslücke sich nicht anders beheben lässt.
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar die Messung ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Kunden darüber unverzüglich in Textform mit dem Grund und nimmt die Messung wieder auf, sobald das zulässig und technisch möglich ist.
(4) Gegenüber Unternehmern darf WEBklar die Website-Analyse außerdem ohne besonderen Grund mit einer Frist von drei Monaten in Textform für alle Kunden einstellen. Der Kunde kann das Wartungs-Abo dann innerhalb von 30 Tagen nach der Mitteilung zum Zeitpunkt der Einstellung kündigen; die Mindestlaufzeit steht dem nicht entgegen.
(5) Weil für die Website-Analyse kein Aufpreis berechnet wird, ändert ihre Einstellung den Preis des Wartungs-Abos nicht. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
## 9. Hinweise
(1) **Keine Rechtsberatung.** Die Zusatzseite und der Hinweis in der Datenschutzerklärung sind Mustertexte für die Standardkonfiguration. WEBklar sorgt dafür, dass sie die Technik richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Eine Prüfung, ob die Website des Kunden insgesamt datenschutzkonform ist, und eine rechtliche Beratung im Einzelfall schuldet WEBklar nicht. Abstimmungen nach dieser Leistungsbeschreibung betreffen nur die Technik.
(2) **Nachweis der Einwilligung.** Ein Protokoll je Besucher wird nicht geführt. Als Nachweis dienen die Dokumentation der Technik und das Archiv der Fassungen von Banner-Text und Zusatzseite (AVV Website-Analyse Nummer 8 Abs. 6).
(3) Die Länderzuordnung nutzt die Datenbank „IP to Country Lite“ von DB-IP.com (Lizenz CC BY 4.0). Im Kundenportal steht dazu unter der Auswertung der Hinweis „Länderdaten: IP-Geolocation by DB-IP“ mit Link auf db-ip.com.
@@ -0,0 +1,135 @@
# Leistungsbeschreibung Öffnungszeiten live
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo
## 1. Geltung
(1) Funktion im Sinne dieser Leistungsbeschreibung ist Öffnungszeiten live. Diese Leistungsbeschreibung gilt, wenn der Kunde die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Kunde die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht.
(2) Die Funktion ist im Wartungs-Abo ohne Aufpreis enthalten (§ 3 Abs. 8 der Abo-Bedingungen). Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen („Nicht enthalten“: neue Funktionen und Rechtstexte) gilt dafür nicht: Die Funktion und der Datenschutz-Absatz nach Nr. 6 Abs. 4 sind Teil dieser Leistung.
(3) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für Öffnungszeiten live diese Leistungsbeschreibung vor. Die Haftungsregeln stehen in Nr. 8 ausgeschrieben da, damit sie auch für Abos früherer Fassungen der Abo-Bedingungen gelten.
## 2. Leistung
(1) Der Kunde trägt seine Öffnungszeiten einmal im Kundenportal ein: bis zu fünf benannte Zeitpläne, je Wochentag bis zu zwei Zeitfenster („rund um die Uhr“ und „nach Vereinbarung“ sind möglich), Abweichungen an Feiertagen, Betriebsurlaub und Sondertage. Die Website zeigt diese Zeiten überall gleich an und berechnet daraus den Live-Status („Jetzt geöffnet · schließt um 18 Uhr“).
(2) An den Stellen der Website, die nach Nr. 3 Abs. 2 dafür vorgesehen sind, erscheinen die Zeiten als Tabelle, als Status oder als Kurzanzeige („Geöffnet“, „Geschlossen“, „Nach Vereinbarung“). Dazu gehören, soweit der Kunde sie einrichtet:
- eine Hinweis-Leiste am Seitenanfang für befristete Hinweise (Info, Urlaub, Aktion, Wichtig) mit Beginn und Ende und auf Wunsch einem Knopf (Link oder Anruf),
- auf dem Handy eine Kontakt-Leiste am unteren Rand mit den Knöpfen „Anrufen“, „Route“ und, wenn der Kunde sie einrichtet, „WhatsApp“,
- die Öffnungszeiten im Seitencode in maschinenlesbarer Form (strukturierte Daten), die Suchmaschinen lesen können,
- eine E-Mail vor Feiertagen, die den Kunden bittet, seine Zeiten für diese Tage zu prüfen, und
- im Kundenportal der Knopf „Für Google kopieren“, der die Zeiten in der Reihenfolge von Google in die Zwischenablage legt, damit der Kunde sie in sein Google-Unternehmensprofil einträgt.
(3) Die Funktion rechnet die Feiertage des Bundeslands ein, das der Kunde wählt; je Feiertag kann der Kunde eigene Zeiten eintragen. Feiertage, die nur örtlich gelten, sowie Heiligabend und Silvester zeigt das Kundenportal als „bitte prüfen“. Alle Zeiten gelten in der Zeitzone Europe/Berlin.
(4) Beim Aufruf der Website lädt der Browser des Besuchers ein kleines Programm und die Öffnungszeiten von project.webklar.com, einem Server der WEBklar GbR bei der Hetzner Online GmbH in Falkenstein (Deutschland). Dabei werden keine Cookies gesetzt, und es wird nichts im Browser gespeichert. Das Programm lädt nichts von anderen Anbietern. WhatsApp und Google Maps werden erst nach dem Klick des Besuchers auf „WhatsApp“ oder „Route“ geöffnet; vorher werden keine Daten an diese Dienste übertragen.
(5) Das Team von WEBklar kann die Hinweis-Leiste und die Kontakt-Leiste für eine Website abschalten, etwa wenn die Seite schon eine eigene Anruf-Leiste hat. Auf Wunsch des Kunden können Mitarbeiter die Öffnungszeiten in der Kundenansicht für ihn ändern; ein Anspruch darauf besteht nicht (Nr. 5 Abs. 1). Den Haken für den WhatsApp-Hinweis (Nr. 6 Abs. 3) setzen sie nicht.
## 3. Einbau und Freischaltung
(1) Der Einbau des Programms erfolgt automatisch nur bei Websites, die WEBklar hostet und über den eigenen Veröffentlichungsablauf bereitstellt: Beim nächsten Veröffentlichen setzt WEBklar die Einbau-Zeile in jede Seite ein, und Hinweis-Leiste, Kontakt-Leiste und Angaben für Suchmaschinen erscheinen ohne weiteren Aufwand. In allen anderen Fällen, etwa bei einer Website bei einem anderen Anbieter, setzt das Team die Einbau-Zeile von Hand ein. Hat WEBklar keinen Zugriff auf die Website, erhält der Kunde die Zeile und setzt sie selbst oder durch seinen Anbieter ein.
(2) Die Tabelle, den Status und die Kurzanzeige setzt das Team an den Stellen ein, an denen auf der Website feste Öffnungszeiten stehen oder stehen sollen, und ersetzt dort die fest eingetragenen Zeiten. In jedem dieser Platzhalter bleibt eine statische Ersatz-Tabelle stehen. Sie erscheint, wenn der Browser kein JavaScript ausführt, wenn die Daten nicht erreichbar oder älter als 24 Stunden sind und nachdem die Funktion abgeschaltet wurde. Sie zeigt den Wochenplan zum Zeitpunkt des Einbaus.
(3) WEBklar schaltet die Funktion frei, sobald der Einbau abgeschlossen ist und der Kunde Öffnungszeiten eingetragen hat (mindestens ein geöffneter Tag im Hauptplan). Solange der Kunde keine Zeiten eingetragen hat, kann das Team die Tabelle nicht einbauen.
(4) Der Einbau dauert erfahrungsgemäß meist wenige Tage. Das ist eine Einschätzung und keine verbindliche Zusage eines Termins.
## 4. Einrichtung
(1) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Kunden.
(2) Hat WEBklar die Funktion auf Wunsch des Kunden schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.
(3) Im Umfang der Einrichtung ist ein Einbau je Website enthalten, einschließlich der Aktualisierung der Ersatz-Tabelle beim Abschalten (Nr. 9 Abs. 4). Weitere Wünsche, etwa zusätzliche Stellen für die Anzeige, eine Sondergestaltung oder andere Änderungen an der Website, gehören nicht zur Einrichtung. Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Kunden in Textform aus; es gilt § 4 der Abo-Bedingungen.
## 5. Nicht enthalten
(1) Nicht enthalten sind:
- die Übertragung der Öffnungszeiten zu Google: WEBklar schreibt nichts automatisch in das Google-Unternehmensprofil des Kunden; der Kunde trägt die Zeiten dort selbst ein, der Knopf „Für Google kopieren“ und die Erinnerung vor Feiertagen helfen dabei,
- Feiertage einer Gemeinde und andere örtliche Besonderheiten (zum Beispiel Brückentage oder Stadtfeste); der Kunde trägt sie als Ausnahmen selbst ein,
- eine Zusage, dass Suchmaschinen die Angaben im Seitencode übernehmen oder wie sie sie anzeigen,
- die Pflege der Zeiten und der Hinweise durch WEBklar und
- Änderungen an der Website, die über den Einbau nach Nr. 3 hinausgehen (Nr. 4 Abs. 3).
## 6. Pflichten des Kunden
(1) Der Kunde prüft seine Zeiten, Feiertage und Hinweistexte vor dem Speichern und hält sie aktuell. Er wählt das richtige Bundesland und trägt örtliche Feiertage und Abweichungen selbst ein.
(2) Preise und Rabatte in Hinweisen gibt er rechtmäßig an, zum Beispiel als Endpreise und, bei Preisermäßigungen für Waren, mit dem niedrigsten Preis der letzten 30 Tage. Hinweise zu Preisen sind Angaben des Kunden, nicht von WEBklar.
(3) Die Kontaktangaben (Telefonnummer, Anschrift, WhatsApp-Nummer) sind auf der Website öffentlich sichtbar. Der Kunde trägt nur Angaben ein, die er veröffentlichen darf. Die WhatsApp-Schaltfläche schaltet er erst ein, nachdem er den WhatsApp-Hinweis in seine Datenschutzerklärung aufgenommen hat, und bestätigt das im Kundenportal mit einem Haken.
(4) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz zu Öffnungszeiten live ein; das gehört zum Einbau. Liegt sie woanders, etwa bei einem externen Generator, erhält der Kunde den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt der Kunde verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Kunden darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
(5) Der Kunde prüft nach dem Einbau, ob die Anzeige auf seiner Website stimmt, und meldet Fehler unverzüglich im Chat des Kundenportals.
(6) Er entfernt oder verändert das Programm von WEBklar nicht. Wünscht er die Hinweis-Leiste oder die Kontakt-Leiste nicht, teilt er das dem Team mit (Nr. 2 Abs. 5).
## 7. Verfügbarkeit und Aktualität
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für die Live-Anzeige. Störungen der Anzeige behebt WEBklar in angemessener Frist.
(2) Ist die Anzeige nicht erreichbar oder sind die Daten älter als 24 Stunden, bleibt auf der Website die Ersatz-Tabelle stehen (Nr. 3 Abs. 2); Hinweis-Leiste und Kontakt-Leiste erscheinen dann nicht.
(3) Änderungen, die der Kunde im Kundenportal speichert, erscheinen nicht sofort auf der Website, sondern mit einer Verzögerung von einigen Minuten (bis zu etwa zehn Minuten). Der Status folgt der Zeitzone Europe/Berlin; geht die Uhr des Geräts des Besuchers um mehr als fünf Minuten falsch, rechnet das Programm mit der Serverzeit von WEBklar.
## 8. Haftung
(1) Die Live-Anzeige ist ein Hilfsmittel. Die Richtigkeit der Zeiten, Ausnahmen, Hinweise und Kontaktangaben liegt beim Kunden: WEBklar zeigt die Angaben des Kunden an und prüft sie nicht auf Richtigkeit.
(2) Die Feiertage stammen aus einer Feiertagsliste für das gewählte Bundesland. WEBklar beseitigt Fehler in den Feiertagsdaten und andere Softwarefehler nach Meldung in angemessener Frist (Nr. 7 Abs. 1). Für Schäden daraus gelten die Absätze 3 bis 5.
(3) Für entgangenen Gewinn und Folgeschäden, etwa weil ein Besucher wegen einer falschen oder fehlenden Angabe vor verschlossener Tür steht, haftet WEBklar nur im Rahmen der Absätze 4 und 5.
(4) WEBklar haftet unbeschränkt bei Vorsatz und grober Fahrlässigkeit, bei Verletzung von Leben, Körper oder Gesundheit, nach dem Produkthaftungsgesetz und soweit WEBklar eine Garantie übernommen hat.
(5) Bei leicht fahrlässiger Verletzung einer wesentlichen Vertragspflicht haftet WEBklar nur für den vertragstypischen, vorhersehbaren Schaden. Wesentlich sind Pflichten, deren Erfüllung die ordnungsgemäße Durchführung des Vertrags überhaupt erst ermöglicht und auf deren Einhaltung der Kunde regelmäßig vertrauen darf. Im Übrigen ist die Haftung für leichte Fahrlässigkeit ausgeschlossen.
(6) Diese Regeln gelten auch zugunsten der Mitarbeiter und Beauftragten von WEBklar. Dieser Abschnitt entspricht § 13 Abs. 1 und 2 der Abo-Bedingungen und gilt unabhängig davon, nach welcher Fassung der Abo-Bedingungen das Wartungs-Abo besteht.
(7) Gesetzliche Rechte des Kunden, die sich nicht ausschließen lassen, bleiben unberührt. Der Haftungsrahmen des Kunden gegenüber seinen Besuchern und Kunden bleibt ebenfalls unberührt.
## 9. Ende der Leistung
(1) Auf Wunsch des Kunden in Textform (im Chat des Kundenportals oder per E-Mail) schaltet WEBklar Öffnungszeiten live jederzeit ab; für den Widerruf gilt Nr. 11.
(2) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo, läuft die Funktion noch 30 Tage unverändert weiter; danach schaltet WEBklar sie ab. Das Gleiche gilt beim Widerruf eines Wechsels ins Wartungs-Abo. WEBklar informiert den Kunden per E-Mail. Die Eingaben des Kunden bleiben nach dem Abschalten 30 Tage gespeichert und werden danach gelöscht; beginnt schon in den 30 Tagen Weiterlauf wieder ein Wartungs-Abo, läuft die Funktion einfach weiter, beginnt es in den 30 Tagen danach, schaltet WEBklar die Funktion mit den vorhandenen Eingaben ohne weitere Kosten wieder ein.
(3) Sobald ein neues Wartungs-Abo beginnt, solange die Eingaben des Kunden noch gespeichert sind (Absatz 4), läuft die Funktion mit den vorhandenen Eingaben weiter oder WEBklar schaltet sie ohne weitere Kosten wieder ein.
(4) Mit dem Abschalten verschwinden Hinweis-Leiste, Kontakt-Leiste, Live-Status und die Angaben für Suchmaschinen; auf der Website steht wieder die Ersatz-Tabelle (Nr. 3 Abs. 2). Hat sich der Wochenplan seit dem Einbau geändert, aktualisiert WEBklar die Ersatz-Tabelle einmal beim Abschalten; das gehört zum Einbau nach Nr. 4 Abs. 3. Hat WEBklar keinen Zugriff auf die Website, erhält der Kunde den aktuellen Text und setzt ihn selbst ein. Nach dem Abschalten wegen des Endes des Abos bleiben die Eingaben des Kunden noch weitere 30 Tage gespeichert und werden danach gelöscht; bis dahin kann der Kunde sie im Kundenportal ansehen.
(5) Schaltet WEBklar die Funktion auf Wunsch des Kunden oder aus einem anderen Grund als dem Ende des Abos ab, löscht WEBklar die Eingaben des Kunden auf sein Verlangen in Textform, spätestens mit dem Löschen des Kundenkontos.
## 10. Änderungen und Einstellung
(1) WEBklar darf die Technik von Öffnungszeiten live verbessern, wenn der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten die Funktion verarbeitet, passt WEBklar die Datenschutzerklärung an und informiert den Kunden, weil sich dann sein Datenschutz-Absatz (Nr. 6 Abs. 4) ändern kann.
(2) WEBklar darf die Funktion für eine Website mit einer Frist von vier Wochen beenden oder für alle Kunden mit einer Frist von drei Monaten einstellen, wenn
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Kunde einer dafür nötigen neuen Fassung dieser Leistungsbeschreibung nicht zustimmt, oder
- eine Sicherheitslücke sich nicht anders beheben lässt.
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar die Funktion ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Kunden darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.
(4) Ist der Kunde Unternehmer, darf WEBklar die Funktion außerdem ohne besonderen Grund mit einer Frist von drei Monaten in Textform für alle Kunden einstellen. Der Kunde kann das Wartungs-Abo dann innerhalb von 30 Tagen nach der Mitteilung zum Zeitpunkt der Einstellung kündigen; die Mindestlaufzeit steht dem nicht entgegen.
(5) Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
## 11. Widerruf
(1) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion.
(2) Hat der Kunde die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat.
## 12. Hinweise
(1) **Keine Rechtsberatung.** Der Datenschutz-Absatz, der WhatsApp-Hinweis und die Hinweise des Kundenportals zu Preisangaben sind Mustertexte für die Standardkonfiguration. WEBklar sorgt dafür, dass sie die Technik richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Eine rechtliche Beratung im Einzelfall, etwa zu Preisangaben, zum Wettbewerbsrecht oder zum Datenschutz der Website des Kunden, schuldet WEBklar nicht.
(2) **Datenschutz.** Wie WEBklar im Kundenportal Daten zu Öffnungszeiten live verarbeitet, steht in der Datenschutzerklärung von WEBklar (Abschnitt 6e). Was beim Aufruf der Website des Kunden übertragen wird, beschreibt Nr. 2 Abs. 4; die Information der Besucher gehört in die Datenschutzerklärung der Website (Nr. 6 Abs. 4).
Anbieter: WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler. Es gelten ergänzend die Abo-Bedingungen und die Datenschutzerklärung.
+63
View File
@@ -0,0 +1,63 @@
Quelle: https://www.gesetze-im-internet.de/bgbeg/art_253anlage_1.html (Anlage 1), abgerufen 2026-10-02
(Fundstelle: BGBl. I 2013, S. 3663 – 3664;
bzgl. der einzelnen Änderungen vgl. Fußnote)
Widerrufsbelehrung
Widerrufsrecht
Sie haben das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen.
Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag .
Um Ihr Widerrufsrecht auszuüben, müssen Sie uns ( ) mittels einer eindeutigen Erklärung (z. B. ein mit der Post versandter Brief oder eine E-Mail) über Ihren Entschluss, diesen Vertrag zu widerrufen, informieren. Sie können dafür das beigefügte Muster-Widerrufsformular verwenden, das jedoch nicht vorgeschrieben ist.
Zur Wahrung der Widerrufsfrist reicht es aus, dass Sie die Mitteilung über die Ausübung des Widerrufsrechts vor Ablauf der Widerrufsfrist absenden.
Folgen des Widerrufs
Wenn Sie diesen Vertrag widerrufen, haben wir Ihnen alle Zahlungen, die wir von Ihnen erhalten haben, einschließlich der Lieferkosten (mit Ausnahme der zusätzlichen Kosten, die sich daraus ergeben, dass Sie eine andere Art der Lieferung als die von uns angebotene, günstigste Standardlieferung gewählt haben), unverzüglich und spätestens binnen vierzehn Tagen ab dem Tag zurückzuzahlen, an dem die Mitteilung über Ihren Widerruf dieses Vertrags bei uns eingegangen ist. Für diese Rückzahlung verwenden wir dasselbe Zahlungsmittel, das Sie bei der ursprünglichen Transaktion eingesetzt haben, es sei denn, mit Ihnen wurde ausdrücklich etwas anderes vereinbart; in keinem Fall werden Ihnen wegen dieser Rückzahlung Entgelte berechnet.
Gestaltungshinweise:Fügen Sie einen der folgenden in Anführungszeichen gesetzten Textbausteine ein:
a)
im Falle eines Dienstleistungsvertrags oder eines Vertrags über die Lieferung von Wasser, Gas oder Strom, wenn sie nicht in einem begrenzten Volumen oder in einer bestimmten Menge zum Verkauf angeboten werden, von Fernwärme oder von digitalen Inhalten, die nicht auf einem körperlichen Datenträger geliefert werden: „des Vertragsabschlusses.“;
b)
im Falle eines Kaufvertrags: „, an dem Sie oder ein von Ihnen benannter Dritter, der nicht der Beförderer ist, die Waren in Besitz genommen haben bzw. hat.“;
c)
im Falle eines Vertrags über mehrere Waren, die der Verbraucher im Rahmen einer einheitlichen Bestellung bestellt hat und die getrennt geliefert werden: „, an dem Sie oder ein von Ihnen benannter Dritter, der nicht der Beförderer ist, die letzte Ware in Besitz genommen haben bzw. hat.“;
d)
im Falle eines Vertrags über die Lieferung einer Ware in mehreren Teilsendungen oder Stücken: „, an dem Sie oder ein von Ihnen benannter Dritter, der nicht der Beförderer ist, die letzte Teilsendung oder das letzte Stück in Besitz genommen haben bzw. hat.“;
e)
im Falle eines Vertrags zur regelmäßigen Lieferung von Waren über einen festgelegten Zeitraum hinweg: „, an dem Sie oder ein von Ihnen benannter Dritter, der nicht der Beförderer ist, die erste Ware in Besitz genommen haben bzw. hat.“
Fügen Sie Ihren Namen, Ihre Anschrift, Ihre Telefonnummer und Ihre E-Mail-Adresse ein.
Wenn Sie dazu verpflichtet sind, eine Funktion bereitzustellen, mit der der Verbraucher den online geschlossenen Vertrag widerrufen kann, fügen Sie Folgendes ein: „Sie können Ihr Widerrufsrecht auch online unter … [Internetadresse oder anderen geeigneten Hinweis darüber eingeben, wo die Widerrufsfunktion verfügbar ist] ausüben. Wenn Sie diese Online-Funktion nutzen, übermitteln wir Ihnen auf einem dauerhaften Datenträger (z. B. durch eine E-Mail) unverzüglich eine Eingangsbestätigung mit Informationen zum Inhalt der Widerrufserklärung sowie dem Datum und der Uhrzeit ihres Eingangs. “ Wenn Sie dem Verbraucher die Wahl einräumen, die Information über seinen Widerruf des Vertrags auf Ihrer Webseite elektronisch auszufüllen und zu übermitteln, fügen Sie Folgendes ein: „Sie können das Muster-Widerrufsformular oder eine andere eindeutige Erklärung auch auf unserer Webseite … [Internet-Adresse einfügen] elektronisch ausfüllen und übermitteln. Machen Sie von dieser Möglichkeit Gebrauch, so werden wir Ihnen unverzüglich (z. B. per E-Mail) eine Bestätigung über den Eingang eines solchen Widerrufs übermitteln. “
Im Falle von Kaufverträgen, in denen Sie nicht angeboten haben, im Falle des Widerrufs die Waren selbst abzuholen, fügen Sie Folgendes ein: „Wir können die Rückzahlung verweigern, bis wir die Waren wieder zurückerhalten haben oder bis Sie den Nachweis erbracht haben, dass Sie die Waren zurückgesandt haben, je nachdem, welches der frühere Zeitpunkt ist.“
Wenn der Verbraucher Waren im Zusammenhang mit dem Vertrag erhalten hat:
a)
Fügen Sie ein:
–
„Wir holen die Waren ab.“ oder
–
„Sie haben die Waren unverzüglich und in jedem Fall spätestens binnen vierzehn Tagen ab dem Tag, an dem Sie uns über den Widerruf dieses Vertrags unterrichten, an … uns oder an [hier sind gegebenenfalls der Name und die Anschrift der von Ihnen zur Entgegennahme der Waren ermächtigten Person einzufügen] zurückzusenden oder zu übergeben. Die Frist ist gewahrt, wenn Sie die Waren vor Ablauf der Frist von vierzehn Tagen absenden.“
b)
fügen Sie ein:
–
„Wir tragen die Kosten der Rücksendung der Waren.“;
–
„Sie tragen die unmittelbaren Kosten der Rücksendung der Waren.“;
–
Wenn Sie bei einem Fernabsatzvertrag nicht anbieten, die Kosten der Rücksendung der Waren zu tragen, und die Waren aufgrund ihrer Beschaffenheit nicht normal mit der Post zurückgesandt werden können: „Sie tragen die unmittelbaren Kosten der Rücksendung der Waren in Höhe von … EUR [Betrag einfügen].“, oder, wenn die Kosten vernünftigerweise nicht im Voraus berechnet werden können: „Sie tragen die unmittelbaren Kosten der Rücksendung der Waren. Die Kosten werden auf höchstens etwa … EUR [Betrag einfügen] geschätzt.“ oder
–
Wenn die Waren bei einem außerhalb von Geschäftsräumen geschlossenen Vertrag aufgrund ihrer Beschaffenheit nicht normal mit der Post zurückgesandt werden können und zum Zeitpunkt des Vertragsschlusses zur Wohnung des Verbrauchers gebracht worden sind: „Wir holen die Waren auf unsere Kosten ab.“ und
c)
fügen Sie ein: „Sie müssen für einen etwaigen Wertverlust der Waren nur aufkommen, wenn dieser Wertverlust auf einen zur Prüfung der Beschaffenheit, Eigenschaften und Funktionsweise der Waren nicht notwendigen Umgang mit ihnen zurückzuführen ist.“
Im Falle eines Vertrags, der die Erbringung von Dienstleistungen oder die Lieferung von Wasser, Gas oder Strom, wenn sie nicht in einem begrenzten Volumen oder in einer bestimmten Menge zum Verkauf angeboten werden, oder von Fernwärme zum Gegenstand hat, fügen Sie Folgendes ein: „Haben Sie verlangt, dass die Dienstleistungen oder Lieferung von Wasser/Gas/Strom/Fernwärme [Unzutreffendes streichen] während der Widerrufsfrist beginnen soll, so haben Sie uns einen angemessenen Betrag zu zahlen, der dem Anteil der bis zu dem Zeitpunkt, zu dem Sie uns von der Ausübung des Widerrufsrechts hinsichtlich dieses Vertrags unterrichten, bereits erbrachten Dienstleistungen im Vergleich zum Gesamtumfang der im Vertrag vorgesehenen Dienstleistungen entspricht. “
+30
View File
@@ -0,0 +1,30 @@
Quelle: https://www.gesetze-im-internet.de/bgbeg/art_253anlage_2.html (Anlage 2), abgerufen 2026-10-02
(Fundstelle: BGBl. I 2013, S. 3665;
bzgl. der einzelnen Änderungen vgl. Fußnote)
Muster-Widerrufsformular
(Wenn Sie den Vertrag widerrufen wollen, dann füllen Sie bitte dieses Formular aus und senden Sie es zurück.)
–
An [hier ist der Name, die Anschrift und die E-Mail-Adresse des Unternehmers durch den Unternehmer einzufügen]:
–
Hiermit widerrufe(n) ich/wir (*) den von mir/uns (*) abgeschlossenen Vertrag über den Kauf der folgenden Waren (*)/die Erbringung der folgenden Dienstleistung (*)
–
Bestellt am (*)/erhalten am (*)
–
Name des/der Verbraucher(s)
–
Anschrift des/der Verbraucher(s)
–
Unterschrift des/der Verbraucher(s) (nur bei Mitteilung auf Papier)
–
Datum
(*) Unzutreffendes streichen.
@@ -0,0 +1,103 @@
Normen, auf die sich die Texte der Auftragsbestätigung, der Kundenseiten und der Mails stützen (Auszüge, unverändert, abgerufen 2026-10-06 von gesetze-im-internet.de).
Die Datei dient als Nachweis der Zählung; test/auftraegeRechtstexte.test.js gleicht die Zitate in den Texten mit ihr ab.
Prüfung vom 06.10.2026 (zweiter Abruf von § 649 und § 650 BGB): Der Kostenanschlag steht in § 649 BGB (amtliche Überschrift „Kostenanschlag“). Absatz 2 lautet: „Ist eine solche Überschreitung des Anschlags zu erwarten, so hat der Unternehmer dem Besteller unverzüglich Anzeige zu machen.“ § 650 BGB regelt dagegen den Werklieferungsvertrag und Verbraucherverträge über die Herstellung digitaler Produkte; ein Verweis auf „§ 650“ für den Kostenanschlag ist falsch. Der Satz „Zeichnet sich eine deutliche Überschreitung ab, melden wir uns vorher.“ (AB_TEXTE.aufwand) stützt sich auf § 649 Absatz 2.
Quelle: https://www.gesetze-im-internet.de/bgb/__312f.html (Auszug: § 312f Absatz 1 und 2 BGB)
(1) Bei außerhalb von Geschäftsräumen geschlossenen Verträgen ist der Unternehmer verpflichtet, dem Verbraucher alsbald auf Papier zur Verfügung zu stellen
1.
eine Abschrift eines Vertragsdokuments, das von den Vertragsschließenden so unterzeichnet wurde, dass ihre Identität erkennbar ist, oder
2.
eine Bestätigung des Vertrags, in der der Vertragsinhalt wiedergegeben ist.
Wenn der Verbraucher zustimmt, kann für die Abschrift oder die Bestätigung des Vertrags auch ein anderer dauerhafter Datenträger verwendet werden. Die Bestätigung nach Satz 1 muss die in Artikel 246a des Einführungsgesetzes zum Bürgerlichen Gesetzbuche genannten Angaben nur enthalten, wenn der Unternehmer dem Verbraucher diese Informationen nicht bereits vor Vertragsschluss in Erfüllung seiner Informationspflichten nach § 312d Absatz 1 auf einem dauerhaften Datenträger zur Verfügung gestellt hat.
(2) Bei Fernabsatzverträgen ist der Unternehmer verpflichtet, dem Verbraucher eine Bestätigung des Vertrags, in der der Vertragsinhalt wiedergegeben ist, innerhalb einer angemessenen Frist nach Vertragsschluss, spätestens jedoch bei der Lieferung der Ware oder bevor mit der Ausführung der Dienstleistung begonnen wird, auf einem dauerhaften Datenträger zur Verfügung zu stellen. Die Bestätigung nach Satz 1 muss die in Artikel 246a des Einführungsgesetzes zum Bürgerlichen Gesetzbuche genannten Angaben enthalten, es sei denn, der Unternehmer hat dem Verbraucher diese Informationen bereits vor Vertragsschluss in Erfüllung seiner Informationspflichten nach § 312d Absatz 1 auf einem dauerhaften Datenträger zur Verfügung gestellt.
Quelle: https://www.gesetze-im-internet.de/bgb/__355.html (Auszug: § 355 Absatz 1 BGB)
(1) Wird einem Verbraucher durch Gesetz ein Widerrufsrecht nach dieser Vorschrift eingeräumt, so sind der Verbraucher und der Unternehmer an ihre auf den Abschluss des Vertrags gerichteten Willenserklärungen nicht mehr gebunden, wenn der Verbraucher seine Willenserklärung fristgerecht widerrufen hat. Der Widerruf erfolgt durch Erklärung gegenüber dem Unternehmer. Aus der Erklärung muss der Entschluss des Verbrauchers zum Widerruf des Vertrags eindeutig hervorgehen. Der Widerruf muss keine Begründung enthalten. Zur Fristwahrung genügt die rechtzeitige Absendung des Widerrufs.
Quelle: https://www.gesetze-im-internet.de/bgb/__356.html (Auszug: § 356 Absatz 4 bis 6 BGB)
(4) Das Widerrufsrecht erlischt spätestens zwölf Monate und 14 Tage nach dem in Absatz 2 oder § 355 Absatz 2 Satz 2 genannten Zeitpunkt. Bei Verträgen über Finanzdienstleistungen gilt Satz 1 nicht, wenn der Verbraucher nicht nach Artikel 246b § 1 Absatz 1 Nummer 16 des Einführungsgesetzes zum Bürgerlichen Gesetzbuche über sein Widerrufsrecht belehrt wurde.
(5) Das Widerrufsrecht erlischt bei Verträgen über die Erbringung von Dienstleistungen auch unter folgenden Voraussetzungen:
1.
bei einem Vertrag, der den Verbraucher nicht zur Zahlung eines Preises verpflichtet, wenn der Unternehmer die Dienstleistung vollständig erbracht hat,
2.
bei einem Vertrag, der den Verbraucher zur Zahlung eines Preises verpflichtet, mit der vollständigen Erbringung der Dienstleistung, wenn der Verbraucher vor Beginn der Erbringung
a)
ausdrücklich zugestimmt hat, dass der Unternehmer mit der Erbringung der Dienstleistung vor Ablauf der Widerrufsfrist beginnt,
b)
bei einem außerhalb von Geschäftsräumen geschlossenen Vertrag die Zustimmung nach Buchstabe a auf einem dauerhaften Datenträger übermittelt hat und
c)
seine Kenntnis davon bestätigt hat, dass sein Widerrufsrecht mit vollständiger Vertragserfüllung durch den Unternehmer erlischt,
3.
bei einem Vertrag, bei dem der Verbraucher den Unternehmer ausdrücklich aufgefordert hat, ihn aufzusuchen, um Reparaturarbeiten auszuführen, mit der vollständigen Erbringung der Dienstleistung, wenn der Verbraucher die in Nummer 2 Buchstabe a und b genannten Voraussetzungen erfüllt hat,
4.
bei einem Vertrag über die Erbringung von Finanzdienstleistungen, wenn der Vertrag von beiden Seiten auf ausdrücklichen Wunsch des Verbrauchers vollständig erfüllt ist, bevor der Verbraucher sein Widerrufsrecht ausübt.
(6) Das Widerrufsrecht erlischt bei Verträgen über die Bereitstellung von nicht auf einem körperlichen Datenträger befindlichen digitalen Inhalten auch unter folgenden Voraussetzungen:
1.
bei einem Vertrag, der den Verbraucher nicht zur Zahlung eines Preises verpflichtet, wenn der Unternehmer mit der Vertragserfüllung begonnen hat,
2.
bei einem Vertrag, der den Verbraucher zur Zahlung eines Preises verpflichtet, wenn
a)
der Unternehmer mit der Vertragserfüllung begonnen hat,
b)
der Verbraucher ausdrücklich zugestimmt hat, dass der Unternehmer mit der Vertragserfüllung vor Ablauf der Widerrufsfrist beginnt,
c)
der Verbraucher seine Kenntnis davon bestätigt hat, dass durch seine Zustimmung nach Buchstabe b mit Beginn der Vertragserfüllung sein Widerrufsrecht erlischt, und
d)
der Unternehmer dem Verbraucher eine Bestätigung gemäß § 312f zur Verfügung gestellt hat.
Quelle: https://www.gesetze-im-internet.de/bgb/__356a.html (Auszug: § 356a Absatz 2 und 4 BGB)
(2) Die Widerrufsfunktion muss dem Verbraucher ermöglichen, eine Widerrufserklärung an den Unternehmer zu übermitteln und dem Unternehmer in oder mit der Widerrufserklärung ohne Weiteres folgende Informationen bereitzustellen oder zu bestätigen:
1.
den Namen des Verbrauchers,
2.
Angaben zur Identifizierung des Vertrags oder des Teils des Vertrags, den der Verbraucher widerrufen möchte,
3.
Angaben zum elektronischen Kommunikationsmittel, mit welchem dem Verbraucher eine Eingangsbestätigung für den Widerruf zu übermitteln ist.
(4) Der Unternehmer hat dem Verbraucher, wenn dieser die Bestätigungsfunktion aktiviert hat, auf einem dauerhaften Datenträger unverzüglich eine Eingangsbestätigung zu übermitteln, die zumindest den Inhalt der Widerrufserklärung nach Absatz 2 sowie das Datum und die Uhrzeit ihres Eingangs enthält.
Quelle: https://www.gesetze-im-internet.de/bgb/__357a.html (Auszug: § 357a Absatz 2 BGB)
(2) Der Verbraucher hat Wertersatz für die bis zum Widerruf erbrachten Dienstleistungen, für die der Vertrag die Zahlung eines Preises vorsieht, oder die bis zum Widerruf erfolgte Lieferung von Wasser, Gas oder Strom in nicht bestimmten Mengen oder nicht begrenztem Volumen oder von Fernwärme zu leisten, wenn
1.
der Verbraucher von dem Unternehmer ausdrücklich verlangt hat, dass mit der Leistung vor Ablauf der Widerrufsfrist begonnen werden soll,
2.
bei einem außerhalb von Geschäftsräumen geschlossenen Vertrag der Verbraucher das Verlangen nach Nummer 1 auf einem dauerhaften Datenträger übermittelt hat und
3.
der Unternehmer den Verbraucher nach Artikel 246a § 1 Absatz 2 Satz 1 Nummer 1 und 3 des Einführungsgesetzes zum Bürgerlichen Gesetzbuche ordnungsgemäß informiert hat.
Bei der Berechnung des Wertersatzes ist der vereinbarte Gesamtpreis zu Grunde zu legen. Ist der vereinbarte Gesamtpreis unverhältnismäßig hoch, so ist der Wertersatz auf der Grundlage des Marktwerts der erbrachten Leistung zu berechnen.
Quelle: https://www.gesetze-im-internet.de/bgbeg/art_246a__1.html (Auszug: Artikel 246a § 1 Absatz 3 EGBGB)
(3) Der Unternehmer hat den Verbraucher auch zu informieren, wenn
1.
dem Verbraucher nach § 312g Absatz 2 Nummer 1, 2, 5 und 7 bis 13 des Bürgerlichen Gesetzbuchs ein Widerrufsrecht nicht zusteht, dass der Verbraucher seine Willenserklärung nicht widerrufen kann, oder
2.
das Widerrufsrecht des Verbrauchers nach § 312g Absatz 2 Nummer 3, 4 und 6 sowie § 356 Absatz 5 und 6 des Bürgerlichen Gesetzbuchs vorzeitig erlöschen kann, über die Umstände, unter denen der Verbraucher ein zunächst bestehendes Widerrufsrecht verliert.
Quelle: https://www.gesetze-im-internet.de/bgb/__640.html (Auszug: § 640 Absatz 2 BGB)
(2) Als abgenommen gilt ein Werk auch, wenn der Unternehmer dem Besteller nach Fertigstellung des Werks eine angemessene Frist zur Abnahme gesetzt hat und der Besteller die Abnahme nicht innerhalb dieser Frist unter Angabe mindestens eines Mangels verweigert hat. Ist der Besteller ein Verbraucher, so treten die Rechtsfolgen des Satzes 1 nur dann ein, wenn der Unternehmer den Besteller zusammen mit der Aufforderung zur Abnahme auf die Folgen einer nicht erklärten oder ohne Angabe von Mängeln verweigerten Abnahme hingewiesen hat; der Hinweis muss in Textform erfolgen.
Quelle: https://www.gesetze-im-internet.de/bgb/__649.html (§ 649 BGB, erneut abgerufen 2026-10-06; die amtliche Überschrift lautet „Kostenanschlag“)
§ 649 Kostenanschlag
(1) Ist dem Vertrag ein Kostenanschlag zugrunde gelegt worden, ohne dass der Unternehmer die Gewähr für die Richtigkeit des Anschlags übernommen hat, und ergibt sich, dass das Werk nicht ohne eine wesentliche Überschreitung des Anschlags ausführbar ist, so steht dem Unternehmer, wenn der Besteller den Vertrag aus diesem Grund kündigt, nur der im § 645 Abs. 1 bestimmte Anspruch zu.
(2) Ist eine solche Überschreitung des Anschlags zu erwarten, so hat der Unternehmer dem Besteller unverzüglich Anzeige zu machen.
Quelle: https://www.gesetze-im-internet.de/bgb/__650.html (Auszug: § 650 Absatz 2 BGB, zur Abgrenzung: § 650 ist nicht der Kostenanschlag; er regelt den Werklieferungsvertrag und Verbraucherverträge über digitale Produkte)
§ 650 Werklieferungsvertrag; Verbrauchervertrag über die Herstellung digitaler Produkte
(2) Auf einen Verbrauchervertrag, bei dem der Unternehmer sich verpflichtet,
1.
digitale Inhalte herzustellen,
2.
einen Erfolg durch eine digitale Dienstleistung herbeizuführen oder
3.
einen körperlichen Datenträger herzustellen, der ausschließlich als Träger digitaler Inhalte dient,
sind die §§ 633 bis 639 über die Rechte bei Mängeln sowie § 640 über die Abnahme nicht anzuwenden. An die Stelle der nach Satz 1 nicht anzuwendenden Vorschriften treten die Vorschriften des Abschnitts 3 Titel 2a. Die §§ 641, 644 und 645 sind mit der Maßgabe anzuwenden, dass an die Stelle der Abnahme die Bereitstellung des digitalen Produkts (§ 327b Absatz 3 bis 5) tritt.
Quelle: https://www.gesetze-im-internet.de/ao_1977/__147.html (Auszug: § 147 Absatz 1 Nummer 2 bis 4, Absatz 3 Satz 1 und Absatz 4 AO)
2.
die empfangenen Handels- oder Geschäftsbriefe,
3.
Wiedergaben der abgesandten Handels- oder Geschäftsbriefe,
4.
Buchungsbelege,
(3) Die in Absatz 1 Nummer 1 und 4a aufgeführten Unterlagen sind zehn Jahre, die in Absatz 1 Nummer 4 aufgeführten Unterlagen acht Jahre und die sonstigen in Absatz 1 aufgeführten Unterlagen sechs Jahre aufzubewahren, sofern nicht in anderen Steuergesetzen kürzere Aufbewahrungsfristen zugelassen sind.
(4) Die Aufbewahrungsfrist beginnt mit dem Schluss des Kalenderjahrs, in dem die letzte Eintragung in das Buch gemacht, das Inventar, die Eröffnungsbilanz, der Jahresabschluss oder der Lagebericht aufgestellt, der Handels- oder Geschäftsbrief empfangen oder abgesandt worden oder der Buchungsbeleg entstanden ist, ferner die Aufzeichnung vorgenommen worden ist oder die sonstigen Unterlagen entstanden sind.
@@ -0,0 +1,15 @@
# Datenschutzerklärung von WEBklar: Abschnitt 6c „Online-Terminbuchung auf Websites unserer Kunden“
Fassung 2026-10-05 (Stand der Datenschutzerklärung 05.10.2026). Markdown-Quelle des Abschnitts 6c in `public/datenschutz.html`; der Abschnitt steht dort wörtlich so (`test/legalTexts.test.js` prüft, dass beide übereinstimmen). Er beschreibt, was WEBklar selbst verarbeitet: als Auftragsverarbeiter für die Gäste der Betriebe und als Verantwortlicher für die Betriebsdaten, den Entwurf des Fragebogens und die Bestellung im Kundenportal. Den Text für die Website des Betriebs enthält `termine-datenschutz-absatz.md`.
## Abschnitt (HTML)
```html
<h2 id="termine">6c. Online-Terminbuchung auf Websites unserer Kunden</h2>
<p>Kunden mit Wartungs-Abo können auf ihrer eigenen Website eine Online-Terminbuchung anbieten („Terminplaner“), sobald wir die Funktion für sie freigeschaltet haben. Dabei verarbeiten wir die Daten der Personen, die dort einen Termin buchen („Gäste“), ausschließlich als Auftragsverarbeiter nach den Weisungen des jeweiligen Kunden, der dafür Verantwortlicher ist. Wir verwenden diese Daten nicht für eigene Zwecke. Verantwortliche sind wir für die Betriebsdaten des Kunden im Kundenportal, für den Entwurf seines Fragebogens und für seine Bestellung. Diese Datenschutzerklärung gilt für unsere eigenen Seiten und das Kundenportal. Wer auf der Website eines Kunden einen Termin bucht, findet die Informationen zur Buchung (Zwecke, Rechtsgrundlagen, Speicherdauer, Rechte) in der Datenschutzerklärung dieser Website. Anfragen von Gästen zu ihren Daten leiten wir an den Kunden weiter.</p>
<p>Für den Betrieb des Terminplaners speichern wir im Auftrag des Kunden: Name, E-Mail-Adresse und, wenn der Kunde diese Felder eingeschaltet hat, Telefonnummer, Nachricht und weitere Angaben des Gastes; die gewählte Leistung, Person oder den gewählten Platz und den Termin; den Stand der Buchung (zum Beispiel bestätigt, abgesagt oder widerrufen) mit den Zeitpunkten; als Nachweis den Zeitpunkt der Bestätigung und der Zustimmungen sowie die Fassung der angezeigten Hinweise; gegebenenfalls den Wunsch nach einer Erinnerung. Der Terminplaner ist nicht für Leistungen der Heilkunde und der Gesundheitsversorgung bestimmt; Gäste werden gebeten, keine Gesundheitsangaben zu machen.</p>
<p>Der Kunde sieht, ändert und löscht die Termine im Kundenportal. Dort verarbeiten wir als Verantwortliche seine Einstellungen für den Terminplaner (Betriebsangaben, Leistungen, Namen und Zeiten der gebuchten Personen oder Plätze, Öffnungszeiten, Absagefristen und Texte), wie die übrigen Portal-Daten (Abschnitt 6) auf Grundlage von Art. 6 Abs. 1 lit. b DSGVO. Bevor der Kunde den Terminplaner bestellt, füllt er im Kundenportal einen Fragebogen aus (Branche und Betrieb, Personen oder Plätze, Leistungen, Zeiten, Regeln, Angaben von Gästen, Benachrichtigungen und Website); seine Eingaben speichern wir als Entwurf seiner Einstellungen. Bestellt er nicht, löschen wir den Entwurf 90 Tage nach der letzten Änderung, bei der Löschung seines Kundenkontos zugleich mit dem Konto. Die Bestellung und ihren Nachweis beschreibt Abschnitt 7. Für jede Person oder jeden Platz kann der Kunde einen geheimen Link erzeugen, mit dem er die bestätigten Termine in seinem Kalenderprogramm abonniert; wer den Link kennt, sieht diese Termine. Der Kunde kann den Link jederzeit durch einen neuen ersetzen.</p>
<p>Im Namen des Kunden senden wir E-Mails an Gäste (Bestätigung der E-Mail-Adresse, Bestätigung, Änderung oder Absage eines Termins, Eingangsbestätigung eines Widerrufs und, wenn der Gast das gewählt hat, eine Erinnerung) und an den Kunden (neue Buchung, Absage, Widerruf). Der Versand läuft über den E-Mail-Dienst der Hetzner Online GmbH (Rechenzentrum Nürnberg, Abschnitt 8); als Absender erscheint der Kunde, Antworten gehen an ihn. Wir verschicken keine Werbung, setzen keine Tracking-Pixel ein und erhalten keine Kopie. Bis zum Versand liegt jede E-Mail in einer Warteschlange. Nach dem Versand löschen wir dort Empfängeradresse und Inhalt, den Eintrag selbst nach sieben Tagen; E-Mails, die nicht zugestellt werden konnten, löschen wir nach 48 Stunden.</p>
<p>Beim Aufruf einer Kundenwebsite mit Terminbuchung lädt der Browser das Programm der Terminbuchung von unserem Server (project.webklar.com) und schon beim Laden der Seite die Konfiguration mit den Texten und Leistungen. Ist die Terminbuchung unmittelbar in die Seite eingebaut, holt sie die freien Termine, sobald der Gast durchklickt; sitzt sie hinter einem Knopf, geschieht das erst nach dem Klick auf den Knopf. Dabei gelten die Protokolle in Abschnitt 3. Die Terminbuchung setzt keine Cookies und speichert nichts im Browser des Gastes. Zum Schutz vor Missbrauch und um die Zahl der Anfragen zu begrenzen (höchstens zehn Buchungen je zehn Minuten und je IP-Adresse), merken wir uns die Zeitpunkte der Aufrufe je IP-Adresse nur im Arbeitsspeicher des Servers, nicht dauerhaft; mit der Buchung speichern wir die IP-Adresse nicht. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO; unser berechtigtes Interesse ist der sichere und störungsfreie Betrieb.</p>
<p>Löschfristen: Nicht bestätigte Buchungen löschen wir 48 Stunden nach Ablauf der Bestätigungsfrist von 30 Minuten, abgesagte oder abgelehnte Termine nach 30 Tagen, bestätigte Termine nach der Frist, die der Kunde eingestellt hat (Vorgabe 90 Tage, einstellbar von 14 bis 365 Tagen nach dem Termin). Nicht bestellte Entwürfe löschen wir 90 Tage nach der letzten Änderung. Endet das Wartungs-Abo oder schalten wir die Funktion ab, schalten wir den Terminplaner spätestens am folgenden Tag ab und löschen alle Termindaten und Einstellungen 30 Tage danach; beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schalten wir ihn mit den vorhandenen Daten wieder ein. Eine Ausnahme ist das Einrichtungs-Ticket, in dem wir die Einrichtung als Arbeitsnachweis festhalten (Abschnitt 6): Es bleibt mit einer Kurzfassung der Einstellungen bestehen und enthält keine Daten von Gästen. In unseren Datensicherungen bleiben gelöschte Daten noch bis zu sieben Tage erhalten und werden dann überschrieben. Die Daten liegen auf unserem Server bei Hetzner im Rechenzentrum Falkenstein (Deutschland), wie die übrigen Portal-Daten (Abschnitt 3).</p>
```
@@ -0,0 +1,73 @@
# Datenschutz-Absatz „Online-Terminbuchung“ für die Website des Betriebs
Fassung 2026-10-05. Grundlage: `recherche-recht.md` Abschnitt 11.1. Der Absatz gehört in die Datenschutzerklärung der Website des **Betriebs** (der Betrieb ist Verantwortlicher, WEBklar Auftragsverarbeiter). Die Einbauanleitung für Mitarbeiter (`docs/funktionen/termine-einbau.md`, TP2) verweist hierher. Für die Datenschutzerklärung von WEBklar selbst gilt `termine-datenschutz-6c.md`.
Zwei Fassungen weiter unten: Sie und Du. Nimm die Fassung, die zur Datenschutzerklärung der Website passt (in der Regel zur `anrede` des Betriebs).
## Platzhalter und optionale Sätze
- `{{Firma}}`, `{{Anschrift}}`, `{{E-Mail}}`, `{{Telefon}}`: Betriebsangaben aus der Einstellung. `{{Tage}}`: Aufbewahrung nach dem Termin (`aufbewahrungTage`, Vorgabe 90, einstellbar 14 bis 365).
- `[[wenn …: …]]`: Satz oder Satzteil nur aufnehmen, wenn die genannte Einstellung aktiv ist; sonst samt Klammern streichen. Beginnt der Satzteil mit „und“, steht nach dem Doppelpunkt ein zweites Leerzeichen: Es ist das Leerzeichen zwischen dem Vordertext und „und“ und fällt mit dem Satzteil weg.
## Löschfristen (Vorgaben des Löschlaufs, im Text unten verwendet)
| Daten | Frist |
|---|---|
| Nicht bestätigte Buchung (Status „verfallen“) | 48 Stunden nach Ablauf der Bestätigungsfrist (30 Minuten) |
| Abgesagter oder abgelehnter Termin | 30 Tage |
| Bestätigter, erledigter oder nicht wahrgenommener Termin | `aufbewahrungTage` (Vorgabe 90) nach dem Ende des Termins |
| Terminanfrage, deren Beginn verstrichen ist | gilt als abgelehnt (durch das System), dann 30 Tage |
| Funktion abgeschaltet (zum Beispiel Wartungs-Abo beendet) | alle Termindaten 30 Tage nach dem Abschalten |
| Versandwarteschlange für E-Mails | Empfängeradresse und Inhalt werden nach dem Versand geleert, der Eintrag nach 7 Tagen gelöscht; nicht zustellbare Einträge nach 48 Stunden |
## Abweichungen von Abschnitt 11.1 der Recherche
1. **Server-Protokolle**: 11.1 sagt, IP-Adresse und Browserangaben würden „nicht gespeichert“. Der Server von WEBklar protokolliert sie aber kurzzeitig (Datenschutzerklärung von WEBklar Abschnitt 3: 14 Tage). Der Absatz sagt das jetzt offen.
2. **Rechtsgrundlage**: Bei einer sofort verbindlichen Buchung kommt der Vertrag mit der Bestätigung zustande; „vorvertragliche Maßnahmen“ passt nur für Terminanfragen. Beide Fälle stehen im Text. Neu: Widerruf über die Widerrufsfunktion (Art. 6 Abs. 1 lit. c DSGVO, § 356a BGB).
3. **Aufruf**: Das Skript der Terminbuchung wird beim Aufruf der Seite geladen, nicht erst beim Klick auf den Knopf; der Text sagt das.
4. **Nachweise**: Gespeichert werden auch der Zeitpunkt der Zustimmungen und die Fassung der angezeigten Texte (Spezifikation, Feld `nachweis`).
5. **Löschfristen** angepasst (siehe Tabelle); 11.1 kannte nur „nicht bestätigt 48 Stunden“, „abgesagt 30 Tage“ und „bestätigt {{90}} Tage“.
## Fassung Sie
> ### Online-Terminbuchung
>
> Auf unserer Website können Sie Termine online buchen. Dafür verarbeiten wir die folgenden Daten:
>
> **Welche Daten?** Ihren Namen, Ihre E-Mail-Adresse, den gewählten Termin, die gewählte Leistung und die gewählte Person oder den gewählten Platz[[wenn Telefon-Feld aktiv: , auf Wunsch Ihre Telefonnummer]][[wenn Nachricht oder Zusatzfelder aktiv: und die Angaben, die Sie uns im Formular mitteilen]]. Außerdem speichern wir, wann Sie den Termin bestätigt haben, welche Fassung der Hinweise Sie bei der Buchung gesehen haben und welche Zustimmungen Sie erteilt haben[[wenn Erinnerung angeboten: , zum Beispiel ob Sie eine Erinnerung gewünscht haben]].
>
> **Wofür und auf welcher Grundlage?** Wir brauchen die Daten, um Ihren Termin zu verwalten, Ihnen den Termin zu bestätigen und Sie über Änderungen oder Absagen zu informieren. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO (Vertrag über den gebuchten Termin, bei einer Terminanfrage vorvertragliche Maßnahmen auf Ihre Anfrage). [[wenn Erinnerung angeboten: Wenn Sie eine Erinnerung vor dem Termin wünschen, beruht diese auf Ihrer Einwilligung nach Art. 6 Abs. 1 lit. a DSGVO. Sie können sie jederzeit mit Wirkung für die Zukunft widerrufen, indem Sie uns eine kurze Nachricht senden oder den Termin über den Link in der E-Mail absagen.]] [[wenn Gäste Verbraucher sein können: Widerrufen Sie einen Vertrag über die Schaltfläche „Vertrag widerrufen“, speichern wir Ihre Erklärung mit Datum und Uhrzeit und senden Ihnen eine Eingangsbestätigung (Art. 6 Abs. 1 lit. c DSGVO in Verbindung mit § 356a BGB).]] Zum Schutz vor Missbrauch (zum Beispiel automatisierten Buchungen) und um die Zahl der Anfragen zu begrenzen, merken wir uns Ihre IP-Adresse kurzzeitig nur im Arbeitsspeicher des Servers, nicht dauerhaft (Art. 6 Abs. 1 lit. f DSGVO; unser berechtigtes Interesse ist der sichere Betrieb der Terminbuchung).
>
> **E-Mails.** Sie erhalten eine E-Mail, in der Sie Ihre E-Mail-Adresse und den Termin per Link bestätigen, danach eine Bestätigung[[wenn Erinnerung angeboten: und, wenn Sie das angekreuzt haben, eine Erinnerung]]. Sagen wir den Termin ab oder verschieben ihn, informieren wir Sie ebenfalls per E-Mail. Die E-Mails enthalten keine Werbung und keine Tracking-Pixel. Absender ist {{Firma}}, Ihre Antworten gehen an uns. Bis zum Versand liegt die E-Mail in einer Warteschlange; nach dem Versand wird dort die Empfängeradresse samt Inhalt gelöscht, nicht zustellbare E-Mails nach 48 Stunden.
>
> **Wer verarbeitet die Daten in unserem Auftrag?** Den Terminkalender betreibt die WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, als unser Auftragsverarbeiter nach Art. 28 DSGVO. Die Daten liegen auf Servern der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, in Deutschland (Rechenzentren Falkenstein und Nürnberg). Der E-Mail-Versand läuft ebenfalls über Hetzner. Eine Übermittlung in Drittländer findet nicht statt.
>
> **Beim Aufruf der Terminbuchung.** Beim Aufruf dieser Seite lädt Ihr Browser das Programm der Terminbuchung vom Server der WEBklar GbR (project.webklar.com), beim Öffnen der Terminbuchung zusätzlich die Termine und Texte. Dabei werden technisch bedingt Ihre IP-Adresse und Angaben zu Ihrem Browser übertragen. Der Server speichert sie kurzzeitig in Protokollen und löscht sie nach 14 Tagen (Art. 6 Abs. 1 lit. f DSGVO; berechtigtes Interesse ist der sichere und funktionsfähige Betrieb). Wir setzen dafür keine Cookies und speichern nichts auf Ihrem Gerät.
>
> **Wie lange speichern wir?** Nicht bestätigte Buchungen (der Bestätigungslink gilt 30 Minuten) löschen wir 48 Stunden nach Ablauf dieser Frist. Bestätigte Termine löschen wir {{Tage}} Tage nach dem Termin, abgesagte oder abgelehnte Termine 30 Tage nach der Absage oder Ablehnung. Terminanfragen, die wir bis zum Terminbeginn nicht beantworten, behandeln wir als abgelehnt. Wird die Online-Terminbuchung abgeschaltet, zum Beispiel weil unser Vertrag mit WEBklar endet, werden alle Termindaten 30 Tage nach dem Abschalten gelöscht. Gesetzliche Aufbewahrungspflichten für Geschäftsunterlagen (zum Beispiel Rechnungen) bleiben davon unberührt; sie erfüllen wir außerhalb des Terminkalenders.
>
> **Pflicht zur Angabe.** Ohne Name, E-Mail-Adresse und Terminwahl können wir keinen Termin online vergeben. [[wenn Telefon hinterlegt: Sie können uns auch telefonisch erreichen: {{Telefon}}.]] Eine automatisierte Entscheidungsfindung findet nicht statt. Bitte machen Sie bei der Buchung keine Angaben zu Ihrer Gesundheit.
>
> **Ihre Rechte.** Sie haben das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung und Datenübertragbarkeit sowie das Recht, der Verarbeitung auf Grundlage berechtigter Interessen zu widersprechen (Art. 15 bis 21 DSGVO). Eine erteilte Einwilligung können Sie jederzeit widerrufen. Sie können sich bei einer Datenschutzaufsichtsbehörde beschweren. Verantwortlich ist {{Firma}}, {{Anschrift}}, E-Mail {{E-Mail}}.
## Fassung Du
> ### Online-Terminbuchung
>
> Auf unserer Website kannst du Termine online buchen. Dafür verarbeiten wir die folgenden Daten:
>
> **Welche Daten?** Deinen Namen, deine E-Mail-Adresse, den gewählten Termin, die gewählte Leistung und die gewählte Person oder den gewählten Platz[[wenn Telefon-Feld aktiv: , auf Wunsch deine Telefonnummer]][[wenn Nachricht oder Zusatzfelder aktiv: und die Angaben, die du uns im Formular mitteilst]]. Außerdem speichern wir, wann du den Termin bestätigt hast, welche Fassung der Hinweise du bei der Buchung gesehen hast und welche Zustimmungen du erteilt hast[[wenn Erinnerung angeboten: , zum Beispiel ob du eine Erinnerung gewünscht hast]].
>
> **Wofür und auf welcher Grundlage?** Wir brauchen die Daten, um deinen Termin zu verwalten, dir den Termin zu bestätigen und dich über Änderungen oder Absagen zu informieren. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO (Vertrag über den gebuchten Termin, bei einer Terminanfrage vorvertragliche Maßnahmen auf deine Anfrage). [[wenn Erinnerung angeboten: Wenn du eine Erinnerung vor dem Termin wünschst, beruht diese auf deiner Einwilligung nach Art. 6 Abs. 1 lit. a DSGVO. Du kannst sie jederzeit mit Wirkung für die Zukunft widerrufen, indem du uns eine kurze Nachricht sendest oder den Termin über den Link in der E-Mail absagst.]] [[wenn Gäste Verbraucher sein können: Widerrufst du einen Vertrag über die Schaltfläche „Vertrag widerrufen“, speichern wir deine Erklärung mit Datum und Uhrzeit und senden dir eine Eingangsbestätigung (Art. 6 Abs. 1 lit. c DSGVO in Verbindung mit § 356a BGB).]] Zum Schutz vor Missbrauch (zum Beispiel automatisierten Buchungen) und um die Zahl der Anfragen zu begrenzen, merken wir uns deine IP-Adresse kurzzeitig nur im Arbeitsspeicher des Servers, nicht dauerhaft (Art. 6 Abs. 1 lit. f DSGVO; unser berechtigtes Interesse ist der sichere Betrieb der Terminbuchung).
>
> **E-Mails.** Du erhältst eine E-Mail, in der du deine E-Mail-Adresse und den Termin per Link bestätigst, danach eine Bestätigung[[wenn Erinnerung angeboten: und, wenn du das angekreuzt hast, eine Erinnerung]]. Sagen wir den Termin ab oder verschieben ihn, informieren wir dich ebenfalls per E-Mail. Die E-Mails enthalten keine Werbung und keine Tracking-Pixel. Absender ist {{Firma}}, deine Antworten gehen an uns. Bis zum Versand liegt die E-Mail in einer Warteschlange; nach dem Versand wird dort die Empfängeradresse samt Inhalt gelöscht, nicht zustellbare E-Mails nach 48 Stunden.
>
> **Wer verarbeitet die Daten in unserem Auftrag?** Den Terminkalender betreibt die WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, als unser Auftragsverarbeiter nach Art. 28 DSGVO. Die Daten liegen auf Servern der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, in Deutschland (Rechenzentren Falkenstein und Nürnberg). Der E-Mail-Versand läuft ebenfalls über Hetzner. Eine Übermittlung in Drittländer findet nicht statt.
>
> **Beim Aufruf der Terminbuchung.** Beim Aufruf dieser Seite lädt dein Browser das Programm der Terminbuchung vom Server der WEBklar GbR (project.webklar.com), beim Öffnen der Terminbuchung zusätzlich die Termine und Texte. Dabei werden technisch bedingt deine IP-Adresse und Angaben zu deinem Browser übertragen. Der Server speichert sie kurzzeitig in Protokollen und löscht sie nach 14 Tagen (Art. 6 Abs. 1 lit. f DSGVO; berechtigtes Interesse ist der sichere und funktionsfähige Betrieb). Wir setzen dafür keine Cookies und speichern nichts auf deinem Gerät.
>
> **Wie lange speichern wir?** Nicht bestätigte Buchungen (der Bestätigungslink gilt 30 Minuten) löschen wir 48 Stunden nach Ablauf dieser Frist. Bestätigte Termine löschen wir {{Tage}} Tage nach dem Termin, abgesagte oder abgelehnte Termine 30 Tage nach der Absage oder Ablehnung. Terminanfragen, die wir bis zum Terminbeginn nicht beantworten, behandeln wir als abgelehnt. Wird die Online-Terminbuchung abgeschaltet, zum Beispiel weil unser Vertrag mit WEBklar endet, werden alle Termindaten 30 Tage nach dem Abschalten gelöscht. Gesetzliche Aufbewahrungspflichten für Geschäftsunterlagen (zum Beispiel Rechnungen) bleiben davon unberührt; sie erfüllen wir außerhalb des Terminkalenders.
>
> **Pflicht zur Angabe.** Ohne Name, E-Mail-Adresse und Terminwahl können wir keinen Termin online vergeben. [[wenn Telefon hinterlegt: Du kannst uns auch telefonisch erreichen: {{Telefon}}.]] Eine automatisierte Entscheidungsfindung findet nicht statt. Bitte mach bei der Buchung keine Angaben zu deiner Gesundheit.
>
> **Deine Rechte.** Du hast das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung und Datenübertragbarkeit sowie das Recht, der Verarbeitung auf Grundlage berechtigter Interessen zu widersprechen (Art. 15 bis 21 DSGVO). Eine erteilte Einwilligung kannst du jederzeit widerrufen. Du kannst dich bei einer Datenschutzaufsichtsbehörde beschweren. Verantwortlich ist {{Firma}}, {{Anschrift}}, E-Mail {{E-Mail}}.
+76
View File
@@ -0,0 +1,76 @@
# Hinweise, Haken und Knopftexte für Gäste (Terminplaner)
Fassung 2026-10-05. Grundlage: `recherche-recht.md` Abschnitt 11.2 (Hinweis unter dem Formular) und 11.3, `recherche-recht-zahlung.md` Abschnitt 7.3 C bis E (Haken, Widerrufshinweis, Widerrufsfunktion). Die Texte in den Abschnitten 1 bis 3 sind die Fassung aus `server/services/termine/rechtstexte.js` (Platzhalter in doppelten geschweiften Klammern); `test/termineRechtstexte.test.js` prüft, dass sie hier wörtlich stehen. Ändere sie deshalb nur in `rechtstexte.js` und übernimm sie hierher. Die Abschnitte 4 bis 6 geben Texte des Widgets und der Mails wieder (Knopftexte: `knopfText` in `server/services/termine/einstellungen.js`; Widerrufshinweis: Mailvorlage `server/mail-templates/G02 Termin bestaetigt.html`; Eingangsbestätigung: `server/mail-templates/G08 Eingang Widerruf.html`); maßgeblich sind die Mailvorlagen und das Widget, nicht dieses Dokument.
Platzhalter: `{{Firma}}` = Betriebsname, `{{Anschrift}}` = Anschrift des Betriebs, `{{n}}` = Stunden vor dem Termin (`erinnerung.stundenVorher`: 2, 12, 24 oder 48). Die Texte zeigen die Vorgabe von 90 Tagen Aufbewahrung nach dem Termin; der Wert kommt aus `aufbewahrungTage` (14 bis 365) der Einstellung.
## 1. Datenschutz-Hinweis unter dem Formular (Kurzfassung nach Art. 13 DSGVO)
Steht in der Zusammenfassung vor dem Absenden. Sie- oder Du-Fassung nach `anrede` des Betriebs. Neben dem Text zeigt das Widget einen Link auf `betrieb.datenschutzUrl` (die Datenschutzerklärung des Betriebs); die Betriebs-E-Mail steht bewusst nicht im Text. Es gibt keinen Haken dafür.
Fassung Sie:
```text
Datenschutz. Wir verwenden Ihre Angaben, um Ihren Termin zu verwalten und zu bestätigen (Art. 6 Abs. 1 lit. b DSGVO). Betrieben wird der Terminkalender in unserem Auftrag von der WEBklar GbR (Server: Hetzner, Deutschland). Wir speichern Ihre Daten bis 90 Tage nach dem Termin, abgesagte Termine 30 Tage, nicht bestätigte Buchungen 48 Stunden nach Ablauf der Bestätigungsfrist. Es werden keine Cookies gesetzt. Bitte machen Sie keine Angaben zu Ihrer Gesundheit. Mehr erfahren Sie in unserer Datenschutzerklärung. Verantwortlich: {{Firma}}, {{Anschrift}}.
```
Fassung Du:
```text
Datenschutz. Wir verwenden deine Angaben, um deinen Termin zu verwalten und zu bestätigen (Art. 6 Abs. 1 lit. b DSGVO). Betrieben wird der Terminkalender in unserem Auftrag von der WEBklar GbR (Server: Hetzner, Deutschland). Wir speichern deine Daten bis 90 Tage nach dem Termin, abgesagte Termine 30 Tage, nicht bestätigte Buchungen 48 Stunden nach Ablauf der Bestätigungsfrist. Es werden keine Cookies gesetzt. Bitte mach keine Angaben zu deiner Gesundheit. Mehr erfährst du in unserer Datenschutzerklärung. Verantwortlich: {{Firma}}, {{Anschrift}}.
```
Gegenüber Abschnitt 11.2 der Recherche: Die Löschfristen sind die des Löschlaufs (nicht bestätigte Buchungen 48 Stunden nach Ablauf der Bestätigungsfrist, abgesagte Termine 30 Tage, vergangene Termine `aufbewahrungTage` nach dem Termin) und der Verantwortliche nennt zusätzlich die Anschrift.
## 2. Haken (Zustimmungen)
Wortlaut immer in der Sie-Form, auch bei Betrieben, die ihre Gäste duzen: Es sind Rechtstexte. Keiner der Haken ist vorangekreuzt.
| Haken | Wann | Pflicht | Wortlaut |
|---|---|---|---|
| Vorzeitiger Beginn | Verbraucher, mit Preis: bei sofort buchbaren Leistungen, wenn der Terminbeginn nicht nach dem Ende der Widerrufsfrist liegt (14 Tage ab dem Tag nach dem Vertragsschluss, Berliner Kalender, §§ 187, 188, 193 BGB; gerechnet mit dem Zeitpunkt der Buchung plus Haltezeit; R29); bei bepreisten Anfragen immer, weil der Tag der Annahme nicht feststeht (R31) | ja | Ich verlange ausdrücklich, dass {{Firma}} mit der Leistung vor Ablauf der Widerrufsfrist beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen Erbringung der Leistung erlischt (§ 356 Abs. 5 Nr. 2 BGB). Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB). |
| Stornobedingungen | der Betrieb hat Stornobedingungen hinterlegt und die gebuchte Leistung hat einen Preis (bei kostenlosen Leistungen fragt der Terminplaner nicht danach) | ja | Ich habe die Stornobedingungen gelesen. |
| Erinnerung | der Betrieb bietet Erinnerungen an | nein | Ich möchte {{n}} Stunden vor dem Termin eine Erinnerung per E-Mail erhalten. Diese Einwilligung kann ich jederzeit widerrufen. |
## 3. Hinweis über dem Nachrichtenfeld
Bitte keine Gesundheitsangaben.
Der Terminplaner ist nicht für den Gesundheitsbereich bestimmt (Spezifikation, Vorgaben). Der Hinweis steht über jedem Freitextfeld.
## 4. Knopftexte (exakt)
| Fall | Knopf im Widget | Knopf auf der Bestätigungsseite |
|---|---|---|
| verbindlich, mit Preis | Zahlungspflichtig buchen | Zahlungspflichtig bestätigen |
| verbindlich, ohne Preis | Termin buchen | Termin bestätigen |
| Anfrage | Termin anfragen | Anfrage absenden |
| Widerruf (Funktion) | Vertrag widerrufen | Widerruf bestätigen |
## 5. Widerrufshinweis in der Bestätigungs-E-Mail
Er ersetzt nicht die Widerrufsbelehrung (`termine-widerrufsbelehrung-muster.md`), sondern führt zu ihr. Gilt für Buchungen von Verbrauchern, auch für Anfragen: Der Vertrag kommt mit der Annahme zustande, ist aber ebenfalls ein Fernabsatzvertrag, und die Frist läuft ab der Annahme (R55, R29). Die Widerrufsfunktion steht im Widget als zweiter Knopf im Fuß und im Knopfmodus dauerhaft neben dem Buchungsknopf (R54).
**Mit Widerrufsrecht** (Bestätigungs-E-Mail G02, vor der Belehrung; das Widget zeigt dort die ausführliche Belehrung zum Aufklappen):
> **Widerrufsrecht.** Wenn Sie Verbraucher sind, können Sie diesen Vertrag innerhalb von 14 Tagen ab Vertragsschluss ohne Angabe von Gründen widerrufen. Dafür nutzen Sie die Schaltfläche „Vertrag widerrufen“ im Terminbereich auf unserer Website oder den Link „Termin verwalten“ in Ihrer Bestätigungs-E-Mail. Die ausführliche Widerrufsbelehrung und das Muster-Widerrufsformular stehen in Ihrer Bestätigungs-E-Mail.
Maßgeblich für die Mail ist die Vorlage G02: Bis zum Beginn des Termins geht der Widerruf über den Knopf „Vertrag widerrufen“; danach über das Formular „Vertrag widerrufen“ auf der Website des Betriebs oder per E-Mail an den Betrieb. Die Verwaltungsseite bietet den Knopf nur bis zum Beginn des Termins an.
**Freizeit-Dienstleistung mit festem Termin** (§ 312g Abs. 2 Nr. 9 BGB; `recht.freizeit`), kein Widerrufsrecht, keine Belehrung, kein Formular, keine Funktion:
> **Für diese Buchung besteht kein Widerrufsrecht.** Ihre Buchung ist verbindlich; Absagen richten sich nach den Stornobedingungen.
Gäste, die nur Unternehmen sind (`recht.gaesteSind = unternehmer`): kein Hinweis.
## 6. Widerrufsfunktion (Texte)
- Schaltfläche oder Link: **Vertrag widerrufen** (im Terminbereich der Website, auf der Verwaltungsseite hinter „Termin verwalten“).
- Formular ohne Anmeldung: Name · Buchungsnummer · E-Mail-Adresse für die Eingangsbestätigung. Schaltfläche: **Widerruf bestätigen**.
- Eingangsbestätigung per E-Mail (G08; die Mailvorlage ist maßgeblich, hier nur der Kern. Die Mail enthält keine Eingaben des Gastes, nur die Buchungsnummer und den Eingang):
> Betreff: Eingangsbestätigung Ihres Widerrufs
>
> Wir bestätigen den Eingang Ihres Widerrufs. Inhalt Ihrer Erklärung: Widerruf des Vertrags zur Buchung {{nummer}}. Eingegangen am {{eingang}} (Datum und Uhrzeit). {{Nur im Block „zugeordnet“: Ihr Termin ist abgesagt.}} Absender: {{Firma}}.
Der Eingang muss mit Datum und Uhrzeit bestätigt werden (§ 356a BGB); das ist auch der Text der Belehrung („Eingangsbestätigung mit Informationen zum Inhalt der Widerrufserklärung sowie dem Datum und der Uhrzeit ihres Eingangs“).
+966
View File
@@ -0,0 +1,966 @@
# Kanzlei-Paket Terminplaner (WEBklar)
Dieses Paket gehört zum Verkaufsweg des Terminplaners der WEBklar GbR (Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein). Es zeigt einer Kanzlei, was verkauft wird, wie der Ablauf ist, welche Texte Kunden und Gäste in Endfassung sehen, wo diese Texte im Programm stehen und was wir wissen möchten.
Fassungen: Leistungsbeschreibung, Auftragsverarbeitungsvertrag (AVV), Widerrufsbelehrung und Bestelltexte tragen die Fassung **2026-10-05** (Konstante `config.termine.avvVersion`). Die Gästetexte (Hinweise, Haken, Belehrung und Datenschutz-Absatz für die Gäste unserer Kunden) tragen die Fassung **2026-10-05** (Konstante `RECHTSTEXTE_VERSION`). Beide Konstanten haben heute dasselbe Datum, sind aber getrennt und können sich später unterscheiden.
## 1. Zweck und Umfang
**Zweck.** Der Inhaber lässt die Texte des Terminplaners durch eine Kanzlei prüfen, bevor der erste fremde Betrieb den Terminplaner nutzt. Bis dahin ist der Terminplaner nicht für fremde Betriebe freigeschaltet. Das Paket soll ohne Rückfragen zeigen, was die Kanzlei prüfen soll und von welchen Annahmen wir ausgehen.
**Kernpaket.** Der Inhaber hat festgelegt, dass die Kanzlei dieses Kernpaket prüft und alles Übrige WEBklar selbst prüft (Hausrunde). Die Kanzlei prüft:
- die Widerrufsbelehrung und das Muster-Widerrufsformular der Bestellung der Einrichtung für 149 € (Abschnitt 3.2),
- den Zustimmungssatz zum vorzeitigen Beginn mit Erlöschen des Widerrufsrechts und Wertersatz (Abschnitt 3.1, Haken 3),
- den Auftragsverarbeitungsvertrag Terminplaner (Anlage A, Auszüge in Abschnitt 3.5),
- die Haftungs- und Ende-mit-dem-Abo-Klauseln der Leistungsbeschreibung (Anlage B, Auszüge in Abschnitt 3.4),
- die Gästetexte mit Gesetzlichkeitsfiktion: Widerrufsbelehrung für Gäste und die Hinweise und Haken im Widget (Abschnitte 3.6 bis 3.10).
**Hausrunde.** Die Datenschutzerklärung von WEBklar (Abschnitt 6c und die Ergänzungen in den Abschnitten 6 und 7), die Hinweise im Kundenportal und die Mailtexte prüft WEBklar selbst. Zu einzelnen Punkten daraus stellen wir trotzdem Fragen (Q15 bis Q19 und Q24); was wir nicht fragen, steht in Abschnitt 6.
**Begriffe.** *Betriebsart* (im Programm „Modus“) des Terminplaners: `off` (abgeschaltet), `pilot` (nur Kunden auf einer Liste von Pilotadressen) oder `on` (alle Kunden mit Wartungs-Abo). *Pilot* ist ein Kunde auf dieser Liste; derzeit sind das nur Konten des Inhabers. `on` setzt der Inhaber erst nach Ihrem Befund und seiner Freigabe. *Kulanz* heißt, dass das Team einem Kunden den Terminplaner ohne Bestellung und ohne Zahlung freischaltet (Abschnitt 2.1). *Fassung* ist ein Stand der Texte mit Datum.
**Keine Rechtsberatung durch WEBklar.** Die Texte sind Mustertexte für die Standardkonfiguration und keine Rechtsberatung (Leistungsbeschreibung Nr. 11 Abs. 1). Die rechtlichen Aussagen in diesem Paket (vor allem „unsere Annahme“ bei den Fragen) sind ein Arbeitsstand von WEBklar und kein Rechtsurteil.
**Fassungen und Änderungen.** Eine ausgelieferte Fassung wird nie überschrieben. Ändern wir nach dem Befund einen Text, bekommt er ein neues Fassungsdatum, neue Dateien (HTML und PDF) und ein neues Archiv der Gästetexte; die Zitate in diesem Paket werden dann angepasst. Ein Test prüft jedes Zitat wörtlich gegen den Quelltext, damit Paket und Programm nicht auseinanderlaufen.
**Aufbau.** Abschnitt 2 beschreibt den Produktablauf, Abschnitt 3 enthält die Texte, Abschnitt 4 unsere Fragen Q1 bis Q26, Abschnitt 5 die Fundstellen im Programm, Abschnitt 6 was nicht Teil der Prüfung ist, Abschnitt 7 die Checkliste für den Inhaber. Hinter jedem Zitat in Abschnitt 3 steht „⚖ Q…“ mit den Fragen, die den Text betreffen. Anlagen: **Anlage A** ist der AVV Terminplaner, **Anlage B** die Leistungsbeschreibung Terminplaner, beide in voller Länge als PDF (Dateien in Abschnitt 3.11). **Anlage C** sind die gerenderten Mails K15 (Bestellbestätigung mit den Zustimmungen) und K16 (Terminplaner bereit); sie folgt nach der Mail-Überarbeitung des Inhabers, weil ihr Wortlaut bis dahin noch nicht endgültig ist.
## 2. Produktablauf in Kurzform
Der Terminplaner ist ein Buchungs-Widget für die Website eines Betriebs (zum Beispiel Friseur, Werkstatt oder Studio) mit einer Verwaltung im Kundenportal von WEBklar. Der Betrieb ist Kunde von WEBklar („Kunde“); seine Besucher, die einen Termin buchen, sind „Gäste“. Verkauft wird die Einrichtung für 149 € einmalig je Website-Projekt, ohne Umsatzsteuer gemäß § 19 UStG. Für den Betrieb berechnet WEBklar neben dem Wartungs-Abo nichts; der Terminplaner läuft, solange das Wartungs-Abo läuft. Bezahlt wird beim Terminplaner nur vor Ort, es gibt keine Online-Zahlung der Gäste.
### 2.1 Bestellung und Einrichtung
| Schritt | Was geschieht | Frage |
|---|---|---|
| 1. Abo-Prüfung | Nur Kunden mit laufendem Wartungs-Abo können bestellen; ein Hosting-Abo genügt nicht. Bestellbar ist der Terminplaner nur, solange das Wartungs-Abo aktiv ist und keine Kündigung vorgemerkt ist. | ⚖ Q25 |
| 2. Fragebogen-Entwurf | Der Kunde beantwortet im Kundenportal einen Fragebogen in acht Schritten. Jeder Schritt wird sofort als Entwurf der späteren Einstellungen gespeichert; ein Entwurf ohne Bestellung wird 90 Tage nach der letzten Änderung gelöscht. Die Branchenfrage kennt den Gesundheitsbereich; wählt der Kunde ihn, ist die Bestellung gesperrt. Zusätzlich warnt eine Stichwortprüfung über Betriebsname, Leistungen und Felder das Team, das die Einrichtung ablehnen kann. | ⚖ Q11, Q24 |
| 3. Bestellung mit vier Haken | „Prüfen und bestellen“: Zusammenfassung, offene Punkte (der Server prüft, ob alles Nötige vorliegt, darunter die Adressen von Impressum und Datenschutzerklärung der Website), volle Widerrufsbelehrung mit Muster-Widerrufsformular, vier Haken und der Knopf „Zahlungspflichtig bestellen · 149 €“ (Abschnitte 3.1 und 3.2). Alle Besteller werden wie Verbraucher behandelt. | ⚖ Q1, Q3, Q19, Q20 |
| 4. Nachweis | Der Server legt zuerst die Zahlungssitzung bei Stripe an und speichert dann den Nachweis: die gezeigten Texte, ihre Fassungen, einen Hash, den Zeitpunkt des Klicks, IP-Adresse und Browser-Kennzeichen, Kundenkonto und angemeldete Person. Erst danach erhält der Browser die Adresse der Zahlungsseite. Der Zeitpunkt des Klicks ist der Anker der Widerrufsfrist. | ⚖ Q2, Q23 |
| 5. Zahlung | Stripe Checkout (Karte oder SEPA-Lastschrift); Stripe erstellt die Rechnung. | ⚖ Q19 |
| 6. Ticket und Bestätigung | Nach dem Zahlungseingang legt das Portal ein Einrichtungs-Ticket mit der Einbauanleitung an. Der Kunde erhält die Bestellbestätigung K15 mit den gezeigten Zustimmungen und, als PDF-Anhang auf einem dauerhaften Datenträger, Leistungsbeschreibung, AVV und Widerrufsbelehrung mit Muster-Widerrufsformular in der Fassung, der er zugestimmt hat. Das Team erhält die Meldung T16. | ⚖ Q3, Q20 |
| 7. Einbau und Freischaltung | WEBklar baut das Widget ein, der Kunde nimmt es ab, WEBklar schaltet es frei; der Kunde erhält die Mail K16 („bereit“). | |
| 8. Betrieb | Gäste buchen im Widget (Abschnitt 2.2), der Kunde verwaltet seine Termine im Kundenportal. | |
**Freischaltung ohne Bestellung (Pilot, Kulanz).** Das Team kann einen Terminplaner ohne Zahlung aktivieren. Sobald die Betriebsart `on` gilt (alle Kunden mit Wartungs-Abo), verlangt das Portal dafür einen Nachweis der Zustimmung des Kunden in Textform (Datum, Weg Chat oder E-Mail, Name des Mitarbeiters), außer für Pilotadressen; danach erhält der Kunde die Mail K16 mit Leistungsbeschreibung und AVV als PDF. Solche Dokumente gelten nicht als durch Kauf freigeschaltet; der tägliche Abo-Abgleich nimmt sie aus (⚖ Q7, Q21).
### 2.2 Gast: Buchung, Widerruf, Daten
1. Der Gast wählt Leistung, Termin und gegebenenfalls Person oder Platz und gibt Name und E-Mail-Adresse an (je nach Einstellung des Kunden auch Telefon, Nachricht, Zusatzfelder). Er sieht den Datenschutz-Hinweis (Abschnitt 3.8), die Haken (Abschnitt 3.7) und, bei Verbrauchern, die Widerrufsbelehrung zum Aufklappen (Abschnitt 3.6). ⚖ Q12, Q13, Q17
2. Die Buchung wird 30 Minuten gehalten. In dieser Zeit bestätigt der Gast per Link in einer E-Mail; erst die Bestätigung macht die Buchung verbindlich, bei einer Terminanfrage nimmt der Kunde später an. Nicht bestätigte Buchungen löscht WEBklar 48 Stunden nach Ablauf der Bestätigungsfrist. ⚖ Q14
3. Die Bestätigungs-E-Mail (G02) enthält bei Verbrauchern Widerrufshinweis, Widerrufsbelehrung und Muster-Widerrufsformular; bei einer Freizeit-Dienstleistung mit festem Termin, die der Kunde so einstuft, steht stattdessen der Hinweis, dass kein Widerrufsrecht besteht (Abschnitt 3.9). ⚖ Q15, Q16
4. Widerrufen kann der Gast über den Knopf „Vertrag widerrufen“ im Terminbereich der Website oder über den Link „Termin verwalten“ in der Bestätigungs-E-Mail; er erhält eine Eingangsbestätigung mit Datum und Uhrzeit (G08). Die Widerrufsfrist beträgt 14 Tage.
5. Der Terminplaner setzt keine Cookies und speichert nichts im Browser des Gastes; die IP-Adresse dient dem Missbrauchsschutz nur im Arbeitsspeicher des Servers. Die Buchung speichert als Nachweis die Fassung der Gästetexte, den Zeitpunkt, den Text des Buchungsknopfs, die Anrede und Hashes der gezeigten Texte; WEBklar archiviert jede Fassung der Gästetexte. ⚖ Q26
6. Löschfristen: bestätigte und erledigte Termine 90 Tage nach dem Terminende (einstellbar 14 bis 365 Tage), abgesagte oder abgelehnte Termine 30 Tage nach der Absage, nicht bestätigte Buchungen 48 Stunden nach Ablauf der Bestätigungsfrist; in der Versandwarteschlange für E-Mails werden Empfängeradresse und Inhalt nach dem Versand geleert und der Eintrag nach 7 Tagen gelöscht; Sicherungen halten gelöschte Daten bis zu sieben Tage. ⚖ Q9
### 2.3 Ende des Abos
1. Ein täglicher Abgleich prüft das Wartungs-Abo. Endet es (auch durch Wechsel ins Hosting-Abo oder durch Widerruf des Abschlusses oder eines Wechsels), schaltet WEBklar den Terminplaner spätestens am folgenden Tag ab und informiert den Kunden mit der Mail K22. Ist etwas nicht lesbar, schaltet der Abgleich nichts ab und versucht es am nächsten Tag erneut. ⚖ Q7
2. Nach dem Abschalten nimmt der Terminplaner keine neuen Buchungen mehr an. Die Termine bleiben im Kundenportal sichtbar und als CSV-Datei exportierbar, Gäste können über ihren Link absagen, bereits angeforderte Erinnerungen laufen weiter, der Kunde informiert seine Gäste.
3. 30 Tage nach dem Abschalten löscht WEBklar alle Termindaten und die Einstellungen; das Einrichtungs-Ticket des Teams bleibt als Arbeitsnachweis mit einer Kurzfassung der Einstellungen bestehen (keine Daten von Gästen). Beginnt innerhalb dieser 30 Tage wieder ein Wartungs-Abo, schaltet WEBklar den Terminplaner mit den vorhandenen Daten und Einstellungen ohne weitere Kosten wieder ein (Rückkehr, Mail K23). Nach der Löschung sind ein neuer Fragebogen und die Einrichtung für 149 € noch einmal nötig. ⚖ Q9
4. Endet das Wartungs-Abo, bevor der Terminplaner freigeschaltet ist, erstattet WEBklar die 149 € vollständig. Der Abgleich schaltet den Terminplaner mit dem Grund „Abo endete vor Einrichtung“ ab und meldet dem Team die Erstattung (T19); die Erstattung löst der Inhaber in Stripe aus, es gibt keine automatische Rückzahlung. In diesem Fall gibt es keine automatische Rückkehr. ⚖ Q6
### 2.4 Widerruf der Bestellung durch den Kunden
Der Kunde widerruft auf der Seite „Vertrag widerrufen“ mit der Auswahl „Terminplaner (Einrichtung)“. Jede Bestellung hat ihre eigene Frist von 14 Tagen ab dem Zeitpunkt des Klicks auf „Zahlungspflichtig bestellen“ (bei mehreren Website-Projekten also mehrere Fristen). Die Widerrufsseite ordnet den Widerruf keinem Projekt zu: Sie bewertet den Eingang gegen das späteste Fristende aller bezahlten Terminplaner-Bestellungen des Kunden; das Team erhält in der Meldung T09 jede Bestellung mit Projekt, Zustand und eigenem Fristende und entscheidet. Der Widerruf erzeugt die Mail K04 an den Kunden und die Meldung T09 an das Team; die Abwicklung (Erstattung in Stripe abzüglich Wertersatz für einen schon begonnenen Einbau, Abschalten des Terminplaners) erledigt das Team von Hand. ⚖ Q2, Q5, Q7
### 2.5 Zahlen
| Wert | Bedeutung | Quelle im Code |
|---|---|---|
| 149 € | Einrichtung je Website-Projekt, einmalig, ohne Umsatzsteuer gemäß § 19 UStG | `priceLabel` des Terminplaners, `amountCents` 14900 |
| 14 Tage | Widerrufsfrist | `WITHDRAWAL_DAYS` |
| 30 Minuten | Haltezeit einer Buchung, Gültigkeit des Bestätigungslinks | `holdMinutes` |
| 48 Stunden | Löschung nicht bestätigter Buchungen nach Ablauf der Bestätigungsfrist | `VERFALLEN_BEHALTEN_MS` |
| 90 Tage | Aufbewahrung bestätigter Termine nach dem Terminende (Vorgabe) und Aufbewahrung von Entwürfen ohne Bestellung | `AUFBEWAHRUNG_VORGABE`, `ENTWURF_TAGE` |
| 7 Tage | Eintrag in der Versandwarteschlange nach dem Versand; Sicherungen des Servers | `GESENDET_BEHALTEN_MS` |
| 30 Tage | Löschung nach dem Abschalten; Löschung abgesagter oder abgelehnter Termine | `LOESCHFRIST_TAGE`, `ABGESAGT_BEHALTEN_MS` |
## 3. Texte in Endfassung
Alle Zitate stehen wörtlich so im Programm oder in den Dokumenten (Abschnitt 5 nennt die Fundstellen). Platzhalter in doppelten geschweiften Klammern stehen für Angaben des jeweiligen Betriebs. In den Zitaten steht vor dem Sternchen in Klammern (\*) und vor spitzen Klammern ein Rückstrich, damit die Darstellung sie nicht als Hervorhebung oder Markup deutet; gemeint sind die Zeichen selbst.
### 3.1 Bestellung des Terminplaners: vier Haken und Knopf
Auf der Bestellseite stehen vor dem Knopf die volle Widerrufsbelehrung mit Muster-Widerrufsformular (Abschnitt 3.2) und diese vier Haken, keiner ist vorangekreuzt. Die Links auf Leistungsbeschreibung, AVV und Widerrufsbelehrung in der Fassung stehen daneben, nicht im Text der Haken. Gespeichert wird genau der gezeigte Text mit Fassung und Zeitpunkt.
**Haken 1 (`unterlagen`): Zustimmung zu Leistungsbeschreibung und AVV**
> Ich stimme der Leistungsbeschreibung Terminplaner und dem Auftragsverarbeitungsvertrag Terminplaner in der Fassung 2026-10-05 zu.
⚖ Q1, Q10
**Haken 2 (`gesundheit`): kein Gesundheitsbereich**
> Mein Angebot liegt nicht im Gesundheitsbereich (zum Beispiel keine Arzt-, Zahnarzt-, Psychotherapie-, Heilpraktiker- oder Physiotherapiepraxis).
⚖ Q11
**Haken 3 (`beginn`): vorzeitiger Beginn, Erlöschen, Wertersatz**
> Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit der Einrichtung des Terminplaners beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen Erbringung der Einrichtung erlischt (§ 356 Abs. 5 Nr. 2 BGB). Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB).
⚖ Q3, Q5
**Haken 4 (`information`): Pflichtinformationen**
> Ich habe die Pflichtinformationen zu dieser Bestellung gelesen: Leistung (Einrichtung des Terminplaners für diese Website: ein Widget an einer Stelle, gestaltet nach dem Fragebogen, mit Einbau und Datenschutz-Absatz), Gesamtpreis 149 € einmalig (ohne Umsatzsteuer gemäß § 19 UStG), voraussichtliche Dauer meist innerhalb von 1–2 Wochen, der Terminplaner endet mit dem Wartungs-Abo. Die Widerrufsbelehrung mit Muster-Widerrufsformular habe ich gelesen.
⚖ Q7, Q19, Q20
**Knopf**
> Zahlungspflichtig bestellen · 149 €
⚖ Q2, Q19
### 3.2 Widerrufsbelehrung und Muster-Widerrufsformular der Funktionen
WEBklar ist hier der Unternehmer. Die Belehrung steht auf der Bestellseite, im PDF-Anhang der Bestellbestätigung K15 und in der Datei `docs/rechtliches/widerrufsbelehrung-funktionen.md` (Fassung 2026-10-05).
**Widerrufsbelehrung und Muster-Widerrufsformular (Bestellung der Einrichtung)**
> Widerrufsbelehrung
>
> Widerrufsrecht
>
> Sie haben das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen.
>
> Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag des Vertragsabschlusses.
>
> Um Ihr Widerrufsrecht auszuüben, müssen Sie uns (WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, Telefon: 0176 23726355, E-Mail: support@webklar.com) mittels einer eindeutigen Erklärung (z. B. ein mit der Post versandter Brief oder eine E-Mail) über Ihren Entschluss, diesen Vertrag zu widerrufen, informieren. Sie können dafür das beigefügte Muster-Widerrufsformular verwenden, das jedoch nicht vorgeschrieben ist.
>
> Sie können Ihr Widerrufsrecht auch online unter https://project.webklar.com/widerrufen.html ausüben. Wenn Sie diese Online-Funktion nutzen, übermitteln wir Ihnen auf einem dauerhaften Datenträger (z. B. durch eine E-Mail) unverzüglich eine Eingangsbestätigung mit Informationen zum Inhalt der Widerrufserklärung sowie dem Datum und der Uhrzeit ihres Eingangs.
>
> Zur Wahrung der Widerrufsfrist reicht es aus, dass Sie die Mitteilung über die Ausübung des Widerrufsrechts vor Ablauf der Widerrufsfrist absenden.
>
> Folgen des Widerrufs
>
> Wenn Sie diesen Vertrag widerrufen, haben wir Ihnen alle Zahlungen, die wir von Ihnen erhalten haben, einschließlich der Lieferkosten (mit Ausnahme der zusätzlichen Kosten, die sich daraus ergeben, dass Sie eine andere Art der Lieferung als die von uns angebotene, günstigste Standardlieferung gewählt haben), unverzüglich und spätestens binnen vierzehn Tagen ab dem Tag zurückzuzahlen, an dem die Mitteilung über Ihren Widerruf dieses Vertrags bei uns eingegangen ist. Für diese Rückzahlung verwenden wir dasselbe Zahlungsmittel, das Sie bei der ursprünglichen Transaktion eingesetzt haben, es sei denn, mit Ihnen wurde ausdrücklich etwas anderes vereinbart; in keinem Fall werden Ihnen wegen dieser Rückzahlung Entgelte berechnet.
>
> Haben Sie verlangt, dass die Dienstleistungen während der Widerrufsfrist beginnen soll, so haben Sie uns einen angemessenen Betrag zu zahlen, der dem Anteil der bis zu dem Zeitpunkt, zu dem Sie uns von der Ausübung des Widerrufsrechts hinsichtlich dieses Vertrags unterrichten, bereits erbrachten Dienstleistungen im Vergleich zum Gesamtumfang der im Vertrag vorgesehenen Dienstleistungen entspricht.
>
> Muster-Widerrufsformular
>
> (Wenn Sie den Vertrag widerrufen wollen, dann füllen Sie bitte dieses Formular aus und senden Sie es zurück.)
>
> – An WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, E-Mail: support@webklar.com:
> – Hiermit widerrufe(n) ich/wir (\*) den von mir/uns (\*) abgeschlossenen Vertrag über den Kauf der folgenden Waren (\*)/die Erbringung der folgenden Dienstleistung (\*)
> – Bestellt am (\*)/erhalten am (\*)
> – Name des/der Verbraucher(s)
> – Anschrift des/der Verbraucher(s)
> – Unterschrift des/der Verbraucher(s) (nur bei Mitteilung auf Papier)
> – Datum
>
> (\*) Unzutreffendes streichen.
⚖ Q1, Q2, Q12
Das Dokument enthält vor der Belehrung einen Einleitungssatz und nach dem Muster-Widerrufsformular „Hinweise zum Widerruf“, die nicht zur gesetzlichen Belehrung gehören:
**Einleitung des Dokuments**
> Das Widerrufsrecht gilt nur für Verbraucher. Wir zeigen die Belehrung und das Muster-Widerrufsformular trotzdem allen, die eine Funktion bestellen. Die Belehrung gilt für die Bestellung der Einrichtung einer Funktion im Kundenportal, zum Beispiel des Terminplaners. Hinweise dazu stehen nach dem Muster-Widerrufsformular.
⚖ Q1
**Hinweise zum Widerruf**
> Diese Hinweise gehören nicht zur gesetzlichen Widerrufsbelehrung. Sie erklären, wie sich das Widerrufsrecht auf die Bestellung einer Funktion auswirkt.
>
> - Das Widerrufsrecht gilt für jede Bestellung einer Funktion gesondert, also für jedes Website-Projekt, für das der Kunde eine Funktion bestellt. Die Frist von vierzehn Tagen beginnt jeweils mit dem Vertragsschluss.
> - Widerruft der Kunde die Einrichtung einer Funktion, endet diese Funktion. Das Wartungs-Abo bleibt davon unberührt.
> - Hat WEBklar auf ausdrücklichen Wunsch des Kunden vor Ablauf der Widerrufsfrist mit der Einrichtung begonnen, schuldet der Kunde bei einem Widerruf einen angemessenen Betrag für die bis dahin erbrachte Leistung. Der Betrag bemisst sich als Anteil am Gesamtpreis; Maß ist der im Arbeitsblatt erfasste Aufwand für die Einrichtung.
> - Die Rückzahlung erfolgt über das ursprüngliche Zahlungsmittel; die Zahlung lief über Stripe.
⚖ Q5, Q7
### 3.3 Zustimmung bei der Bestellung der News-Pinnwand (Altbestand)
Die News-Pinnwand (30 € einmalig) wird seit 2026-09-30 über denselben Bestellweg verkauft, bisher mit einem Haken und ohne Widerrufsbelehrung. Zum Vergleich der Text, den neue Bestellungen jetzt sehen, und der Haken, der bis zur Korrektur galt (Programmstand 3a0b008 vor der Korrektur). Die Leistungsbeschreibung der News-Pinnwand aus dieser Zeit steht in Frage Q4.
**Neuer Haken der News-Pinnwand**
> Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit dem Einbau beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen Erbringung des Einbaus erlischt (§ 356 Abs. 5 Nr. 2 BGB). Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB).
⚖ Q4, Q3
**Alte Fassung (Stand 3a0b008): früherer Haken der News-Bestellung**
> Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit dem Einbau beginnt. Mir ist bekannt, dass mein Widerrufsrecht erlischt, sobald der Einbau vollständig erbracht ist (§ 356 Abs. 4 BGB).
⚖ Q4
### 3.4 Leistungsbeschreibung Terminplaner (Anlage B), Auszüge
Die Datei ist `docs/rechtliches/leistungsbeschreibung-terminplaner.md`; sie ist als Anlage B vollständig beigefügt. Zitiert sind die Nummern, auf die sich unsere Fragen beziehen; weitere Nummern behandeln Geltung, Leistung, Voraussetzungen und Verfügbarkeit.
**Nr. 4 Abs. 3: Gesundheitsbereich**
> (3) Der Terminplaner ist nicht für Angebote im Gesundheitsbereich bestimmt, weil dort besondere Kategorien personenbezogener Daten anfallen können (Art. 9 DSGVO). Der Kunde setzt ihn dort nicht ein und fordert von seinen Gästen keine Gesundheitsangaben. WEBklar darf die Einrichtung für eine solche Website ablehnen oder den Terminplaner dort abschalten.
⚖ Q11
**Nr. 4 Abs. 9: Datenschutz-Absatz auf der Website des Kunden**
> (9) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz für die Online-Terminbuchung ein; das gehört zur Einrichtung und wird nicht auf das Stundenkontingent angerechnet. Liegt sie woanders, etwa bei einem externen Generator, erhält der Kunde den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt der Kunde verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Kunden darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
⚖ Q18
**Nr. 5: Einrichtung und Preis**
> (1) Die Einrichtung umfasst die Einrichtung nach dem Fragebogen, den Einbau des Widgets in die Website, den Datenschutz-Absatz nach Nr. 4 Abs. 9, die Abnahme durch den Kunden und die Freischaltung durch WEBklar.
>
> (2) Die Einrichtung kostet 149 € einmalig je Website-Projekt, ohne Umsatzsteuer gemäß § 19 UStG.
>
> (3) Die voraussichtliche Dauer der Einrichtung nennt die Bestellseite vor der Bestellung. Sie ist eine Einschätzung und keine verbindliche Zusage eines Termins.
>
> (4) Der Kunde bezahlt bei der Bestellung im Kundenportal über Stripe und erhält eine Rechnung.
>
> (5) Der Preis steht neben dem Stundenkontingent des Wartungs-Abos: Die Einrichtung wird weder auf das Kontingent angerechnet noch damit bezahlt. Für den Betrieb des Terminplaners berechnet WEBklar neben dem Wartungs-Abo nichts weiter; er läuft, solange das Wartungs-Abo läuft (Nr. 8).
⚖ Q19, Q20
**Nr. 7: Haftung für verpasste Termine**
> (1) Der Terminplaner ist ein Hilfsmittel. Die Einhaltung der Termine bleibt Sache des Kunden.
>
> (2) WEBklar übernimmt keine Gewähr für die Zustellung der E-Mails (Nr. 6 Abs. 4).
>
> (3) Für entgangenen Gewinn und Folgeschäden aus verpassten Terminen haftet WEBklar nur im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen.
>
> (4) Der Haftungsrahmen des Kunden gegenüber seinen Gästen bleibt unberührt.
⚖ Q8
**Nr. 8: Ende der Leistung**
> (1) Mit dem Ende des Wartungs-Abos endet der Terminplaner. WEBklar schaltet ihn spätestens am folgenden Tag ab und informiert den Kunden per E-Mail. Das gilt auch bei einem Wechsel ins Hosting-Abo und beim Widerruf des Abschlusses oder eines Wechsels ins Wartungs-Abo.
>
> (2) Mit dem Abschalten nimmt der Terminplaner keine neuen Buchungen mehr an, und das Widget zeigt den Ausweichtext. Die Termine bleiben im Kundenportal sichtbar, und der Kunde kann sie als CSV-Datei exportieren. Gäste können über ihren Link absagen. Bereits angeforderte Erinnerungen laufen weiter. Der Kunde informiert seine Gäste.
>
> (3) WEBklar löscht alle Termindaten und die Einstellungen 30 Tage nach dem Abschalten. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Einstellungen bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6 des AVV Terminplaner).
>
> (4) Beginnt innerhalb dieser 30 Tage wieder ein Wartungs-Abo, schaltet WEBklar den Terminplaner mit den vorhandenen Daten und Einstellungen ohne weitere Kosten wieder ein. Nach der Löschung ist eine neue Einrichtung nötig (neuer Fragebogen, neuer Preis nach Nr. 5).
>
> (5) Endet das Wartungs-Abo, bevor WEBklar den Terminplaner freigeschaltet hat, erstattet WEBklar den Einrichtungspreis vollständig.
>
> (6) Auf Wunsch des Kunden in Textform schaltet WEBklar den Terminplaner jederzeit ab. Eine Erstattung des Einrichtungspreises folgt daraus nicht; für den Widerruf gilt Nr. 10.
⚖ Q6, Q7, Q9
**Nr. 9 Abs. 4: Einstellung ohne Grund**
> (4) Eine Einstellung ohne einen Grund nach Absatz 2 ist ausgeschlossen. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
⚖ Q8
**Nr. 10: Widerruf**
> (1) Verbraucher können die Bestellung der Einrichtung nach der Widerrufsbelehrung widerrufen. Belehrung und Muster-Widerrufsformular stehen in der Anlage „Widerrufsbelehrung und Muster-Widerrufsformular (Einrichtung von Funktionen)“.
>
> (2) Ob das Widerrufsrecht erlischt und ob der Kunde einen angemessenen Betrag für die bis dahin erbrachte Leistung schuldet, richtet sich nach der Zustimmung, die der Kunde bei der Bestellung gibt, und nach den Hinweisen zum Widerruf in der Anlage.
>
> (3) Mit dem Widerruf endet der Terminplaner.
⚖ Q3, Q5, Q7
**Nr. 11: Hinweise**
> (1) **Keine Rechtsberatung.** Der Hinweis am Buchungsformular, die Texte der Haken, der Datenschutz-Absatz und die Widerrufsbelehrung für Gäste sind Mustertexte für die Standardkonfiguration. WEBklar sorgt dafür, dass sie die Technik richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Die Einstufung seiner Leistungen als Leistung für Verbraucher oder Unternehmer und als Freizeitleistung trägt der Kunde. Eine rechtliche Beratung im Einzelfall schuldet WEBklar nicht.
>
> (2) **Barrierefreiheit.** Das Widget ist für die Bedienung mit Tastatur und Hilfstechniken gestaltet (Beschriftungen, Fokusführung, Fehlermeldungen als Live-Region). Eine Konformität nach WCAG 2.1 AA oder EN 301 549 sagt WEBklar nicht zu. Ob der Kunde als Dienstleistungserbringer unter das BFSG fällt (Ausnahme für Kleinstunternehmen, § 3 Abs. 3 BFSG), entscheidet und prüft der Kunde.
⚖ Q15, Q18, Q22
### 3.5 Auftragsverarbeitungsvertrag Terminplaner (Anlage A), Auszüge
Die Datei ist `docs/rechtliches/avv-terminplaner.md`; sie ist als Anlage A vollständig beigefügt (mit der Anlage der technischen und organisatorischen Maßnahmen).
**Vorbemerkung Abs. 1 und 2: Abschluss**
> (1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Einrichtung des Terminplaners bestellt und dabei per Haken diesem Vertrag und der Leistungsbeschreibung „Terminplaner“ („Leistungsbeschreibung“) in der angezeigten Fassung zustimmt. Das Portal speichert die Fassung, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte und die Widerrufsbelehrung in dieser Fassung als PDF enthält und auf sie verlinkt.
>
> (2) Ein Mitarbeiter von WEBklar darf die Funktion nur einschalten, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. WEBklar speichert die Fassung, das Datum und den Weg dieser Erklärung, den Zeitpunkt des Einschaltens und den Namen des Mitarbeiters. Der Kunde erhält anschließend eine E-Mail mit diesen beiden Texten als PDF.
⚖ Q10, Q21
**Nr. 3 Abs. 1: Daten der Gäste**
> (1) Daten der Gäste: Name und E-Mail-Adresse; auf Einstellung des Kunden außerdem Telefonnummer, eine Nachricht und Zusatzfelder, die der Kunde im Formular festlegt; die gebuchte Leistung, die Person oder der Platz und der Termin; der Status der Buchung mit den Zeitpunkten (Anlegen, Bestätigung, Annahme, Absage, Widerruf); Absagegrund und Vermerke zu einer Absage oder einem Widerruf (Freitext, auch mit dem Namen aus einer Widerrufserklärung); Preis, Zahlart, Zahlungsstatus und die Angabe, ob als Verbraucher gebucht wurde; der Nachweis der Zustimmungen (Fassung der Gästetexte, Zeitpunkt, Text des Buchungsknopfs, Anrede und Hashes (Prüfsummen) der gezeigten Texte); der Wunsch nach einer Erinnerung; der Zustellstatus der E-Mails an den Gast.
⚖ Q10
**Nr. 4 Abs. 2: Weisungen**
> (2) Mit der Bestellung weist der Kunde WEBklar an, den Terminplaner nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen im Kundenportal (Leistungen, Personen oder Plätze, Zeiten, Felder des Formulars, Aufbewahrungsdauer, Erinnerungen, Einschalten und Ausschalten). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Terminplaner abschalten lassen (Nr. 8 Abs. 6 der Leistungsbeschreibung). Die Löschung auf Weisung nach Nummer 9 Abs. 6 setzt WEBklar immer um.
⚖ Q10
**Nr. 7: Unterauftragsverarbeiter**
> (1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Terminplaner läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der Buchungs-E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt.
>
> (2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Terminplaner abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6).
>
> (3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).
>
> (4) Die Zahlung der Einrichtung über Stripe betrifft nur den Vertrag zwischen dem Kunden und WEBklar. Daten von Gästen oder Beschäftigten des Kunden gehen nicht an Stripe. Stripe ist deshalb kein Unterauftragsverarbeiter dieses Vertrags.
⚖ Q10
**Nr. 8 Abs. 5: Nachweis und Archiv der Gästetexte**
> (5) Die Buchung speichert als Nachweis der Zustimmungen die Fassung der Gästetexte, den Zeitpunkt, den Text des Buchungsknopfs, die Anrede und Hashes der übrigen Texte, die der Gast gesehen hat, nicht diese Texte selbst. Damit sich der Wortlaut nachweisen lässt, bewahrt WEBklar jede Fassung der Gästetexte (Hinweis am Formular, Texte der Haken, Datenschutz-Absatz und Widerrufsbelehrung für Gäste) mit dem Zeitraum ihrer Verwendung auf, bis drei Jahre nach dem Ende des Terminplaners. WEBklar stellt sie dem Kunden auf Verlangen zur Verfügung.
⚖ Q26
**Nr. 9: Löschung und Rückgabe**
> (1) Nicht bestätigte Buchungen löscht WEBklar 48 Stunden nach Ablauf der Bestätigungsfrist von 30 Minuten.
>
> (2) Abgesagte und abgelehnte Termine löscht WEBklar 30 Tage nach der Absage oder Ablehnung. Terminanfragen, die der Kunde bis zum angefragten Terminbeginn nicht beantwortet, behandelt der Terminplaner als abgelehnt; die 30 Tage laufen dann ab diesem Zeitpunkt.
>
> (3) Bestätigte und erledigte Termine löscht WEBklar nach der Aufbewahrungsdauer, gerechnet ab dem Ende des Termins. Der Kunde stellt sie im Kundenportal ein; die Vorgabe sind 90 Tage, einstellbar sind 14 bis 365 Tage.
>
> (4) In der Versandwarteschlange für E-Mails leert WEBklar die Empfängeradresse und den Inhalt einer E-Mail nach dem Versand und löscht den Eintrag 7 Tage nach dem Versand. E-Mails, die nicht zugestellt werden können, löscht WEBklar mit Empfängeradresse und Inhalt 48 Stunden nach dem endgültigen Fehlschlag.
>
> (5) Der Löschlauf läuft täglich; gelöscht wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu löschen, als ein Lauf schafft, folgt der Rest im nächsten Lauf.
>
> (6) Ist der Terminplaner abgeschaltet oder das Wartungs-Abo beendet, löscht WEBklar alle Termindaten und die Einstellungen 30 Tage danach. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Einstellungen bestehen (zum Beispiel Betriebsangaben, Namen der Leistungen, die ersten acht Namen der Personen oder Plätze und die Wünsche des Kunden zum Einbau) und enthält keine Daten von Gästen; dafür gilt die Datenschutzerklärung von WEBklar (Abschnitt 6). Bis zur Löschung kann der Kunde die Termine im Kundenportal als CSV-Datei exportieren. Beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schaltet WEBklar den Terminplaner mit den vorhandenen Daten und Einstellungen wieder ein (Nr. 8 Abs. 4 der Leistungsbeschreibung). Früher löscht der Kunde einzelne Gastdaten selbst mit der Aktion „Gastdaten löschen“ (Nummer 8 Abs. 2); alle Daten löscht WEBklar auf Weisung des Kunden in Textform unverzüglich.
>
> (7) Entwürfe aus dem Fragebogen, aus denen keine Bestellung geworden ist, löscht WEBklar 90 Tage nach der letzten Änderung.
>
> (8) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.
>
> (9) Gesetzliche Aufbewahrungspflichten bleiben unberührt.
⚖ Q7, Q9, Q10
### 3.6 Gäste: Widerrufsbelehrung und Muster-Widerrufsformular
Der Terminplaner erzeugt diese Belehrung mit den Angaben des Betriebs (alle vier Angaben sind Pflicht) und zeigt sie Verbrauchern beim Buchen und in der Bestätigungs-E-Mail. Grundlage ist das Muster der Anlage 1 und das Muster-Widerrufsformular der Anlage 2 zu Art. 246a § 1 Abs. 2 Satz 2 EGBGB (Dateien `docs/rechtliches/quellen/anlage-1.txt` und `anlage-2.txt`, abgerufen 2026-10-02). Gestaltungshinweise für einen Dienstleistungsvertrag sind angewendet: Fristbeginn „des Vertragsabschlusses“, Wertersatz für Dienstleistungen; die Widerrufsfunktion nennt keine Internetadresse, sondern einen anderen geeigneten Hinweis. Ausgelassen sind die Textbausteine zu Waren und der Absatz zum elektronischen Webformular. Die Anrede ist immer „Sie“.
**Widerrufsbelehrung und Muster-Widerrufsformular für Gäste (Platzhalter für die Betriebsangaben)**
> Widerrufsbelehrung
>
> Widerrufsrecht
>
> Sie haben das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen.
>
> Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag des Vertragsabschlusses.
>
> Um Ihr Widerrufsrecht auszuüben, müssen Sie uns ({{Name des Betriebs}}, {{Anschrift}}, Telefon: {{Telefon}}, E-Mail: {{E-Mail}}) mittels einer eindeutigen Erklärung (z. B. ein mit der Post versandter Brief oder eine E-Mail) über Ihren Entschluss, diesen Vertrag zu widerrufen, informieren. Sie können dafür das beigefügte Muster-Widerrufsformular verwenden, das jedoch nicht vorgeschrieben ist.
>
> Sie können Ihr Widerrufsrecht auch online unter „Vertrag widerrufen“ (Schaltfläche im Terminbereich auf unserer Website sowie Link „Termin verwalten“ in Ihrer Bestätigungs-E-Mail) ausüben. Wenn Sie diese Online-Funktion nutzen, übermitteln wir Ihnen auf einem dauerhaften Datenträger (z. B. durch eine E-Mail) unverzüglich eine Eingangsbestätigung mit Informationen zum Inhalt der Widerrufserklärung sowie dem Datum und der Uhrzeit ihres Eingangs.
>
> Zur Wahrung der Widerrufsfrist reicht es aus, dass Sie die Mitteilung über die Ausübung des Widerrufsrechts vor Ablauf der Widerrufsfrist absenden.
>
> Folgen des Widerrufs
>
> Wenn Sie diesen Vertrag widerrufen, haben wir Ihnen alle Zahlungen, die wir von Ihnen erhalten haben, einschließlich der Lieferkosten (mit Ausnahme der zusätzlichen Kosten, die sich daraus ergeben, dass Sie eine andere Art der Lieferung als die von uns angebotene, günstigste Standardlieferung gewählt haben), unverzüglich und spätestens binnen vierzehn Tagen ab dem Tag zurückzuzahlen, an dem die Mitteilung über Ihren Widerruf dieses Vertrags bei uns eingegangen ist. Für diese Rückzahlung verwenden wir dasselbe Zahlungsmittel, das Sie bei der ursprünglichen Transaktion eingesetzt haben, es sei denn, mit Ihnen wurde ausdrücklich etwas anderes vereinbart; in keinem Fall werden Ihnen wegen dieser Rückzahlung Entgelte berechnet.
>
> Haben Sie verlangt, dass die Dienstleistungen während der Widerrufsfrist beginnen soll, so haben Sie uns einen angemessenen Betrag zu zahlen, der dem Anteil der bis zu dem Zeitpunkt, zu dem Sie uns von der Ausübung des Widerrufsrechts hinsichtlich dieses Vertrags unterrichten, bereits erbrachten Dienstleistungen im Vergleich zum Gesamtumfang der im Vertrag vorgesehenen Dienstleistungen entspricht.
>
> Muster-Widerrufsformular
>
> (Wenn Sie den Vertrag widerrufen wollen, dann füllen Sie bitte dieses Formular aus und senden Sie es zurück.)
>
> – An {{Name des Betriebs}}, {{Anschrift}}, {{E-Mail}}:
> – Hiermit widerrufe(n) ich/wir (\*) den von mir/uns (\*) abgeschlossenen Vertrag über den Kauf der folgenden Waren (\*)/die Erbringung der folgenden Dienstleistung (\*)
> – Bestellt am (\*)/erhalten am (\*)
> – Name des/der Verbraucher(s)
> – Anschrift des/der Verbraucher(s)
> – Unterschrift des/der Verbraucher(s) (nur bei Mitteilung auf Papier)
> – Datum
>
> (\*) Unzutreffendes streichen.
⚖ Q12, Q13
### 3.7 Gäste: Haken und Hinweis zu Gesundheitsangaben
Die Haken stehen immer in der Sie-Form. Keiner ist vorangekreuzt.
**Haken „Vorzeitiger Beginn“** (Pflicht, nur für Verbraucher bei Leistungen mit Preis, wenn der Termin vor Ablauf der Widerrufsfrist liegt; bei Anfragen mit Preis immer)
> Ich verlange ausdrücklich, dass {{Firma}} mit der Leistung vor Ablauf der Widerrufsfrist beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen Erbringung der Leistung erlischt (§ 356 Abs. 5 Nr. 2 BGB). Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB).
⚖ Q13, Q3
**Haken „Stornobedingungen“** (Pflicht, wenn der Betrieb Stornobedingungen hinterlegt hat und die Leistung einen Preis hat)
> Ich habe die Stornobedingungen gelesen.
⚖ Q16
**Haken „Erinnerung“** (freiwillig; nur wenn der Betrieb Erinnerungen anbietet; `{{n}}` ist 2, 12, 24 oder 48)
> Ich möchte {{n}} Stunden vor dem Termin eine Erinnerung per E-Mail erhalten. Diese Einwilligung kann ich jederzeit widerrufen.
⚖ Q17
**Hinweis über jedem Freitextfeld**
> Bitte keine Gesundheitsangaben.
⚖ Q11
### 3.8 Gäste: Datenschutz-Hinweis am Buchungsformular
Der Hinweis steht in der Zusammenfassung vor dem Absenden, zusammen mit einem Link auf die Datenschutzerklärung des Betriebs; es gibt keinen Haken dafür. Sie- oder Du-Fassung nach der Anrede des Betriebs; die Löschfristen sind die des Löschlaufs.
**Fassung Sie**
> Datenschutz. Wir verwenden Ihre Angaben, um Ihren Termin zu verwalten und zu bestätigen (Art. 6 Abs. 1 lit. b DSGVO). Betrieben wird der Terminkalender in unserem Auftrag von der WEBklar GbR (Server: Hetzner, Deutschland). Wir speichern Ihre Daten bis 90 Tage nach dem Termin, abgesagte Termine 30 Tage, nicht bestätigte Buchungen 48 Stunden nach Ablauf der Bestätigungsfrist. Es werden keine Cookies gesetzt. Bitte machen Sie keine Angaben zu Ihrer Gesundheit. Mehr erfahren Sie in unserer Datenschutzerklärung. Verantwortlich: {{Firma}}, {{Anschrift}}.
⚖ Q17, Q9
**Fassung Du**
> Datenschutz. Wir verwenden deine Angaben, um deinen Termin zu verwalten und zu bestätigen (Art. 6 Abs. 1 lit. b DSGVO). Betrieben wird der Terminkalender in unserem Auftrag von der WEBklar GbR (Server: Hetzner, Deutschland). Wir speichern deine Daten bis 90 Tage nach dem Termin, abgesagte Termine 30 Tage, nicht bestätigte Buchungen 48 Stunden nach Ablauf der Bestätigungsfrist. Es werden keine Cookies gesetzt. Bitte mach keine Angaben zu deiner Gesundheit. Mehr erfährst du in unserer Datenschutzerklärung. Verantwortlich: {{Firma}}, {{Anschrift}}.
⚖ Q17, Q9
### 3.9 Gäste: Hinweise in der Bestätigungs-E-Mail (G02)
Die Bestätigungs-E-Mail (Vorlage `server/mail-templates/G02 Termin bestaetigt.html`) enthält je nach Buchung einen der beiden folgenden Hinweise (hier die Sie-Fassung); danach folgt bei Widerrufsrecht die Belehrung aus Abschnitt 3.6. Die Du-Fassung ist inhaltlich gleich.
**Widerrufsrecht besteht (Verbraucher)**
> Widerrufsrecht. Wenn Sie Verbraucher sind, können Sie diesen Vertrag innerhalb von 14 Tagen ab Vertragsschluss ohne Angabe von Gründen widerrufen. Bis zum Beginn des Termins geht das über den Knopf „Vertrag widerrufen“; danach über das Formular „Vertrag widerrufen“ auf der Website von {{betrieb_name}} oder per E-Mail an {{betrieb_email}}. Die ausführliche Widerrufsbelehrung und das Muster-Widerrufsformular finden Sie weiter unten in dieser E-Mail.
⚖ Q2, Q12
**Kein Widerrufsrecht (Freizeit-Dienstleistung mit festem Termin)**
> Für diese Buchung besteht kein Widerrufsrecht (§ 312g Abs. 2 Nr. 9 BGB). Absagen richten sich nach den Stornobedingungen.
⚖ Q15, Q16
### 3.10 Gäste: Knopftexte
Die Knopftexte hängen davon ab, ob die Buchung verbindlich oder eine Anfrage ist und ob die Leistung einen Preis hat. Im Widget und auf der Bestätigungsseite heißen die Knöpfe:
**Verbindliche Buchung mit Preis: Widget, dann Bestätigungsseite**
> Zahlungspflichtig buchen
>
> Zahlungspflichtig bestätigen
⚖ Q14
**Verbindliche Buchung ohne Preis: Widget, dann Bestätigungsseite**
> Termin buchen
>
> Termin bestätigen
⚖ Q14
**Anfrage: Widget, dann Bestätigungsseite**
> Termin anfragen
>
> Anfrage absenden
⚖ Q14
**Widerrufsfunktion: Knopf im Terminbereich, dann Bestätigung des Formulars**
> Vertrag widerrufen
>
> Widerruf bestätigen
⚖ Q12, Q14
### 3.11 Anlagen A und B
- **Anlage A:** Auftragsverarbeitungsvertrag Terminplaner, Fassung 2026-10-05, als PDF `public/rechtliches/avv-terminplaner-2026-10-05.pdf` (Quelle `docs/rechtliches/avv-terminplaner.md`).
- **Anlage B:** Leistungsbeschreibung Terminplaner, Fassung 2026-10-05, als PDF `public/rechtliches/leistungsbeschreibung-terminplaner-2026-10-05.pdf` (Quelle `docs/rechtliches/leistungsbeschreibung-terminplaner.md`).
- Die Widerrufsbelehrung mit Muster-Widerrufsformular der Funktionen steht in Abschnitt 3.2 vollständig; ihr PDF ist `public/rechtliches/widerrufsbelehrung-funktionen-2026-10-05.pdf`.
- **Anlage C** (gerenderte Mails K15 und K16) wird nach der Mail-Überarbeitung des Inhabers nachgereicht (Abschnitt 7.4).
## 4. Fragen an die Kanzlei
Jede Frage hat vier Teile. **Frage:** was wir wissen möchten. **Fundstelle im Text:** wo der Text steht (Abschnitt 3, Anlage A oder B, Programm) und, als Kennung H1 bis C8, welche Punkte der früheren Listen offener Rechtsfragen die Frage mit abdeckt (Abschnitt 6.3 ordnet alle Kennungen zu). **Unsere Annahme:** der Stand, mit dem wir arbeiten; bitte bestätigen oder korrigieren. **Alternative:** was wir stattdessen täten. Die Zählung von § 356 BGB haben wir am 2026-10-05 auf gesetze-im-internet.de abgeglichen; für die übrigen Normen bitten wir um Gegenprüfung (Q3). Einordnungen wie „Annahme“ sind keine Rechtsurteile.
### Q1: Alle Besteller wie Verbraucher
**Frage.** Der Inhaber hat entschieden, alle Besteller der Einrichtung (149 €) wie Verbraucher zu behandeln: Alle sehen die volle Widerrufsbelehrung mit Muster-Widerrufsformular und alle setzen die Haken zum vorzeitigen Beginn. Die Besteller sind meist Unternehmer (Betriebe mit eigener Website); ob jemand als Verbraucher (§ 13 BGB) oder Unternehmer (§ 14 BGB) bestellt, kann WEBklar nicht verlässlich prüfen (Existenzgründer, Nebenerwerb). Genügt der Satz am Anfang des Dokuments „Das Widerrufsrecht gilt nur für Verbraucher. Wir zeigen die Belehrung und das Muster-Widerrufsformular trotzdem allen, die eine Funktion bestellen.“, damit Unternehmern kein vertragliches Widerrufsrecht entsteht und die Haken für sie nichts Falsches sagen?
**Fundstelle im Text.** Abschnitt 3.1 (Haken 1 bis 4), Abschnitt 3.2 (Einleitung des Dokuments und Belehrung), Leistungsbeschreibung Nr. 10 Abs. 1 („Verbraucher können die Bestellung der Einrichtung nach der Widerrufsbelehrung widerrufen“).
**Unsere Annahme.** Ja. Verbraucher haben das gesetzliche Widerrufsrecht, die Haken wirken bei ihnen; Unternehmern räumt die Einleitung kein Widerrufsrecht ein, die Haken schaden ihnen nicht. Die Abwicklung eines Widerrufs ist Handarbeit des Teams (Meldung T09); ob ein Unternehmer erstattet bekommt, entscheidet das Team im Einzelfall.
**Alternative.** Vor den Haken fragen, ob als Verbraucher oder als Unternehmer bestellt wird (Haken 3 und Belehrung nur für Verbraucher), oder den Terminplaner nur an Unternehmer verkaufen (Bestätigung nach § 14 BGB) und die Belehrung als freiwillige Zusage geben.
### Q2: Beginn der Widerrufsfrist
**Frage.** Wann beginnt die Widerrufsfrist der Bestellung? Wir rechnen ab dem Zeitpunkt des Klicks auf „Zahlungspflichtig bestellen · 149 €“ (gespeichert im Nachweis); das Fristende wird nach § 193 BGB auf einen Werktag verschoben. Die Belehrung steht vor dem Klick auf der Bestellseite, auf einem dauerhaften Datenträger (PDF-Anhang der Bestellbestätigung K15) erhält der Kunde sie erst nach dem Zahlungseingang. Beginnt die Frist mit dem Klick (Annahme durch WEBklar), erst mit dem Zahlungseingang (bei SEPA-Lastschrift Tage später) oder erst mit dem Zugang von K15 (§ 356 Abs. 3 BGB)? Gilt dasselbe für die Gäste (Vertragsschluss bei verbindlicher Buchung mit der Bestätigung per Link, bei einer Anfrage mit der Annahme durch den Betrieb)?
**Fundstelle im Text.** Abschnitt 3.1 (Knopf), Abschnitt 3.2 („ab dem Tag des Vertragsabschlusses“), Abschnitt 3.9 (G02), Abschnitt 2.4; Programm: `withdrawFeature` rechnet mit dem Zeitpunkt des Klicks aus dem Nachweis (ersatzweise mit dem Bestellzeitpunkt), `withdrawalDeadline` mit 14 Tagen.
**Unsere Annahme.** Der Klick ist der Vertragsschluss: Mit ihm löst der Kunde die Zahlung aus; danach gibt es keine gesonderte Annahmeerklärung von WEBklar. WEBklar kann die Einrichtung nur aus den Gründen von Leistungsbeschreibung Nr. 4 Abs. 3 (Gesundheitsbereich, Q11) ablehnen oder abschalten. Die Belehrung ist vor dem Klick lesbar; K15 bestätigt sie mit den PDFs auf einem dauerhaften Datenträger.
**Alternative.** Die Frist ab Zahlungseingang oder ab Zugang von K15 rechnen und die Belehrung entsprechend formulieren („ab dem Tag, an dem Sie diese Bestellbestätigung erhalten“).
### Q3: Zustimmungssatz: Erlöschen, Wertersatz, Kenntnis, dauerhafter Datenträger
**Frage.** Genügt Haken 3 („Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit der Einrichtung des Terminplaners beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen Erbringung der Einrichtung erlischt (§ 356 Abs. 5 Nr. 2 BGB). Widerrufe ich … schulde ich einen angemessenen Betrag … (§ 357a Abs. 2 BGB).“)? Bitte prüfen: (a) die Zählung der Norm für das Erlöschen bei Dienstleistungen (§ 356 Abs. 5 Nr. 2 BGB) und für den Wertersatz (§ 357a Abs. 2 BGB); (b) ob das ausdrückliche Verlangen und die Bestätigung der Kenntnis im selben Haken stehen dürfen; (c) ob der Wertersatz-Hinweis im Haken ausreicht oder auch in die Belehrung gehört; (d) ob K15 mit den Texten der Zustimmung, dem Zeitpunkt und den PDFs die nötige Bestätigung auf einem dauerhaften Datenträger ist (die gerenderte Mail folgt als Anlage C nach der Mail-Überarbeitung des Inhabers).
**Fundstelle im Text.** Abschnitt 3.1 (Haken 3), Abschnitt 3.2 („Hinweise zum Widerruf“), Abschnitt 3.3 (Haken der News), Leistungsbeschreibung Nr. 10 Abs. 2; Programm: `termineConsent` (Register), `onFeaturePaid` (K15, Block „Zustimmungen“ und Anhänge).
**Unsere Annahme.** Der Haken erfüllt beides: Verlangen, Kenntnis vom Erlöschen und Hinweis auf den Wertersatz in einem Satz mit Zeitpunkt im Nachweis; K15 gibt den Satz zusammen mit der Belehrung auf einem dauerhaften Datenträger wieder. Die Zählung entspricht der Fassung auf gesetze-im-internet.de vom 2026-10-05.
**Alternative.** Zwei Haken (Verlangen und Kenntnis getrennt); die Bestätigung als eigener, ausdrücklich so benannter Absatz in K15.
### Q4: News-Altkunden
**Frage.** Seit 2026-09-30 verkauft WEBklar die News-Pinnwand (30 € einmalig) über denselben Bestellweg wie jetzt den Terminplaner. Bis zur Korrektur sah die Bestellung so aus („3a0b008“ ist die Kennung des Programmstands vor der Korrektur). Alte Fassung (Stand 3a0b008), der Haken:
> Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit dem Einbau beginnt. Mir ist bekannt, dass mein Widerrufsrecht erlischt, sobald der Einbau vollständig erbracht ist (§ 356 Abs. 4 BGB).
Alte Fassung (Stand 3a0b008), Leistungsbeschreibung der News-Pinnwand, Abschnitt 6 „Widerrufsrecht“ (nur diese zwei Absätze, keine gesetzliche Musterbelehrung, kein Muster-Widerrufsformular):
> Sind Sie Verbraucher, können Sie diesen Vertrag innerhalb von 14 Tagen ohne Angabe von Gründen widerrufen. Die Frist beginnt mit dem Vertragsschluss. Zum Widerruf genügt eine eindeutige Erklärung an uns, zum Beispiel über project.webklar.com/widerrufen.html.
>
> Verlangen Sie ausdrücklich, dass wir vor Ablauf der Widerrufsfrist mit dem Einbau beginnen, müssen Sie uns bei einem Widerruf einen angemessenen Betrag für die bis dahin erbrachte Leistung zahlen. Das Widerrufsrecht erlischt nach § 356 Abs. 4 BGB, wenn wir die Leistung vollständig erbracht haben und Sie vorher ausdrücklich zugestimmt haben, dass wir vor Ablauf der Widerrufsfrist beginnen, und Ihre Kenntnis bestätigt haben, dass Sie Ihr Widerrufsrecht bei vollständiger Vertragserfüllung verlieren.
Alte Fassung (Stand 3a0b008), Abschnitt 2 derselben Leistungsbeschreibung zum Preis:
> Der Einbau kostet 30,00 € einmalig, inklusive Umsatzsteuer. Die Nutzung der Pinnwand danach kostet nichts; es entstehen keine laufenden Kosten und kein Abo. Sie bezahlen bei der Bestellung im Kundenportal über Stripe und erhalten eine Rechnung.
Die Bestellbestätigung K15 enthielt nach unserem Stand weder eine Belehrung noch Anhänge. Der Haken zitierte also § 356 Abs. 4 BGB und enthielt keinen Hinweis auf den Wertersatz (§ 357a Abs. 2 BGB), den die Leistungsbeschreibung nur allgemein nannte; der Preis war „inklusive Umsatzsteuer“ angegeben (richtig ist § 19 UStG, Q19). Lief bei diesen News-Altkunden, soweit sie Verbraucher sind, die Widerrufsfrist nach § 356 Abs. 3 BGB nicht an? Wenn ja: Sollen wir die Belehrung per Mail nachholen (Form, Inhalt, Zeitpunkt, Hinweis zum Wertersatz), oder genügt es, nur künftig die neuen Texte zu verwenden? Was gilt für bereits eingebaute und freigeschaltete News-Pinnwände (Erlöschen des Widerrufsrechts, Wertersatz)? Die Zahl der betroffenen Fälle liefert der Inhaber vor der Übergabe: [Anzahl der Bestellungen seit 2026-09-30 mit dem alten Text].
**Fundstelle im Text.** Abschnitt 3.3 (neuer und früherer Haken), Abschnitt 3.2 (Belehrung der Funktionen, die die News künftig ebenfalls nutzt); Programm: `config.features.news` (neuer Haken; bestehende Bestellungen behalten den damals gespeicherten Text und die Fassung). Die alten Texte stammen aus dem Programmstand 3a0b008 (`server/config.js`, `public/vertragsunterlagen/funktionen-leistungsbeschreibung.html`). Die Leistungsbeschreibung der News-Pinnwand vom 30.09.2026 liegt unverändert unter `public/vertragsunterlagen/funktionen-leistungsbeschreibung-2026-09-30.html` (unveränderte Kopie des Standes 3a0b008, durch eine Prüfsumme gesichert), weil Bestellungen seit dem 30.09.2026 auf ihr beruhen; die geltende Datei (Stand 05.10.2026) verweist am Anfang auf sie.
**Unsere Annahme.** Nach unserer Lesart (kein Rechtsurteil) kann die Frist bei fehlender ordnungsgemäßer Belehrung nach § 356 Abs. 3 BGB nicht angelaufen sein. Der Inhaber entscheidet nach Ihrem Befund: Belehrung nur nachholen, wenn die Frist danach nicht lief; sonst gelten die neuen Texte nur für künftige Bestellungen.
**Alternative.** Allen News-Altkunden vorsorglich die Belehrung samt Muster-Formular per Mail schicken (mit dem Hinweis, dass die Frist damit beginnt), unabhängig vom Befund; oder, wenn die Kanzlei keinen Handlungsbedarf sieht, nichts tun.
### Q5: Wertersatz: Anteil am Gesamtpreis oder Stundenaufwand
**Frage.** Die Muster-Belehrung verlangt beim Widerruf einen angemessenen Betrag, der dem Anteil der bis zur Unterrichtung erbrachten Dienstleistungen im Vergleich zum Gesamtumfang entspricht (§ 357a Abs. 2 BGB). Unsere „Hinweise zum Widerruf“ sagen: „Der Betrag bemisst sich als Anteil am Gesamtpreis; Maß ist der im Arbeitsblatt erfasste Aufwand für die Einrichtung.“ Intern war ursprünglich eine Berechnung „nach Stundenaufwand“ vorgesehen. Ist diese Berechnung (Anteil an 149 € nach dem erfassten Aufwand im Verhältnis zum Gesamtaufwand der Einrichtung dieses Projekts) zulässig, oder muss der Gesamtumfang vorab festgelegt werden, damit der Anteil bestimmbar ist?
**Fundstelle im Text.** Abschnitt 3.1 (Haken 3), Abschnitt 3.2 (Muster-Belehrung, Absatz „Haben Sie verlangt …“, und „Hinweise zum Widerruf“, dritter Punkt), Leistungsbeschreibung Nr. 10 Abs. 2; Mails K04 und T09; Programm `withdrawFeature`.
**Unsere Annahme.** Anteilig am Preis (nicht Stundensatz mal Zeit). Das Maß der erbrachten Leistung ist der im Arbeitsblatt des Einrichtungs-Tickets erfasste Aufwand, der Gesamtumfang der Aufwand der Einrichtung dieses Projekts nach dem Ticket. Eine feste Gesamtstundenzahl steht nicht im Vertrag.
**Alternative.** Den Gesamtumfang in der Leistungsbeschreibung festlegen (zum Beispiel feste Schritte oder eine Stundenzahl) und den Wertersatz nach erledigten Schritten bemessen; oder bei Widerruf nach Einbaubeginn pauschal einen festen Anteil ansetzen.
### Q6: Volle Erstattung, wenn das Abo vor der Freischaltung endet
**Frage.** Leistungsbeschreibung Nr. 8 Abs. 5 lautet: „Endet das Wartungs-Abo, bevor WEBklar den Terminplaner freigeschaltet hat, erstattet WEBklar den Einrichtungspreis vollständig.“ Ist die Klausel wirksam und klar (§ 307 BGB), auch wenn WEBklar den Einbau schon begonnen oder fertiggestellt hat und auch wenn der Kunde das Abo selbst beendet? Außerdem: Nr. 8 Abs. 4 sagt, dass WEBklar den Terminplaner bei einem neuen Wartungs-Abo innerhalb der 30 Tage wieder einschaltet. Das Programm tut das nur für den Abschaltgrund „Abo beendet“ (`abo_ende`, nach Freischaltung), nie für `abo_ende_vor_einrichtung`: Nach der Erstattung bleibt der Terminplaner aus; ob und wie er wieder eingerichtet wird, entscheidet das Team im Einzelfall. Muss der Text das sagen?
**Fundstelle im Text.** Abschnitt 3.4 (Nr. 8 Abs. 4 bis 6), Abschnitt 2.3 Punkt 4; Programm: `gleicheAboAb` (Grund `abo_ende_vor_einrichtung`, Meldung T19 an das Team, keine automatische Rückkehr), Mail K22 Block „vor_einrichtung“.
**Unsere Annahme.** Die Klausel begünstigt den Kunden und ist unproblematisch. Die Rückkehr gilt nur für den Fall nach der Freischaltung; für den Erstattungsfall ist sie nicht gewollt, weil die Erstattung voll geschuldet bleibt.
**Alternative.** Die Erstattung auf den Fall beschränken, dass WEBklar noch nicht mit dem Einbau begonnen hat (sonst Wertersatz); Nr. 8 Abs. 4 um den Satz ergänzen, dass die Rückkehr nicht gilt, wenn der Einrichtungspreis erstattet wurde.
### Q7: Ende mit dem Abo, auch bei Wechsel und Widerruf; Ausnahme für Pilot und Kulanz
**Frage.** (a) Leistungsbeschreibung Nr. 8 Abs. 1 lässt den Terminplaner mit dem Wartungs-Abo enden, auch beim Wechsel ins Hosting-Abo und beim Widerruf des Abschlusses oder eines Wechsels ins Wartungs-Abo. Ist diese Kopplung zulässig, obwohl der Kunde die Einrichtung einmalig bezahlt hat und sie ein eigener Vertrag ist? Wie verhält sich der Widerruf des Abos zum Einrichtungsvertrag, insbesondere wenn der Terminplaner schon freigeschaltet ist und deshalb nichts erstattet wird? (b) Ausnahme: Terminplaner, die das Team ohne Bestellung freigeschaltet hat (Pilot, Kulanz; im Programm `unlockedByPurchase` nicht wahr), nimmt der Abgleich aus und zählt sie im Tagesbericht als „ausgenommen“; sie laufen weiter, bis WEBklar sie abschaltet. Genügt der Satz „WEBklar schaltet ihn spätestens am folgenden Tag ab“ trotzdem, oder braucht es eine Klausel, die diese Kulanzfälle nennt? (c) Der Abgleich schaltet bei Lesefehlern nichts ab und versucht es am nächsten Tag; ein Abo im Status „Zahlungsverzug“ gilt als laufend. Ist „spätestens am folgenden Tag“ dann zu streng gesagt?
**Fundstelle im Text.** Abschnitt 3.4 (Nr. 8 Abs. 1 und 6, Nr. 10), Abschnitt 3.1 (Haken 4: „der Terminplaner endet mit dem Wartungs-Abo“), Abschnitt 2.3, Abschnitt 2.1 (Freischaltung ohne Bestellung); Programm: `gleicheAboAb`, `endeTermineFuerKunde`.
**Unsere Annahme.** Die Kopplung ist zulässig, weil der Terminplaner nur mit einem Wartungs-Abo betrieben wird und der Kunde das in Haken 4 liest. Kulanzfälle sind keine Verträge über die Einrichtung; ihre Ausnahme braucht keine Klausel. „Spätestens am folgenden Tag“ beschreibt den Normalfall.
**Alternative.** Nr. 8 Abs. 1 auf „in der Regel am folgenden Tag“ abschwächen; eine Kulanzklausel („Terminplaner, die WEBklar ohne Bestellung freigeschaltet hat, schaltet WEBklar nach eigenem Ermessen ab“) aufnehmen; beim Widerruf des Abos nach Freischaltung eine anteilige Erstattung prüfen.
### Q8: Haftung für verpasste Termine; Erstattung bei Einstellung
**Frage.** (a) Leistungsbeschreibung Nr. 7 Abs. 3 (Abschnitt 3.4) beschränkt die Haftung für entgangenen Gewinn und Folgeschäden aus verpassten Terminen auf den Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen (unbeschränkt bei Vorsatz, grober Fahrlässigkeit, Verletzung von Leben, Körper oder Gesundheit; bei leicht fahrlässiger Verletzung wesentlicher Pflichten begrenzt auf den vertragstypischen, vorhersehbaren Schaden; im Übrigen ausgeschlossen). Ist die Klausel gegenüber Verbrauchern mit § 309 Nr. 7 BGB vereinbar, und ist die Verweisung hinreichend transparent? (b) Braucht es gegenüber Unternehmern einen Höchstbetrag (etwa den Jahresbetrag des Wartungs-Abos)? (c) Nr. 9 Abs. 4 schließt eine Einstellung ohne Grund aus. Wann WEBklar den Terminplaner beenden oder aussetzen darf, regeln Nr. 9 Abs. 2 und 3:
> (2) WEBklar darf den Terminplaner für eine Website mit einer Frist von vier Wochen beenden oder für alle Kunden mit einer Frist von drei Monaten einstellen, wenn
> - ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Kunde einer dafür nötigen neuen Fassung nicht zustimmt (AVV Terminplaner Nummer 12), oder
> - eine Sicherheitslücke sich nicht anders beheben lässt.
>
> Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
>
> (3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar den Terminplaner ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Kunden darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.
Zur Erstattung des Einrichtungspreises, wenn WEBklar den Terminplaner danach beendet, schweigt der Text. Braucht es eine (anteilige) Erstattung?
**Fundstelle im Text.** Abschnitt 3.4 (Nr. 7 und Nr. 9 Abs. 4), Leistungsbeschreibung Nr. 9 Abs. 2 und 3 (oben zitiert), Abo-Bedingungen § 13 (`public/abo-bedingungen.html`).
**Unsere Annahme.** Die Verweisung auf § 13 genügt; ein Höchstbetrag ist nicht nötig, weil § 13 Abs. 2 die Haftung schon auf den vorhersehbaren Schaden begrenzt. Bei Einstellung aus den Gründen von Nr. 9 Abs. 2 erstattet WEBklar nichts, weil der Terminplaner bis dahin lief; die Rechte aus § 327r BGB bleiben.
**Alternative.** Einen Höchstbetrag gegenüber Unternehmern festlegen; bei Einstellung durch WEBklar den Einrichtungspreis anteilig für die Restlaufzeit erstatten.
### Q9: 30 Tage nach dem Abschalten: Anker, Rückkehr, Sicherungen
**Frage.** Der Terminplaner endet mit dem Wartungs-Abo; WEBklar schaltet ihn spätestens am folgenden Tag ab und löscht alle Termindaten und Einstellungen 30 Tage nach dem Abschalten. Bis dahin kann der Kunde die Termine im Kundenportal ansehen und als CSV-Datei exportieren; beginnt ein neues Wartungs-Abo, schaltet WEBklar ohne Kosten mit den vorhandenen Daten wieder ein (Rückkehr, nur beim Abschaltgrund „Abo beendet“, `abo_ende`). Fragen: (a) Ist der Anker (Zeitpunkt des Abschaltens, `disabledAt`, nicht der Tag des Abo-Endes) und die Frist von 30 Tagen mit Art. 28 Abs. 3 lit. g DSGVO vereinbar? (b) Darf die Rückkehr ohne neue Zustimmung zur Fassung automatisch erfolgen? (c) Gelöschte Daten bleiben in der täglichen Vollsicherung bis zu sieben Tage (Rückspielen holt der Löschlauf nach): genügt das? (d) Der tägliche Löschlauf löscht frühestens nach Ablauf der jeweiligen Frist, nicht sekundengenau; „48 Stunden“ und „30 Tage“ sind frühestmögliche Zeitpunkte (H4, A3). (e) Terminanfragen, die der Kunde bis zum angefragten Terminbeginn nicht beantwortet, gelten ab dann als abgelehnt, und die 30 Tage laufen ab diesem Zeitpunkt (AVV Nr. 9 Abs. 2). (f) Der Satz im Datenschutz-Absatz der Kunden-Website („Wird die Online-Terminbuchung abgeschaltet, zum Beispiel weil unser Vertrag mit WEBklar endet, werden alle Termindaten 30 Tage nach dem Abschalten gelöscht.“) und der Satz in Abschnitt 6c stimmen mit dem Programm überein, seit der tägliche Abgleich das Abschalten übernimmt. (g) Das Einrichtungs-Ticket, in dem das Team die Einrichtung als Arbeitsnachweis festhält, bleibt nach der Löschung mit einer Kurzfassung der Einstellungen bestehen (Betriebsangaben, Namen der Leistungen, die ersten acht Namen der Personen oder Plätze, Wünsche des Kunden zum Einbau; keine Daten von Gästen). Leistungsbeschreibung Nr. 8 Abs. 3 und AVV Nr. 9 Abs. 6 sagen das; die Datenschutzerklärung (Abschnitt 6) nennt die Arbeitsnachweise und die Aufbewahrung bis zum Ablauf der Verjährungsfrist. Ist das neben der Löschpflicht des Art. 28 Abs. 3 lit. g DSGVO zulässig, auch wenn der Kunde die Löschung aller Daten anweist (AVV Nr. 9 Abs. 6)?
**Fundstelle im Text.** Abschnitt 3.4 (Nr. 8 Abs. 3 und 4), Abschnitt 3.5 (AVV Nr. 9, besonders Abs. 2, 6 und 8), Abschnitt 2.3; frühere Punkte H4, A3, C4 (Sicherungen), C7 (Satz in 6c); Mail K22 und Datenschutzerklärung 6c (Hausrunde); Programm: `LOESCHFRIST_TAGE`, `loescheAbgeschaltete`, `gleicheAboAb`; Einrichtungs-Ticket: `termineTicketDetails`.
**Unsere Annahme.** Ja. 30 Tage ab dem Abschalten sind angemessen, weil der Kunde in dieser Zeit Termine ansehen und exportieren kann und eine Rückkehr kostenlos ist. Die Rückkehr stellt den früheren Stand wieder her; die zugestimmte Fassung gilt weiter, eine neue Fassung braucht ihre Zustimmung nach AVV Nr. 12. Der Sicherungsüberhang von sieben Tagen folgt aus der täglichen Vollsicherung bei Hetzner (letzte sieben Tagessicherungen) und steht offen im Vertrag.
**Alternative.** Das Modell der Website-Analyse: Löschung am 61. Tag nach dem Ende, Kopie der Daten auf Verlangen bis zum 46. Tag (AVV Website-Analyse Nr. 9 Abs. 2); oder 30 Tage ab dem Tag des Abo-Endes statt ab dem Abschalten.
### Q10: AVV Terminplaner: Aufbau, Hetzner, Drittland, Daten, Weisungen
**Frage.** Bitte den AVV (Anlage A) prüfen; besonders: (a) der Abschluss durch Haken bei der Bestellung (Textform; Art. 28 Abs. 9 DSGVO) mit gespeicherter Fassung, Zeitpunkt, Konto und Person, und durch Zustimmung in Textform bei Freischaltung durch das Team (Q21); (b) die Wahl eines eigenen Vertrags (Geschwister des AVV der Website-Analyse, Vorrang vor Anlage 1 der Abo-Bedingungen) statt eines Rahmen-AVV oder eines Zusatzes, damit Kunden der Website-Analyse nicht neu zustimmen müssen; (c) Nr. 7: Hetzner Online GmbH, Rechenzentrum Falkenstein (Server und Sicherung) und Nürnberg (SMTP-Dienst des Webhostings) und der Satz „Eine Übermittlung in Drittländer findet nicht statt“, auch wenn ein Kunde den geheimen Kalender-Link in einem Kalenderdienst eines Anbieters wie Google oder Apple abonniert (das Abonnement richtet der Kunde ein, WEBklar übermittelt dorthin nichts) (A2); (d) Nr. 7 Abs. 4: Stripe ist kein Unterauftragsverarbeiter, weil keine Daten von Gästen oder Beschäftigten an Stripe gehen; (e) Aufruf des Widgets: IP-Adresse und Browser-Kennzeichen erhält der Server technisch bei jedem Abruf, Server-Protokolle 14 Tage (Art. 6 Abs. 1 lit. f DSGVO) (A1); im Knopfmodus lädt das Widget die Konfiguration schon beim Laden der Seite (A6, C8), vom selben Server wie das Skript; (f) Beschäftigtendaten: Namen, Zeiten und der geheime Kalender-Link; der Kunde informiert seine Beschäftigten (C3); (g) Nr. 3 Abs. 1: Absagegrund und Vermerke (Freitext, auch mit dem Namen aus einer Widerrufserklärung) sowie Preis, Zahlart, Zahlungsstatus und die Angabe, ob als Verbraucher gebucht wurde; Nr. 2 Abs. 3: Zählungen ohne Personenbezug, die WEBklar für den sicheren Betrieb braucht, sind keine eigenen Zwecke; (h) Nr. 4 Abs. 2 und Nr. 9 Abs. 6: Löschung auf Weisung in Textform „unverzüglich“, ohne feste Frist.
**Fundstelle im Text.** Abschnitt 3.5 (Vorbemerkung Abs. 1 und 2, Nr. 3 Abs. 1, Nr. 4 Abs. 2, Nr. 7, Nr. 9), Anlage A vollständig mit der Anlage der technischen und organisatorischen Maßnahmen; frühere Punkte A1, A2, A6, C3, C8; Programm: `baueNachweis`, `datenschutzAbsatz`.
**Unsere Annahme.** Der AVV ist vollständig und passt zum Programm (Datenarten, Löschfristen, Unterauftragsverarbeiter sind durch Tests an den Code gebunden). Eine Drittlandübermittlung durch WEBklar findet nicht statt; das Kalender-Abonnement liegt in der Verantwortung des Kunden. „Unverzüglich“ genügt für die Löschung auf Weisung.
**Alternative.** Ein Rahmen-AVV mit einer Anlage je Funktion (Abschnitt 6.2); im AVV einen Hinweis auf das Kalender-Abonnement bei Drittanbietern aufnehmen; für die Löschung auf Weisung eine feste Frist nennen (zum Beispiel 14 Tage).
### Q11: Gesundheits-Ausschluss
**Frage.** Der Terminplaner ist nicht für den Gesundheitsbereich bestimmt (Art. 9 DSGVO). Schutzmaßnahmen: die Branchenfrage im Fragebogen (der Gesundheitsbereich ist wählbar, die Bestellung dann aber gesperrt), Haken 2 mit Aufzählung (Arzt-, Zahnarzt-, Psychotherapie-, Heilpraktiker- oder Physiotherapiepraxis), eine weiche Stichwortprüfung über Betriebsname, Leistungen und Felder, die das Team warnt, das Recht in Leistungsbeschreibung Nr. 4 Abs. 3, die Einrichtung abzulehnen oder den Terminplaner dort abzuschalten, der Hinweis „Bitte keine Gesundheitsangaben“ über jedem Freitextfeld und im Datenschutz-Hinweis (A5), die Pflicht des Kunden in AVV Nr. 11 Abs. 4. Reicht das, damit WEBklar als Auftragsverarbeiter keine besonderen Kategorien verarbeitet? Und: Leistungsbeschreibung Nr. 4 Abs. 3 sagt nichts zur Erstattung, wenn WEBklar die Einrichtung wegen des Gesundheitsbereichs ablehnt oder abschaltet; muss der Text das regeln?
**Fundstelle im Text.** Abschnitt 3.1 (Haken 2), Abschnitt 3.4 (Nr. 4 Abs. 3), Abschnitt 3.7 (Hinweis), Anlage A Nr. 3 Abs. 3 und Nr. 11 Abs. 4; früherer Punkt A5; Programm: `BRANCHEN`, `gesundheitsWarnungen`, `bestellpruefung`.
**Unsere Annahme.** Die Kombination aus Frage, Haken, Warnung und Ablehnungsrecht genügt; die Aufzählung im Haken ist beispielhaft. Lehnt WEBklar die Einrichtung ab, erstattet WEBklar den Preis aus Kulanz (nach Widerruf oder Kulanz, kein Satz im Text).
**Alternative.** Eine ausdrückliche Erstattungsklausel für die Ablehnung; die Aufzählung im Haken durch „Heilkunde und Gesundheitsversorgung“ ersetzen; eine zweite Bestätigung im Fragebogen.
### Q12: Gäste-Belehrung: Abweichungen vom Muster und Gesetzlichkeitsfiktion
**Frage.** Die Belehrung für Gäste (Abschnitt 3.6) beruht auf dem Muster der Anlage 1 zu Art. 246a § 1 Abs. 2 Satz 2 EGBGB, angepasst für einen Dienstleistungsvertrag. Genügt sie für die Gesetzlichkeitsfiktion, obwohl sie abweicht? (a) Die Klammer zu den Lieferkosten im Absatz „Folgen des Widerrufs“ bleibt stehen, weil der Gestaltungshinweis keine Streichung bei Dienstleistungen vorsieht (B1); dasselbe gilt für die Belehrung der Funktionen (Abschnitt 3.2). (b) Die Widerrufsfunktion nennt statt einer Internetadresse einen „anderen geeigneten Hinweis“: die Schaltfläche „Vertrag widerrufen“ im Terminbereich und den Link „Termin verwalten“ in der Bestätigungs-E-Mail; die Adresse der Seite fehlt (B5). (c) „… dass die Dienstleistungen … beginnen soll“ steht im Singular des Gesetzestexts (B2). (d) Das Widget zeigt die Belehrung aufklappbar in der Zusammenfassung vor dem Knopf, die Bestätigungs-E-Mail enthält sie als Text (H1). (e) Die Fiktion gilt nur, wenn das Muster inhaltlich und gestalterisch dem Gesetz entspricht; Zeilenumbrüche, Überschriften und die Aufzählung im Formular sind unsere Darstellung (B6).
**Fundstelle im Text.** Abschnitt 3.6, Abschnitt 3.2 (Lieferkosten-Klammer und Widerrufsfunktion mit Adresse der Widerrufsseite bei den Funktionen), Abschnitt 3.9, Abschnitt 3.10 („Vertrag widerrufen“); frühere Punkte H1, B1, B2, B5, B6; Programm: `widerrufsbelehrung`, Muster in `docs/rechtliches/quellen/`.
**Unsere Annahme.** Ja: Die Abweichungen sind die Ausfüllung der Lücken, die das Muster vorsieht, oder der Gesetzestext selbst; die aufklappbare Darstellung ist zulässig, weil sie vor dem Absenden vollständig erreichbar ist.
**Alternative.** Die Klammer zu den Lieferkosten streichen; die Widerrufsfunktion zusätzlich mit der Adresse der Seite des Betriebs nennen; die Belehrung ohne Aufklappen offen anzeigen; „beginnen sollen“ im Plural.
### Q13: Gäste: Haken zum vorzeitigen Beginn und Wertersatz
**Frage.** Der Haken „Vorzeitiger Beginn“ (Abschnitt 3.7) wird nur Verbrauchern bei Leistungen mit Preis gezeigt, wenn der Termin vor Ablauf der Widerrufsfrist beginnt; bei Anfragen mit Preis immer, weil der Tag der Annahme nicht feststeht. Die Frist rechnen wir mit 14 Tagen ab dem Tag nach dem Vertragsschluss im Berliner Kalender (§§ 187, 188, 193 BGB), gerechnet ab Buchungszeitpunkt plus Haltezeit. (a) Genügt der Haken (Verlangen, Kenntnis, Wertersatz) in dieser Form? (b) Der Absatz zum Wertersatz steht immer in der Belehrung, auch wenn der Gast keinen vorzeitigen Beginn verlangt hat, weil das Muster ihn für Dienstleistungen vorsieht (B4). Ist das unschädlich? (c) Liegt der Termin nach Ablauf der Frist, zeigen wir keinen Haken: stimmt das?
**Fundstelle im Text.** Abschnitt 3.7 (Haken „Vorzeitiger Beginn“), Abschnitt 3.6 (Absatz „Haben Sie verlangt …“); früherer Punkt B4; Programm: `ZUSTIMMUNG_VORZEITIG`, `brauchtZustimmungVorzeitig`.
**Unsere Annahme.** Ja zu (a) bis (c): Der Haken wirkt nur, wo ein Widerrufsrecht noch besteht und die Leistung vor Fristende beginnt; der Muster-Absatz zum Wertersatz bleibt unverändert, um die Fiktion nicht zu gefährden.
**Alternative.** Den Haken bei jeder Buchung mit Preis zeigen; den Wertersatz-Absatz nur bei gesetztem Haken in die Belehrung aufnehmen (Abweichung vom Muster).
### Q14: Anfrage mit Festpreis, Bestätigungslink und Seitenknopf als Bestellung
**Frage.** Wann ist die Buchung eines Gastes eine Bestellung mit Zahlungspflicht im Sinne von § 312j Abs. 3 BGB? Unser Ablauf: Bei einer verbindlichen Buchung mit Preis trägt der Knopf im Widget die Aufschrift „Zahlungspflichtig buchen“; der Gast bestätigt dann per Link in einer E-Mail, und die Bestätigungsseite hat den Knopf „Zahlungspflichtig bestätigen“; erst damit kommt der Vertrag zustande. Bezahlt wird vor Ort. Bei einer Terminanfrage, auch mit Festpreis, heißen die Knöpfe „Termin anfragen“ und „Anfrage absenden“, der Vertrag kommt erst mit der Annahme durch den Betrieb zustande. Ohne Preis lauten die Knöpfe „Termin buchen“ und „Termin bestätigen“. Genügt das? Muss der Knopf einer Anfrage mit Festpreis ebenfalls „zahlungspflichtig“ heißen? Ist der Seitenknopf auf der Bestätigungsseite die maßgebliche Bestellung?
**Fundstelle im Text.** Abschnitt 3.10 (Knopftexte), Abschnitt 2.2 Punkt 2; Programm: `knopfText`.
**Unsere Annahme.** Ja: Die zahlungspflichtige Handlung ist die Bestätigung auf der Bestätigungsseite; bei einer Anfrage entsteht die Zahlungspflicht erst mit der Annahme, die der Betrieb erklärt und nicht der Gast.
**Alternative.** Auch den Knopf einer Anfrage mit Festpreis „Zahlungspflichtig anfragen“ nennen; bei einem Preis den Preis am Knopf zeigen.
### Q15: Einstufung als Freizeitleistung und Hinweis im Portal
**Frage.** Im Kundenportal stuft der Betrieb selbst ein, ob seine Termine Freizeitveranstaltungen zu einem festen Termin sind (§ 312g Abs. 2 Nr. 9 BGB); kreuzt er das an, zeigt der Terminplaner keine Widerrufsbelehrung, kein Formular und keine Widerrufsfunktion, und die Bestätigung nennt den Satz aus Abschnitt 3.9. Der Hinweis dazu im Portal lautet:
> Nur ankreuzen, wenn deine Termine Freizeitveranstaltungen zu einem festen Termin sind (§ 312g Abs. 2 Nr. 9 BGB), zum Beispiel ein einzelner Tanzabend, ein Kurs oder ein Escape Room. Nach unserer Einschätzung gilt bei Friseur, Kosmetik, Nagel- und Fußpflege, Tattoo, Fotoshootings, Coaching und Beratung in der Regel ein Widerrufsrecht; bei Massage und Wellness ist die Einordnung unklar, im Zweifel gilt ein Widerrufsrecht. Kreuzt du es an, zeigt der Terminplaner keine Widerrufsbelehrung. Kreuzt du es zu Unrecht an, können deine Gäste noch bis zu 12 Monate und 14 Tage widerrufen. Das ist keine Rechtsberatung: Im Zweifel nicht ankreuzen oder dich rechtlich beraten lassen.
Ist diese Einschätzung vertretbar, vor allem für Fotoshootings, Coaching, Beratung, Tattoo und Massage? Genügt „Das ist keine Rechtsberatung“, oder haftet WEBklar, wenn ein Betrieb zu Unrecht ankreuzt und Gäste noch bis zu 12 Monate und 14 Tage widerrufen können? Ein zweiter Hinweis im Portal (`WIDERRUF_HINWEIS`) erklärt dem Betrieb, dass Privatpersonen eine Buchung 14 Tage nach Vertragsschluss widerrufen können (bei Anfragen zählt die Annahme), dass das Widerrufsrecht seiner Stornoregel vorgeht, dass bei vorzeitigem Beginn Wertersatz geschuldet sein kann (§ 357a Abs. 2 BGB) und dass der Terminplaner Belehrung und Knopf „Vertrag widerrufen“ automatisch zeigt.
**Fundstelle im Text.** Abschnitt 3.9 (Satz „kein Widerrufsrecht“), Leistungsbeschreibung Nr. 4 Abs. 8 und Nr. 11 Abs. 1 (die Einstufung trägt der Kunde); Programm: `FREIZEIT_HINWEIS`, `WIDERRUF_HINWEIS`, `zeigtWiderruf`.
**Unsere Annahme.** Friseur, Kosmetik, Nagel- und Fußpflege, Tattoo, Fotoshootings, Coaching und Beratung fallen in der Regel nicht unter die Ausnahme; bei Massage und Wellness ist die Einordnung unklar, im Zweifel besteht ein Widerrufsrecht. Der Hinweis stellt die Einstufung klar dem Betrieb zu.
**Alternative.** Die Einstufung nicht dem Betrieb überlassen: Der Terminplaner zeigt die Belehrung immer, die Freizeit-Ausnahme gibt es nicht; oder die Beispiele im Hinweis streichen.
### Q16: Storno-Muster (50 Prozent, 24 Stunden) und Wegfall bei kostenlosen Leistungen
**Frage.** Die Vorlagen schlagen dem Betrieb Stornobedingungen vor, zum Beispiel für Salon, Kosmetik und Beratung:
> Sie können Ihren Termin bis 24 Stunden vorher kostenlos absagen oder verschieben (Link in der Bestätigungsmail). Danach oder bei Nichterscheinen berechnen wir für den freigehaltenen Termin 50 % des Preises. Dem Kunden bleibt der Nachweis vorbehalten, dass uns kein oder ein geringerer Aufwand entstanden ist. Ihr gesetzliches Widerrufsrecht bleibt unberührt.
(Bei Tattoo und Fotografie 48 Stunden.) Im Portal steht neben dem Textfeld der Hinweis:
> Das ist ein Muster, kein Rechtstext. Bitte prüfe es und passe es an deine Bedingungen an.
(a) Hält das Muster einer Kontrolle nach § 307 BGB (und § 309 Nr. 5 BGB gegenüber Verbrauchern) stand, soweit es als Vorschlag für den Betrieb dient? (b) Hat keine Leistung einer Vorlage einen Preis (Vorlagen „handwerk“ und „sonstiges“), gibt es keinen Vorschlag. Bietet ein Betrieb einzelne kostenlose Leistungen neben Leistungen mit Preis an, bleibt sein Stornotext stehen, aber der Pflicht-Haken „Ich habe die Stornobedingungen gelesen.“ entfällt bei den kostenlosen Leistungen. Genügt das? (c) Die Bestätigungs-E-Mail G02 zeigt den Stornotext immer, wenn der Betrieb einen hinterlegt hat; ein Betrieb kann auch dann einen Text eingeben, wenn er kostenlose Leistungen anbietet, und dann steht er auch in der Bestätigung kostenloser Buchungen. Der Satz „Absagen richten sich nach den Stornobedingungen“ im Block „kein_widerruf“ (Abschnitt 3.9) steht dagegen bei jeder Buchung, die der Betrieb als Freizeit-Dienstleistung einstuft, also auch bei kostenlosen Buchungen und auch ohne hinterlegten Stornotext. Die Einbauanleitung für Mitarbeiter nennt den Stornotext unabhängig vom Preis. Ist das bei kostenlosen Leistungen irreführend? Wir wollen für G02 einen neutralen Satz festlegen (Checkliste, Abschnitt 7.4).
**Fundstelle im Text.** Abschnitt 3.7 (Haken „Stornobedingungen“), Abschnitt 3.9 (G02); Programm: `stornoText`, `STORNO_PROZENT`, `pruefeEingabe`, `STORNO_MUSTER_HINWEIS`.
**Unsere Annahme.** Der Vorschlag ist ein Muster mit Nachweisvorbehalt und dem Hinweis auf das Widerrufsrecht; der Betrieb prüft und passt ihn an. Bei kostenlosen Leistungen gibt es nichts zu stornieren, der Haken ist dort nicht nötig. Der Satz in G02 wird für Buchungen ohne Preis neutral gefasst.
**Alternative.** Keinen Stornovorschlag mitliefern; oder 50 Prozent durch eine feste Gebühr oder einen Staffelsatz ersetzen.
### Q17: Datenschutz- und Impressum-Link; Widerrufssatz am Erinnerungshaken
**Frage.** (a) Der Datenschutz-Hinweis am Buchungsformular verweist auf die Datenschutzerklärung des Betriebs; das Widget ließe den Link sonst still weg. Deshalb verlangt das Bestell-Gate vor der Bestellung die Adressen von Impressum und Datenschutzerklärung (H3). Ist die Pflicht ausreichend, oder müssen Links zu Datenschutzerklärung und Impressum direkt am Haken stehen? (b) Der Erinnerungshaken endet mit „Diese Einwilligung kann ich jederzeit widerrufen.“ (Art. 7 Abs. 3 DSGVO), ohne Widerrufsweg und ohne Link am Haken; die Einzelheiten stehen im Datenschutz-Absatz (H2). Genügt das?
**Fundstelle im Text.** Abschnitt 3.7 (Haken „Erinnerung“), Abschnitt 3.8 (Datenschutz-Hinweis); frühere Punkte H2, H3; Programm: `ZUSTIMMUNG_ERINNERUNG`, `formularHinweis`, `bestellpruefung`.
**Unsere Annahme.** Ja: Der Satz am Haken informiert vor der Einwilligung über das Recht zum Widerruf, die Einzelheiten und der Weg stehen im Hinweis und im Datenschutz-Absatz.
**Alternative.** Den Widerrufsweg („über den Link in der E-Mail oder per Nachricht an {{E-Mail}}“) in den Haken schreiben; Links zu Datenschutzerklärung und Impressum neben den Haken setzen.
### Q18: Datenschutz-Absatz auf der Kundenseite: Einfügen durch WEBklar im Preis
**Frage.** Leistungsbeschreibung Nr. 4 Abs. 9: Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau den Datenschutz-Absatz für die Online-Terminbuchung ein, das gehört zur Einrichtung und wird nicht auf das Stundenkontingent angerechnet; sonst erhält der Kunde den Absatz als Textbaustein. Der Kunde bleibt für seine Datenschutzerklärung verantwortlich; fällt WEBklar ein offensichtlicher Widerspruch auf, weist WEBklar darauf hin, eine Prüfpflicht entsteht nicht. (a) Ist das Einfügen eines Mustertextes durch WEBklar eine Rechtsdienstleistung (§ 2 RDG), oder bleibt es Technik und Mustertext (Nr. 11 Abs. 1)? (b) Reicht die Abgrenzung der Verantwortung? (c) Dieselbe Regel steht in der Leistungsbeschreibung der Website-Analyse (Nr. 4 Abs. 4); wir wollen sie für beide gleich halten.
**Fundstelle im Text.** Abschnitt 3.4 (Nr. 4 Abs. 9, Nr. 11 Abs. 1); `docs/rechtliches/leistungsbeschreibung-website-analyse.md` Nr. 4 Abs. 4; Programm: `datenschutzAbsatz`.
**Unsere Annahme.** Das Einfügen eines unveränderten Mustertextes mit Hinweis ist keine Rechtsdienstleistung im Einzelfall; die Verantwortung bleibt beim Kunden.
**Alternative.** WEBklar fügt nichts ein und liefert nur den Textbaustein; oder WEBklar fügt ein und der Kunde bestätigt den Text ausdrücklich.
### Q19: Preisangabe nach § 19 UStG; Preis am Knopf
**Frage.** (a) Die Einrichtung steht überall sinngemäß als „149 € einmalig, ohne Umsatzsteuer gemäß § 19 UStG“, im Wortlaut so: im Haken 4 als „Gesamtpreis 149 € einmalig (ohne Umsatzsteuer gemäß § 19 UStG)“ (Abschnitt 3.1), in Leistungsbeschreibung Nr. 5 Abs. 2:
> (2) Die Einrichtung kostet 149 € einmalig je Website-Projekt, ohne Umsatzsteuer gemäß § 19 UStG.
und in der Bestellbestätigung K15 als Zusatz zur Preiszeile:
> einmalig, ohne Umsatzsteuer gemäß § 19 UStG; der Terminplaner läuft, solange Dein Wartungs-Abo läuft
Früher stand bei den News „inklusive Umsatzsteuer“ (Q4); das haben wir korrigiert. Ist die Angabe als Endpreis richtig, solange WEBklar Kleinunternehmer bleibt, und was ist bei Überschreiten der Grenze zu tun? (b) Der Knopf trägt den Preis: „Zahlungspflichtig bestellen · 149 €“ (Abschnitt 3.1). Ist der Zusatz mit § 312j Abs. 3 BGB vereinbar und nötig?
**Fundstelle im Text.** Abschnitt 3.1 (Haken 4 und Knopf), Abschnitt 3.4 (Nr. 5); Programm: `ustHinweis`, `TERMINE_PREIS`, `termineConsent`.
**Unsere Annahme.** WEBklar bleibt Kleinunternehmer (der Inhaber bestätigt das, Abschnitt 7.1); der Preis am Knopf schadet nicht, weil die gesetzlichen Worte „zahlungspflichtig bestellen“ unverändert davor stehen.
**Alternative.** Knopf ohne Preis und den Preis direkt darüber zeigen; bei Überschreiten der Kleinunternehmergrenze Preise und Texte neu fassen (neues Fassungsdatum).
### Q20: Pflichtinformationen (Art. 246a § 1 EGBGB) als Haken und in K15
**Frage.** Haken 4 bestätigt, dass der Besteller die Pflichtinformationen gelesen hat: Leistung (Einrichtung des Terminplaners für diese Website: ein Widget an einer Stelle, gestaltet nach dem Fragebogen, mit Einbau und Datenschutz-Absatz), Gesamtpreis, voraussichtliche Dauer, Ende mit dem Wartungs-Abo, und dass er die Widerrufsbelehrung gelesen hat. Genügen Haken 4 zusammen mit der Belehrung, den versionierten Links auf Leistungsbeschreibung und AVV und der Bestellbestätigung K15 (Zustimmungstexte und PDF-Anhänge) den Informationspflichten nach Art. 246a § 1 Abs. 1 EGBGB? Wird zwischen Klick und Zahlung eine neue Fassung veröffentlicht, schickt K15 die Unterlagen der Fassung, der der Kunde zugestimmt hat (der vereinbarten Fassung), nicht die aktuelle; fehlen sie, geht K15 ohne Anhänge und das Team sendet sie von Hand. Ist das ausreichend? Die gerenderte Mail K15 folgt als Anlage C nach der Mail-Überarbeitung des Inhabers.
**Fundstelle im Text.** Abschnitt 3.1 (Haken 4), Abschnitt 3.4 (Nr. 5), Abschnitt 2.1 Schritt 6; Programm: `k15Rechtsunterlagen`, `onFeaturePaid`, `legalPdfAttachments`.
**Unsere Annahme.** Ja: Die Informationen stehen vor der Bestellung im Text der Seite und in den Unterlagen; der Haken macht die Kenntnisnahme nachweisbar; K15 gibt sie auf einem dauerhaften Datenträger wieder.
**Alternative.** Die Pflichtinformationen als eigenen Textblock auf der Bestellseite und in K15 im Wortlaut wiederholen, statt sie im Haken zu bündeln.
### Q21: Freischaltung durch das Team mit Zustimmung in Textform; K16 mit PDFs
**Frage.** Ohne Bestellung (Kulanz, Pilot) schaltet das Team einen Terminplaner in der Betriebsart `on` (alle Kunden mit Wartungs-Abo, Abschnitt 1) nur frei, wenn der Kunde vorher in Textform (Chat im Kundenportal oder E-Mail) zugestimmt hat; Pilotadressen sind davon ausgenommen. Das Portal speichert als Nachweis (ohne Stripe-Sitzung, IP und Browser; Konto ist der Mitarbeiter) diesen Satz, mit Fassung, Datum und Weg des Falls:
> Der Kunde hat der Leistungsbeschreibung Terminplaner und dem Auftragsverarbeitungsvertrag Terminplaner (Fassung \<Fassung>) am \<TT>.\<MM>.\<JJJJ> per Chat im Kundenportal in Textform zugestimmt und bestätigt, dass sein Angebot nicht im Gesundheitsbereich liegt.
Bei der Zustimmung per E-Mail steht „per E-Mail“ statt „per Chat im Kundenportal“. Für den Chat liefert das Ticketsystem dem Mitarbeiter diese Textvorlage zum Kopieren (die Adressen sind die versionierten Seiten der beiden Unterlagen):
> Damit wir den Terminplaner für Dich einschalten können, brauchen wir Deine Zustimmung in Textform. Es gelten die Leistungsbeschreibung Terminplaner und der Auftragsverarbeitungsvertrag Terminplaner in der Fassung \<Fassung>: \<Adresse der Leistungsbeschreibung> · \<Adresse des AVV>. Bitte antworte hier mit „Ich stimme zu“ und bestätige, dass Dein Angebot nicht im Gesundheitsbereich liegt.
Danach geht K16 mit Leistungsbeschreibung und AVV als PDF an den Kunden. Genügt das? (a) Zustimmung durch eine Chat-Antwort für den AVV (Art. 28 Abs. 9 DSGVO) und die Leistungsbeschreibung; (b) der Nachweis ist der Eintrag des Mitarbeiters, die Chat-Nachricht bleibt im Portal; (c) K16 nennt die Anhänge nicht im Text und ist die Bestätigung auf einem dauerhaften Datenträger (die gerenderte Mail folgt als Anlage C nach der Mail-Überarbeitung des Inhabers); (d) für diesen Weg gibt es keine Widerrufsbelehrung (kein Kauf, kein Preis); (e) auch das Wiedereinschalten eines abgeschalteten Terminplaners, den das Team ohne Bestellung freigeschaltet hatte, verlangt dieselbe Textform (bei einem gekauften oder schon aktiven Terminplaner entfällt sie).
**Fundstelle im Text.** Abschnitt 3.5 (Vorbemerkung Abs. 2), Abschnitt 2.1 (Freischaltung ohne Bestellung); Programm: `termineTextformNoetig`, `textformFuerAktivierung`, `adminActivate`, `textformNachweisText`, `meldeFreigeschaltet`; Zusammenhang mit Q7.
**Unsere Annahme.** Ja: Textform genügt für die Zustimmung zu Leistungsbeschreibung und AVV, der Nachweis des Mitarbeiters mit dem Chatverlauf im Portal ist hinreichend, und K16 liefert den dauerhaften Datenträger.
**Alternative.** Die Zustimmung durch den Kunden selbst im Portal (Haken) einholen, auch für Kulanzfälle; K16 im Text auf die Anhänge hinweisen lassen.
### Q22: BFSG-Satz ohne Konformitätszusage
**Frage.** Leistungsbeschreibung Nr. 11 Abs. 2 sagt: Das Widget ist für die Bedienung mit Tastatur und Hilfstechniken gestaltet (Beschriftungen, Fokusführung, Fehlermeldungen als Live-Region); eine Konformität nach WCAG 2.1 AA oder EN 301 549 sagt WEBklar nicht zu; ob der Kunde als Dienstleistungserbringer unter das BFSG fällt (Ausnahme für Kleinstunternehmen, § 3 Abs. 3 BFSG), entscheidet und prüft der Kunde. Ist die Formulierung ohne Konformitätszusage zulässig und für den Kunden klar genug? Nach unserem Stand gab es für das Widget bisher keinen Lauf mit einem automatischen Prüfwerkzeug oder einem Screenreader.
**Fundstelle im Text.** Abschnitt 3.4 (Nr. 11 Abs. 2).
**Unsere Annahme.** Ja. Der Satz sagt nur, was gestaltet wurde, und weist die Prüfung des BFSG dem Kunden zu.
**Alternative.** Den Satz streichen; oder eine Prüfung des Widgets beauftragen und die Aussage danach anpassen.
### Q23: Nachweis der Bestellung mit IP-Adresse und Browser; Aufbewahrung drei Jahre
**Frage.** Beim Klick auf „Zahlungspflichtig bestellen“ speichert das Portal als Nachweis die gezeigten Texte und Fassungen, den Zeitpunkt, die Stripe-Sitzung, IP-Adresse und Browserkennung, das Konto und die angemeldete Person. Die Datenschutzerklärung sagt dazu:
> Bestellen Sie eine Funktion (News-Pinnwand, Terminplaner), speichern wir als Nachweis die Texte und Fassungen, denen Sie zugestimmt haben, den Zeitpunkt Ihres Klicks auf „Zahlungspflichtig bestellen“, die Stripe-Sitzung, IP-Adresse und Browserkennung sowie Ihr Konto und die angemeldete Person. Rechtsgrundlagen sind Art. 6 Abs. 1 lit. b, c und f DSGVO; unser berechtigtes Interesse ist der Nachweis des Vertragsschlusses und der Zustimmungen, insbesondere für den Widerruf. Diese Angaben löschen wir drei Jahre nach Ablauf des Jahres, in dem Ihr letztes Abo endet. Bestellen Sie ohne Abo (zum Beispiel nur die News-Pinnwand), löschen wir diese Angaben spätestens drei Jahre nach Ablauf des Jahres der Bestellung. Den Entwurf Ihres Fragebogens speichern wir bis zu 90 Tage nach Ihrer letzten Änderung und löschen ihn danach, wenn Sie nichts bestellt haben.
Ist die Speicherung der IP-Adresse und der Browserkennung für den Nachweis des Vertragsschlusses und der Zustimmungen erforderlich (Art. 6 Abs. 1 lit. f DSGVO) und die Aufbewahrung bis drei Jahre nach Ablauf des Jahres, in dem das letzte Abo endet, angemessen? Genügt die Information in der Datenschutzerklärung? Für Bestellungen ohne Abo (nur News) nennt die Erklärung eine eigene Frist: drei Jahre nach Ablauf des Jahres der Bestellung. Das Programm löscht diese Nachweise heute noch nicht; die Löschung wird vor Ablauf der ersten Frist (31.12.2029) nachgerüstet. Ist diese Frist angemessen?
**Fundstelle im Text.** Abschnitt 2.1 Schritt 4, Datenschutzerklärung § 7 (Hausrunde); Programm: `speichereZustimmung`, `zustimmungFuerSitzung`, `findDueDeletions` (Löschung der Nachweise gemeinsam mit den Abo-Daten des Kunden; für Kunden ohne Abo noch offen, Abschnitt 7.7).
**Unsere Annahme.** Ja: Der Nachweis dient dem Widerrufsrecht und der Beweisführung; die Frist entspricht der regelmäßigen Verjährung samt Ablauf des Jahres.
**Alternative.** IP-Adresse und Browserkennung weglassen (nur Zeit, Texte, Konto); oder eine kürzere Frist, etwa bis zum Ablauf der Widerrufsfrist und der Gewährleistung.
### Q24: Rollen in Abschnitt 6c; Entwurfsaufbewahrung 90 Tage
**Frage.** Die Datenschutzerklärung von WEBklar (Abschnitt 6c) beschreibt WEBklar als Auftragsverarbeiter für die Daten der Gäste und als Verantwortlichen für die Betriebsdaten im Kundenportal, den Entwurf des Fragebogens und die Bestellung (C1). (a) Stimmt die Rollenverteilung? (b) Der Satz „mit der Buchung speichern wir die IP-Adresse nicht“ ist durch einen Test belegt (Buchung ohne IP und Browser-Kennzeichen); für den Missbrauchsschutz merkt sich der Server Zeitpunkte je IP-Adresse nur im Arbeitsspeicher (C2). Genügt die Darstellung? (c) Anfragen von Gästen leitet WEBklar an den Kunden weiter (A4). (d) Entwürfe aus dem Fragebogen ohne Bestellung löscht WEBklar 90 Tage nach der letzten Änderung: Ist diese Frist zweckgebunden und angemessen?
**Fundstelle im Text.** Abschnitt 2.1 Schritt 2, Anlage A Nr. 8 und Nr. 9 Abs. 7; Datenschutzerklärung 6c (`docs/rechtliches/termine-datenschutz-6c.md`, Hausrunde); frühere Punkte C1, C2, A4; Programm: `ENTWURF_TAGE`.
**Unsere Annahme.** Die Rollen sind richtig verteilt (wie beim Abschnitt 6b der Website-Analyse); 90 Tage Entwurf sind angemessen, weil Kunden Fragebögen über Wochen ausfüllen.
**Alternative.** Entwürfe kürzer aufbewahren (30 Tage) oder nur auf ausdrückliches Speichern des Kunden.
### Q25: Mindestlaufzeit des Wartungs-Abos und Terminplaner
**Frage.** Das Wartungs-Abo hat eine Mindestlaufzeit von zwölf Monaten (§ 8 der Abo-Bedingungen) und ist danach zum Monatsende kündbar. Der Terminplaner hat keine eigene Laufzeit und kein Sonderkündigungsrecht; er endet mit dem Abo. Bestellbar ist er nur bei aktivem Abo ohne vorgemerkte Kündigung (Abschnitt 2.1 Schritt 1). (a) Ist es unbedenklich, dass der Kunde 149 € einmalig für etwas zahlt, das mit dem Abo enden kann, zum Beispiel wenn die Mindestlaufzeit bald endet? (b) Reicht Haken 4 („der Terminplaner endet mit dem Wartungs-Abo“) für die Transparenz, oder braucht die Bestellseite einen Hinweis auf das Ende der laufenden Mindestlaufzeit?
**Fundstelle im Text.** Abschnitt 3.1 (Haken 4), Abschnitt 3.4 (Nr. 8 Abs. 1 und 5), Abo-Bedingungen § 8 (`public/abo-bedingungen.html`); Programm: `bestellbar`, `laeuft`.
**Unsere Annahme.** Ja: Ohne vorgemerkte Kündigung läuft das Abo weiter; wird es später gekündigt, greift die Erstattung nur vor der Freischaltung, danach hat der Kunde den Terminplaner genutzt.
**Alternative.** Auf der Bestellseite das Ende der Mindestlaufzeit anzeigen; bei Beendigung in den ersten Monaten nach der Freischaltung anteilig erstatten.
### Q26: Nachweis der Gäste: Hash und Archiv der Textfassung
**Frage.** Die Buchung eines Gastes speichert als Nachweis die Fassung der Gästetexte, den Zeitpunkt, den Text des Buchungsknopfs, die Anrede und Hashes der gezeigten Texte, nicht diese Texte selbst. WEBklar archiviert jede Fassung der Gästetexte (Hinweis am Formular, Texte der Haken, Datenschutz-Absatz, Belehrung für Gäste) mit Platzhaltern für die Betriebsangaben bis drei Jahre nach dem Ende des Terminplaners. Die Betriebsangaben (Name, Anschrift, Stornotext, Stunden der Erinnerung, Aufbewahrung) ändern sich später; der Stornotext hat eine eigene Version. Genügt der Hash zusammen mit dem Archiv der Textfassung, um die Zustimmung eines Gastes nachzuweisen? Der Nachweis beruht auf dem Textstand des Servers zum Zeitpunkt der Buchung, nicht auf dem, was das Widget angezeigt hat; wird die Fassung während der Haltezeit von 30 Minuten gewechselt, können beide abweichen (Änderungen sollen außerhalb der Stoßzeiten eingespielt werden).
**Fundstelle im Text.** Abschnitt 2.2 Punkt 5, Abschnitt 3.5 (AVV Nr. 8 Abs. 5); Archiv `docs/rechtliches/archiv/termine-gaestetexte-2026-10-05.md`; Programm: `baueNachweis`, `baueArchiv`.
**Unsere Annahme.** Ja: Mit der Fassung, dem Archiv und den Betriebsangaben des Zeitpunkts lässt sich der gezeigte Wortlaut rekonstruieren; das genügt für den Nachweis, ohne Volltexte je Buchung zu speichern.
**Alternative.** Den vollständigen Text je Buchung speichern (mehr Daten, mehr Speicher); oder den Text je Betrieb und Fassung einmal als Schnappschuss ablegen.
## 5. Fundstellen im Code
Pfade gelten im Repository des Kundenportals (`Webklar-Kundenbereich`); Bezeichner sind Funktionen oder Konstanten und stehen in der genannten Datei. Zeilennummern nennen wir nicht, weil sie sich ändern.
| Thema | Datei | Funktion/Konstante |
|---|---|---|
| Fassung der Vertragsunterlagen und Bestelltexte | `server/config.js` | `avvVersion` |
| Bestell-Haken und Knopf des Terminplaners (Register) | `server/config.js` | `termineConsent`, `TERMINE_PREIS` |
| Preisangabe und Umsatzsteuerhinweis | `server/config.js` | `ustHinweis` |
| Preis im Produktkatalog (14 900 Cent) | `server/config.js` | `termineSetup`, `amountCents` |
| Widerrufsbelehrung der Funktionen (WEBklar als Unternehmer) | `server/services/termine/rechtstexte.js` | `widerrufsbelehrungFunktion`, `WEBKLAR_KONTAKT` |
| Gästetexte: Belehrung, Haken, Hinweis, Datenschutz-Absatz | `server/services/termine/rechtstexte.js` | `widerrufsbelehrung`, `ZUSTIMMUNG_VORZEITIG`, `ZUSTIMMUNG_STORNO`, `ZUSTIMMUNG_ERINNERUNG`, `GESUNDHEIT_HINWEIS`, `formularHinweis`, `datenschutzAbsatz` |
| Fassung der Gästetexte | `server/services/termine/rechtstexte.js` | `RECHTSTEXTE_VERSION` |
| Knopftexte der Gäste, Widerrufsfunktion | `server/services/termine/einstellungen.js` | `knopfText`, `zeigtWiderruf` |
| Wann der Haken zum vorzeitigen Beginn gilt | `server/services/termine/einstellungen.js` | `brauchtZustimmungVorzeitig` |
| Eingabeprüfung der Buchung, Storno-Haken nur bei Preis, Nachweis mit Hashes | `server/services/termine/ablauf.js` | `pruefeEingabe`, `baueNachweis` |
| Storno-Vorschlag der Vorlagen (50 %, 24 Stunden) | `server/services/termine/vorlagen.js` | `stornoText`, `STORNO_PROZENT` |
| Zustimmungsnachweis der Bestellung | `server/services/featureConsents.js` | `speichereZustimmung`, `zustimmungFuerSitzung` |
| Bestellung: Reihenfolge Zahlungssitzung, Nachweis, Weiterleitung | `server/routes/funktionen.js` | `/:key/checkout`, `speichereNachweis`, `createFeatureCheckout` |
| Bestellbestätigung K15 mit PDF-Anhängen der vereinbarten Fassung | `server/services/siteFeatures.js` | `onFeaturePaid`, `k15Rechtsunterlagen` |
| Adressen und Dateien der Rechtsunterlagen | `server/services/legalDocs.js` | `legalPaths`, `legalPdfAttachments`, `featureLegalAttachments` |
| Abo-Prüfung (laufend, bestellbar) | `server/services/aboStatus.js` | `laeuft`, `bestellbar` |
| Täglicher Abo-Abgleich, Mail K22/K23, Erstattung T19 | `server/services/termine/aboAbgleich.js` | `gleicheAboAb`, `meldeAboEnde` |
| Abschalten, Wiedereinschalten, Abschaltgründe | `server/services/siteFeatures.js` | `disable`, `activate`, `DISABLED_REASONS` |
| Löschfrist nach dem Abschalten (30 Tage) | `server/services/termine/frist.js` | `LOESCHFRIST_TAGE` |
| Löschlauf, Fristen der Buchungen | `server/services/termine/jobs.js` | `loescheAbgeschaltete`, `VERFALLEN_BEHALTEN_MS`, `ABGESAGT_BEHALTEN_MS`, `AUFBEWAHRUNG_VORGABE` |
| Aufbewahrung der Entwürfe (90 Tage) | `server/services/termine/entwuerfe.js` | `ENTWURF_TAGE` |
| Versandwarteschlange für E-Mails | `server/services/termine/outbox.js` | `GESENDET_BEHALTEN_MS`, `FEHLER_BEHALTEN_MS` |
| Aufbewahrung der Zustimmungsnachweise der Bestellung | `server/services/retention.js` | `findDueDeletions`, `applyDeletions` |
| Gesundheits-Ausschluss: Branchen, Stichwortwarnung, Bestell-Gate | `server/services/termine/fragebogen.js` | `BRANCHEN`, `gesundheitsWarnungen`, `bestellpruefung` |
| Freischaltung ohne Bestellung: Textform nötig? | `server/services/termine/schalter.js` | `termineTextformNoetig` |
| Freischaltung ohne Bestellung: Prüfung von Datum und Weg, PDFs | `server/routes/admin/funktionenAdmin.js` | `textformFuerAktivierung`, `unterlagenAlsAnhang` |
| Freischaltung ohne Bestellung: Nachweis und Aktivierung | `server/services/siteFeatures.js` | `adminActivate`, `textformNachweisText` |
| Widerruf der Bestellung: Fristende je Bestellung, spätestes Fristende entscheidet über „rechtzeitig“ | `server/routes/abo.js` | `withdrawFeature` |
| Widerrufsfrist (14 Tage), Fristende | `server/services/deadlines.js` | `WITHDRAWAL_DAYS`, `withdrawalDeadline` |
| Widerruf durch Gäste (Widerrufsfunktion) | `server/routes/publicTermine.js` | `/:siteKey/widerruf`, `/termin/widerruf`, `widerrufeGast`, `widerrufeOhneLink` |
| Bestätigungs-E-Mail an Gäste, Hinweis kein Widerrufsrecht | `server/mail-templates/G02 Termin bestaetigt.html` | `kein_widerruf`, `widerrufsbelehrung` |
| Versand der Bestätigungs-E-Mail | `server/services/termine/mails.js` | `gastG02` |
| Eingangsbestätigung des Widerrufs | `server/mail-templates/G08 Eingang Widerruf.html` | `{{eingang}}`, `{{nummer}}`, `nach_frist` |
| Mail „Terminplaner beendet“ (30 Tage, Rückkehr) | `server/mail-templates/K22 Terminplaner beendet.html` | `loeschdatum`, `Rückkehr` |
| Mail „Terminplaner läuft wieder“ | `server/mail-templates/K23 Terminplaner laeuft wieder.html` | `{{kunde_name}}`, `innerhalb der Löschfrist` |
| Hinweise im Kundenportal (Widerruf, Freizeit, Storno-Muster) | `public/termine-tab-einstellungen.js` | `WIDERRUF_HINWEIS`, `FREIZEIT_HINWEIS`, `STORNO_MUSTER_HINWEIS` |
| Archiv der Gästetexte je Fassung | `scripts/termine-texte-archiv.mjs` | `baueArchiv` |
| HTML-Seiten der Vertragsunterlagen | `scripts/build-legal-html.mjs` | `termineDokumente` |
| PDFs der Vertragsunterlagen | `scripts/build-legal-pdfs.sh` | `termine_version`, `TERMINE_VERSION` |
| Muster der Widerrufsbelehrung (Anlage 1) | `docs/rechtliches/quellen/anlage-1.txt` | `Folgen des Widerrufs` |
| Muster-Widerrufsformular (Anlage 2) | `docs/rechtliches/quellen/anlage-2.txt` | `Muster-Widerrufsformular` |
| Datenschutzerklärung von WEBklar: Abschnitt 6c, § 6, § 7 | `public/datenschutz.html` | `id="termine"` |
## 6. Nicht Teil der Kanzleiprüfung
Dieser Abschnitt nennt, was wir der Kanzlei nicht zur Prüfung vorlegen, und warum.
### 6.1 Hausrunde
Diese Texte prüft WEBklar selbst (wie bei der Website-Analyse), weil sie keine der Kernfragen aus Abschnitt 1 betreffen:
- die Datenschutzerklärung von WEBklar mit Abschnitt 6c „Online-Terminbuchung auf Websites unserer Kunden“ und den Ergänzungen in § 6 (Kundenansicht) und § 7 (Nachweise der Funktionsbestellung); Quelle `docs/rechtliches/termine-datenschutz-6c.md` und `public/datenschutz.html`. Nur die Rollenverteilung fragen wir (Q24),
- die Hinweise im Kundenportal (Widerrufs-, Freizeit- und Storno-Hinweise); zu zwei Punkten fragen wir (Q15, Q16),
- die Mailtexte (K15, K16, K22, K23, K04, T09, G02, G08); sie gehen durch die Mail-Überarbeitung des Inhabers. Die offene Formulierung in G02 steht in Q16 und in Abschnitt 7,
- die Einbauanleitung und das Einrichtungs-Ticket für Mitarbeiter.
### 6.2 Später
Diese Themen gehören nicht zu diesem Verkaufsweg und stehen für spätere Pakete bereit:
- Abo-Bedingungen in der Fassung 6 bzw. ein Rahmen-AVV mit einer Anlage je Funktion,
- Themen der Online-Zahlung der Gäste (ein Satz zu Stripe in Abschnitt 6c, Zahlungsdienste, Vorauszahlung in den AGB, Preisangabenverordnung),
- Pflichtangaben auf Geschäftsbriefen (§ 37a HGB),
- Rechtsmissbrauch bei spätem Widerruf,
- Minderjährige,
- Mitgliedschaften,
- wiederholtes Versenden der Mails G08 und K21 nach Fristende,
- Widerruf nur vor Terminbeginn im Verwaltungslink.
### 6.3 Frühere Listen „Für die Rechtsprüfung offen“
Beim Fertigstellen der Gästetexte und der Datenschutzerklärung haben wir Listen offener Rechtsfragen aus den Dokumenten entfernt. Jeder Punkt dieser Listen hat eine Kennung: H1 bis H4 (Hinweise-Dokument), A1 bis A6 (Datenschutz-Absatz für die Website der Betriebe), B1 bis B6 (Muster der Widerrufsbelehrung für Gäste), C1 bis C8 (Entwurf von Abschnitt 6c der Datenschutzerklärung). Jeder Punkt wird entweder bei einer Frage in Abschnitt 4 behandelt (die Kennung steht dort in der Fundstelle) oder ist hier mit Grund aufgeführt:
| Kennung | Punkt | Wo behandelt |
|---|---|---|
| H1 | Belehrung vor dem Vertragsschluss: aufklappbare Darstellung im Widget | Q12 |
| H2 | Widerruf der Einwilligung am Erinnerungshaken | Q17 |
| H3 | Link zur Datenschutzerklärung neben dem Hinweis | Q17 |
| H4 | Bestätigungsfrist: 48 Stunden sind der früheste Zeitpunkt der Löschung | Q9 |
| A1 | Server-Protokolle: 14 Tage und Rechtsgrundlage | Q10 |
| A2 | Drittländer: gilt nur, solange Server und Mailversand bei Hetzner in Deutschland bleiben | Q10 |
| A3 | Löschlauf: Fristen sind die frühesten Zeitpunkte | Q9 |
| A4 | Kontakt der Gäste: Anfragen gehen an den Betrieb, WEBklar leitet weiter | Q24 |
| A5 | Gesundheitsbereich: Hinweis „Bitte keine Gesundheitsangaben“ | Q11 |
| A6 | Knopfmodus: Konfiguration schon beim Laden der Seite | Q10 |
| B1 | Lieferkosten-Klammer in der Belehrung | Q12 |
| B2 | „beginnen soll“ im Singular | Q12 |
| B3 | Anschrift und Telefon des Betriebs sind Pflicht | hier: technisch erledigt. `widerrufsbelehrung()` wirft einen Fehler, wenn eine der vier Angaben fehlt, und die Einstellungsprüfung verlangt beide Angaben. Keine Rechtsfrage. |
| B4 | Wertersatz-Absatz steht immer in der Belehrung | Q13 |
| B5 | Hinweis statt Internetadresse bei der Widerrufsfunktion | Q12 |
| B6 | Gesetzlichkeitsfiktion des Musters | Q12 |
| C1 | Rolle: Auftragsverarbeiter für die Gäste, Verantwortlicher für die Betriebsdaten | Q24 |
| C2 | IP-Adresse im Arbeitsspeicher: Satz „mit der Buchung speichern wir die IP-Adresse nicht“ | Q24 |
| C3 | Kalender-Abo-Link: Information der Beschäftigten | Q10 |
| C4 | Sicherungen: gelöschte Daten bis zu sieben Tage | Q9 |
| C5 | Zahlung der Gäste (spätere Online-Zahlung): Satz zu Stripe in Abschnitt 6c | hier: gehört zur späteren Online-Zahlung der Gäste (Abschnitt 6.2); der Terminplaner kennt heute nur die Zahlung vor Ort. |
| C6 | Geschäftsbriefe: Bestätigungen mit Preis sind keine Rechnungen | hier: Abschnitt 6.2 (Pflichtangaben auf Geschäftsbriefen); die Aufbewahrung nach Abschnitt 9 der Datenschutzerklärung betrifft den Terminplaner nicht. |
| C7 | Ende des Wartungs-Abos: Satz in Abschnitt 6c | Q9 |
| C8 | Knopfmodus: Formulierung zum Laden der Konfiguration in Abschnitt 6c | Q10 |
## 7. Checkliste für den Inhaber
Diese Punkte erledigt der Inhaber; kein Punkt ist Aufgabe der Kanzlei. Die Reihenfolge entspricht dem Ablauf bis zum ersten fremden Betrieb.
### 7.1 Vor der Beauftragung der Kanzlei
- [ ] Die PDFs der Anlagen A und B und der Widerrufsbelehrung einmal am Bildschirm ansehen (Kopfzeile „WEBklar · …“, „Seite x von y“, Tabelle der Anlage des AVV); wir konnten sie nur über ihre HTML-Seiten prüfen.
- [ ] Die Zahl der News-Bestellungen seit 2026-09-30 ermitteln, die mit dem alten Zustimmungstext und ohne Widerrufsbelehrung abgeschlossen wurden, und der Kanzlei nennen (Q4).
- [ ] Bestätigen, dass WEBklar Kleinunternehmer nach § 19 UStG bleibt (Q19).
### 7.2 Hetzner (nur im Konto prüfbar)
- [ ] Auftragsverarbeitungsvertrag mit Hetzner für die Cloud **und** für das Webhosting abgeschlossen (die Buchungs-E-Mails laufen über den SMTP-Dienst des Webhostings).
- [ ] Anlage 1 des Vertrags nennt „Terminbucher“, „Beschäftigte“ und „Buchungsdaten“ als Betroffene und Datenarten.
- [ ] Die Kontoadresse bei Hetzner stimmt mit dem Impressum überein.
- [ ] Die Datensicherung (Backups) ist eingeschaltet; AVV Nr. 9 Abs. 8 und die Abo-Bedingungen versprechen sieben Tagessicherungen.
### 7.3 Uploads in WOMS „Rechtliches“
- [ ] Das Setup-Skript von WOMS mit API-Key ausführen: Es legt die drei Karten „Leistungsbeschreibung Terminplaner“, „AVV Terminplaner (Auftragsverarbeitung)“ und „Widerrufsbelehrung und Muster-Widerrufsformular (Funktionen)“ an.
- [ ] Für diese drei Karten die Version der Fassung hochladen (PDF oder HTML aus `public/rechtliches/`; Markdown ist im Bucket nicht erlaubt).
- [ ] Die neue Datenschutzerklärung (Stand 05.10.2026, `docs/WEBklar-Datenschutzerklaerung.pdf`) als neue Version der Karte „Datenschutzerklärung“ hochladen.
- [ ] „Gültig ab“ in `docs/rechtliches/archiv/README.md` eintragen (Fassung der Gästetexte vom 2026-10-05 steht auf „offen bis Livegang“; bei der Fassung vom 2026-10-02 prüfen, ob „gültig ab 2026-10-03 (Pilot)“ stimmt).
### 7.4 Mailtexte
- [ ] Die Platzhalter „ENTWURF (Mailrunde): …“ in `mails.danach` und `mails.bereit` des Terminplaners (Register in `server/config.js`) durch den endgültigen Wortlaut ersetzen. Sie stehen heute in K15 und K16 an Kunden.
- [ ] Die Kopfkommentare von K22 und K23 („Texte sind Entwurf für die Mail-Überarbeitung“) in der Mail-Überarbeitung abarbeiten.
- [ ] K23 (Terminplaner läuft wieder) sagt „Deine Termine und Einstellungen sind unverändert da“. Für die Termine stimmt das nicht immer, weil der Löschlauf auch bei abgeschaltetem Terminplaner abgesagte und erledigte Termine nach ihren Fristen löscht. Den Satz in der Mail-Überarbeitung abschwächen.
- [ ] Nach der Mail-Überarbeitung die gerenderten Mails K15 (mit den Zustimmungen) und K16 als **Anlage C** beilegen und der Kanzlei nachreichen; ihre Angemessenheit als Bestätigung auf einem dauerhaften Datenträger fragen Q3, Q20 und Q21.
- [ ] G02 zeigt die Stornobedingungen und den Satz „Absagen richten sich nach den Stornobedingungen“ (Block `kein_widerruf`) auch bei kostenlosen Buchungen. Einen neutralen Satz festlegen, der für Buchungen ohne Preis passt (Q16).
### 7.5 Beauftragung und Freigabe
- [ ] Die Kanzlei mit der Prüfung des Kernpakets und den Fragen Q1 bis Q26 beauftragen (Anlagen A und B beilegen; Umfang und Frist vereinbaren).
- [ ] Den Befund einarbeiten: Texte ändern, neues Fassungsdatum setzen, HTML und PDFs neu erzeugen, Archiv der Gästetexte und Prüfsummen nachziehen. Dieses Paket (`docs/rechtliches/termine-kanzlei-paket.md`) wird dabei nachgezogen; sein Test meldet jedes Zitat, das nicht mehr wörtlich im Programm oder in den Dokumenten steht.
- [ ] Über die News-Altfälle entscheiden (Q4): Belehrung per Mail nachholen oder nur künftig die neuen Texte.
- [ ] Die Fassung vor dem ersten fremden Betrieb ausdrücklich freigeben; erst danach die Adresse des ersten fremden Betriebs in die Pilotliste aufnehmen.
- [ ] Beim Livegang die Reihenfolge in Abschnitt 7.6 einhalten; den Datenschutz-Abschnitt der Test-Website des Terminplaners auf den Endtext bringen.
### 7.6 Livegang: Reihenfolge
Diese Schritte gelten für den Tag, an dem die Betriebsart `on` gesetzt wird. Die Reihenfolge ist wichtig, vor allem: beide Schemata vor dem Code-Deploy und `on` als letzter Schritt.
- [ ] Vor dem Code-Deploy das Schema des Terminplaners anlegen, erst mit `--check`: `node scripts/termine-setup.mjs` ergänzt in `termineConfig` die Spalte `fragebogen`, in `termineBookings` die Indizes `idx_status_ends` und `idx_status_updated` und in `termineOutbox` den Index `idx_booking`. Ohne sie scheitern Schreibzugriffe und Abfragen des neuen Programms (der Fragebogen, der tägliche Löschlauf und das Löschen von Gastdaten).
- [ ] Vor dem Code-Deploy das Schema der Funktionen anlegen, erst mit `--check`: `node scripts/feature-setup.mjs` (neue Felder von `portalFeatures` und die Collection `featureConsents` mit ihren Indizes). Der Zeitpunkt ist der Code-Deploy, nicht erst der erste Löschlauf der Aufbewahrungsfristen: Schon die erste Bestellung des neuen Programms schreibt die neuen Felder, und ohne Schema antwortet Appwrite mit „Unknown attribute“.
- [ ] Hat sich `public/datenschutz.html` geändert, das PDF der Datenschutzerklärung mit `scripts/build-legal-pdfs.sh datenschutz` neu drucken, nicht mit `all` (das druckt auch die PDFs der Abo-Bedingungen und des Leitfadens neu); die Prüfsumme `docs/WEBklar-Datenschutzerklaerung.pdf.sha256` gehört dazu.
- [ ] Den Code zu einer ruhigen Zeit deployen. Der Nachweis einer Buchung speichert die Fassung der Gästetexte, die der Server beim Speichern hat; bucht ein Gast genau während des Deploys, kann sie von der abweichen, die er auf dem Bildschirm sah.
- [ ] Nach dem Deploy die Uploads in WOMS „Rechtliches“ erledigen (Abschnitt 7.3).
- [ ] Nach dem Deploy und nur mit Freigabe des Inhabers: `scripts/feature-consent-felder-leeren.mjs` zuerst als Probelauf ausführen (ohne Argument oder mit `--dry-run`; schreibt nichts), die Liste prüfen und erst dann mit `--apply` ausführen. Das Skript leert IP-Adresse und Browserkennung der Bestellung an den Funktionsdokumenten (`portalFeatures`), soweit der Nachweis (`featureConsents`) sie trägt; neue Bestellungen speichern sie dort nicht mehr.
- [ ] Als allerletzten Schritt `TERMINE_ENABLED=on` setzen, nachdem alle Punkte oben und die Freigabe der Fassung (Abschnitt 7.5) erledigt sind.
### 7.7 Nach dem Livegang
- [ ] Die Löschfrist für die Nachweise von Kunden ohne Abo (nur News) umsetzen, bevor die erste Frist abläuft (31.12.2029). Die Datenschutzerklärung (Abschnitt 7) sagt für Bestellungen ohne Abo „spätestens drei Jahre nach Ablauf des Jahres der Bestellung“; der Löschlauf (`findDueDeletions`) erfasst heute nur Kunden mit einem Abo, ein Kunde ohne Abo ist nie fällig. Umsetzung: Kunden ohne Abo-Dokument, aber mit Nachweisen in `featureConsents`, mit dem Ankerjahr der letzten Bestellung in den Plan aufnehmen und nur die Nachweise und die Bestellangaben am Funktionsdokument löschen, nicht die Beiträge.
@@ -0,0 +1,62 @@
# Widerrufsbelehrung und Muster-Widerrufsformular (Terminplaner, Dienstleistung)
Fassung 2026-10-05. Quelle des Wortlauts: Anlage 1 und Anlage 2 zu Art. 246a § 1 Abs. 2 Satz 2 EGBGB (Fundstelle BGBl. I 2013, S. 3663 bis 3665), abgerufen am 2026-10-02 von gesetze-im-internet.de; die Textdateien liegen unverändert in `docs/rechtliches/quellen/anlage-1.txt` und `anlage-2.txt`.
Der Text unten ist die Fassung, die `widerrufsbelehrung(betrieb)` in `server/services/termine/rechtstexte.js` erzeugt, mit Platzhaltern statt der Betriebsdaten. `test/termineRechtstexte.test.js` prüft, dass (a) jeder Absatz des Musters wörtlich aus den Quelldateien im Ergebnis steht und (b) der Textblock unten genau der Ausgabe der Funktion entspricht. Ändere den Text deshalb nur in `rechtstexte.js` und übernimm ihn hierher.
## Wie das Muster für einen Termin (Dienstleistung) ausgefüllt ist
| Gestaltungshinweis in Anlage 1 | Umsetzung |
|---|---|
| Frist: Textbaustein a) für Dienstleistungsverträge | „des Vertragsabschlusses.“ (Fristbeginn ist der Vertragsschluss: bei einer sofort verbindlichen Buchung die Bestätigung per Link, bei einer Anfrage die Annahme durch den Betrieb) |
| „Fügen Sie Ihren Namen, Ihre Anschrift, Ihre Telefonnummer und Ihre E-Mail-Adresse ein.“ | Name, Anschrift, Telefon, E-Mail aus den Betriebsangaben der Einstellung (`betrieb.name`, `adresse`, `telefon`, `email`); alle vier sind Pflicht, fehlt oder ist eine Angabe leer, wirft `widerrufsbelehrung()` einen TypeError |
| Widerrufsfunktion bei online geschlossenen Verträgen (§ 356a BGB) | Satzrahmen des Gesetzes wörtlich („Sie können Ihr Widerrufsrecht auch online unter … ausüben. Wenn Sie diese Online-Funktion nutzen, …“); in die Lücke „…“ kommt statt einer Internetadresse ein „anderer geeigneter Hinweis“: „Vertrag widerrufen“ (Schaltfläche im Terminbereich auf unserer Website sowie Link „Termin verwalten“ in Ihrer Bestätigungs-E-Mail); der Gesetzeswortlaut zur Eingangsbestätigung bleibt unverändert |
| Absatz zum elektronisch ausfüllbaren Webformular | ausgelassen: Es gibt keine eigene Webformular-Funktion außer der Widerrufsfunktion |
| Textbausteine zu Waren (Besitz, Rückzahlungsverweigerung, Abholung oder Rücksendung, Rücksendekosten, Wertverlust) | ausgelassen: Ein Termin hat keine Waren |
| Dienstleistungen: Wertersatz („Haben Sie verlangt, dass die Dienstleistungen …“) | enthalten; „oder Lieferung von Wasser/Gas/Strom/Fernwärme [Unzutreffendes streichen]“ ist gestrichen, wie der Gestaltungshinweis es vorsieht |
| Muster-Widerrufsformular (Anlage 2) | enthalten; in der Zeile „An …“ stehen Name, Anschrift und E-Mail des Betriebs; die (*)-Alternativen bleiben unverändert |
Anrede immer „Sie“, auch bei Betrieben, die ihre Gäste duzen: Es ist der gesetzliche Mustertext.
## Text (Platzhalter in doppelten geschweiften Klammern)
```text
Widerrufsbelehrung
Widerrufsrecht
Sie haben das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen.
Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag des Vertragsabschlusses.
Um Ihr Widerrufsrecht auszuüben, müssen Sie uns ({{Name des Betriebs}}, {{Anschrift}}, Telefon: {{Telefon}}, E-Mail: {{E-Mail}}) mittels einer eindeutigen Erklärung (z. B. ein mit der Post versandter Brief oder eine E-Mail) über Ihren Entschluss, diesen Vertrag zu widerrufen, informieren. Sie können dafür das beigefügte Muster-Widerrufsformular verwenden, das jedoch nicht vorgeschrieben ist.
Sie können Ihr Widerrufsrecht auch online unter „Vertrag widerrufen“ (Schaltfläche im Terminbereich auf unserer Website sowie Link „Termin verwalten“ in Ihrer Bestätigungs-E-Mail) ausüben. Wenn Sie diese Online-Funktion nutzen, übermitteln wir Ihnen auf einem dauerhaften Datenträger (z. B. durch eine E-Mail) unverzüglich eine Eingangsbestätigung mit Informationen zum Inhalt der Widerrufserklärung sowie dem Datum und der Uhrzeit ihres Eingangs.
Zur Wahrung der Widerrufsfrist reicht es aus, dass Sie die Mitteilung über die Ausübung des Widerrufsrechts vor Ablauf der Widerrufsfrist absenden.
Folgen des Widerrufs
Wenn Sie diesen Vertrag widerrufen, haben wir Ihnen alle Zahlungen, die wir von Ihnen erhalten haben, einschließlich der Lieferkosten (mit Ausnahme der zusätzlichen Kosten, die sich daraus ergeben, dass Sie eine andere Art der Lieferung als die von uns angebotene, günstigste Standardlieferung gewählt haben), unverzüglich und spätestens binnen vierzehn Tagen ab dem Tag zurückzuzahlen, an dem die Mitteilung über Ihren Widerruf dieses Vertrags bei uns eingegangen ist. Für diese Rückzahlung verwenden wir dasselbe Zahlungsmittel, das Sie bei der ursprünglichen Transaktion eingesetzt haben, es sei denn, mit Ihnen wurde ausdrücklich etwas anderes vereinbart; in keinem Fall werden Ihnen wegen dieser Rückzahlung Entgelte berechnet.
Haben Sie verlangt, dass die Dienstleistungen während der Widerrufsfrist beginnen soll, so haben Sie uns einen angemessenen Betrag zu zahlen, der dem Anteil der bis zu dem Zeitpunkt, zu dem Sie uns von der Ausübung des Widerrufsrechts hinsichtlich dieses Vertrags unterrichten, bereits erbrachten Dienstleistungen im Vergleich zum Gesamtumfang der im Vertrag vorgesehenen Dienstleistungen entspricht.
Muster-Widerrufsformular
(Wenn Sie den Vertrag widerrufen wollen, dann füllen Sie bitte dieses Formular aus und senden Sie es zurück.)
– An {{Name des Betriebs}}, {{Anschrift}}, {{E-Mail}}:
– Hiermit widerrufe(n) ich/wir (*) den von mir/uns (*) abgeschlossenen Vertrag über den Kauf der folgenden Waren (*)/die Erbringung der folgenden Dienstleistung (*)
– Bestellt am (*)/erhalten am (*)
– Name des/der Verbraucher(s)
– Anschrift des/der Verbraucher(s)
– Unterschrift des/der Verbraucher(s) (nur bei Mitteilung auf Papier)
– Datum
(*) Unzutreffendes streichen.
```
## Wo der Text erscheint
- G02 (Bestätigung an den Gast), Block `widerruf`, als `{{widerrufsbelehrung}}` (die Mailvorlage setzt den Text HTML-maskiert ein); nur wenn die Buchung ein Widerrufsrecht hat (verbindlich, Verbraucher).
- Bei einer Freizeit-Dienstleistung mit festem Termin (§ 312g Abs. 2 Nr. 9 BGB) und bei Gästen, die nur Unternehmen sind, entfallen Belehrung, Formular und Widerrufsfunktion; der Hinweis steht dann in `termine-hinweise.md`.
@@ -0,0 +1,22 @@
# Entwurf: Abschnitt 6f der Datenschutzerklärung (Buchungskalender)
Stand 2026-10-07, Fassung der Rechtstexte 2026-10-07. **Entwurf – Prüfung durch Kanzlei offen.** Der Text kommt als Abschnitt `6f` in `public/datenschutz.html`, hinter 6d (Webshop; 6e ist für die Öffnungszeiten reserviert und noch nicht live) und vor Abschnitt 7. Er wird erst mit dem Livegang übernommen (`docs/funktionen/unterkunft-livegang.md`, Schritt 9), danach die PDF-Fassung `scripts/build-legal-pdfs.sh datenschutz` neu drucken. Mit der Übernahme zugleich im Abschnitt 12 „Empfänger im Überblick“ die Zeile zu Stripe und in Abschnitt 3 die Verweise prüfen. Der Text ist an Abschnitt 6d angelehnt und beschreibt, was die Software tut; ändert sich der AVV Buchungskalender, hier mitändern.
## Text (als HTML-Absätze übernehmen)
**6f. Buchungskalender auf Websites unserer Kunden**
Kunden können auf ihrer eigenen Website einen Buchungskalender für ihre Unterkünfte betreiben („Buchungskalender“), sobald wir die Funktion für sie freigeschaltet haben. Dabei verarbeiten wir die Daten der Personen, die dort eine Unterkunft anfragen oder buchen („Gäste“), ausschließlich als Auftragsverarbeiter nach den Weisungen des jeweiligen Kunden, der dafür Verantwortlicher ist (Auftragsverarbeitungsvertrag Buchungskalender). Wir verwenden diese Daten nicht für eigene Zwecke. Verantwortliche sind wir für die Betriebsdaten des Kunden im Kundenportal und für seine Bestellung der Funktion. Diese Datenschutzerklärung gilt für unsere eigenen Seiten und das Kundenportal. Wer auf der Website eines Kunden anfragt oder bucht, findet die Informationen dazu (Zwecke, Rechtsgrundlagen, Speicherdauer, Rechte) in der Datenschutzerklärung dieser Website. Anfragen von Gästen zu ihren Daten leiten wir an den Kunden weiter.
Für den Betrieb des Buchungskalenders speichern wir im Auftrag des Kunden: bei einer Anfrage Name, E-Mail-Adresse, auf Wunsch Firma, Telefonnummer und Nachricht, die Unterkunft, den Zeitraum und die Zahl der Personen sowie bis zur Entscheidung des Kunden einen Hashwert der IP-Adresse (siehe unten); nach der Freigabe die Posten und den Preis des Aufenthalts, die Stornobedingungen und die Fassung der Rechtstexte, die galten; bei der Buchung die Rechnungsadresse, die Zahlart und die Rechnung (und bei einer Stornierung die Stornorechnung) mit lückenloser Rechnungsnummer, in der der Kunde als Aussteller genannt ist; den Stand der Buchung (zum Beispiel gebucht, bezahlt, storniert) mit den Zeitpunkten, bei einer Online-Zahlung die Kennungen der Zahlung bei Stripe und erstattete Beträge, auch automatische Erstattungen (siehe unten). Zahlungsdaten wie die Kartennummer gibt der Gast nur auf der Zahlungsseite von Stripe ein; wir erhalten sie nicht (Online-Zahlung derzeit nur per Karte, auch mit Apple Pay und Google Pay). Der Kunde sieht und bearbeitet Anfragen, Buchungen und Rechnungen im Kundenportal. Im Namen des Kunden stellen wir die Rechnungen aus (Rechnung im Namen und im Auftrag des Kunden, § 14 Abs. 2 Satz 4 UStG) und senden E-Mails an Gäste (Eingang der Anfrage, Freigabe mit Link zur Buchung, Ablehnung, Buchungsbestätigung mit Rechnung, Zahlungseingang, Ablauf einer Reservierung, Stornierung) und an den Kunden (neue Anfrage, neue Buchung, Hinweis bei fehlgeschlagenem Kalenderabgleich) über den E-Mail-Dienst der Hetzner Online GmbH (Rechenzentrum Nürnberg, Abschnitt 8). Als Absender erscheint der Kunde, Antworten gehen an ihn. Bis zum Versand liegt jede E-Mail in einer Warteschlange; nach dem Versand löschen wir dort Empfängeradresse und Inhalt, den Eintrag selbst nach sieben Tagen; E-Mails, die nicht zugestellt werden konnten, löschen wir nach 48 Stunden. Der Link zur Buchungsseite enthält ein geheimes Kennzeichen, das wir nur als Prüfsumme speichern; bei einer Online-Zahlung übergeben wir den Link mit dem Kennzeichen als Rücksprungadresse an Stripe, sodass er für Stripe und im Stripe-Dashboard des Kunden sichtbar ist (er gilt bis 90 Tage nach der Abreise, bei nicht zustande gekommenen Buchungen 30 Tage, und öffnet nur diese eine Buchungsseite). Das Widget setzt keine Cookies. Der Kunde kann Kalender anderer Plattformen einbinden und die geheime Kalenderadresse des Buchungskalenders bei ihnen eintragen; wir lesen aus fremden Kalendern nur belegte Zeiträume, nicht Angaben zu Gästen, und die Adresse des Buchungskalenders enthält nur belegte Nächte. Zum Schutz vor Missbrauch begrenzen wir die Zahl der Anfragen je IP-Adresse; die Zeitpunkte der Aufrufe merken wir uns nur im Arbeitsspeicher des Servers, nicht dauerhaft. Mit der Anfrage speichern wir nicht die IP-Adresse, sondern einen Hashwert davon, der mit einem geheimen Schlüssel unseres Servers berechnet wird (Pseudonym); ohne den Schlüssel lässt sich daraus keine IP-Adresse ableiten. Wir verwenden ihn nur für den Missbrauchsschutz und entfernen ihn, sobald der Kunde die Anfrage freigibt oder ablehnt, spätestens nach 30 Tagen. Im Übrigen gelten die Protokolle in Abschnitt 3.
Online-Zahlungen im Buchungskalender laufen über Stripe Connect unmittelbar auf das eigene Stripe-Konto des Kunden; wir erhalten und verwahren kein Geld der Gäste und beteiligen uns nicht am Umsatz. Beim Start einer Zahlung übergeben wir Stripe die Bezeichnung des Aufenthalts mit Rechnungsnummer, den Betrag, die Kennungen der Buchung, die E-Mail-Adresse des Gastes zur Vorbelegung und den Rücksprung-Link zur Buchungsseite. Wird ein Aufenthalt storniert, schließen wir eine offene Zahlungsseite. Geht eine Zahlung nach einer Stornierung oder dem Ablauf der Reservierung ein oder zahlt der Gast eine bezahlte Buchung ein zweites Mal, erstatten wir den Betrag im Namen des Kunden und nach seiner dauerhaften Weisung automatisch in voller Höhe über sein Stripe-Konto; der Stand der Buchung ändert sich dadurch nicht. Um dieses Konto zu eröffnen, übermitteln wir, sobald der Kunde im Kundenportal die Verbindung mit Stripe auslöst, seine E-Mail-Adresse, die Adresse seiner Website und den Namen seines Projekts an die Stripe Payments Europe, Limited, 1 Grand Canal Street Lower, Grand Canal Dock, Dublin, D02 H210, Irland („Stripe“), zusammen mit internen Kennzeichen, mit denen wir das Konto seinem Projekt zuordnen. Die weiteren Angaben für das Konto gibt der Kunde direkt bei Stripe ein. Wir speichern die Kennung des Stripe-Kontos und seinen Stand (ob Zahlungen und Auszahlungen möglich sind), den uns Stripe meldet, und zeigen ihn dem Kunden im Kundenportal an. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO (Vertrag mit dem Kunden). Stripe verarbeitet die Daten für sein Konto als eigener Verantwortlicher; dabei können Daten an die Stripe, LLC in den USA übermittelt werden, die nach dem EU-US Data Privacy Framework zertifiziert ist (Abschnitt 7). Es gelten die Datenschutzhinweise von Stripe: stripe.com/de/privacy.
Löschfristen: Den IP-Hash entfernen wir, sobald der Kunde eine Anfrage freigibt oder ablehnt, spätestens nach 30 Tagen. Anfragen, aus denen keine Buchung wurde, anonymisieren wir nach sechs Monaten. Bei gebuchten Aufenthalten anonymisieren wir Name, Firma, E-Mail-Adresse, Telefonnummer und Nachricht drei Jahre nach dem Ende des Jahres der Abreise; die Rechnungen und die Rechnungsadresse bewahren wir für den Kunden acht Jahre auf (Buchungsbelege, § 147 AO), solange der Buchungskalender läuft. Schaltet der Kunde den Buchungskalender ab oder endet der Vertrag über den Betrieb seiner Website, schalten wir den Buchungskalender ab und weisen den Kunden danach per E-Mail mit dem Löschdatum darauf hin, seine Rechnungen herunterzuladen. Der Kunde kann Anfragen, Buchungen und Rechnungen danach 90 Tage lang im Kundenportal lesen; danach löschen wir alle Daten des Buchungskalenders (Unterkünfte, Preise, Anfragen, Buchungen, Rechnungen, Kalenderverbindungen, Sperren und Einstellungen). Wird das Kundenkonto gelöscht, schalten wir den Buchungskalender ab und löschen seine Daten 90 Tage danach; einen Hinweis per E-Mail gibt es dann nicht, weil mit dem Konto auch die E-Mail-Adresse gelöscht wird. Beginnt innerhalb dieser Frist wieder ein Vertrag über den Betrieb der Website, schalten wir den Buchungskalender auf Wunsch des Kunden mit den vorhandenen Daten wieder ein. Eine Ausnahme ist das Einrichtungs-Ticket, in dem wir die Einrichtung als Arbeitsnachweis festhalten (Abschnitt 6): Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Gästen. Löschen wir Daten oder anonymisieren wir sie, bleiben sie in unseren Datensicherungen noch bis zu sieben Tage erhalten und werden dann überschrieben. Die Daten liegen auf unserem Server bei Hetzner im Rechenzentrum Falkenstein (Deutschland), wie die übrigen Portal-Daten (Abschnitt 3). Die Bestellung der Funktion und ihren Nachweis beschreibt Abschnitt 7.
## Hinweise für die Übernahme
- Online-Zahlung gibt es derzeit nur per Karte (kein SEPA); bei einer Erweiterung die Texte (6f, AVV Nr. 3 und 7, LB Nr. 3, Absatz) mitändern.
- Die Aufbewahrung der Rechnungen für 8 Jahre durch WEBklar ist ein Dienst für den Kunden und endet mit der Löschung 90 Tage nach dem Abschalten (Frage U6 im Kanzlei-Paket).
- Abschnitt 12 „Empfänger im Überblick“ der Datenschutzerklärung: Stripe-Zeile um „Buchungskalender“ ergänzen.
- Abschnitt 7 (Bestellung der Funktion): Der Satz zu Zustimmungsnachweisen gilt für den Buchungskalender entsprechend; prüfen, ob die Aufzählung der Funktionen dort ergänzt werden muss.
@@ -0,0 +1,34 @@
# Fragen an die Kanzlei: Buchungskalender
Stand 2026-10-07. Gegenstand der Prüfung: Leistungsbeschreibung Buchungskalender und AVV Buchungskalender (Fassung 2026-10-07, beide als „Entwurf – Prüfung durch Kanzlei offen“ gekennzeichnet), Abschnitt 6f der Datenschutzerklärung (`unterkunft-datenschutz-6f-entwurf.md`), Muster-Datenschutz-Absatz für die Website des Vermieters (`docs/funktionen/unterkunft-datenschutz-absatz.md`), Muster-Stornobedingungen (`docs/funktionen/unterkunft-storno-muster.md`), die Zustimmungen der Bestellung (Wortlaut in `server/config.js`, Register `unterkunft`) und die Buchungsseite. Der Buchungskalender ist für Ferienwohnungen, Monteurzimmer und kleine Pensionen gedacht; erste Kundin ist eine Vermieterin von Monteurunterkünften (Unternehmer als Gäste, Verbraucher möglich). Die Arbeitsweise der Software steht im Entwurf der Spezifikation `docs/superpowers/specs/2026-10-07-buchungskalender-design.md`; Muster ist das Kanzlei-Paket zum Webshop (`webshop-kanzlei-paket.md`).
## Fragen
**U1. Button-Lösung über Mail-Link (§ 312j Abs. 3 BGB).** Die Anfrage des Gastes ist unverbindlich (Schaltfläche „Anfrage senden“). Der Vertrag entsteht erst, wenn der Gast nach der Freigabe durch den Vermieter über den Link in der E-Mail die Buchungsseite öffnet und „Zahlungspflichtig buchen“ klickt; unmittelbar darüber stehen alle Pflichtangaben. Genügt das für die Button-Lösung, auch wenn der Vermieter den Preis vor der Freigabe ändern kann und der Gast ihn erst auf der Buchungsseite verbindlich sieht? Ist die Beschriftung „Zahlungspflichtig buchen“ auch bei der Zahlart „vor Ort“ (keine Zahlung im Moment der Buchung) zulässig, oder braucht es eine andere Beschriftung („Verbindlich buchen“)? Muss die Anfrage-Schaltfläche anders heißen, damit niemand sie für verbindlich hält?
**U2. Pflichtinformationen (Art. 246a § 1 EGBGB, § 312d BGB) und Rolle des Vermieters.** Welche Angaben muss die Buchungsseite zeigen, welche stehen in der Buchungsbestätigung (dauerhafter Datenträger, § 312f BGB), und welche muss der Vermieter auf seiner Website selbst vorhalten (Impressum, Informationen zum Anbieter, Verbraucherstreitbeilegung nach § 36 VSBG)? Reicht die Buchungsseite (Anbieter, Unterkunft, Zeitraum, Leistung, Gesamtpreis, Zahlart, Stornobedingungen, Hinweis zum Widerrufsrecht), und was fehlt für Beherbergung typischerweise (zum Beispiel Preisangabenverordnung, Pflichtangaben zur Unterkunft, Hausordnung)? Welche Pflichten treffen WEBklar als Betreiber der Seite im Namen des Vermieters? Insbesondere: Die Bestätigungsmail (G19) nennt den Anbieter mit Name und Anschrift aus den Einstellungen, aber ohne Impressum-Link und ohne Steuernummer; muss der Anbieter dort vollständig mit Impressum und Steuernummer genannt werden?
**U3. Kein Widerrufsrecht (§ 312g Abs. 2 Satz 1 Nr. 9 BGB).** Wir weisen auf der Buchungsseite darauf hin, dass für die Buchung zu einem bestimmten Zeitraum kein Widerrufsrecht besteht. Gilt die Ausnahme auch für Monteurunterkünfte, die langfristig oder für Unternehmer und Verbraucher gemischt vermietet werden, und für Aufenthalte, deren Zeitraum über Wochen reicht? Insbesondere: Ist ein langer Monteursaufenthalt (Wochen bis Monate, eher Wohnzweck als Beherbergung) noch „Beherbergung zu Freizeitzwecken“, oder entfällt die Ausnahme, und was bedeutet das für die Belehrung? Ist die Formulierung („Ein Widerrufsrecht besteht nicht (§ 312g Abs. 2 Satz 1 Nr. 9 BGB).“) ausreichend, und wie ist die Belehrungspflicht zu verstehen, wenn eine Ausnahme vorliegt?
**U4. Umsatzsteuer der Endreinigung.** Die Software kennt je Vermieter einen Steuersatz für die Übernachtung (Standard 7 %) und einen für die Endreinigung (Standard 7 %) und rechnet die Steuer aus den Bruttobeträgen heraus. Ist es zulässig, die Endreinigung als unselbständige Nebenleistung der Beherbergung mit demselben Satz zu behandeln (§ 12 Abs. 2 Nr. 11 UStG), oder wäre 19 % der sichere Standard? Wie ist es bei Kleinunternehmern (§ 19 UStG, Hinweistext auf der Rechnung) und bei langfristigen Vermietungen, bei denen die Steuerbefreiung nach § 4 Nr. 12 UStG greifen kann? Wie weit darf WEBklar Voreinstellungen vorgeben, ohne dass daraus eine Steuerberatung wird?
**U5. Rechnung im Namen des Vermieters (§ 14 Abs. 2 Satz 4 UStG).** WEBklar stellt die Rechnungen in Namen und Auftrag des Vermieters aus; er stimmt bei der Bestellung und mit einem Haken in den Einstellungen zu. Genügt das als Zustimmung (Textform?) und ist die Abrechnung durch einen Dritten damit sauber geregelt, auch für die Stornorechnung (Rechnungskorrektur mit eigener Nummer) und die lückenlose Nummerierung je Vermieter und Jahr? Gilt das Konstrukt auch für Rechnungen an Verbraucher (dann ist die Rechnung keine Pflichtrechnung, sondern Beleg) und reichen die Pflichtangaben nach § 14 Abs. 4 UStG, wenn der Vermieter unvollständige Stammdaten hinterlegt? Wer haftet für falsche Steuerangaben auf der Rechnung (§ 14c UStG)?
**U6. Aufbewahrung durch WEBklar (8 Jahre).** WEBklar speichert die Rechnungen und die Rechnungsadresse der Gäste 8 Jahre lang, solange der Buchungskalender läuft, und löscht sie 90 Tage nach dem Abschalten; der Vermieter wird vorher aufgefordert, sie herunterzuladen. Reicht dieser Dienst, ohne dass WEBklar selbst aufbewahrungspflichtig oder zum Buchführungssystem wird (§ 147 AO, GoBD), und wie ist die Spannung zwischen der 8-Jahres-Frist und der Löschung nach 90 Tagen zu regeln? Die Gästedaten ohne Rechnungsbezug werden 3 Jahre nach dem Ende des Abreisejahres anonymisiert und Anfragen ohne Buchung nach 6 Monaten: Sind die Fristen angemessen (Art. 5 Abs. 1 lit. e DSGVO)?
**U7. Rolle im Datenschutz, AVV Buchungskalender.** Der Vermieter ist Verantwortlicher, WEBklar Auftragsverarbeiter (Muster AVV Webshop). Ist die Rollenverteilung für die Rechnungserstellung und die Aufbewahrung richtig (kein eigener Zweck von WEBklar)? Ist die Behandlung von Stripe (Vertragspartner des Vermieters, kein Unterauftragsverarbeiter) und der importierten Kalender anderer Plattformen (nur Zeiträume, keine Gästedaten) vertretbar? Genügt die Bezeichnung des IP-Hash als pseudonymes Datum mit Löschung bei der Entscheidung? Der geheime Buchungslink (Token, nur als Prüfsumme gespeichert) gilt bis 90 Tage nach der Abreise, bei abgelehnten oder abgelaufenen Anfragen 30 Tage nach der Entscheidung; er ist für Stripe und im Stripe-Dashboard des Vermieters sichtbar (Rücksprungadresse). Ist die Frist angemessen? Wird das Kundenkonto gelöscht, schaltet WEBklar den Buchungskalender ab und löscht die Daten 90 Tage später ohne E-Mail-Hinweis (die Adresse ist dann gelöscht): ausreichend?
**U8. Haftung bei Doppelbuchung durch iCal-Verzögerung.** Der Abgleich mit Booking.com, Airbnb und ähnlichen Plattformen geschieht über iCal mit Verzögerung von Minuten bis Stunden; jede Anfrage braucht die Freigabe, und beim Freigeben und Buchen prüft die Software erneut. Eine Doppelbuchung ist trotzdem möglich. Ist der Haftungsausschluss in Nr. 4 und Nr. 11 der Leistungsbeschreibung (keine Echtzeit-Verfügbarkeit, kein Ersatz von Folgeschäden außer im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen) wirksam, auch gegenüber Verbrauchern? Die Leistungsbeschreibung richtet sich nur an Vermieter, die Unternehmer sind; für deren Gäste gilt das nicht. Ist eine hervorgehobene Warnung im Portal sinnvoll oder nötig?
**U9. Zustimmungen der Bestellung.** Genügen die sechs Haken (Unterlagen, Verantwortung, Rechnung im Auftrag, Stripe, Beginn der Leistung und Erlöschen des Widerrufsrechts, Pflichtinformationen) und die Widerrufsbelehrung der Funktionen, wenn der Vermieter in der Praxis Unternehmer ist? Muss der Haken zu Stripe entfallen, wenn der Vermieter keine Online-Zahlung nutzt?
**U10. Kein Meldeschein, keine Kurtaxe, keine Anzahlung.** Die Leistungsbeschreibung nimmt Meldeschein, Kurtaxe und Anzahlungen aus dem Umfang und weist die Pflichten dem Vermieter zu (Nr. 2 Abs. 7, Nr. 5 Abs. 6). Ist das ausreichend deutlich, und wie ist eine Haftung von WEBklar wegen fehlender Hinweise ausgeschlossen?
**U11. Muster-Stornobedingungen.** Der Text `unterkunft-storno-muster.md` ist eine Vorlage für Vermieter (Staffel nach Zeit bis zur Anreise, Erstattungen, Verhältnis zu § 537 BGB und § 308 Nr. 7 BGB bei Verbrauchern, Pauschalen nach § 309 Nr. 5 BGB). Ist er als Vorlage mit Hinweis „keine Rechtsberatung“ vertretbar? Welche Pauschalen sind üblich und zulässig?
**U12. Datenschutz-Absatz für die Vermieter-Website.** Ist der Muster-Absatz `unterkunft-datenschutz-absatz.md` (Verantwortlicher, Rechtsgrundlagen Art. 6 Abs. 1 lit. b und f, Stripe, E-Mails, Speicherdauer) und der Abschnitt 6f der WEBklar-Erklärung in sich schlüssig und vollständig?
## Prüfpunkte vor dem Livegang
- Fassung der Rechtstexte: Ändert die Kanzlei etwas, gibt es eine neue Fassung (Datum, `config.unterkunft.fassung`, Zustimmungs-Pin in `test/featureConsentTexte.test.js`); ausgelieferte Fassungen werden nie überschrieben.
- Danach Upload in WOMS „Rechtliches“ und Schalter `UNTERKUNFT_ENABLED` von `pilot` auf `on` (Livegang-Schritt 10).
@@ -0,0 +1,3 @@
# Fragen an die Kanzlei: Webshop
Die Arbeitsliste der Fragen F1 bis F18 ist in das Kanzlei-Paket übernommen und dort um F19 bis F29 ergänzt worden. Maßgeblich ist jetzt: **`docs/rechtliches/webshop-kanzlei-paket.md`**, Abschnitt 4. Diese Datei bleibt nur als Verweis bestehen.
+394
View File
@@ -0,0 +1,394 @@
# Kanzlei-Paket Webshop (WEBklar)
Dieses Paket gehört zum Verkaufsweg des Webshops der WEBklar GbR (Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein). Es zeigt einer Kanzlei, was verkauft wird, wie der Ablauf ist, welche Texte Händler und Käufer in Endfassung sehen, wo diese Texte im Programm stehen und was wir wissen möchten. Es ist wie das Paket des Terminplaners (`docs/rechtliches/termine-kanzlei-paket.md`) aufgebaut; Fragen, die dort schon gestellt sind, nennen wir mit Verweis.
Fassungen: Leistungsbeschreibung Webshop, Auftragsverarbeitungsvertrag Webshop (AVV) und die Bestelltexte tragen die Fassung **2026-10-05** (Konstante `config.shop.fassung`). Die Widerrufsbelehrung mit Muster-Widerrufsformular ist die gemeinsame der Funktionen (`docs/rechtliches/widerrufsbelehrung-funktionen.md`, Fassung des Terminplaners). Alle Texte sind noch nie ausgeliefert worden; der Webshop bleibt bis zu Ihrem Befund hinter dem Schalter `SHOP_ENABLED` (nur Pilot).
## 1. Zweck und Umfang
**Zweck.** Der Inhaber lässt die Texte des Webshops durch eine Kanzlei prüfen, bevor der erste fremde Betrieb ihn nutzt (Schalter `SHOP_ENABLED=on`). Das Paket soll ohne Rückfragen zeigen, was die Kanzlei prüfen soll und von welchen Annahmen wir ausgehen.
**Was verkauft wird.** WEBklar verkauft nicht Waren, sondern ein Hilfsmittel: ein Shop-Widget für die Website eines Händlers samt Verwaltung im Kundenportal, Einrichtung einmalig 299 €, ohne Umsatzsteuer gemäß § 19 UStG, keine laufende Gebühr, keine Umsatzbeteiligung. Die Käufer bezahlen über Stripe Connect direkt an den Händler (eigenes Stripe-Konto, Direct Charges); WEBklar erhält und verwahrt kein Geld. Vertragspartner der Käufer ist der Händler. Der Webshop läuft, solange WEBklar die Website des Händlers betreibt; er ist nicht an Abo-Daten im Programm gekoppelt (Entscheidung W5, Abschnitt 2.5).
**Kernpaket.** Wir bitten die Kanzlei, zu prüfen:
- Leistungsbeschreibung Webshop (Anlage A) und AVV Webshop (Anlage B), vor allem Haftung, Ende mit dem Vertrag über den Betrieb der Website, ausgeschlossene Waren und Rolle bei Zahlungen (F1 bis F18),
- die Bestelltexte der Einrichtung: fünf Haken und Knopf (Abschnitt 3.1, F26),
- die Muster-Rechtstexte für Händler (Anlage C: AGB, Widerrufsbelehrung inklusive § 356a BGB, Muster-Widerrufsformular, Versand und Zahlung; F25),
- die Kasse im Widget: Knopf, Pflichtinformationen, Widerrufsfunktion und Gewährleistungshinweis (Abschnitte 3.3 und 3.4, F19, F20, F24),
- die Pflichtangaben am Artikel: Grundpreis, Lebensmittel, Produktsicherheit (F21 bis F23).
**Hausrunde.** Die Datenschutzerklärung von WEBklar (Abschnitt 6d), den Muster-Absatz für die Datenschutzerklärung des Händlers (Anlage D), die Mailtexte und die Hinweise im Kundenportal prüft WEBklar selbst. Zu einzelnen Punkten daraus stellen wir trotzdem Fragen (F27, F28).
**Begriffe.** *Händler* ist der Kunde von WEBklar, der den Webshop betreibt; *Käufer* sind die Besucher seiner Website, die bestellen. *Betriebsart* des Webshops ist `off` (abgeschaltet), `pilot` (nur Adressen einer Pilotliste) oder `on` (alle Kunden). Derzeit ist der Webshop nur für Konten des Inhabers freigeschaltet. `on` setzt der Inhaber erst nach Ihrem Befund und seiner Freigabe.
**Keine Rechtsberatung durch WEBklar.** Die Texte für Händler sind Muster ohne Gewähr (Leistungsbeschreibung Nr. 6). Die rechtlichen Aussagen in diesem Paket (vor allem „unsere Annahme“ bei den Fragen) sind ein Arbeitsstand von WEBklar und kein Rechtsurteil.
**Fassungen und Änderungen.** Ändern wir nach dem Befund einen Text, bekommt er ein neues Fassungsdatum und neue Dateien (HTML, PDF und Prüfsumme); die Zitate in diesem Paket werden dann angepasst.
**Aufbau.** Abschnitt 2 beschreibt den Produktablauf, Abschnitt 3 enthält die Texte, Abschnitt 4 unsere Fragen F1 bis F29, Abschnitt 5 die Fundstellen im Programm, Abschnitt 6 was nicht Teil der Prüfung ist, Abschnitt 7 die Checkliste für den Inhaber vor `SHOP_ENABLED=on`. Anlagen: **Anlage A** Leistungsbeschreibung Webshop, **Anlage B** AVV Webshop, **Anlage C** Muster-Rechtstexte der Händler, **Anlage D** Muster-Datenschutzabsatz für die Händler-Website (Pfade in Abschnitt 3.5).
## 2. Produktablauf in Kurzform
### 2.1 Anfrage, Freigabe und Bestellung der Einrichtung
| Schritt | Was geschieht |
|---|---|
| 1. Anfrage | Der Händler fragt den Webshop im Kundenportal für eine seiner Websites an: was er verkauft (Freitext und Warenart), ungefähre Artikelzahl, Versand und/oder Abholung, zwei Pflicht-Haken (er ist Unternehmer; er verkauft keine der ausgeschlossenen Waren), Nachricht an das Team. |
| 2. Prüfung | Das Team prüft die Anfrage (erlaubte Waren, Eignung) und nimmt sie an oder lehnt sie mit Begründung ab. Der Händler erhält eine Mail mit der Entscheidung. Ein Anspruch auf Freigabe besteht nicht (LB Nr. 2 Abs. 1). |
| 3. Bestellung | Nach der Freigabe bestellt der Händler die Einrichtung für 299 €: Zusammenfassung, volle Widerrufsbelehrung mit Muster-Widerrufsformular, fünf Haken, Knopf „Zahlungspflichtig bestellen · 299 €“ (Abschnitte 3.1 und 3.2). Eine Bindung an ein Abo besteht nicht. |
| 4. Nachweis | Der Server legt zuerst die Zahlungssitzung bei Stripe an und speichert den Nachweis: gezeigte Texte, Fassungen, Hash, Zeitpunkt des Klicks, IP-Adresse und Browser-Kennzeichen, Kundenkonto und angemeldete Person. Der Zeitpunkt des Klicks ist der Anker der Widerrufsfrist. |
| 5. Zahlung und Ticket | Stripe Checkout; danach legt das Portal ein Einrichtungs-Ticket mit der Einbauanleitung an, der Händler erhält die Bestellbestätigung mit den Unterlagen (Leistungsbeschreibung, AVV, Widerrufsbelehrung als PDF) in der Fassung, der er zugestimmt hat. |
| 6. Einrichtung | Der Händler verbindet sein Stripe-Konto (Onboarding bei Stripe), legt Artikel, Versand, Zahlungsarten und Rechtstexte an und bestätigt die Rechtstexte (Abschnitt 3.2). WEBklar baut das Widget und den Link „Vertrag widerrufen“ in die Website ein (Anleitung `docs/funktionen/shop-einbau.md`), fügt den Datenschutz-Absatz ein (Anlage D), macht mit dem Händler einen Testkauf mit Erstattung und schließt das Ticket; das schaltet die Funktion frei. Schaltet ein Mitarbeiter die Funktion stattdessen ohne Zahlung ein (Betriebsart `on`, Kunde nicht in der Pilotliste), verlangt das Portal vorher die Zustimmung des Kunden in Textform (Datum, Weg) und speichert sie als Zustimmungsnachweis (`featureConsents`, mit Mitarbeiter, ohne IP-Adresse und Browser-Kennzeichen); der Kunde erhält danach Leistungsbeschreibung und AVV als PDF (AVV Vorbemerkung Abs. 2). |
| 7. Öffnen | Der Händler öffnet den Shop (Schalter „Shop geöffnet“), sobald Einbau fertig, Stripe-Konto zahlungsfähig, Rechtstexte bestätigt, ein Artikel aktiv und Versand oder Abholung eingerichtet sind. Der Server prüft das bei jeder Bestellung erneut. |
### 2.2 Käufer: Bestellung
1. Der Käufer wählt Artikel (mit Grundpreis, Lebensmittel- und Produktsicherheitsangaben, Widerrufsausschluss), legt sie in den Warenkorb (im Sitzungsspeicher des Tabs, kein Cookie) und wählt Versand (nur Deutschland und Österreich) oder Abholung. Ganz oben im Widget stehen Lieferländer und Zahlungsarten.
2. In der Kasse stehen unmittelbar über dem Knopf die Pflichtinformationen, der Gewährleistungshinweis (Abschnitt 3.4) und der Knopf „Zahlungspflichtig bestellen“ (Abschnitt 3.3). Der Server rechnet alle Preise neu, prüft und reserviert den Bestand (30 Minuten).
3. Der Käufer wird zu Stripe Checkout geleitet und gibt dort Name, Adresse und Zahlung ein (Karte, Apple Pay, Google Pay, Klarna, SEPA-Lastschrift je nach Einstellung des Händlers; kein PayPal). Die Stripe-Kasse verlangt keine Telefonnummer (WEBklar erhebt und speichert auch keine) und fragt die Rechnungsanschrift nur ab, soweit die Zahlungsart sie braucht. SEPA gilt erst bei Eingang als bezahlt; bis dahin erhält der Käufer einmal eine Eingangsmail (G15) ohne Vertragsschluss.
4. Nach der Zahlung erhält der Käufer die Bestellbestätigung (G09, Abschnitt 3.3), der Händler die Mail „Neue Bestellung“ (K25).
5. Der Händler pflegt Status (versendet, abholbereit, abgeschlossen), erstattet über das Portal (Stripe-Rückzahlung auf dem Konto des Händlers) und exportiert Bestellungen als CSV.
### 2.3 Käufer: Widerruf (§ 356a BGB)
Der Käufer widerruft über „Vertrag widerrufen“ im Widget, auf der Standalone-Seite oder über den Link in der Fußzeile jeder Seite der Website: Schaltfläche, Formular (Name, E-Mail-Adresse, Bestellnummer „falls zur Hand“; ohne Bestellnummer ist die Erklärung Pflicht und soll Bestelldatum und Artikel nennen), Schaltfläche „Widerruf bestätigen“. Der Eingang wird gespeichert, der Käufer erhält sofort die Eingangsbestätigung mit Datum und Uhrzeit (G12), der Händler die Mail K26. Der Händler bearbeitet den Widerruf und die Erstattung selbst (LB Nr. 5 Abs. 6). Ohne Anmeldung, auch ohne passende Bestellung, mit Mengenbegrenzung gegen Missbrauch. ⚖ F20
### 2.4 Daten und Fristen
Käuferdaten liegen auf dem Server bei Hetzner (Falkenstein), E-Mails laufen über den E-Mail-Dienst von Hetzner (Nürnberg). Nicht bezahlte oder stornierte Bestellungen werden nach 30 Tagen gelöscht; bei bezahlten Bestellungen und Widerrufen werden Name, E-Mail-Adresse und Anschriften drei Jahre nach dem Ende des Bestelljahres anonymisiert (Positionen, Beträge, Bestellnummer und Status bleiben); der IP-Hash (Hashwert der IP-Adresse für den Missbrauchsschutz, nur in der offenen Bestellung) wird gelöscht, sobald die Bestellung bezahlt oder storniert wird, spätestens mit der Löschung nicht bezahlter Bestellungen nach 30 Tagen; eine Telefonnummer des Käufers wird nicht gespeichert. Nach dem Abschalten sind Bestellungen 90 Tage lesbar und als CSV exportierbar, danach löscht der tägliche Job alle Daten des Webshops (Artikel, Bilder, Bestellungen, Widerrufe, Einstellungen, Archiv der bestätigten Rechtstexte); das Einrichtungs-Ticket bleibt ohne Käuferdaten. Gelöschte Daten bleiben bis zu sieben Tage in den Sicherungen. ⚖ F14
### 2.5 Ende mit dem Vertrag über den Betrieb der Website
Der Webshop läuft, solange WEBklar die Website des Händlers betreibt (Hosting-Abo, Wartungs-Abo oder ein anderer Vertrag über den Betrieb der Website; LB Nr. 10 Abs. 1). Das Programm prüft das **nicht**: Es liest keine Abo-Daten für den Webshop und schaltet nicht von selbst ab. Das Team schaltet zum Vertragsende von Hand ab (Ticketsystem, Kunde, Funktionen, Webshop, „Abschalten“) und schreibt dem Händler vorher eine E-Mail. Beginnt innerhalb der 90 Tage wieder ein solcher Vertrag, schaltet das Team auf Wunsch wieder ein; endet der Vertrag, bevor der Webshop freigeschaltet ist, erstattet WEBklar die 299 € vollständig (von Hand in Stripe). ⚖ F9, F29
### 2.6 Widerruf der Bestellung durch den Händler
Der Händler (auch ein Existenzgründer oder Verbraucher) widerruft auf der Seite „Vertrag widerrufen“ von WEBklar mit der Auswahl „Webshop (Einrichtung)“. Die Frist von 14 Tagen beginnt mit dem Klick auf „Zahlungspflichtig bestellen“. Das Team erhält die Mitteilung T09 mit Fristende, Preis und Projekt; es wird nichts automatisch geändert. Das Team prüft, erstattet in Stripe und schaltet den Webshop ab; mit dem Widerruf endet der Webshop (LB Nr. 9 Abs. 6). ⚖ F8
### 2.7 Zahlen
| Wert | Bedeutung | Quelle im Code |
|---|---|---|
| 299 € | Einrichtung je Website-Projekt, einmalig, ohne Umsatzsteuer gemäß § 19 UStG | `SHOP_PREIS` in `server/config.js` |
| 14 Tage | Widerrufsfrist (Händler wie Käufer) | `WITHDRAWAL_DAYS` |
| 30 Minuten | Haltezeit einer offenen Bestellung (Checkout-Sitzung) | `bestellAblaufMinuten` in `server/config.js`, `legeBestellungAn` |
| 30 Tage | Löschung nicht bezahlter oder stornierter Bestellungen | `taeglicherJob` |
| 3 Jahre nach Ende des Bestelljahres | Anonymisierung der Käuferdaten | `anonymisierenJahre` in `server/config.js` |
| 90 Tage | Lesen und Export nach dem Abschalten, danach Löschung | `taeglicherJob` |
| 7 Tage | Eintrag in der Versandwarteschlange nach dem Versand; Sicherungen | AVV Nr. 9 |
## 3. Texte in Endfassung
Alle Zitate stehen wörtlich so im Programm oder in den Dokumenten (Abschnitt 5 nennt die Fundstellen).
### 3.1 Bestellung der Einrichtung: fünf Haken und Knopf
Auf der Bestellseite stehen vor dem Knopf die volle Widerrufsbelehrung mit Muster-Widerrufsformular und diese fünf Haken, keiner ist vorangekreuzt. Die Links auf Leistungsbeschreibung, AVV und Widerrufsbelehrung in der Fassung stehen daneben, nicht im Text der Haken. Gespeichert wird genau der gezeigte Text mit Fassung und Zeitpunkt. ⚖ F26
**Haken 1 (`unterlagen`)**
> Ich stimme der Leistungsbeschreibung Webshop und dem Auftragsverarbeitungsvertrag Webshop in der Fassung 2026-10-05 zu.
**Haken 2 (`verantwortung`)**
> Ich verkaufe als Unternehmer in eigenem Namen. Für meine Produkte, Preise, Produktangaben und Rechtstexte (AGB, Widerrufsbelehrung, Impressum, Datenschutzerklärung) bin ich selbst verantwortlich; WEBklar stellt Muster bereit und berät nicht rechtlich. Ich verkaufe keine der in der Leistungsbeschreibung ausgeschlossenen Waren.
**Haken 3 (`stripe`)**
> Die Zahlungen meiner Käufer laufen über mein eigenes Stripe-Konto zu den Bedingungen von Stripe. WEBklar erhält und verwahrt kein Geld meiner Käufer und erhebt keine Umsatzbeteiligung.
**Haken 4 (`beginn`)**
> Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit der Einrichtung des Webshops beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen Erbringung der Einrichtung erlischt (§ 356 Abs. 5 Nr. 2 BGB). Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB).
**Haken 5 (`information`)**
> Ich habe die Pflichtinformationen zu dieser Bestellung gelesen: Leistung (Einrichtung des Webshops für diese Website: Einbau des Shop-Widgets, Verwaltung im Portal, Muster-Rechtstexte), Gesamtpreis 299 € einmalig (ohne Umsatzsteuer gemäß § 19 UStG), voraussichtliche Dauer meist innerhalb von 2–3 Wochen, keine laufende Gebühr von WEBklar; der Webshop läuft, solange WEBklar die Website betreibt. Die Widerrufsbelehrung mit Muster-Widerrufsformular habe ich gelesen.
**Knopf**
> Zahlungspflichtig bestellen · 299 €
### 3.2 Bestätigung der Rechtstexte durch den Händler
Bevor der Händler den Shop öffnet, bestätigt er im Portal (Reiter Rechtliches) die Muster oder seine geänderten Texte mit diesem Haken. Zeit, Person und Hash der Texte werden gespeichert und die bestätigte Fassung wird zusätzlich im Archiv der Rechtstexte abgelegt (⚖ F16); jede Änderung der Texte hebt die Bestätigung auf. Neue Bestellungen werden mit der zuletzt bestätigten Fassung geschlossen. ⚖ F25
> Ich habe die Texte geprüft. Ich bin als Verkäufer für Rechtstexte, Produktangaben und Preise verantwortlich. WEBklar stellt Muster bereit und berät nicht rechtlich.
### 3.3 Kasse im Widget und Bestellbestätigung
Über dem Knopf zeigt das Widget (Quelle `public/embed/shop.js`, Zusammenfassung vom Server) unter den Überschriften „Ihre Bestellung“ (Positionen mit Einzel- und Summenpreis, Zwischensumme, Versandkosten oder Lieferart, Gesamtsumme, Steuerhinweis), „Lieferung und Zahlung“ (Lieferart, Lieferzeit, Zahlungsarten, Anbieter) und „Widerrufsrecht“, die Links „AGB lesen“, „Widerrufsbelehrung lesen“, „Versand & Zahlung lesen“, den Gewährleistungshinweis und den Satz „Name, Lieferadresse und Zahlung geben Sie im nächsten Schritt bei unserem Zahlungsdienstleister Stripe ein.“ Der Knopf heißt „Zahlungspflichtig bestellen“. Das Widerrufsrecht steht dort so:
> Sie haben ein gesetzliches Widerrufsrecht, soweit unten für einzelne Artikel nichts anderes steht. Einzelheiten finden Sie in der Widerrufsbelehrung.
Die Widerrufsfunktion führt mit diesen Texten:
> Hier können Sie Ihren Vertrag widerrufen. Bitte geben Sie Ihren Namen, die E-Mail-Adresse Ihrer Bestellung und möglichst die Bestellnummer an. Sie steht in Ihrer Bestätigungs-E-Mail.
> Bitte prüfen Sie Ihre Angaben. Mit „Widerruf bestätigen“ erklären Sie den Widerruf Ihres Vertrags.
Die Bestellbestätigung G09 (`server/mail-templates/G09 Bestellbestaetigung.html`) und die Eingangsbestätigung G12 (`G12 Eingang Widerruf.html`) sind (zusammen mit der Eingangsmail G15 bei ausstehender Zahlung) Anlage E in der gerenderten Fassung nach der Mail-Überarbeitung (Abschnitt 7.4). ⚖ F19, F20, F27
### 3.4 Hinweis auf die gesetzliche Gewährleistung
Der Hinweis steht hervorgehoben über dem Knopf (als Grafik `public/img/gewaehrleistung-eu.webp`, Wortlaut als Alternativtext) und in der Bestellbestätigung. Wortlaut (`gewaehrleistungsHinweis()`):
> GESETZLICHE GEWÄHRLEISTUNG
>
> Mindestens zwei Jahre gesetzliche Gewährleistung der Vertragsmäßigkeit für Waren, die in der Europäischen Union verkauft werden.
>
> Verbraucherinnen und Verbraucher können ihre Rechte im Rahmen des gesetzlichen Gewährleistungsrechts geltend machen, z. B. wenn die Waren nicht der Beschreibung entsprechen, nicht bestimmungsgemäß funktionieren.
>
> Verkäufer haften für jede Vertragswidrigkeit, die zum Zeitpunkt der Lieferung der Waren bestand und innerhalb des Zeitraums der gesetzlichen Gewährleistung erkennbar wird. Verkäufer müssen in solchen Fällen Folgendes anbieten: kostenlose Nachbesserung oder kostenlose Ersatzlieferung, in bestimmten Fällen eine Preisminderung oder eine vollständige Erstattung des Kaufpreises.
>
> In einigen Ländern gilt ein längerer Zeitraum für die gesetzliche Gewährleistung. Für gebrauchte Waren kann ein kürzerer Zeitraum gelten, jedoch nicht weniger als ein Jahr.
>
> Für weitere Informationen zu Ihren Rechten in einem bestimmten Land scannen Sie den nachstehenden QR-Code oder fragen Sie den Verkäufer. europa.eu/youreurope/garantien
>
> Was ist zu tun, wenn Sie vertragswidrige Waren erhalten? 1. Melden Sie dem Verkäufer das Problem so bald wie möglich. 2. Legen Sie einen Kaufnachweis vor, z. B. die Quittung, Rechnung oder einen Kontoauszug.
>
> Verkäufer und Hersteller können auch gewerbliche Garantien gewähren, die unabhängig von der gesetzlichen Gewährleistung gelten. Diese GARAN-Kennzeichnung zeigt beispielsweise, dass der Hersteller eine gewerbliche Haltbarkeitsgarantie ohne zusätzliche Kosten gewährt, die die gesamte Ware abdeckt.
⚖ F24
### 3.5 Dokumente und Fundstellen der Texte
| Anlage | Dokument | Pfad |
|---|---|---|
| A | Leistungsbeschreibung Webshop (Markdown, HTML, PDF) | `docs/rechtliches/leistungsbeschreibung-webshop.md`, `public/rechtliches/leistungsbeschreibung-webshop-2026-10-05.html` und `.pdf` |
| B | Auftragsverarbeitungsvertrag Webshop (Markdown, HTML, PDF) | `docs/rechtliches/avv-webshop.md`, `public/rechtliches/avv-webshop-2026-10-05.html` und `.pdf` |
| C | Muster-Rechtstexte der Händler (Belehrung, Formular, AGB, Versand und Zahlung, Gewährleistungshinweis) | `docs/funktionen/shop-muster-rechtstexte.md` (Quelltext der Muster: `server/services/shop/rechtstexte.js`) |
| D | Muster-Datenschutzabsatz für die Website des Händlers | `docs/funktionen/shop-datenschutz-absatz.md` |
| E | Gerenderte Mails G09 bis G15 und K24 bis K26 (nach der Mail-Überarbeitung nachgereicht) | `server/mail-templates/` |
| – | Gemeinsame Widerrufsbelehrung der Funktionen mit Muster-Widerrufsformular | `docs/rechtliches/widerrufsbelehrung-funktionen.md` |
| – | Bestelltexte (fünf Haken, Knopf) | `server/config.js`, `shopConsent` |
| – | Datenschutzerklärung von WEBklar, Abschnitt 6d (Hausrunde) | `public/datenschutz.html`, `id="webshop"` |
| – | Einbauanleitung für das Team, inklusive Abschalten von Hand | `docs/funktionen/shop-einbau.md` |
| – | Hinweis auf die gesetzliche Gewährleistung (Grafik und Text) | `public/img/gewaehrleistung-eu.webp`, `server/services/shop/rechtstexte.js`, `gewaehrleistungsHinweis` |
| – | Widerruf der Bestellung durch den Händler | `public/widerrufen.html`, `server/routes/abo.js` |
## 4. Fragen an die Kanzlei
Die Markierung ⚖ steht nur hier und in den Abschnittsverweisen, nie in den veröffentlichten Texten. „Unsere Annahme“ ist ein Arbeitsstand von WEBklar und kein Rechtsurteil. Kürzel: **LB** = Leistungsbeschreibung Webshop, **AVV** = Auftragsverarbeitungsvertrag Webshop, **AB** = Abo-Bedingungen. Die Fragen F1 bis F18 betreffen Anlagen A und B, F19 bis F29 den Ablauf im Widget und die übrigen Texte.
## Leistungsbeschreibung (Anlage A)
### F1: LB Nr. 1 Abs. 4: Webshop im Hosting-Abo
**Text.** Leistungsbeschreibung A Nr. 2 Abs. 3 (keine Formulare mit Server) und Nr. 4 (Rechtstexte nicht enthalten) sowie Leistungsbeschreibung B Nr. 3 stehen dem Webshop „nicht entgegen“, auch im Hosting-Abo.
**Frage.** Reicht diese Klarstellung, um die statische Hosting-Website mit einem eingebundenen Widget zu vereinbaren, oder muss die Leistungsbeschreibung A selbst geändert werden (Änderungsverfahren § 15 AB)? Ist der Vorrang der LB vor den AB (Abs. 3) wirksam?
**Unsere Annahme.** Ja, weil das Widget auf dem Server von WEBklar läuft und die Website statisch bleibt.
### F2: LB Nr. 3: Ausgeschlossene Waren und Abschaltung
**Text.** Katalog ausgeschlossener Waren (Alkohol, Tabak, Arzneimittel, Waffen und Feuerwerk, Erotik, lebende Tiere, digitale Inhalte, Gutscheine, Abonnements, Stripe-Liste); Abschalten nach Hinweis, bei Gefährdung auch ohne Hinweis; dann keine Erstattung des Einrichtungspreises, wenn der Händler den Verstoß zu vertreten hat.
**Frage.** Ist der Katalog gegenüber Unternehmern als AGB-Klausel haltbar (§ 307 BGB, auch bei Verweis auf eine fremde Liste wie die von Stripe, die sich ändert)? Ist die Abschaltung ohne vorherigen Hinweis und der Ausschluss der Erstattung angemessen? Sollte der Verweis auf die Stripe-Liste dynamisch bleiben?
### F3: LB Nr. 4: Zahlungen, Rolle von WEBklar, ZAG
**Text.** Stripe Connect mit eigenem Konto des Händlers (Direct Charges), WEBklar erhält und verwahrt kein Geld, keine Umsatzbeteiligung, „WEBklar ist kein Zahlungsdienstleister“; PayPal nicht verfügbar.
**Frage.** Trägt diese Konstruktion ohne Erlaubnis nach dem ZAG (Bereichsausnahme für technische Dienstleister, § 2 Abs. 1 Nr. 9 ZAG)? Ist der Satz „WEBklar ist kein Zahlungsdienstleister“ als Aussage in der LB unbedenklich oder sollte er entfallen? Bleibt das so, wenn der Webshop später eine Plattformgebühr oder PayPal bekommt?
**Unsere Annahme.** Keine Erlaubnis nötig, solange kein Geld über WEBklar läuft und keine Gebühr einbehalten wird.
### F4: LB Nr. 5: Pflichten des Händlers und Rolle als Hosting-Anbieter
**Text.** Der Händler verantwortet Artikel, Preise, Produktangaben (Lebensmittel, Produktsicherheit, Grundpreis), Rechtstexte, Lieferung, Gewährleistung, Rechnungen und Aufbewahrung.
**Frage.** Ist die Verteilung vollständig und vertretbar? Trifft WEBklar als Anbieter des Widgets eigene Pflichten (Verordnung über digitale Dienste für Hosting-Dienste; Produktsicherheitsverordnung für Online-Marktplätze), obwohl WEBklar nicht Verkäufer ist und keinen Marktplatz betreibt? Brauchen wir eine Meldestelle für rechtswidrige Angebote?
### F5: LB Nr. 6: Muster-Rechtstexte ohne Gewähr
**Text.** WEBklar stellt Muster-AGB, Muster-Widerrufsbelehrung und Muster-Widerrufsformular, Versand und Zahlung bereit („Muster ohne Gewähr“, keine Rechtsberatung); das Widget zeigt den Hinweis auf die gesetzliche Gewährleistung (Verordnung (EU) 2025/1960).
**Frage.** Ist die Bereitstellung von Mustern als Nebenleistung eines technischen Produkts nach dem Rechtsdienstleistungsgesetz zulässig (§ 2 Abs. 3, § 5 RDG)? Genügt der Hinweis „Muster ohne Gewähr“ mit Prüfempfehlung? Kann WEBklar für Fehler der Muster haften (Abs. 2 verspricht richtige Beschreibung der Technik)?
### F6: LB Nr. 8: Haftung
**Text.** Wie beim Terminplaner: Haftung für entgangenen Gewinn und Folgeschäden nur im Rahmen von § 13 Abs. 1 und 2 AB; keine Haftung für Inhalte, Preise und Rechtstexte des Händlers, Mustertexte, Ausfälle von Stripe, Rückbuchungen und Zahlungsausfälle.
**Frage.** Hält die Klausel der Inhaltskontrolle stand (Abs. 3 und 4 sind Ausschlüsse außerhalb des Einflussbereichs, keine Haftungsbegrenzung für eigene Pflichtverletzung)? Reicht der Verweis auf § 13 AB oder braucht der Webshop mit Zahlungsbezug eine eigene Haftungsgrenze (etwa Höchstbetrag)?
### F7: LB Nr. 9 Abs. 5: Mehraufwand und Stundenkontingent
**Text.** Mehraufwand wird nach Freigabe mit 100 € je Stunde berechnet und nicht auf das Stundenkontingent angerechnet.
**Frage.** Das ist eine Entscheidung des Inhabers, keine Rechtsfrage; sie ist hier nur zur Kontrolle gelistet: Soll der Händler im Wartungs-Abo den Mehraufwand wahlweise über sein Kontingent abwickeln können (wie beim Terminplaner nach § 4 AB)?
### F8: LB Nr. 9 Abs. 6: Widerruf der Einrichtung
**Text.** Verbraucher können die Bestellung der Einrichtung widerrufen; Verweis auf die gemeinsame Widerrufsbelehrung der Funktionen (Beispiel Terminplaner), Erlöschen nach der Zustimmung bei der Bestellung.
**Zitat der gemeinsamen Belehrung (`docs/rechtliches/widerrufsbelehrung-funktionen.md`).**
> Das Widerrufsrecht gilt nur für Verbraucher. Wir zeigen die Belehrung und das Muster-Widerrufsformular trotzdem allen, die eine Funktion bestellen. Die Belehrung gilt für die Bestellung der Einrichtung einer Funktion im Kundenportal, zum Beispiel des Terminplaners. Hinweise dazu stehen nach dem Muster-Widerrufsformular.
Die Belehrung nennt als Beispiel nur den Terminplaner, die Bestellung des Webshops verweist auf dieselbe Datei.
**Frage.** Der Webshop richtet sich nur an Unternehmer (Nr. 1 Abs. 1), die Bestellung kann aber ein Existenzgründer oder ein Verbraucher vor Gewerbebeginn aufgeben. Ist es richtig, allen Bestellern die Belehrung zu zeigen? Muss die gemeinsame Belehrung den Webshop namentlich nennen, oder genügt das „zum Beispiel“ mit dem Terminplaner, weil der Webshop eine Funktion im Sinne des ersten Satzes ist? Eine Änderung der Datei würde auch die Fassung des Terminplaners betreffen (die Kanzlei-Prüfung der Belehrung selbst ist Teil des Pakets Terminplaner).
### F9: LB Nr. 10: Ende mit dem Vertrag über den Betrieb der Website, Löschfrist, Wiedereinschaltung
**Text.** Der Webshop läuft, solange WEBklar die Website des Händlers betreibt (Hosting-Abo, Wartungs-Abo oder ein anderer Vertrag über den Betrieb der Website). Endet dieser Vertrag, endet auch der Webshop; WEBklar schaltet ihn zum Vertragsende ab und informiert den Händler vorher per E-Mail (Abs. 1, gilt auch beim Widerruf des Vertrags). 90 Tage Lesen und Export, danach Löschung; Wiedereinschaltung innerhalb der 90 Tage auf Wunsch ohne Kosten, wenn wieder ein Vertrag über den Betrieb der Website beginnt (Abs. 5); volle Erstattung des Einrichtungspreises, wenn der Vertrag vor der Freischaltung endet (Abs. 6). Technisch ist der Webshop nicht mit den Abo-Daten gekoppelt: Die Bestellung ist nicht an ein Abo gebunden, und das Abschalten zum Vertragsende erledigt das Team von Hand (WOMS, Funktionen, Webshop, „Abschalten“).
**Frage.** Ist die Kopplung des Webshops an den Vertrag über den Betrieb der Website wirksam, auch wenn der Händler einen laufenden Betrieb mit offenen Bestellungen und Widerrufen hat? Reicht der Hinweis an den Händler, seine Käufer zu informieren (Abs. 2), und die Vorab-Mail vor dem Abschalten? Sind die Erstattungs- und Wiedereinschaltungsregeln so vereinbar? Ist die Formulierung „anderer Vertrag über den Betrieb der Website“ bestimmt genug (§ 305c, § 307 BGB)?
### F10: LB Nr. 11 Abs. 2: Beendigung bei Änderungen von Stripe
**Text.** WEBklar darf den Webshop (je Website mit vier Wochen, für alle mit drei Monaten Frist) beenden oder einstellen, auch wenn Stripe die Anbindung beendet oder wesentlich ändert.
**Frage.** Ist dieser zusätzliche Beendigungsgrund (gegenüber dem Terminplaner) wirksam, oder muss er enger gefasst werden? Was gilt für den bereits gezahlten Einrichtungspreis?
### F11: LB Nr. 12 Abs. 2: Barrierefreiheit (BFSG)
**Text.** Wie beim Terminplaner: keine Zusage einer Konformität (WCAG 2.1 AA, EN 301 549); ob der Händler unter das BFSG fällt, entscheidet er selbst.
**Frage.** Ein Online-Shop ist ein Dienstleistungsangebot im elektronischen Geschäftsverkehr. Ist WEBklar mit dem Widget selbst Normadressat oder nur der Händler? Ist die Ausnahme für Kleinstunternehmen (§ 3 Abs. 3 BFSG) richtig dargestellt? Reicht „keine Zusage“, oder sollte WEBklar einen Mindeststandard für das Widget zusagen?
## Auftragsverarbeitungsvertrag (Anlage B)
### F12: AVV Vorbemerkung Abs. 4 und Nr. 2: Rollen
**Text.** Händler = Verantwortlicher, WEBklar = Auftragsverarbeiter, Stripe = Vertragspartner des Händlers (kein Unterauftragsverarbeiter). WEBklar verarbeitet Bestell- und Widerrufsdaten, versendet Bestell-E-Mails im Namen des Händlers und betreibt den Missbrauchsschutz. Dazu gehört ein gesalzener Hashwert der IP-Adresse in der Bestellung (Obergrenze von drei offenen Bestellungen je IP-Adresse in 30 Minuten; AVV Nr. 3 Abs. 4), gelöscht bei der Löschung oder Anonymisierung der Bestellung.
**Frage.** Ist WEBklar für alle diese Vorgänge reiner Auftragsverarbeiter, oder wird es beim Missbrauchsschutz (Arbeitsspeicher, Mengenbegrenzung, IP-Hash in der Bestellung) oder bei der Widerrufsfunktion (Eingangsbestätigung) zum eigenen Verantwortlichen (Art. 4 Nr. 7 oder Art. 26 DSGVO)?
### F13: AVV Nr. 7 Abs. 4: Stripe und Datenweitergabe
**Text.** Stripe ist kein Unterauftragsverarbeiter; Zahlung, Kartendaten und Auszahlung verantworten Händler und Stripe. WEBklar ruft auf Weisung des Händlers die Zahlungs- und Adressangaben der Checkout-Sitzung ab und veranlasst Erstattungen. Daten von Käufern gehen nicht von WEBklar an Stripe.
**Frage.** Ist es richtig, den Datenfluss von Stripe zu WEBklar als Verarbeitung im Auftrag des Händlers zu beschreiben? Ist die Abgrenzung (Stripe als Auftragsverarbeiter und teils eigener Verantwortlicher laut DPA von Stripe) für die Datenschutzerklärung des Händlers richtig? Ist der Satz zu Drittlandübermittlungen von Stripe („regeln Kunde und Stripe“) ausreichend?
### F14: AVV Nr. 9: Fristen für Anonymisierung und Löschung
**Text.** Nicht bezahlte Bestellungen (offen oder storniert): Löschung 30 Tage nach dem Anlegen. IP-Hash: gelöscht, sobald die Bestellung bezahlt oder storniert wird, spätestens mit dieser Löschung. Keine Telefonnummer des Käufers. Bezahlte Bestellungen und Widerrufe: Käuferdaten werden 3 Jahre nach dem Ende des Bestelljahres anonymisiert; Positionen, Beträge, Bestellnummer und Status bleiben. Nach dem Abschalten: 90 Tage Lesen und Export, dann Löschung aller Daten (auch des Archivs der Rechtstexte). Versandwarteschlange: Inhalt sofort nach dem Versand leeren, Eintrag nach 7 Tagen löschen, bei Fehlschlag nach 48 Stunden. Sicherung bis zu sieben Tage.
**Frage.** Sind 3 Jahre nach Ende des Bestelljahres für die Käuferdaten angemessen (Gewährleistung zwei Jahre, regelmäßige Verjährung drei Jahre ab Jahresende)? Entsteht ein Konflikt mit den Aufbewahrungspflichten des Händlers (zum Beispiel sechs oder acht Jahre nach AO und HGB), wenn WEBklar die Namen schon früher anonymisiert und der Händler den Export nicht gezogen hat? Reicht der Hinweis in Nr. 9 Abs. 8 und LB Nr. 5 Abs. 8, oder soll die Frist einstellbar sein?
### F15: AVV Nr. 8 Abs. 2: Betroffenenrechte nur auf Weisung im Chat
**Text.** Auskunft und Löschung/Anonymisierung einer einzelnen Bestellung geschehen auf Weisung des Händlers in Textform (Chat), nicht über eine Aktion im Portal; der Händler prüft vorher seine Aufbewahrungspflichten.
**Frage.** Genügt das als Unterstützung nach Art. 28 Abs. 3 lit. e DSGVO? Welche Reaktionszeit sollte der AVV nennen („unverzüglich“)?
### F16: AVV Nr. 8 Abs. 5: Archiv der bestätigten Rechtstexte und Prüfsumme
**Text.** Bestätigt der Händler seine Rechtstexte, legt WEBklar jede bestätigte Fassung einmal im Archiv ab (Wortlaut, Prüfsumme, Zeitpunkt, bestätigende Person des Händlers; keine Käuferdaten). Die Bestellung speichert die Prüfsumme der Fassung, die der Käufer sah; die Bestellbestätigung G09 zeigt die archivierte Fassung zu dieser Prüfsumme und nicht die beim Versand aktuelle (fehlt sie ausnahmsweise, gilt die aktuell bestätigte). WEBklar löscht das Archiv mit den übrigen Daten des Webshops 90 Tage nach dem Abschalten (AVV Nr. 9 Abs. 6), nicht früher; es ersetzt nicht die Aufbewahrung des Händlers (AVV Nr. 9 Abs. 8). Der Wortlaut bleibt also auch über die Anonymisierung der Bestellung (drei Jahre nach dem Bestelljahr) hinaus erhalten, solange der Webshop betrieben wird.
**Frage.** Genügt das als Nachweis des Händlers für den Vertragsinhalt (Bestellbestätigung mit den vollständigen Texten, § 312f Abs. 2 BGB)? Ist es richtig, die Fassungen bis 90 Tage nach dem Abschalten ohne weitere Frist aufzubewahren (Speicherbegrenzung; die Fassungen enthalten keine Käuferdaten, nur die bestätigende Person des Händlers), oder soll der AVV eine kürzere oder längere Frist nennen?
### F17: AVV Nr. 3 Abs. 5: Warenkorb im Browser
**Text.** Kein Cookie; der Warenkorb (Artikelkennungen und Mengen, kein Personenbezug) liegt im Sitzungsspeicher (sessionStorage) des Tabs, „technisch erforderlich“.
**Frage.** Ist das nach § 25 Abs. 2 Nr. 2 TDDDG ohne Einwilligung zulässig? Welche Information muss der Händler dazu in seiner Datenschutzerklärung geben (der Musterabsatz dazu entsteht in Aufgabe J2)?
### F18: AVV insgesamt: Übernahme der Textbausteine des Terminplaners
**Text.** Nummern 5, 6, 7 Abs. 1 bis 3, 10, 11 Abs. 1 und 12 sowie die Anlage sind inhaltlich aus dem AVV Terminplaner übernommen.
**Frage.** Gilt die Prüfung des AVV Terminplaner für diese Teile unverändert, oder ist wegen der Zahlungsdaten und der Adressdaten der Käufer (Liefer- und Rechnungsadresse, IP-Hash) eine Anpassung der Maßnahmen in der Anlage nötig?
## Ablauf, Pflichtangaben, Datenschutz und Ende
### F19: Vertragsschluss im Widget, § 312j BGB: Platz des Knopfes und Stripe nur als Zahlung
**Text.** Die verbindliche Bestellung geschieht im Widget des Händlers mit dem Knopf „Zahlungspflichtig bestellen“ (Abschnitt 3.3). Unmittelbar darüber stehen Positionen, Einzel- und Gesamtpreise, Versandkosten, Gesamtsumme, Lieferart und Lieferzeit, Zahlungsarten, Hinweis auf Widerrufsrecht oder -ausschluss je Artikel, Links auf AGB, Widerrufsbelehrung und Versand & Zahlung, der Gewährleistungshinweis (Abschnitt 3.4) und die Anbieterangaben. Name und Lieferadresse gibt der Käufer erst danach auf der Zahlungsseite von Stripe ein (ohne Telefonnummer, Rechnungsanschrift nur soweit nötig); Stripe Checkout ist nur die Zahlung. Bis zur Zahlung (30 Minuten) ist die Bestellung „offen“ und der Bestand reserviert; zahlt der Käufer nicht, verfällt sie.
**Frage.** (a) Erfüllt der Knopf mit der Beschriftung „Zahlungspflichtig bestellen“ im Widget § 312j Abs. 3 BGB, obwohl noch Daten (Name, Adresse) und die Zahlung folgen? (b) Erfüllen die Angaben über dem Knopf § 312j Abs. 2 BGB und Art. 246a § 1 Abs. 1 EGBGB, auch wenn die Lieferadresse noch fehlt? (c) Wann kommt der Vertrag zustande: mit dem Klick, mit der Zahlung oder mit der Bestätigung? Passt das zur Bestellbestätigung und zu den Mustertexten? (d) Ist die Informationspflicht zu Beginn des Bestellvorgangs (§ 312j Abs. 1 BGB: Lieferbeschränkungen und Zahlungsmittel) durch die Zeile ganz oben im Widget erfüllt?
**Unsere Annahme.** Ja: Der Händler gibt mit dem Klick im Widget das verbindliche Angebot ab; die Zahlung bei Stripe schließt den Vorgang ab; die Hinweise stehen unmittelbar über dem Knopf.
**Alternative.** Den Knopf erst auf der letzten Seite zeigen, nachdem Stripe die Daten erhoben hat (nicht möglich, weil Stripe Checkout die Seite betreibt), oder Name und Adresse im Widget abfragen und Stripe nur für die Zahlung nutzen.
### F20: Widerrufsfunktion nach § 356a BGB: Ausgestaltung
**Text.** Die Schaltfläche „Vertrag widerrufen“ steht in der Fußzeile des Widgets, auf der Standalone-Seite unter `/shop/<siteKey>/widerruf` und, vom Team eingebaut, als Link in der Fußzeile jeder Seite der Website des Händlers (Anleitung `docs/funktionen/shop-einbau.md`, Abschnitt 3). Sie führt ohne Anmeldung zu einem Formular (Name, E-Mail-Adresse, Bestellnummer „falls zur Hand“; ohne Bestellnummer muss die Erklärung Bestelldatum und Artikel nennen), danach zur Schaltfläche „Widerruf bestätigen“. WEBklar speichert den Eingang und sendet dem Erklärenden sofort eine Eingangsbestätigung mit Inhalt, Datum und Uhrzeit (G12) auf einem dauerhaften Datenträger; der Händler erhält die Mail K26. Die Funktion ist auch erreichbar, wenn keine Bestellung zur Angabe gefunden wird; der Händler klärt das. Die Bestellnummer ist ein freiwilliges Feld; fehlt sie, verlangt das Formular Bestelldatum und Artikel in der Erklärung (Freitext), damit der Händler zuordnen kann.
**Frage.** (a) Genügt diese Ausgestaltung § 356a BGB (Erreichbarkeit „während der gesamten Widerrufsfrist“, Schaltfläche, Bestätigungsschaltfläche, Eingangsbestätigung)? (b) Reichen Name, E-Mail-Adresse und entweder die Bestellnummer oder Bestelldatum und Artikel in der Erklärung zur Zuordnung („Identifikation“ des Vertrags), oder muss die Bestellung zwingend gefunden werden? (c) Ist der Link auf jeder Seite der Website des Händlers die richtige Verortung („Online-Benutzeroberfläche“), obwohl das Widget nur auf einer Seite läuft, und wer trägt die Pflicht, wenn der Händler den Link entfernt (LB Nr. 5 Abs. 6)? (d) Ist der Zeitstempel in Europe/Berlin mit Eingangsbestätigung ausreichend?
**Unsere Annahme.** Ja; die Funktion nimmt jede Erklärung an, die Zuordnung ist Sache des Händlers.
**Alternative.** Bestellnummer und E-Mail-Adresse als Pflichtfelder; Zuordnung mit Fehlermeldung bei Nichtfinden (würde Widerrufe abweisen und ist nach unserer Einschätzung riskanter).
### F21: PAngV, Grundpreis immer je 1 kg, 1 l, 1 m, 1 m²
**Text.** Das Portal rechnet den Grundpreis immer auf 1 kg, 1 l, 1 m oder 1 m² um und zeigt ihn als „x € / 1 kg“; die Umrechnung auf 100 g oder 100 ml (die für Waren mit einem Nenngewicht bis 250 g erlaubt wäre) ist nicht vorgesehen. Der Händler gibt Menge und Einheit des Artikels an. Der Grundpreis steht neben dem Gesamtpreis im Artikel und in der Kasse.
**Frage.** (a) Ist es zulässig, auch bei kleinen Mengen immer den Grundpreis je 1 kg oder 1 l anzugeben? (b) Reicht die Pflege durch den Händler (Menge und Einheit je Artikel) oder braucht es Ausnahmen (Waren nach Stück, lose Ware, Mengen, bei denen ein Grundpreis entfällt)? (c) Wie ist der Grundpreis bei Varianten mit unterschiedlicher Menge zu zeigen?
**Unsere Annahme.** Zulässig und eindeutiger; die Ausnahmen kennt der Händler.
### F22: Lebensmittel im Fernabsatz: lose Backwaren und Allergene
**Text.** Der Händler kann einen Artikel als Lebensmittel markieren; dann ist ein Pflichtfeld „Pflichtangaben“ auszufüllen (Bezeichnung, Zutaten, Allergene hervorgehoben, Menge, gegebenenfalls Hersteller), das vor dem Kauf am Artikel angezeigt wird. Ein typischer Kunde ist eine Bäckerei mit losen (nicht vorverpackten) Waren. Das Widerrufsrecht kann der Händler bei schnell verderblichen Waren ausschließen (§ 312g Abs. 2 Nr. 2 BGB). Die Lieferung und Abholung von Speisen ist in der Recherche als offen markiert.
**Frage.** (a) Genügt das Freitextfeld, um Art. 14 LMIV (Pflichtangaben im Fernabsatz vor Vertragsschluss) und die Allergenkennzeichnung nicht vorverpackter Ware zu erfüllen? (b) Muss der Shop die Angaben strukturieren (Allergenliste statt Freitext)? (c) Gilt für Lebensmittel, die der Händler selbst ausliefert oder zur Abholung bereitstellt, § 312 Abs. 2 Nr. 8 BGB oder der Ausschluss nach § 312g Abs. 2 Nr. 2 BGB, und was soll die Muster-Widerrufsbelehrung dazu sagen?
**Unsere Annahme.** Freitext genügt, wenn der Händler vollständig angibt; wir weisen ihn in der Leistungsbeschreibung (Nr. 5 Abs. 3) darauf hin.
### F23: Produktsicherheit (GPSR): Pflichtfelder am Artikel
**Text.** Je Artikel erfasst der Händler den Hersteller (Name, Anschrift, E-Mail oder Website; Vorgabe „ich selbst“), bei Herstellern außerhalb der EU die verantwortliche Person in der EU sowie Warn- und Sicherheitshinweise. Das Widget zeigt diese Angaben am Artikel. Eine Produktkennung (Typ, Charge, Seriennummer) und ein Bild jedes Produkts als eigene Pflichtfelder gibt es nicht; Bilder sind optional (bis vier).
**Frage.** Erfüllen diese Felder Art. 19 der Verordnung (EU) 2023/988 (GPSR) für Angebote im Fernabsatz? Fehlen Pflichtangaben (Produktkennung, Bild)? Trifft WEBklar als Anbieter des Widgets selbst Pflichten (siehe auch F4)?
**Unsere Annahme.** Die Felder decken die Pflichtangaben für die üblichen Waren ab; die Verantwortung bleibt beim Händler.
### F24: Hinweis auf die gesetzliche Gewährleistung (Verordnung (EU) 2025/1960 Anhang I)
**Text.** Seit dem 27.09.2026 ist der harmonisierte Hinweis hervorgehoben zu zeigen. Der Webshop zeigt die Grafik (`public/img/gewaehrleistung-eu.webp`) unmittelbar über dem Knopf „Zahlungspflichtig bestellen“, mit dem Wortlaut als Alternativtext, und führt denselben Text in der Bestellbestätigung. Der Wortlaut steht in Abschnitt 3.4. Der Abgleich der Grafikdatei mit der Originaldatei der Kommission steht noch aus.
**Frage.** (a) Entspricht Wortlaut und Gestaltung Anhang I (Größe, Hervorhebung, Position in der Kasse und im Angebot)? (b) Genügt die Einblendung in der Kasse, oder muss der Hinweis schon am Artikel stehen? (c) Genügt der Text in der Bestellbestätigung? (d) Wie ist der Hinweis bei Waren zu halten, die nicht dem Gewährleistungsrecht für Waren unterliegen?
**Unsere Annahme.** Kasse und Bestätigung genügen; die Grafik prüfen wir vor dem Livegang gegen die Datei der Kommission.
### F25: Muster-Rechtstexte des Händlers (AGB, Widerrufsbelehrung, Widerrufsformular, Versand und Zahlung)
**Text.** Das Portal befüllt für den Händler Muster nach `docs/funktionen/shop-muster-rechtstexte.md` (Anlage C). Die Widerrufsbelehrung folgt Anlage 1 zu Art. 246a § 1 Abs. 2 S. 2 EGBGB (Fassung 2026 mit dem Baustein zur Widerrufsfunktion), enthält beide Fristbeginn-Alternativen (eine Ware, mehrere getrennt gelieferte Waren), Rücksendekosten zulasten des Käufers (umschaltbar), Abholung und Ausschlussgründe in allgemeiner Form (§ 312g Abs. 2 Nr. 1, 2, 3 BGB). Dazu kommen Muster-AGB für kleine Shops, das Muster-Widerrufsformular (Anlage 2) und Versand- und Zahlungsinformationen, die aus den Einstellungen erzeugt werden. Der Händler bestätigt die Texte mit dem Satz aus Abschnitt 3.2; die Prüfsumme der Texte steht an jeder Bestellung.
**Frage.** (a) Erfüllen die Muster, richtig ausgefüllt, die Gesetzlichkeitsfiktion (Art. 246a § 1 Abs. 2 S. 3 EGBGB)? (b) Stimmen die Muster-AGB mit der Technik überein (Vertragsschluss, Eigentumsvorbehalt, Gewährleistung, Streitbeilegung nach § 36 VSBG ohne Link auf die eingestellte OS-Plattform)? (c) Ist es zulässig, Muster als Nebenleistung zu liefern (siehe F5)? (d) Teilsendungen und regelmäßige Lieferung sind nicht vorgesehen: Muss die Belehrung das ausdrücklich sagen?
**Unsere Annahme.** Ja, wenn der Händler die Platzhalter richtig ausfüllt; Abweichungen im Einzelfall prüft der Händler (LB Nr. 6).
**Alternative.** Nur die Belehrung als Muster, AGB nur als Gliederung.
### F26: Bestelltexte der Einrichtung: fünf Haken
**Text.** Die Bestellung der Einrichtung für 299 € trägt fünf Haken und den Knopf „Zahlungspflichtig bestellen · 299 €“ (Abschnitt 3.1). Alle Besteller sehen die volle Widerrufsbelehrung mit Muster-Widerrufsformular; Haken 4 verlangt den vorzeitigen Beginn mit Erlöschen und Wertersatz (§ 356 Abs. 5 Nr. 2, § 357a Abs. 2 BGB); Haken 5 bündelt die Pflichtinformationen.
**Frage.** Wie bei der Bestellung des Terminplaners (dort Q1 bis Q3, Q5, Q19, Q20): Genügen die fünf Haken, einschließlich Haken 3 („Die Zahlungen meiner Käufer laufen über mein eigenes Stripe-Konto …“) und Haken 5? Ist „der Webshop läuft, solange WEBklar die Website betreibt“ im Haken 5 als Information zur Laufzeit ausreichend, auch wenn das Ende des Webshops nicht an ein Abo, sondern an irgendeinen Vertrag über den Betrieb der Website gekoppelt ist (siehe F9)?
**Unsere Annahme.** Ja; wir gleichen die Antworten zum Terminplaner an.
### F27: Bestellbestätigung an den Käufer (G09) und Absender
**Text.** G09 enthält Anbieter, Bestellnummer, Positionen, Preise, Versand, Gesamtsumme, Lieferart und Lieferzeit, Adresse, Zahlungsart, Hinweis auf Widerrufsausschlüsse, die vollständigen AGB, die vollständige Widerrufsbelehrung und das Muster-Widerrufsformular im Mailtext, den Link „Vertrag widerrufen“ und den Gewährleistungshinweis. Absender ist die Versandadresse von WEBklar mit dem Namen des Shops, Antworten gehen an den Händler.
**Frage.** (a) Erfüllt G09 § 312f Abs. 2 BGB (Bestätigung auf einem dauerhaften Datenträger mit den Vertragsinhalten und den Informationen nach Art. 246a EGBGB)? (b) Ist der Absender (Domain von WEBklar, Name des Shops, Reply-To Händler) transparent genug, oder muss der Händler als Absender erscheinen? (c) Müssen die Mails der Anlage zur Prüfung beigelegt werden? (Sie liegen als gerenderte Fassung nach der Mail-Überarbeitung vor.)
**Unsere Annahme.** Ja.
### F28: Datenschutz: Abschnitt 6d, Stripe-Konto und Muster-Absatz
**Text.** (a) Datenschutzerklärung von WEBklar Abschnitt 6d („Webshop auf Websites unserer Kunden“): Rolle Auftragsverarbeiter für Käuferdaten, Verantwortliche für Betriebsdaten und Bestellung der Funktion. (b) Eröffnung des Stripe-Kontos des Händlers: WEBklar übermittelt auf Klick des Händlers dessen E-Mail-Adresse, die Adresse seiner Website und den Namen seines Projekts an Stripe Payments Europe Ltd. (Dublin), speichert Konto-ID und Stand und stützt das auf Art. 6 Abs. 1 lit. b DSGVO. (c) Muster-Absatz für die Datenschutzerklärung der Website des Händlers (`docs/funktionen/shop-datenschutz-absatz.md`, Anlage D): Stripe als eigener Verantwortlicher (Betrugsvorbeugung), Drittlandübermittlung (EU-US Data Privacy Framework), Klarna und SEPA über Stripe, Warenkorb im Sitzungsspeicher, E-Mails, Speicherdauer.
**Frage.** (a) Sind Rollen und Rechtsgrundlagen richtig (siehe auch F12, F13)? (b) Ist für die Übermittlung an Stripe bei der Kontoeröffnung eine Einwilligung oder Information des Händlers nötig, die über den Klick und Abschnitt 6d hinausgeht? (c) Reicht der Muster-Absatz für die Informationspflichten nach Art. 13 DSGVO (Verantwortlicher, Zwecke, Rechtsgrundlagen, Empfänger, Drittland, Dauer, Rechte), insbesondere für die Aussage zu Stripe und Klarna? (d) Passt die Speicherdauer (30 Tage unbezahlt, 3 Jahre bis zur Anonymisierung) zur Aufbewahrung des Händlers (siehe F14)?
**Unsere Annahme.** Ja; der Muster-Absatz folgt dem AVV Webshop.
### F29: Ende des Webshops mit dem Vertrag über den Betrieb der Website, Abschalten von Hand
**Text.** Der Webshop ist nicht mit Abo-Daten im Programm gekoppelt, die Bestellung hängt nicht an einem Abo, und das Programm schaltet nicht von selbst ab (Entscheidung W5). Das Team schaltet zum Vertragsende von Hand ab und schreibt dem Händler vorher (Anleitung `docs/funktionen/shop-einbau.md`, Abschnitt 8). LB Nr. 10 Abs. 1 verspricht dieses Vorgehen; ein verspätetes Abschalten bleibt ein Organisationsrisiko von WEBklar.
**Frage.** Entstehen aus einem verspäteten Abschalten Ansprüche des Händlers oder der Käufer gegen WEBklar (etwa wenn der Shop nach dem Vertragsende weiter bestellbar bleibt)? Wie ist „zum Vertragsende“ in Abs. 1 zu fassen (Tag der Beendigung oder Tag danach)? Genügt die Haftungsregelung der LB Nr. 8 dafür?
**Unsere Annahme.** Das Risiko ist gering und beschränkt sich auf die Zeit zwischen Vertragsende und Abschalten; die Haftung ist nach LB Nr. 8 begrenzt.
## 5. Fundstellen im Code
Pfade gelten im Repository des Kundenportals (`Webklar-Kundenbereich`); Bezeichner sind Funktionen oder Konstanten und stehen in der genannten Datei. Zeilennummern nennen wir nicht, weil sie sich ändern.
| Thema | Datei | Funktion/Konstante |
|---|---|---|
| Fassung, Preis, Betriebsart | `server/config.js` | `shop.fassung`, `SHOP_PREIS`, `SHOP_ENABLED` |
| Bestell-Haken und Knopf des Webshops (Register) | `server/config.js` | `shopConsent` |
| Betriebsart und Pilotliste | `server/services/shop/schalter.js` | `shopModus`, `shopFuerKunde` |
| Anfrage und Freigabe | `server/services/shop/anfrage.js` | – |
| Einstellungen, Zahlungsarten, Versand | `server/services/shop/einstellungen.js` | `STANDARD_EINSTELLUNGEN` |
| Artikel, Produktsicherheit, Lebensmittel, Grundpreis | `server/services/shop/produkte.js`, `server/services/shop/geld.js` | `normalisiereProdukt`, `grundpreisText` |
| Berechnung der Kasse (Preise nur aus der Datenbank) | `server/services/shop/preise.js` | `berechne` |
| Muster-Rechtstexte, Bestätigung, Gewährleistungshinweis | `server/services/shop/rechtstexte.js` | `musterWiderrufsbelehrung`, `musterWiderrufsformular`, `musterAgb`, `versandUndZahlung`, `BESTAETIGUNG_TEXT`, `gewaehrleistungsHinweis` |
| Stripe Connect: Konto, Onboarding, Checkout, Erstattung | `server/services/shop/connect.js` | `kontoAnlegen`, `onboardingLink`, `checkoutAnlegen`, `erstatten`, `connectEreignis` |
| Bestellung anlegen (IP-Hash, Stripe-Kasse ohne Telefonnummer), abschließen, Eingangsmail G15, verwerfen, Status, Erstattung, CSV | `server/services/shop/bestellungen.js` | `legeBestellungAn`, `ipHashVon`, `schliesseAb`, `meldeEingang`, `verwirf`, `setzeStatus`, `erstatte`, `csv` |
| Archiv der bestätigten Rechtstexte | `server/services/shop/archiv.js` | `archiviereRechtstexte`, `holeArchivierteRechtstexte` |
| Zustimmung in Textform bei Freischaltung durch das Team | `server/routes/admin/funktionenAdmin.js`, `server/services/shop/schalter.js`, `server/services/featureConsents.js` | `textformFuerAktivierung`, `shopTextformNoetig`, `speichereZustimmung` |
| Widerrufsfunktion der Käufer | `server/services/shop/widerrufe.js` | `normalisiereWiderruf` |
| Mails an Käufer und Händler | `server/services/shop/mails.js`, `server/mail-templates/` | `kaeuferWerte`, `haendlerK25`, `haendlerK26`; G09 bis G15, K24 bis K26, T21 |
| Öffentliche Schnittstelle des Widgets | `server/routes/publicShop.js` | Routen unter `/api/public/shop/v1/` |
| Widget | `public/embed/shop.js` | – |
| Löschfristen, Anonymisierung, Abschalten der Daten | `server/services/shop/jobs.js`, `server/services/shop/loeschen.js` | `taeglicherJob`, `anonymisierGrenze`, `loescheShopDaten` |
| Kundenlöschung | `server/services/shop/kundenLoeschung.js` | `schliesseShopsVonKunde` |
| Einrichtungs-Ticket mit Snippet und Schritten | `server/services/shop/ticketDetails.js` | `shopTicketDetails` |
| Rechtsunterlagen je Fassung | `server/services/legalDocs.js` | Satz `shop` |
| Widerruf der Bestellung | `server/routes/abo.js`, `public/widerrufen.html` | `ORDERS`, `FEATURE_OF_ORDER`, `orderLabel` |
| HTML-Seiten und PDFs der Vertragsunterlagen | `scripts/build-legal-html.mjs`, `scripts/build-legal-pdfs.sh` | Satz und Modus `shop` |
| Datenschutzerklärung von WEBklar | `public/datenschutz.html` | `id="webshop"` (Abschnitt 6d) |
## 6. Nicht Teil der Kanzleiprüfung
- **Hausrunde:** Datenschutzerklärung von WEBklar (Abschnitt 6d und die Ergänzungen in den Abschnitten 6, 7 und 12), die Hinweise im Kundenportal, die Mailtexte, die Einbauanleitung. Wir stellen nur die Fragen F27 und F28.
- **Gemeinsame Widerrufsbelehrung der Funktionen:** wird im Paket des Terminplaners geprüft; hier nur F8.
- **Die Rechtstexte der Händler im Einzelfall:** Jeder Händler bleibt für seine Texte verantwortlich (LB Nr. 5 Abs. 2 und Nr. 6). Wir bitten um Prüfung der Muster, nicht jedes Händlers.
- **Später:** Plattformgebühr, PayPal, Verkauf an Verbraucher außerhalb von Deutschland und Österreich, digitale Inhalte, Gutscheine, Abonnements, Rechnungserstellung durch WEBklar. Dafür gibt es keine Texte.
- **Steuerrecht der Händler** (Umsatzsteuer, Aufbewahrung): nur soweit F14 und F21 betroffen sind.
## 7. Checkliste für den Inhaber vor `SHOP_ENABLED=on`
Diese Punkte erledigt der Inhaber; kein Punkt ist Aufgabe der Kanzlei. Die Reihenfolge entspricht dem Ablauf bis zum ersten fremden Betrieb.
### 7.1 Vor der Beauftragung der Kanzlei
- [ ] Die PDFs der Anlagen A und B am Bildschirm ansehen (Kopfzeile, „Seite x von y“, Tabelle der Anlage des AVV); wir konnten sie nur über ihre HTML-Seiten prüfen.
- [ ] Bestätigen, dass WEBklar Kleinunternehmer nach § 19 UStG bleibt (Preis 299 € ohne Umsatzsteuer).
- [ ] Entscheiden, wie das Team den Vertragsende-Termin eines Händlers nicht verpasst (Erinnerung im Ticket, Kalender oder Abo-Liste), da der Webshop von Hand abgeschaltet wird (F29, `docs/funktionen/shop-einbau.md` Abschnitt 8).
- [ ] Entscheiden, ob Mehraufwand wahlweise über das Stundenkontingent abgerechnet werden darf (F7).
- [ ] Die Grafik `public/img/gewaehrleistung-eu.webp` mit der Originaldatei der Kommission abgleichen (F24).
### 7.2 Hetzner und Stripe (nur im Konto prüfbar)
- [ ] Auftragsverarbeitungsvertrag mit Hetzner für die Cloud **und** für das Webhosting (E-Mail-Versand) abgeschlossen; Anlage 1 nennt „Käufer“ und „Bestelldaten“ als Betroffene und Datenarten.
- [ ] Datensicherung eingeschaltet (AVV Nr. 9 Abs. 7 verspricht sieben Tagessicherungen).
- [ ] Stripe-Dashboard von WEBklar: Connect aktivieren (Plattformprofil: Verkäufer verkaufen eigene Waren, Direct Charges, keine Gebühr), Branding für das Onboarding setzen.
- [ ] Prüfen, dass der Vertrag mit Stripe die Nutzung von Connect für diesen Zweck erlaubt (F3).
### 7.3 Technik
- [ ] `node scripts/shop-setup.mjs --check`, dann `node scripts/shop-setup.mjs` (fünf Collections, darunter das Archiv der bestätigten Rechtstexte) und `node scripts/stripe-products.mjs --anlegen` (Produkt `shopSetup`), vor dem Code-Deploy.
- [ ] Connect-Webhook in Stripe anlegen (Ereignisse: `checkout.session.completed`, `checkout.session.async_payment_succeeded`, `checkout.session.async_payment_failed`, `checkout.session.expired`, `account.updated`, `charge.refunded`), Geheimnis als `STRIPE_CONNECT_WEBHOOK_SECRET` in `/home/webklar/apps/.env`, Zeilen `SHOP_ENABLED` und `SHOP_PILOT_EMAILS` in der Compose-Datei.
- [ ] Code nach `main` (nach dem Terminplaner), WOMS-Zweig nach `test`, Deploy zu einer ruhigen Zeit.
- [ ] PDF der Datenschutzerklärung mit `scripts/build-legal-pdfs.sh datenschutz` (nicht `all`), PDFs des Webshops mit `scripts/build-legal-pdfs.sh shop`; Prüfsummen mit den Tests abgleichen.
### 7.4 Uploads in WOMS „Rechtliches“ und Mailtexte
- [ ] In WOMS „Rechtliches“ Karten „Leistungsbeschreibung Webshop“ und „AVV Webshop“ bereitstellen (Setup-Skript von WOMS) und die Fassung 2026-10-05 hochladen; die neue Datenschutzerklärung (Stand 05.10.2026, `docs/WEBklar-Datenschutzerklaerung.pdf`) als neue Version hochladen.
- [ ] Mailtexte G09 bis G15, K24 bis K26, T21 in der Mail-Überarbeitung auf den endgültigen Wortlaut bringen, danach gerendert als Anlage E nachreichen (F27).
### 7.5 Pilot, Beauftragung und Freigabe
- [ ] Pilot mit dem Testkonto (`SHOP_ENABLED=pilot`): Anfrage, Freigabe, Bestellung, Einbau auf der Test-Website, Stripe-Konto, Testkauf für 1 € mit Erstattung, Widerruf über „Vertrag widerrufen“, Abschalten und Wiedereinschalten.
- [ ] Die Kanzlei mit der Prüfung des Kernpakets und den Fragen F1 bis F29 beauftragen (Anlagen A bis E beilegen; Umfang und Frist vereinbaren).
- [ ] Den Befund einarbeiten: Texte ändern, neues Fassungsdatum setzen (`config.shop.fassung`), HTML und PDFs neu erzeugen (`node scripts/build-legal-html.mjs shop`, `scripts/build-legal-pdfs.sh shop`), Prüfsummen und Tests nachziehen; dieses Paket nachziehen.
- [ ] Die Fassung ausdrücklich freigeben; erst danach `SHOP_ENABLED=on` setzen.
- [ ] Vor dem ersten fremden Betrieb: der Händler bestätigt seine Rechtstexte (Abschnitt 3.2), wir fügen den Datenschutz-Absatz (Anlage D) ein, Testkauf mit dem Händler.
@@ -0,0 +1,54 @@
# Widerrufsbelehrung und Muster-Widerrufsformular (Einrichtung von Funktionen)
**Fassung 2026-10-05**
Das Widerrufsrecht gilt nur für Verbraucher. Wir zeigen die Belehrung und das Muster-Widerrufsformular trotzdem allen, die eine Funktion bestellen. Die Belehrung gilt für die Bestellung der Einrichtung einer Funktion im Kundenportal, zum Beispiel des Terminplaners. Hinweise dazu stehen nach dem Muster-Widerrufsformular.
## Widerrufsbelehrung
### Widerrufsrecht
Sie haben das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen.
Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag des Vertragsabschlusses.
Um Ihr Widerrufsrecht auszuüben, müssen Sie uns (WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, Telefon: 0176 23726355, E-Mail: support@webklar.com) mittels einer eindeutigen Erklärung (z. B. ein mit der Post versandter Brief oder eine E-Mail) über Ihren Entschluss, diesen Vertrag zu widerrufen, informieren. Sie können dafür das beigefügte Muster-Widerrufsformular verwenden, das jedoch nicht vorgeschrieben ist.
Sie können Ihr Widerrufsrecht auch online unter https://project.webklar.com/widerrufen.html ausüben. Wenn Sie diese Online-Funktion nutzen, übermitteln wir Ihnen auf einem dauerhaften Datenträger (z. B. durch eine E-Mail) unverzüglich eine Eingangsbestätigung mit Informationen zum Inhalt der Widerrufserklärung sowie dem Datum und der Uhrzeit ihres Eingangs.
Zur Wahrung der Widerrufsfrist reicht es aus, dass Sie die Mitteilung über die Ausübung des Widerrufsrechts vor Ablauf der Widerrufsfrist absenden.
### Folgen des Widerrufs
Wenn Sie diesen Vertrag widerrufen, haben wir Ihnen alle Zahlungen, die wir von Ihnen erhalten haben, einschließlich der Lieferkosten (mit Ausnahme der zusätzlichen Kosten, die sich daraus ergeben, dass Sie eine andere Art der Lieferung als die von uns angebotene, günstigste Standardlieferung gewählt haben), unverzüglich und spätestens binnen vierzehn Tagen ab dem Tag zurückzuzahlen, an dem die Mitteilung über Ihren Widerruf dieses Vertrags bei uns eingegangen ist. Für diese Rückzahlung verwenden wir dasselbe Zahlungsmittel, das Sie bei der ursprünglichen Transaktion eingesetzt haben, es sei denn, mit Ihnen wurde ausdrücklich etwas anderes vereinbart; in keinem Fall werden Ihnen wegen dieser Rückzahlung Entgelte berechnet.
Haben Sie verlangt, dass die Dienstleistungen während der Widerrufsfrist beginnen soll, so haben Sie uns einen angemessenen Betrag zu zahlen, der dem Anteil der bis zu dem Zeitpunkt, zu dem Sie uns von der Ausübung des Widerrufsrechts hinsichtlich dieses Vertrags unterrichten, bereits erbrachten Dienstleistungen im Vergleich zum Gesamtumfang der im Vertrag vorgesehenen Dienstleistungen entspricht.
## Muster-Widerrufsformular
(Wenn Sie den Vertrag widerrufen wollen, dann füllen Sie bitte dieses Formular aus und senden Sie es zurück.)
– An WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, E-Mail: support@webklar.com:
– Hiermit widerrufe(n) ich/wir (*) den von mir/uns (*) abgeschlossenen Vertrag über den Kauf der folgenden Waren (*)/die Erbringung der folgenden Dienstleistung (*)
– Bestellt am (*)/erhalten am (*)
– Name des/der Verbraucher(s)
– Anschrift des/der Verbraucher(s)
– Unterschrift des/der Verbraucher(s) (nur bei Mitteilung auf Papier)
– Datum
(*) Unzutreffendes streichen.
## Hinweise zum Widerruf
Diese Hinweise gehören nicht zur gesetzlichen Widerrufsbelehrung. Sie erklären, wie sich das Widerrufsrecht auf die Bestellung einer Funktion auswirkt.
- Das Widerrufsrecht gilt für jede Bestellung einer Funktion gesondert, also für jedes Website-Projekt, für das der Kunde eine Funktion bestellt. Die Frist von vierzehn Tagen beginnt jeweils mit dem Vertragsschluss.
- Widerruft der Kunde die Einrichtung einer Funktion, endet diese Funktion. Das Wartungs-Abo bleibt davon unberührt.
- Hat WEBklar auf ausdrücklichen Wunsch des Kunden vor Ablauf der Widerrufsfrist mit der Einrichtung begonnen, schuldet der Kunde bei einem Widerruf einen angemessenen Betrag für die bis dahin erbrachte Leistung. Der Betrag bemisst sich als Anteil am Gesamtpreis; Maß ist der im Arbeitsblatt erfasste Aufwand für die Einrichtung.
- Die Rückzahlung erfolgt über das ursprüngliche Zahlungsmittel; die Zahlung lief über Stripe.
@@ -0,0 +1,52 @@
# Datenschutzerklärung von WEBklar: Abschnitt 6e „Öffnungszeiten live auf Websites unserer Kunden“ (Entwurf)
Entwurf für die Rechtsrunde (Spec „Öffnungszeiten live“, Abschnitt 9 Nr. 3), Stand 05.10.2026. Keine Rechtsberatung. **`public/datenschutz.html` ist nicht geändert**: Der Abschnitt wird erst nach der Freigabe durch den Inhaber (und nach der Kanzlei, soweit sie die Fragen aus `zeiten-kanzlei-fragen.md` bearbeitet) in die Erklärung übernommen. Reihenfolge der Rechtsrunde: Leistungsbeschreibung, dann dieser Abschnitt samt Website-Absatz (`docs/funktionen/website-datenschutz-absatz.md`), dann Mailtexte, dann Schalter `ZEITEN_ENABLED=on`.
Aufbau wie Abschnitt 6c (`termine-datenschutz-6c.md`): Rolle, gespeicherte Angaben, E-Mails, Aufruf der Kundenwebsite, Löschfristen. Die Leistungsbeschreibung verweist auf diesen Abschnitt (`/datenschutz.html#zeiten`, Nr. 12 Abs. 2): **Sie darf erst live gehen, wenn der Abschnitt mit dem Anker `id="zeiten"` in der Erklärung steht.**
Einbau in `public/datenschutz.html`: zwischen 6c und „7. Abos und Zahlung über Stripe“. Beim Übernehmen ändert sich der Stand der Erklärung (Abschnitt 15); die Tests `test/legalTexts.test.js` (Abschnitt 6c und Kundenansicht) und `test/zeitenRecht.test.js` (dieser Entwurf) werden dann angepasst.
**Nachtrag 06.10.2026 (nach dem Zusammenführen mit main):** Die Nummer 6d ist seit dem Webshop-Livegang vergeben („6d. Webshop auf Websites unserer Kunden“, Anker `id="webshop"`). Dieser Abschnitt heißt deshalb **6e** und steht hinter dem Webshop-Abschnitt und vor „7. Abos und Zahlung über Stripe“; der Anker `id="zeiten"` bleibt.
## Abschnitt 6e (HTML)
```html
<h2 id="zeiten">6e. Öffnungszeiten live auf Websites unserer Kunden</h2>
<p>Kunden können auf ihrer eigenen Website ihre Öffnungszeiten live anzeigen lassen („Öffnungszeiten live“): als Tabelle und als Status („Jetzt geöffnet“), als Hinweis-Leiste am Seitenanfang, auf dem Handy als Kontakt-Leiste mit Knöpfen zum Anrufen, für die Route und für WhatsApp sowie als Angaben für Suchmaschinen im Seitencode. Für die Besucher dieser Websites verarbeiten wir Daten ausschließlich als Auftragsverarbeiter nach den Weisungen des jeweiligen Kunden, der dafür Verantwortlicher ist. Wir verwenden diese Daten nicht für eigene Zwecke. Verantwortliche sind wir für die Angaben, die der Kunde im Kundenportal für die Funktion einträgt, für die E-Mails an den Kunden und für seine Bestellung. Diese Datenschutzerklärung gilt für unsere eigenen Seiten und das Kundenportal. Wer eine Kundenwebsite besucht, findet die Informationen zum Aufruf (Zwecke, Rechtsgrundlagen, Speicherdauer, Rechte) in der Datenschutzerklärung dieser Website.</p>
<p>Im Kundenportal speichern wir, was der Kunde für die Funktion einträgt: seine Öffnungszeiten (benannte Zeitpläne, Zeiten je Wochentag, Abweichungen an Feiertagen, Betriebsurlaub und Sondertage sowie das gewählte Bundesland), Hinweise für die Hinweis-Leiste (Text, Art, Zeitraum und gegebenenfalls ein Knopf mit Link oder Telefonnummer), Kontaktangaben für die Kontakt-Leiste (Telefonnummer, Anschrift für die Route und auf Wunsch WhatsApp-Nummer und Starttext der Nachricht, dazu der Zeitpunkt, zu dem der Kunde bestätigt hat, den WhatsApp-Hinweis in seine Datenschutzerklärung aufgenommen zu haben), die Anzeige-Einstellungen, die unser Team für die Website des Kunden setzt, sowie Zeitpunkt und Urheber (Kunde oder Mitarbeiter) der letzten Änderung. Das sind überwiegend Betriebsdaten; personenbezogene Daten enthalten sie nur, soweit der Kunde sie einträgt, zum Beispiel die Telefonnummer eines Inhabers. Was auf der Website erscheinen soll (Zeiten, Hinweise, Telefonnummer, Anschrift, WhatsApp-Nummer, Name des Betriebs), ist dort öffentlich sichtbar. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO, wie bei den übrigen Portal-Daten (Abschnitt 6). Unsere Mitarbeiter können die Zeiten im Auftrag des Kunden ansehen und ändern (Abschnitt 6); den Haken für den WhatsApp-Hinweis setzen sie nicht. Die Bestellung und ihren Nachweis beschreibt Abschnitt 7.</p>
<p>Hat der Kunde die Funktion eingeschaltet, erinnern wir ihn per E-Mail an die E-Mail-Adresse seines Kundenkontos vor Feiertagen: Die E-Mail nennt die Tage der nächsten sieben Tage, an denen ein Feiertag seines Bundeslands liegt oder an denen wir um Prüfung bitten, und fragt, ob seine Zeiten für diese Tage stimmen. Für jeden Tag senden wir höchstens eine Erinnerung; dafür speichern wir den Tag als Merker (ohne weitere Angaben) und löschen den Merker nach 30 Tagen. Außerdem senden wir dem Kunden E-Mails zum Stand der Funktion (eingeschaltet, Ende des Wartungs-Abos mit der Möglichkeit, die Funktion zu behalten, abgeschaltet). Der Versand läuft über den E-Mail-Dienst der Hetzner Online GmbH (Abschnitt 8). Die E-Mails enthalten keine Werbung. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO, weil die Erinnerung zur Funktion gehört.</p>
<p>Beim Aufruf einer Kundenwebsite mit Öffnungszeiten live lädt der Browser ein kleines Programm von unserem Server (project.webklar.com) und die Öffnungszeiten des Kunden. Das Programm ruft die Daten einmal je Seitenaufruf ab und danach höchstens alle zehn Minuten erneut. Dabei gelten die Protokolle in Abschnitt 3. Das Programm setzt keine Cookies, speichert nichts im Browser des Besuchers und lädt nichts von anderen Anbietern. Die Knöpfe „Route“ und „WhatsApp“ sind Links: Erst der Klick des Besuchers öffnet Google Maps oder WhatsApp; vorher übertragen wir nichts an diese Dienste, und danach gelten deren Datenschutzhinweise. Zum Schutz vor Missbrauch begrenzen wir die Abrufe je IP-Adresse (höchstens 120 je Minute); dafür merken wir uns die Zeitpunkte der Abrufe je IP-Adresse nur im Arbeitsspeicher des Servers, nicht dauerhaft. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO; unser berechtigtes Interesse ist der sichere und störungsfreie Betrieb.</p>
<p>Löschfristen: Haben Sie die Funktion im Wartungs-Abo ohne Aufpreis eingeschaltet und endet das Wartungs-Abo, läuft sie noch 30 Tage weiter; in dieser Zeit können Sie sie für einmalig 30 € behalten. Andernfalls schalten wir sie danach ab, bewahren Ihre Eingaben noch weitere 30 Tage auf und löschen sie dann; beginnt in dieser Zeit wieder ein Wartungs-Abo, schalten wir die Funktion mit den vorhandenen Eingaben wieder ein. Haben Sie die Funktion für 30 € bestellt, bleiben Ihre Eingaben, solange die Funktion läuft. Schalten wir sie auf Ihren Wunsch ab, löschen wir Ihre Eingaben auf Ihr Verlangen, spätestens mit der Löschung Ihres Kundenkontos. In unseren Datensicherungen bleiben gelöschte Daten noch bis zu sieben Tage erhalten und werden dann überschrieben. Die Daten liegen auf unserem Server bei Hetzner im Rechenzentrum Falkenstein (Deutschland), wie die übrigen Portal-Daten (Abschnitt 3).</p>
```
## Ergänzung Abschnitt 6 (Mitarbeiter im Kundenauftrag)
Heute steht in Abschnitt 6 (wörtlich, `public/datenschutz.html`; der Satz ist durch `legal_datenschutz_kundenansicht` in `test/legalTexts.test.js` gepinnt):
> Um Sie zu unterstützen, können unsere Mitarbeiter Ihr Portal so öffnen, wie Sie es sehen, und dabei für Sie News-Beiträge anlegen, ändern oder löschen und, wenn Sie den Terminplaner nutzen, dessen Einstellungen und Termine ansehen und ändern.
Vorgeschlagen:
> Um Sie zu unterstützen, können unsere Mitarbeiter Ihr Portal so öffnen, wie Sie es sehen, und dabei für Sie News-Beiträge anlegen, ändern oder löschen, wenn Sie den Terminplaner nutzen, dessen Einstellungen und Termine ansehen und ändern sowie, wenn Sie Öffnungszeiten live nutzen, Ihre Öffnungszeiten, Hinweise und Kontaktangaben ansehen und ändern.
Der folgende Satz („Bestellungen, Zahlungen, Änderungen an Ihrem Passwort sowie Nachrichten oder Anträge in Ihrem Namen sind dabei ausgeschlossen.“) und das Protokoll (Mitarbeiter und Zeitpunkt, 12 Monate) bleiben unverändert und decken auch das Speichern der Zeiten durch Mitarbeiter ab (Protokollaktion `zeiten_gespeichert`). Beim Übernehmen die beiden Pins in `test/legalTexts.test.js` (`legal_datenschutz_kundenansicht`) auf den neuen Satz umstellen.
## Folgeänderung Abschnitt 7 (Nachweis beim Einschalten)
Abschnitt 7 nennt heute die Funktionen und den Klick auf „Zahlungspflichtig bestellen“. Beim Einschalten im Wartungs-Abo gibt es keine Zahlung und keine Stripe-Sitzung, gespeichert wird der Nachweis der Zustimmung trotzdem. Heute (wörtlich):
> Bestellen Sie eine Funktion (News-Pinnwand, Terminplaner), speichern wir als Nachweis die Texte und Fassungen, denen Sie zugestimmt haben, den Zeitpunkt Ihres Klicks auf „Zahlungspflichtig bestellen“, die Stripe-Sitzung, IP-Adresse und Browserkennung sowie Ihr Konto und die angemeldete Person. Rechtsgrundlagen sind Art. 6 Abs. 1 lit. b, c und f DSGVO; unser berechtigtes Interesse ist der Nachweis des Vertragsschlusses und der Zustimmungen, insbesondere für den Widerruf. Diese Angaben löschen wir drei Jahre nach Ablauf des Jahres, in dem Ihr letztes Abo endet. Bestellen Sie ohne Abo (zum Beispiel nur die News-Pinnwand), löschen wir diese Angaben spätestens drei Jahre nach Ablauf des Jahres der Bestellung. Den Entwurf Ihres Fragebogens speichern wir bis zu 90 Tage nach Ihrer letzten Änderung und löschen ihn danach, wenn Sie nichts bestellt haben.
Vorgeschlagen (Ergänzung der Klammer und des Klicks; der Rest bleibt):
> Bestellen Sie eine Funktion (News-Pinnwand, Terminplaner, Öffnungszeiten live) oder schalten Sie Öffnungszeiten live im Wartungs-Abo ein, speichern wir als Nachweis die Texte und Fassungen, denen Sie zugestimmt haben, den Zeitpunkt Ihres Klicks auf „Zahlungspflichtig bestellen“ oder „Einschalten“, bei einer Bestellung die Stripe-Sitzung, jeweils die IP-Adresse und die Browserkennung sowie Ihr Konto und die angemeldete Person.
Diese Folgeänderung steht nicht in Spec 9 Nr. 3; sie ergibt sich daraus, dass der Gratisweg einen Nachweis speichert. Gegen den Code geprüft (`speichereNachweisAusAnfrage` in `server/services/featureConsents.js`, Zeilen 196 und 197): Der Gratisweg speichert IP-Adresse und Browserkennung genauso wie die Bestellung; nur die Stripe-Sitzung entfällt (ohne `sessionId` trägt der Nachweis keine). Der Satz nennt deshalb die Stripe-Sitzung nur bei der Bestellung, IP-Adresse und Browserkennung bei jeder Zustimmung.
## Offene Punkte für die Rechtsrunde
1. **Rolle bei Websites ohne Abo oder bei Fremdhosting.** Der Entwurf sagt wie 6c: Wir sind Auftragsverarbeiter der Kunden für die Besucherdaten (Server-Protokolle). Mit Abo deckt das Anlage 1 der Abo-Bedingungen, ohne Abo oder bei Fremdhosting besteht kein Auftragsverarbeitungsvertrag (gleiche Lücke wie bei der News-Pinnwand). Frage `ZQ1` in `zeiten-kanzlei-fragen.md`.
2. **Löschung bei Abschaltung durch das Team** (Spec Abschnitt 5: keine automatische Löschung bei Grund `team`). Der Entwurf sagt „auf Verlangen, spätestens mit der Löschung des Kundenkontos“. Soll stattdessen eine feste Frist gelten (zum Beispiel 30 Tage wie beim Ende des Abos)? Entscheidung des Inhabers; dann auch Leistungsbeschreibung Nr. 9 Abs. 5 und das Programm (`abschalten.js`) anpassen.
3. **Erinnerungs-E-Mail.** Rechtsgrundlage Art. 6 Abs. 1 lit. b DSGVO (Teil der Funktion). Es ist keine Werbung; ob sie wegen des Hinweises „bei Google auch eintragen“ als Werbung gilt, ist nicht anzunehmen, steht aber in der Kanzlei-Frage `ZQ6`.
4. **Nachweis beim Einschalten** (Folgeänderung Abschnitt 7, siehe oben): beantwortet. Beim Einschalten im Wartungs-Abo werden IP-Adresse und Browserkennung ebenfalls gespeichert, nur die Stripe-Sitzung entfällt (der Satz ist entsprechend gefasst). Offen bleibt allein die rechtliche Prüfung des Satzes.
5. **Kontaktdaten öffentlich.** Die Telefonnummer und Anschrift stehen im Seitencode der Kundenwebsite (Abschnitt 6e, zweiter Absatz). Das ist die Entscheidung des Kunden; die Leistungsbeschreibung (Nr. 6 Abs. 3) verlangt, nur veröffentlichungsfähige Angaben einzutragen.
@@ -0,0 +1,43 @@
# K02 und K03: Block `funktionen` mit Öffnungszeiten live (Entwurf)
Entwurf für die Rechtsrunde (Spec „Öffnungszeiten live“, Abschnitte 7 und 9; Task 33), Stand 05.10.2026. **Keine Codeänderung:** Der Block `funktionen` in `server/mail-templates/K02 Kuendigungsbestaetigung.html` und `K03 Eingang Kuendigung.html` ist Rechtstext mit Pins; dieser Entwurf schlägt den neuen Satz nur vor. Der Inhaber entscheidet, die Kanzlei sieht ihn mit der Mailrunde (Spec 9: Oberflächen- und Mailtexte). Erst danach werden die Vorlagen und die Pins geändert.
Beide Vorlagen sind Kündigungsbestätigungen an den Kunden und stehen in der Sie-Form. Der Block erscheint nur beim Wartungs-Abo (`server/routes/abo.js`: K02 mit `funktionen: isWartung(sub)`, K03 mit `funktionen: !storage && isWartung(sub)`), nicht bei Hosting-Abo und nicht bei der Kündigung von Zusatzspeicher.
## Heute (gepinnt)
Der Block besteht aus diesem Satzpaar, in beiden Vorlagen wörtlich gleich:
> Die Website-Analyse und der Terminplaner gehören zum Wartungs-Abo und enden mit ihm. Die Termindaten löschen wir 30 Tage nach dem Abschalten; vorher können Sie Ihre Termine im Kundenportal ansehen und exportieren.
Pins, die der Satz heute hat:
- `test/mailTemplates.test.js`, Test „K02, K03: Block funktionen nennt Terminplaner und Löschfrist, nur mit Block (in K03 nur innerhalb von abo)“: prüft das ganze Satzpaar mit einem regulären Ausdruck (beginnend mit „Die Website-Analyse und der Terminplaner gehören zum Wartungs-Abo und enden mit ihm.“ bis „… ansehen und exportieren.“), dass der Block ohne `funktionen` fehlt und dass „Terminplaner“ bei Hosting-Abo und bei Speicher-Kündigung nirgends steht.
- `test/publicForms.test.js`, Konstante `FUNKTIONEN_SATZ` (nur der erste Satz, „… und enden mit ihm.“).
- Die Vorlagen-Kommentare (Zeile 12 beider Vorlagen) beschreiben den Block als „Hinweis, dass Website-Analyse und Terminplaner mit dem Wartungs-Abo enden“.
## Vorschlag
Der heutige Satz bleibt unverändert der Anfang (damit beide Pins ohne Änderung grün bleiben); angehängt wird ein Satz für Öffnungszeiten live, der die 30 Tage zum Behalten und die 30 Tage Datenfrist nach Z11 und Leistungsbeschreibung Nr. 9 nennt:
> Die Website-Analyse und der Terminplaner gehören zum Wartungs-Abo und enden mit ihm. Die Termindaten löschen wir 30 Tage nach dem Abschalten; vorher können Sie Ihre Termine im Kundenportal ansehen und exportieren. Haben Sie Öffnungszeiten live im Wartungs-Abo eingeschaltet, läuft die Funktion nach dem Ende des Abos noch 30 Tage weiter; in dieser Zeit können Sie sie im Kundenportal für einmalig 30 € behalten. Behalten Sie sie nicht, schalten wir sie danach ab, auf Ihrer Website stehen dann wieder feste Öffnungszeiten, und Ihre Eingaben löschen wir 30 Tage später.
Warum so:
- Der Satz ist bedingt („Haben Sie … eingeschaltet“), weil der Block bei jeder Kündigung eines Wartungs-Abos erscheint, auch bei Kunden ohne die Funktion.
- Wer die Funktion für 30 € bestellt hat, behält sie ohne Abo (Leistungsbeschreibung Nr. 9 Abs. 2 letzter Satz); für ihn gilt der Satz nicht, was die Bedingung „im Wartungs-Abo eingeschaltet“ ausdrückt. Ein eigener Satz dazu wäre in einer Kündigungsbestätigung verwirrend.
- Die Einzelheiten (Frist mit Datum, Knopf „Behalten“) sagt K29 („Wartungs-Abo beendet“), sobald das Ende des Abos erkannt ist; K02 und K03 sagen nur, dass es das gibt.
- „Abschalten“ bedeutet: Hinweis-Leiste, Kontakt-Leiste, Status und Google-Angaben verschwinden, die feste Ersatz-Tabelle der Website bleibt stehen (Leistungsbeschreibung Nr. 9 Abs. 4).
Im HTML ändert sich nur der Inhalt der Zelle zwischen `<!-- WENN funktionen -->` und `<!-- ENDE funktionen -->`; Aufbau und Blockkommentare bleiben. Die Kopfkommentare beider Vorlagen sagen dann „Website-Analyse, Terminplaner und Öffnungszeiten live“.
## Alternative
Ein eigener Block `zeiten` (nur für Kunden, bei denen die Funktion im Abo läuft): kein Text für Kunden ohne Funktion und die Zeile steht ohne „Haben Sie …“. Dafür braucht das Programm in `server/routes/abo.js` an beiden Stellen eine zusätzliche Abfrage der Funktion, beide Vorlagen einen neuen Block, und die Pins im Mail-Pin-Test (K01 bis K30) ändern sich. Nur sinnvoll, wenn der Inhaber den bedingten Satz nicht möchte.
## Beim Übernehmen
1. Vorlagen K02 und K03 ändern (Zelle des Blocks, Kopfkommentar).
2. `test/mailTemplates.test.js` (K02/K03-Test): zusätzlicher Wortlaut-Pin für den neuen Satz; die bestehenden Prüfungen bleiben.
3. Den Mail-Pin-Test (K01 bis K30, ebenfalls `test/mailTemplates.test.js`) und, falls sich der Block ändert, die Prüfung in `test/publicForms.test.js` nachziehen.
4. Kanzlei-Frage `ZQ5` (Ende mit dem Abo, 30 Tage zum Behalten) in `zeiten-kanzlei-fragen.md` abwarten, weil sie den Inhalt des Satzes betrifft.
+134
View File
@@ -0,0 +1,134 @@
# Fragen an die Kanzlei: Öffnungszeiten live (⚖)
Stand 05.10.2026, Entwurf (Spec „Öffnungszeiten live“, Abschnitt 9, und Task 33). Dies sind die Fragen zu den Texten dieser Funktion; sie ergänzen das Kanzlei-Paket des Terminplaners (`docs/rechtliches/termine-kanzlei-paket.md`, Fragen Q1 bis Q26).
**Warum eine eigene Datei.** Das Kanzlei-Paket des Terminplaners liegt auf dem Zweig `feature/terminplaner-tp2` und ist in diesen Zweig (`feature/oeffnungszeiten`) noch nicht eingeflossen; ändern wir es hier, entsteht beim Zusammenführen ein Konflikt. Die Fragen tragen deshalb die Kennung `ZQ1` bis `ZQ12`. Beim Zusammenführen: als `Q27` bis `Q38` in Abschnitt 4 des Kanzlei-Pakets übernehmen, die Fundstellen in Abschnitt 5 ergänzen, die Fassung der Leistungsbeschreibung in Abschnitt 3.4 daneben stellen. In veröffentlichten Dateien stehen keine ⚖-Marker.
Jede Frage hat vier Teile wie im Paket des Terminplaners: **Frage** (was wir wissen möchten), **Fundstelle im Text** (wo der Wortlaut steht), **Unsere Annahme** (der Stand, mit dem wir arbeiten; bitte bestätigen oder korrigieren), **Alternative** (was wir stattdessen täten). Einordnungen wie „Annahme“ sind keine Rechtsurteile. Die Texte, um die es geht: `public/vertragsunterlagen/zeiten-leistungsbeschreibung.html` (Leistungsbeschreibung, im Folgenden „LB“), `docs/rechtliches/zeiten-datenschutz-6e-entwurf.md` (Abschnitt 6e), `docs/rechtliches/zeiten-k02-k03-entwurf.md` (K02/K03), `docs/funktionen/website-datenschutz-absatz.md` (Website-Absatz) und die Haken im Register (`server/config.js`, `features.zeiten`).
Reihenfolge der Rechtsrunde (gemeinsam mit TP2): gemeinsame Bausteine (TP2 Block F) → Leistungsbeschreibung → Datenschutz § 6e/§ 6 und Website-Absatz → Oberflächen- und Mailtexte → diese Fragen ans Kanzlei-Paket → Inhaber lädt die Fassungen in WOMS „Rechtliches“ hoch → Schalter `ZEITEN_ENABLED=on`.
### ZQ1: ⚖ AVV Funktionen per Haken?
**Frage.** Für Öffnungszeiten live verarbeitet WEBklar beim Aufruf einer Kundenwebsite Besucherdaten (IP-Adresse, Zeitpunkt, Browserangaben in Server-Protokollen, 14 Tage). Mit einem Wartungs-Abo deckt Anlage 1 der Abo-Bedingungen (Auftragsverarbeitung, Server-Protokolle der Website) das ab. Beim 30-€-Weg ohne Abo oder bei Websites, die woanders liegen, gibt es keinen Auftragsverarbeitungsvertrag; das ist dieselbe Lücke wie bei der News-Pinnwand. Soll sie mit einem zusätzlichen Haken „AVV Funktionen“ beim Bestellen und Einschalten geschlossen werden (wie der Haken für den AVV Terminplaner und den AVV Website-Analyse), und genügt ein gemeinsamer AVV „Funktionen“ für News-Pinnwand und Öffnungszeiten live? Oder ist WEBklar für die Protokolle des eigenen Servers ohnehin selbst Verantwortlicher, sodass kein AVV nötig ist?
**Fundstelle im Text.** Spec Abschnitt 9 Nr. 5; Register `features.zeiten.avv: null`; Abschnitt 6e Absatz 1 (Rolle als Auftragsverarbeiter) und Offener Punkt 1; Website-Absatz Variante B (`[Prüfen: AVV mit WEBklar erforderlich]`); LB Nr. 12 Abs. 2.
**Unsere Annahme.** Mit Abo genügt Anlage 1. Ohne Abo und bei Fremdhosting braucht es einen AVV; wir würden einen kurzen AVV „Funktionen“ per Haken einführen (Aufbau und Nummern wie AVV Terminplaner Nr. 4 bis 9, ohne Termindaten, Unterauftragsverarbeiter nur Hetzner).
**Alternative.** Nichts ändern, weil die IP-Adresse in Server-Protokollen beim Abruf eines Skripts von `project.webklar.com` in der Verantwortung des Betreibers dieses Servers liegt; oder Öffnungszeiten live nur mit Wartungs-Abo oder bei WEBklar-Hosting verkaufen; oder der Rahmen-AVV mit Anlage je Funktion (TP2 Spec 7.4 „Später“).
### ZQ2: Behalten-Zustimmung nutzt den Haken „Einbau beginnt“
**Frage.** Endet das Wartungs-Abo, kann der Kunde Öffnungszeiten live im Kundenportal für einmalig 30 € behalten (LB Nr. 9 Abs. 2). Der Weg „Behalten“ verlangt dieselbe Zustimmung wie die Neubestellung, also auch den zweiten Haken (`beginn`): „Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit dem Einbau beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit vollständiger Erbringung der Leistung erlischt (§ 356 Abs. 5 Nr. 2 BGB). Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB).“ Beim Behalten gibt es keinen Einbau; die Funktion läuft bereits, es entsteht kein neues Ticket. Passt der Haken dann? Ist das Behalten ein eigener Vertrag mit eigenem Widerrufsrecht für Verbraucher (Frist ab dem Klick), und was ist die „Leistung“, deren vollständige Erbringung das Widerrufsrecht erlöschen lässt (der Weiterbetrieb ohne Abo hat kein Ende)?
**Fundstelle im Text.** Register `features.zeiten.consent.items` (Haken `leistung` und `beginn`); Programm: `POST /api/funktionen/zeiten/behalten` (`pruefeZustimmung(feature, body)`: „die der Bestellung, beide Haken“), `server/services/zeiten/behalten.js`; Spec Abschnitt 3.3; LB Nr. 9 Abs. 2 und Nr. 11.
**Unsere Annahme.** Der Haken ist für das Behalten unschädlich, weil er nur das Verlangen des Beginns und die Kenntnis vom Erlöschen bestätigt. Der Wortlaut „mit dem Einbau“ ist aber ungenau; wir würden für das Behalten „mit der Leistung“ oder „mit dem Weiterbetrieb“ sagen. Der Widerruf eines Behalten-Vertrags führt zur Abschaltung (LB Nr. 11 Abs. 3).
**Alternative.** Ein eigener Haken für das Behalten („Ich verlange, dass die Funktion nach dem Ende meines Wartungs-Abos ohne Unterbrechung weiterläuft …“); oder kein Beginn-Haken, wenn für das Behalten kein Widerrufsrecht besteht (Kunde ist Unternehmer, Leistung sofort).
### ZQ3: Beginn-Haken der bezahlten Bestellung: Wertersatz-Satz und Fassung
**Frage.** Der Haken `beginn` der Bestellung für 30 € trug zunächst nur das Zitat zum Erlöschen des Widerrufsrechts. Nach der Abschlussprüfung ist der Satz zum Wertersatz ergänzt, wörtlich wie in den Haken der News-Pinnwand und des Terminplaners (TP2 Q3 und Q5). Er lautet jetzt: „Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit dem Einbau beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit vollständiger Erbringung der Leistung erlischt (§ 356 Abs. 5 Nr. 2 BGB). Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB).“ Die Fassung der Zustimmung ist dafür auf `2026-10-05` gesetzt (die übrigen Funktionen-Unterlagen tragen ebenfalls `2026-10-05`; die Gratis-Zustimmung `consentFrei` und die LB bleiben bei `2026-10-04`, weil ihr Text unverändert ist). Bitte bestätigen: (a) Genügt der Satz in dieser Form, auch mit „der Leistung“ statt „des Einbaus“ im ersten Satzteil (bei der News-Pinnwand steht „des Einbaus“)? (b) Dürfen die Fassungen je Funktion unterschiedlich nummeriert sein, oder soll der Nachweis eine gemeinsame Fassung der Funktionen-Unterlagen tragen? Die LB nennt den Wertersatz weiterhin in Nr. 11 Abs. 2.
**Fundstelle im Text.** Register `features.zeiten.consent` (Fassung `2026-10-05`, Haken `leistung` und `beginn`) und `consentFrei` (Fassung `2026-10-04`); LB Nr. 11 Abs. 2; Widerrufsbelehrung der Funktionen (Hinweise zum Widerruf, Wertersatz als Anteil am Gesamtpreis); Test `test/zeitenRegister.test.js` pinnt den Wortlaut und prüft, dass der Satz mit dem der News-Pinnwand und des Terminplaners übereinstimmt.
**Unsere Annahme.** Der Wertersatz-Satz gehört wie beim Terminplaner in den Haken (Frage Q3 des Terminplaners gilt entsprechend) und ist jetzt enthalten; der Wortlaut bleibt Entwurf bis zur Freigabe, danach wird die Fassung auf den Tag der Freigabe gesetzt (Pins in `test/zeitenRegister.test.js` mitziehen).
**Alternative.** Den Satz wieder aus dem Haken nehmen und nur in der Belehrung und der LB lassen; eine gemeinsame Fassung für alle Funktionen.
### ZQ4: Haftung, ausgeschrieben für Kunden ohne Abo (LB Nr. 8)
**Frage.** Die LB schreibt die Haftung aus, statt auf § 13 der Abo-Bedingungen zu verweisen, weil Kunden ohne Wartungs-Abo die Abo-Bedingungen nicht haben. Nr. 8 Abs. 4 und 5 geben § 13 Abs. 1 und 2 wörtlich wieder, Abs. 6 den Satz zugunsten der Mitarbeiter und Beauftragten (§ 13 Abs. 5) und den Hinweis, dass der Abschnitt auch für Kunden ohne Abo gilt. Abs. 1 bis 3 sagen: Die Live-Anzeige ist ein Hilfsmittel, die Richtigkeit der Angaben liegt beim Kunden, Fehler in Feiertagsdaten und Softwarefehler beseitigt WEBklar nach Meldung, für entgangenen Gewinn und Folgeschäden (etwa weil ein Besucher wegen einer Fehlanzeige vor verschlossener Tür steht) gelten Abs. 4 und 5. Fragen: (a) Ist die Klausel gegenüber Verbrauchern mit § 309 Nr. 7 BGB vereinbar und hinreichend transparent? (b) Braucht es gegenüber Unternehmern einen Höchstbetrag? (c) Fehlen § 13 Abs. 3 (Datenverlust, Wiederherstellungsaufwand) und Abs. 4 (kein Schadensersatz nach § 536a Abs. 1 BGB für anfängliche Mängel) für Kunden ohne Abo? (d) Ist die Zusage „Fehler beseitigt WEBklar in angemessener Frist“ (Nr. 7 Abs. 1, Nr. 8 Abs. 2) für Kunden ohne Abo zu weit?
**Fundstelle im Text.** LB Nr. 7 und Nr. 8; Abo-Bedingungen § 13 (`public/abo-bedingungen.html`); der Test `test/zeitenRecht.test.js` hält den Wortlaut beider Klauseln gleich; TP2 Q8 (Haftung für verpasste Termine).
**Unsere Annahme.** Die Spiegelung von § 13 Abs. 1 und 2 genügt; ein Höchstbetrag ist nicht nötig, weil Abs. 2 den Schaden auf den vertragstypischen, vorhersehbaren begrenzt. Die Abs. 3 und 4 sind für diese Funktion nicht nötig (die Funktion speichert keine Daten Dritter; ein Schadensersatz für anfängliche Mängel ist bei einem Einmalpreis von 30 € kein praktisches Thema).
**Alternative.** Abs. 3 und 4 des § 13 ebenfalls ausschreiben; einen Höchstbetrag (zum Beispiel den Preis der Funktion oder den Jahresbetrag des Wartungs-Abos) gegenüber Unternehmern festlegen.
### ZQ5: Ende mit dem Abo: 30 Tage zum Behalten, Abschalten, Rückkehr (LB Nr. 9)
**Frage.** Wer Öffnungszeiten live im Wartungs-Abo ohne Aufpreis eingeschaltet hat, behält die Funktion nach dem Ende des Abos noch 30 Tage; in dieser Zeit kann er sie für einmalig 30 € behalten, sonst schaltet WEBklar sie ab (auf der Website bleibt die feste Ersatz-Tabelle), die Eingaben bleiben noch weitere 30 Tage und werden dann gelöscht; beginnt in dieser Zeit wieder ein Wartungs-Abo, läuft die Funktion mit den Eingaben weiter. Wer sie für 30 € bestellt hat, behält sie auch ohne Abo. Fragen: (a) Ist die Kopplung der Gratis-Funktion an das Abo mit dieser Frist zulässig, obwohl sie ein „freiwilliges Extra“ ist und die Abo-Bedingungen unverändert bleiben (Z19)? (b) Darf die Rückkehr bei neuem Abo ohne neue Zustimmung automatisch erfolgen? (c) Frist und Anker der Löschung (30 Tage nach dem Abschalten) und der Sicherungsüberhang von sieben Tagen. (d) Bei Abschaltung durch WEBklar auf Wunsch des Kunden oder aus anderem Grund als dem Ende des Abos gibt es im Programm keine automatische Löschung; die LB sagt „auf Verlangen, spätestens mit dem Löschen des Kundenkontos“ (Nr. 9 Abs. 5). Genügt das, oder braucht es eine feste Frist? (e) Gehört das einmalige Aktualisieren der Ersatz-Tabelle beim Abschalten (Nr. 9 Abs. 4) zu einer Leistung, die WEBklar auch nach dem Ende des Abos schuldet?
**Fundstelle im Text.** LB Nr. 9 Abs. 1 bis 5, Nr. 4 Abs. 3; Abschnitt 6e (Löschfristen) und Offener Punkt 2; K02/K03 (`zeiten-k02-k03-entwurf.md`); Programm: `server/services/zeiten/aboAbgleich.js` (30 Tage Frist zum Behalten, 30 Tage Datenfrist), `abschalten.js`; Spec Z11 und Abschnitt 5; Parallele: TP2 Q7 und Q9 (Terminplaner endet mit dem Abo).
**Unsere Annahme.** Ja zu (a) bis (c): Der Kunde hat die Funktion nur durch das Abo ohne Aufpreis; die Frist zum Behalten und die Datenfrist sind angemessen, die Rückkehr stellt den früheren Stand wieder her. Zu (d) genügt „auf Verlangen“, weil die Eingaben Betriebsdaten sind und der Kunde die Löschung jederzeit verlangen kann. Zu (e) ja, weil es zum Einbau gehört, den der Preis enthält (Z20).
**Alternative.** Löschung auch bei Abschaltung durch das Team nach 30 Tagen (das Programm müsste dafür angepasst werden); keine Frist zum Behalten (Abschaltung mit dem Ende des Abos); Rückkehr nur auf Antrag.
### ZQ6: Mails zur Funktion: Erinnerung vor Feiertagen und Zustandsmails
**Frage.** WEBklar sendet dem Kunden E-Mails: K27 (eingeschaltet), K28 (Erinnerung 1 bis 7 Tage vor einem Feiertag oder einem Prüftag: „Stimmen Ihre Zeiten an diesen Tagen?“, mit einem Knopf zum Editor und dem Hinweis, die Zeiten auch bei Google einzutragen), K29 (Wartungs-Abo beendet, Frist, „für einmalig 30 € behalten“), K30 (abgeschaltet). Sind das Vertragsmails (Art. 6 Abs. 1 lit. b DSGVO) ohne Einwilligung, oder wirkt K28 wegen des Hinweises „bei Google auch eintragen“ oder des Verweises auf die Funktion als Werbung (§ 7 UWG, Art. 6 Abs. 1 lit. f DSGVO mit Widerspruchsmöglichkeit)? Muss K28 einen Hinweis zum Abbestellen tragen? Die Erinnerung lässt sich im Programm nicht abschalten; der Kunde kann nur die Funktion abschalten lassen.
**Fundstelle im Text.** Abschnitt 6e Absatz 3; LB Nr. 2 Abs. 2; Mailvorlagen K27 bis K30 (`server/mail-templates/`), Spec Abschnitt 7 (K28: eine Mail für alle neuen Tage, Merker, 08:00 bis 20:00 Uhr Berliner Zeit, kein BCC ans Team).
**Unsere Annahme.** Die Erinnerung ist Teil der vertraglichen Leistung (LB Nr. 2 Abs. 2) und deshalb eine Vertragsmail ohne Einwilligung; der Hinweis auf Google dient der Durchführung (die Zeiten dort aktuell zu halten), nicht der Werbung.
**Alternative.** Hinweis auf Google aus K28 streichen; einen Abbestell-Link („Erinnerungen ausschalten“) mit Einstellung im Editor vorsehen.
### ZQ7: Einstellung ohne Grund bei Unternehmern im Gratisweg (LB Nr. 10 Abs. 4)
**Frage.** Die LB folgt in Nr. 10 der Einstellungsregel der Website-Analyse (Nr. 8 dort): Beenden oder Einstellen mit vier Wochen oder drei Monaten nur bei Rechtslage oder nicht behebbarer Sicherheitslücke (Abs. 2), zusätzlich nach Abs. 4 **ohne** besonderen Grund mit drei Monaten gegenüber Unternehmern, die die Funktion im Wartungs-Abo ohne Aufpreis haben; der Kunde kann das Abo dann innerhalb von 30 Tagen zum Einstellungszeitpunkt kündigen, auch vor Ablauf der Mindestlaufzeit. Wer die Funktion für 30 € bestellt hat, kann sie nur aus den Gründen von Abs. 2 verlieren (Abs. 5). Fragen: (a) Ist die Einstellung ohne Grund für ein kostenloses Zusatzangebot zulässig, auch wenn die Funktion im Abo „enthalten“ beworben wird (Z4)? (b) Ändert das Sonderkündigungsrecht in Abs. 4 die Abo-Bedingungen so, dass das Verfahren nach § 15 nötig wäre (Z19: freiwilliges Extra, keine Änderung der Abo-Bedingungen)? (c) Braucht es bei Einstellung einer gekauften Funktion durch WEBklar eine (anteilige) Erstattung des Preises (vgl. TP2 Q8)?
**Fundstelle im Text.** LB Nr. 10 Abs. 1 bis 5; Abo-Bedingungen §§ 3 Abs. 7, 8, 15; LB der Website-Analyse Nr. 8 (`docs/rechtliches/leistungsbeschreibung-website-analyse.md`); Spec Abschnitt 9 Nr. 1 („Einstellung/Änderung nach Vorbild Analyse Nr. 8“).
**Unsere Annahme.** Ja zu (a) und (b) wie bei der Analyse: Das Sonderkündigungsrecht stellt den Kunden besser; die Abo-Bedingungen bleiben unverändert. Zu (c): Bei Einstellung aus den Gründen von Abs. 2 erstattet WEBklar nichts, weil die Funktion bis dahin lief; die Rechte aus § 327r BGB bleiben.
**Alternative.** Abs. 4 streichen (Einstellung nur aus Gründen); bei gekauften Funktionen anteilig erstatten.
### ZQ8: Website-Absatz: WhatsApp und Google Maps erst nach Klick, Haken als Nachweis
**Frage.** (a) Der Absatz „Öffnungszeiten auf dieser Website“ (Variante A und B) sagt, dass die Knöpfe „Route“ und „WhatsApp“ einfache Links sind, dass Google Maps und WhatsApp erst nach dem Klick geöffnet werden und dass vorher nichts an Google oder Meta übertragen wird; das Skript setzt keine Cookies und speichert nichts im Browser. Genügt das, oder braucht es für diese Links eine Einwilligung (§ 25 TDDDG, Art. 6 Abs. 1 lit. a DSGVO)? (b) Der Kunde bestätigt im Portal mit einem Haken, dass er den „WhatsApp-Hinweis“ (Baustein WhatsApp) in seine Datenschutzerklärung aufgenommen hat; der Zeitpunkt wird gespeichert, Mitarbeiter setzen den Haken in der Kundenansicht nicht. Reicht der Haken als Nachweis? (c) Wenn WEBklar den Absatz beim Einbau in die Datenschutzerklärung einfügt (LB Nr. 6 Abs. 4), ist das Einfügen eines Mustertextes eine Rechtsdienstleistung (§ 2 RDG)? Das ist dieselbe Regel wie TP2 Q18 und soll für alle Funktionen gleich bleiben. (d) Die Angaben im Seitencode (strukturierte Daten zu den Öffnungszeiten) enthalten keine Daten von Besuchern; ist ein Satz dazu nötig?
**Fundstelle im Text.** `docs/funktionen/website-datenschutz-absatz.md` (Variante A und B, Bausteine Route und WhatsApp); LB Nr. 2 Abs. 4, Nr. 6 Abs. 3 und 4; Abschnitt 6e Absatz 4; Programm: `public/embed/zeiten.js` (keine Cookies, kein Storage, keine Anfragen an Dritte; Links erst beim Klick), Spec Z23 (Route öffnet Google Maps erst beim Klick).
**Unsere Annahme.** (a) Ein Link, den der Besucher anklickt, ist keine Übertragung durch WEBklar oder den Kunden vor dem Klick; eine Einwilligung ist nicht nötig. (b) Der Haken genügt als Nachweis. (c) Wie TP2 Q18: Technik und Mustertext, keine Rechtsdienstleistung im Einzelfall. (d) Ein Satz genügt, wie im Entwurf.
**Alternative.** Vor dem ersten Klick einen Hinweis einblenden („Sie verlassen diese Website“); die Schaltfläche „WhatsApp“ nur auf ausdrücklichen Wunsch einschalten.
### ZQ9: Vertragsrahmen ohne Abo (LB Nr. 1 Abs. 3)
**Frage.** Für Kunden ohne Wartungs-Abo gilt die LB „allein“. Sie regelt Leistung, Preis, Haftung, Ende, Einstellung und Widerruf, verweist für den Widerruf auf die Belehrung der Funktionen und für Datenschutz auf die Datenschutzerklärung. Fehlen Regeln, die sonst die Abo-Bedingungen liefern (Zahlung und Zahlungsverzug, Mängelrechte für digitale Dienstleistungen §§ 327 ff. BGB, Verjährung, Gerichtsstand, Textform, Änderungen der Bedingungen)? Sollen die Abo-Bedingungen auch für Kunden ohne Abo teilweise vereinbart werden (Verweis auf bestimmte Paragrafen), oder reichen die gesetzlichen Regeln?
**Fundstelle im Text.** LB Nr. 1 Abs. 3, Nr. 4 (Zahlung über Stripe), Nr. 7 Abs. 1 (Mängel in angemessener Frist), Nr. 8 (Haftung ausgeschrieben); Spec Abschnitt 9 Nr. 1 und Z19.
**Unsere Annahme.** Die gesetzlichen Regeln genügen für eine Einmalleistung von 30 €; die Haftung ist wegen der AGB-Kontrolle ausgeschrieben.
**Alternative.** Ausgewählte Paragrafen der Abo-Bedingungen (Zahlung, Mängel, Textform, Änderungen) in die LB aufnehmen oder auf sie verweisen.
### ZQ10: Hinweis zu Preisangaben in der Hinweis-Leiste
**Frage.** Der Editor zeigt beim Anlegen eines Hinweises, dessen Text „%“ oder „statt“ enthält, einen Hinweis zu Preisangaben (Endpreise, bei Waren der niedrigste Preis der letzten 30 Tage). LB Nr. 6 Abs. 2 verpflichtet den Kunden, Preise und Rabatte in Hinweisen rechtmäßig anzugeben, zum Beispiel als Endpreise und „bei Preisermäßigungen für Waren, mit dem niedrigsten Preis der letzten 30 Tage“; Nr. 12 Abs. 1 schließt eine Rechtsberatung aus. Sind diese Aussagen zutreffend und ausreichend, insbesondere die Beschränkung der 30-Tage-Regel auf Waren (Preisangabenverordnung) und die Abgrenzung, dass die Angaben des Kunden nicht von WEBklar geprüft werden (Nr. 8 Abs. 1)? Braucht es einen Hinweis zur Preisangabe auch für Leistungen, die Dienstleistungen sind?
**Fundstelle im Text.** LB Nr. 6 Abs. 2, Nr. 8 Abs. 1, Nr. 12 Abs. 1; Spec Abschnitt 3.4 Nr. 4 (Hinweis-Leiste); die genaue Zeile im Portal-Editor steht in `public/zeiten-leisten.js`.
**Unsere Annahme.** Der Hinweis im Editor und die Pflicht in der LB reichen als Warnung; die Prüfung bleibt beim Kunden. Die Zitate zur Preisangabenverordnung werden mit der Freigabe geprüft.
**Alternative.** Keinen Hinweis im Editor, nur die Pflicht in der LB; oder den Hinweis auf Preisermäßigungen insgesamt ausweiten.
### ZQ11: Aktivierung durch das Team ohne Zustimmung des Kunden
**Frage.** Das Team kann Öffnungszeiten live im WOMS für einen Kunden einschalten (Aktion „Aktivieren“, auch aus dem Zustand „abgeschaltet“). Das Programm speichert dabei keinen Zustimmungsnachweis (`einschaltenZeiten` mit `by: 'team'` ohne `nachweis`; Spec Abschnitt 5 und Ruling R32 lassen die Zustimmung für diesen Weg entfallen). Die LB setzt aber voraus, dass der Kunde den Haken setzt und auf „Einschalten“ oder „Zahlungspflichtig bestellen“ klickt (Nr. 1 Abs. 1). Beim Terminplaner verlangt TP2 für die Aktivierung durch das Team die Zustimmung des Kunden in Textform (F13a, `textformFuerAktivierung` in `server/routes/admin/funktionenAdmin.js` auf dem Zweig `feature/terminplaner-tp2`, Zeile 99: `if (key !== 'termine') return null`; die Prüfung gilt also nur für `termine`, und dort nur im Modus „on“, nicht für Pilotadressen). Soll diese Regel auf `zeiten` ausgeweitet werden (Kunde bestätigt in Textform, das Team vermerkt es), oder genügt es, dass die Funktion im Abo ohne Aufpreis ein freiwilliges Zusatzangebot ist und der Kunde die Aktivierung durch das Team ausdrücklich wünscht (Chat, Telefon)? Gilt das auch, wenn das Team ein Dokument wieder einschaltet, das der Kunde einmal gekauft hat?
**Fundstelle im Text.** LB Nr. 1 Abs. 1 und 2; Spec Abschnitt 5 (Team-Aktivierung), Ruling R32; Programm: `server/services/zeiten/einschalten.js` (`by: 'team'`), `server/routes/admin/funktionenAdmin.js` (`zeitenAktivieren`); TP2 F13a (Textform bei Aktivierung durch das Team, nur Terminplaner).
**Unsere Annahme.** Für die Gratis-Funktion genügt der ausdrückliche Wunsch des Kunden, den das Team im Feld „Notiz“ vermerkt; ein Zustimmungsnachweis fehlt dann bewusst. Bei einem Kunden ohne Wartungs-Abo wäre die Textform sauberer.
**Alternative.** Die Textform-Prüfung von TP2 auch für `zeiten` verlangen (Aktivieren nur mit Hinweis „Kunde hat in Textform zugestimmt“ und Notiz); die Aktivierung durch das Team nur für Kunden mit laufendem Wartungs-Abo zulassen.
### ZQ12: Pflichtinformationen als Haken und in K15
**Frage.** Beim Terminplaner gibt es neben `beginn` den Haken `information` („Ich habe die Pflichtinformationen zu dieser Bestellung gelesen: Leistung …, Gesamtpreis …, voraussichtliche Dauer …“; `server/config.js`, `features.termine.consent`, TP2 Q20), und K15 (Bestätigung der Bestellung) trägt die Pflichtinformationen als Anhang. Für Öffnungszeiten live gibt es beides nicht: Das Register hat nur die Haken `leistung` und `beginn`, und K15 trägt für diese Funktion keine Vertragsunterlagen (der Block `anlagen` mit Fassung, Links und PDF-Anhang entfällt, weil das Register weder `avv` noch `widerruf` hat und `unterlagenAdressen` in `server/services/siteFeatures.js` dann `null` liefert; die Mail nennt nur Funktion, Preis und Dauer im Text). Genügt das für einen Verbrauchervertrag im elektronischen Geschäftsverkehr (§ 312d BGB, Art. 246a EGBGB, § 312j BGB: Information vor Vertragsschluss, Bestätigung auf einem dauerhaften Datenträger), weil die LB und die Bestellseite die Pflichtinformationen enthalten, oder braucht es wie beim Terminplaner einen eigenen Haken `information` und die Pflichtinformationen als Anhang oder Abschnitt der K15?
**Fundstelle im Text.** Register `features.zeiten.consent` (Haken `leistung`, `beginn`) gegen `features.termine.consent` (Haken `information`); Bestellseite `public/zeiten-bestellen.js` (Preis, Belehrung, Leistungsbeschreibung vor dem Knopf); K15 (`server/mail-templates/K15 Bestellung Funktion.html`, `bestellMail` und `unterlagenAdressen` in `server/services/siteFeatures.js`); LB Nr. 2 bis 4 und 11; TP2 Q20.
**Unsere Annahme.** Die Bestellseite zeigt Leistung, Preis, Umsatzsteuerhinweis, Widerrufsbelehrung und LB-Link vor dem Knopf; das genügt für die vorvertragliche Information. Ob die Bestätigungsmail (K15) die Pflichtinformationen und die Belehrung zusätzlich tragen muss (Textform auf einem dauerhaften Datenträger), ist offen; für den Terminplaner hat TP2 es mit dem Block `anlagen` gelöst.
**Alternative.** Wie beim Terminplaner: Haken `information` im Register (Fassung hochzählen) und die Pflichtinformationen als Abschnitt in K15.
## Nicht Teil der Kanzleiprüfung
- Der Hinweis in der Hinweis-Leiste („Aktion“ verlangt ein Enddatum, Texte höchstens 140 Zeichen) ist Oberfläche, keine Rechtsaussage.
- Technische Beobachtung beim Schreiben der Texte (kein Kanzlei-Thema, inzwischen behoben): Die öffentliche Widerrufsseite `widerrufen.html` kannte die Auswahl „Terminplaner (Einrichtung)“, aber keine „Öffnungszeiten live“; ein Verbraucher, der sie für 30 € bestellt hat, hätte „Funktion (z. B. News-Pinnwand)“ wählen müssen, und die Abwicklung hätte das der News-Pinnwand zugeordnet. Die Auswahl „Öffnungszeiten live“ gibt es jetzt (`ORDERS`, `FEATURE_OF_ORDER` und `orderLabel` in `server/routes/abo.js`, Radioknopf mit `data-text` in `public/widerrufen.html`).
File diff suppressed because it is too large. Load diff
Loaded 100 of 1246 files, more files were not shown because too many files have changed in this diff. Show more