Ö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>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 09:16:42 +00:00
1 parent aa9ee35ab6
commit ea4457e6a4
4 files changed
+84 -40

No files matched your search

+4 -3
View File
@@ -23,10 +23,10 @@ Der Einbau ist **ein Einbau je Website** und ist im Preis (30 € oder im Wartun
```
- 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"`; 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"`).
- `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.
- 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
@@ -45,6 +45,7 @@ Ein Platzhalter ist ein Element mit dem Attribut `data-webklar-zeiten`. Innen st
| `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.
@@ -100,7 +101,7 @@ Zur Abnahme außerdem: Seite in der Vorschau öffnen und mit ausgeschaltetem Jav
`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 neu starten. 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.
**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.
@@ -37,14 +37,14 @@ Abschnitt 7 nennt heute die Funktionen und den Klick auf „Zahlungspflichtig be
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, IP-Adresse und Browserkennung sowie Ihr Konto und die angemeldete Person.
> 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. Ob beim Einschalten ebenfalls IP-Adresse und Browserkennung gespeichert werden, ist vor der Übernahme gegen `speichereZustimmung` zu prüfen und der Satz entsprechend zu fassen.
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): Umfang der gespeicherten Daten gegen den Code abgleichen.
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 6d, zweiter Absatz). Das ist die Entscheidung des Kunden; die Leistungsbeschreibung (Nr. 6 Abs. 3) verlangt, nur veröffentlichungsfähige Angaben einzutragen.
+28 -8
View File
@@ -2,7 +2,7 @@
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 `ZQ10`. Beim Zusammenführen: als `Q27` bis `Q36` 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.
**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-6d-entwurf.md` (Abschnitt 6d), `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`).
@@ -20,7 +20,7 @@ Reihenfolge der Rechtsrunde (gemeinsam mit TP2): gemeinsame Bausteine (TP2 Block
### 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).“ 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)?
**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.
@@ -28,15 +28,15 @@ Reihenfolge der Rechtsrunde (gemeinsam mit TP2): gemeinsame Bausteine (TP2 Block
**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 und Fassung
### ZQ3: Beginn-Haken der bezahlten Bestellung: Wertersatz-Satz und Fassung
**Frage.** Der Haken `beginn` der Bestellung für 30 € lautet: „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).“ Er enthält nicht den Satz zum Wertersatz, den die Haken der News-Pinnwand und des Terminplaners tragen („Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB).“, TP2 Q3 und Q5). Die LB nennt den Wertersatz in Nr. 11 Abs. 2. Genügt das, oder muss der Satz in den Haken? Und die Fassung der Zustimmung (`2026-10-04`) weicht von der Fassung der übrigen Funktionen-Unterlagen (`2026-10-05`) ab: Ist das für den Nachweis unschädlich?
**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-04`, Haken `leistung` und `beginn`) und `consentFrei`; LB Nr. 11 Abs. 2; Widerrufsbelehrung der Funktionen (Hinweise zum Widerruf, Wertersatz als Anteil am Gesamtpreis); Test `test/zeitenRegister.test.js` pinnt den heutigen Wortlaut.
**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); der Haken wird vor dem Livegang um ihn ergänzt und die Fassung auf den Tag der Freigabe gesetzt.
**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 nur in der Belehrung und der LB lassen (Haken unverändert).
**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)
@@ -108,7 +108,27 @@ Reihenfolge der Rechtsrunde (gemeinsam mit TP2): gemeinsame Bausteine (TP2 Block
**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): Die öffentliche Widerrufsseite `widerrufen.html` kennt die Auswahl „Terminplaner (Einrichtung)“, aber keine „Öffnungszeiten live“; ein Verbraucher, der sie für 30 € bestellt hat, wählt dort heute „Funktion (z. B. News-Pinnwand)“, und die Abwicklung ordnet das der News-Pinnwand zu (`FEATURE_OF_ORDER` in `server/routes/abo.js`). Das ist vor dem Livegang im Code zu ergänzen.
- 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`).
@@ -6,37 +6,52 @@ Stand 05.10.2026, für Kenso. Grundlage: Spec Abschnitt 13.6, das Protokoll der
| Repo | Worktree / Branch | Stand | Basis und Abstand |
|---|---|---|---|
| Portal (+ Runner) | `dev-zeiten`, `feature/oeffnungszeiten` | Inhalt von `90e8607` plus diese Notiz | enthält TP2 bis `a629b25`; TP2 steht inzwischen auf `af63b69` (34 Commits weiter) |
| WOMS | `ticket.webklar.com/dev-zeiten`, `feature/oeffnungszeiten` | `3d67aa8` | auf WOMS-TP2 `72eedeb`; TP2 steht inzwischen auf `4c3f67e` (5 Commits weiter) |
| Portal (+ Runner) | `dev-zeiten`, `feature/oeffnungszeiten` | Inhalt von `90e8607` plus diese Notiz und die Fixrunde nach der Abschlussprüfung (`ecf67d6` Server, `aa9ee35` Skript und Editor, danach die Doku) | enthält TP2 bis `a629b25`; TP2 steht inzwischen auf `af63b69` (34 Commits weiter) |
| WOMS | `ticket.webklar.com/dev-zeiten`, `feature/oeffnungszeiten` | `919d96f` (Fixrunde: Statustext der Einrichtung) auf `3d67aa8` | auf WOMS-TP2 `72eedeb`; TP2 steht inzwischen auf `4c3f67e` (5 Commits weiter) |
| Relaunch | `webklar.com/dev-zeiten-station`, `feature/zeiten-station` | `8f5d522` | auf `main` `4139f31` (kein Abstand, Stand 05.10.) |
Letzte Prüfung 05.10.2026, alles grün: Portal 4544/4545 (der eine Fehler war der bekannte wacklige `previewDevRouter`-Test, einzeln 12/12), Portal unter Node 20 (wie in Produktion) 4543/4543 ohne `analyticsContract`, Runner 177/177, UI-Prüfungen 31/31 + 8/8 + alle OK, WOMS 377/377 + `vite build`, Relaunch 349/349 + Build (Haupt-JS 128 084 Byte gz, Grenze 133 120).
Letzte Prüfung vor der Fixrunde 05.10.2026, alles grün: Portal 4544/4545 (der eine Fehler war der bekannte wacklige `previewDevRouter`-Test, einzeln 12/12), Portal unter Node 20 (wie in Produktion) 4543/4543 ohne `analyticsContract`, Runner 177/177, UI-Prüfungen 31/31 + 8/8 + alle OK, WOMS 377/377 + `vite build`, Relaunch 349/349 + Build (Haupt-JS 128 084 Byte gz, Grenze 133 120).
Nach der Fixrunde (Abschlussprüfung, 05.10.2026), alles grün: Portal 4564/4564 (`npm test`), Runner 177/177, UI-Prüfungen Skript 34/34 und Portal 9/9, WOMS 378/378 + `vite build`. Der Node-20-Lauf (Schritt A.2) und die Relaunch-Prüfung (A.4) stehen für nach der TP2-Zusammenführung an, sie laufen ohnehin neu.
## A. Reihenfolge am Livegang-Tag
1. **TP2 geht zuerst live** (Portal und WOMS) nach seiner eigenen Reihenfolge (Kanzlei-Paket Abschnitt 7.6, mit `termine-setup.mjs` und `feature-setup.mjs`). Öffnungszeiten live baut auf TP2 auf und braucht dessen Schema (`aboEndeMailAt`, `disabledReason`, `featureConsents`).
2. **Portal neu aufsetzen.** In `dev-zeiten`: das jetzt live gegangene TP2 einmal hineinführen (`git merge main`; ist TP2 noch nicht in `main`: `git merge feature/terminplaner-tp2`). Es gibt einen Merge statt Rebase, damit die Commit-Nummern des Protokolls gültig bleiben. Bei der ersten Zusammenführung (`1d5a565`) überlappten 14 Dateien, das gleiche ist wieder zu erwarten (`server/config.js`, `siteFeatures.js`, `routes/funktionen.js`, `public/funktionen.js` …). Danach alles erneut laufen lassen:
2. **Portal neu aufsetzen.** In `dev-zeiten`: das jetzt live gegangene TP2 einmal hineinführen, mit `git fetch origin && git merge origin/main`. **Nicht** `git merge main`: die lokale Ref `main` steht noch auf `f1b43ca` (vor TP2), das gäbe „Already up to date“, und der Push würde abgelehnt. Ist TP2 noch nicht in `origin/main`: `git merge feature/terminplaner-tp2`. Es gibt einen Merge statt Rebase, damit die Commit-Nummern des Protokolls gültig bleiben. Eine Probe-Zusammenführung mit TP2 `af63b69` (`git merge-tree --write-tree`) ergab im Portal drei Textkonflikte, die so zu lösen sind (bei der ersten Zusammenführung `1d5a565` überlappten 14 Dateien):
- `server/routes/admin/funktionenAdmin.js`: unser Zeiten-Zweig (`zeitenAktivieren`, `zeitenAbschalten`, `ersatzTicket` in der Antwort) **und** TP2s Textform-/K16-Warnung (`warnung`) bleiben beide; die Antwort trägt beides.
- `server/services/retention.js`: unser `zeitenIds` (Zeiten vor dem Abschalten des Kunden löschen) **und** TP2s `consentIds`/`featureFieldIds` bleiben beide. TP2 `9a2a70b` („Fehlschlag hält den Kunden für den nächsten Lauf“) löst vermutlich die Retention-Hälfte von N9 (eine fehlgeschlagene `loescheZeitenVonKunde` wird nicht wiederholt): bei der Konfliktlösung prüfen, ob `loescheZeitenVonKunde` bei jedem Lauf aufgerufen wird, und nicht vorher separat patchen.
- `server/services/siteFeatures.js`: Textkonflikt, beide Seiten behalten. TP2 `b974b3a` legt `disable()` unter die Sperre `feature-activate:<id>` (Sperren sind nicht wiederbetretbar): nach der Zusammenführung prüfen, dass **kein Halter von `feature-activate` `disable()` aufruft** (`aboAbgleich` und `zeitenAbschalten` rufen es unter `feature-admin` auf: erlaubte Reihenfolge).
Danach alles erneut laufen lassen:
- `nice npm test` (nur `previewDevRouter` darf wackeln, dann einzeln wiederholen), `cd runner && nice npm test`
- Node 20: `docker run --rm --cpus 3 -v /home/webklar/apps/kundenbereich.webklar.com/dev-zeiten:/app:ro -w /app node:20 sh -c 'node --test --test-concurrency=3 $(ls test/*.test.js | grep -v analyticsContract)'`
- UI, je einzeln mit `nice` und vorher `free -m`: `test/ui/zeiten-embed.ui.mjs`, `test/ui/zeiten-portal.ui.mjs`, `test/ui/funktionen.ui.mjs`
3. **WOMS neu aufsetzen.** In `ticket.webklar.com/dev-zeiten`: die WOMS-Hauptlinie (`test`, mit TP2) hineinführen, dann `nice npm test` und `nice npx vite build`. `node_modules` ist dort im Git verfolgt und wurde von `npm ci` verändert: **nie stagen**.
3. **WOMS neu aufsetzen.** In `ticket.webklar.com/dev-zeiten`: die WOMS-Hauptlinie (`test`, mit TP2) hineinführen (`git fetch origin && git merge origin/test`). Erwarteter Konflikt (1 Datei): `src/components/customer/CustomerFunktionen.jsx`; dort bleiben unser `ersatzTicket` **und** TP2s `speicherStatus`/`K16_WARNUNG` (TP2 `4c3f67e` ändert dieselben `save()`-Zeilen sowie das Ende von `src/lib/funktionenView.js` und seinen Test, das Portal fügt dieselbe POST-Route um `warnung` ein). `K16_WARNUNG` nennt einen „AVV“, den Öffnungszeiten live nicht hat: nach der Zusammenführung prüfen und für die Zeiten anpassen. Danach `nice npm test` und `nice npx vite build`. `node_modules` ist dort im Git verfolgt und wurde von `npm ci` verändert: **nie stagen**.
4. **Relaunch prüfen.** `git log --oneline 4139f31..main` im Relaunch-Repo; gibt es neue Commits, `feature/zeiten-station` darauf aufsetzen, dann `nice npm test`, `nice npm run build` und `gzip -9 -c dist/assets/index-*.js | wc -c` (höchstens 133 120).
5. **Schema anlegen, vor dem Portal-Deploy.** `scripts/zeiten-setup.mjs` legt `oeffnungszeiten` und `zeitenErinnerungen` an und ergänzt `portalFeatures` um `unlockedBy`, `aboEndeAt`, `einbau`, `behaltenSessionId`, `k24At`. Wichtig: `activate()` schreibt jetzt `aboEndeAt` und `behaltenSessionId` bei **jeder** Funktion; ohne die Felder scheitert die Team-Aktivierung von News und Terminplaner mit „Unknown attribute“. Das Skript braucht den neuen Code, also aus dem Worktree in einem Wegwerf-Container (kein `docker compose run`: der Container bekäme die Traefik-Labels des Portals). Erst `--check`, dann ohne:
5. **Schema anlegen, vor dem Portal-Deploy.** `scripts/zeiten-setup.mjs` legt `oeffnungszeiten` und `zeitenErinnerungen` an und ergänzt `portalFeatures` um `unlockedBy`, `aboEndeAt`, `einbau`, `behaltenSessionId`, `k24At`. Seit der Fixrunde schreibt `activate()` `aboEndeAt` und `behaltenSessionId` nur noch für Öffnungszeiten live (Parameter `extra`); News und Terminplaner brauchen die Felder nicht mehr und laufen auch vor dem Skript. Trotzdem erst das Schema, dann der Deploy: die Zeiten-Pfade schreiben die Felder. Das Skript braucht den neuen Code, also aus dem Worktree in einem Wegwerf-Container (kein `docker compose run`: der Container bekäme die Traefik-Labels des Portals). Die Env-Datei enthält Zugangsdaten: mit `umask 077` anlegen, nicht erst danach `chmod`. Erst `--check` (beim ersten Lauf Exit 1, erwartet: es fehlt ja noch alles), dann ohne, dann **noch einmal `--check`** (muss grün sein; die Index-Reihenfolge der Schema-Skripte hat am 30.09. schon einmal überrascht):
```
docker inspect webklar-kundenbereich-server -f '{{range .Config.Env}}{{println .}}{{end}}' > /root/zeiten-env.tmp && chmod 600 /root/zeiten-env.tmp
(umask 077; docker inspect webklar-kundenbereich-server -f '{{range .Config.Env}}{{println .}}{{end}}' > /root/zeiten-env.tmp)
docker run --rm --env-file /root/zeiten-env.tmp --network webklar-network -v /home/webklar/apps/kundenbereich.webklar.com/dev-zeiten:/app:ro -w /app node:20-alpine node scripts/zeiten-setup.mjs --check
docker run --rm --env-file /root/zeiten-env.tmp --network webklar-network -v /home/webklar/apps/kundenbereich.webklar.com/dev-zeiten:/app:ro -w /app node:20-alpine node scripts/zeiten-setup.mjs
docker run --rm --env-file /root/zeiten-env.tmp --network webklar-network -v /home/webklar/apps/kundenbereich.webklar.com/dev-zeiten:/app:ro -w /app node:20-alpine node scripts/zeiten-setup.mjs --check
rm /root/zeiten-env.tmp
```
(Der Aufruf ist so noch nie gegen die Produktion gelaufen; er folgt dem Muster der früheren Schema-Skripte. Das Skript ist idempotent, `--check` schreibt nichts.)
6. **Stripe prüfen.** `node scripts/stripe-products.mjs --pruefen` (gleicher Wegwerf-Container). Erwartet wird ein Preis `webklar_zeiten_einrichtung_3000` (30 €, einmalig, `tax_behavior` inclusive, Standard-Produktbild). Fehlt er: `--anlegen`; danach `--pruefen` nochmal.
7. **Einstellungen.** In `/home/webklar/apps/.env`: `ZEITEN_ENABLED=pilot`, `ZEITEN_PILOT_EMAILS=kenso.gri@gmail.com`, `ZEITEN_KEIN_NEUBAU=emmas-schatzkiste`. In `/home/webklar/apps/docker-compose.yml`, Dienst `kundenbereich-server`, drei Zeilen neben `TERMINE_ENABLED`: `- ZEITEN_ENABLED=${ZEITEN_ENABLED:-off}`, `- ZEITEN_PILOT_EMAILS=${ZEITEN_PILOT_EMAILS:-}`, `- ZEITEN_KEIN_NEUBAU=${ZEITEN_KEIN_NEUBAU:-emmas-schatzkiste}`. Neue Umgebungswerte gelten erst nach `docker compose up -d kundenbereich-server`, ein bloßes `docker restart` liest sie nicht (nach dem Deploy in Schritt 9 ausführen).
8. **Runner: kein Image-Bau nötig.** Der Runner ist `node:20-alpine` mit eingehängtem `build/runner` (das Protokoll sagt „Image neu bauen“, das stimmt für die echte Konfiguration nicht). `deploy.sh` startet ihn neu, wenn sich `runner/` ändert und die Warteschlange leer ist. `ZEITEN_PUBLIC_BASE` nicht setzen: die Vorgabe `https://project.webklar.com/embed/` stimmt. Im Deploy-Log nach „Preview-Runner: runner/ geaendert, starte neu…“ suchen; steht dort „Neustart uebersprungen“, später von Hand `docker restart webklar-preview-runner` (ein alter Runner verwirft das neue Feld `zeiten` stillschweigend, der Einbau meldete dann „angestoßen“, setzt aber nichts ein).
9. **Pushen, in dieser Reihenfolge** (du pushst, ich nicht):
1. Portal samt Runner (`main` des Portal-Repos).
2. WOMS erst **nach** dem Portal, sonst antwortet das Portal auf die neuen Zeiten-Aktionen mit 404. Der WOMS-Webhook liefert jeden gepushten Branch aus: `feature/oeffnungszeiten` also nie allein pushen, solange das Portal nicht live ist. Danach `node scripts/setup-legal.mjs` im WOMS (legt die Rechtliches-Karte `leistungsbeschreibung-zeiten` an, Schlüssel ≤ 36 Zeichen).
3. Relaunch (`main`). Die Station „Öffnungszeiten live“ steht dort auf `bald: true` („Bald verfügbar“) und bleibt so bis Schritt C.
10. **Pilot-Probelauf** (Abschnitt B), danach Rechtsrunde abschließen (Abschnitt D), dann Abschnitt C.
7. **Einstellungen, vor dem Push.** Zuerst Sicherungen: `cp /home/webklar/apps/docker-compose.yml /home/webklar/apps/docker-compose.yml.bak-2026-10-05-zeiten` und `(umask 077; cp /home/webklar/apps/.env /home/webklar/apps/.env.bak-2026-10-05-zeiten)` (`.env` enthält Zugangsdaten). Dann in `/home/webklar/apps/.env`: `ZEITEN_ENABLED=pilot`, `ZEITEN_PILOT_EMAILS=kenso.gri@gmail.com`, `ZEITEN_KEIN_NEUBAU=emmas-schatzkiste`. In `/home/webklar/apps/docker-compose.yml`, Dienst `kundenbereich-server`, drei Zeilen neben `TERMINE_ENABLED`: `- ZEITEN_ENABLED=${ZEITEN_ENABLED:-off}`, `- ZEITEN_PILOT_EMAILS=${ZEITEN_PILOT_EMAILS:-}`, `- ZEITEN_KEIN_NEUBAU=${ZEITEN_KEIN_NEUBAU:-emmas-schatzkiste}`. 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 9): dann startet der Container mit dem alten Code und den neuen Werten (die kennt er nicht, harmlos), und der Neustart durch `deploy.sh` nach dem Push ist der einzige mit dem neuen Code (jeder Containerstart führt `apk add` und `npm install` aus).
8. **Runner: kein Image-Bau nötig.** Der Runner ist `node:20-alpine` mit eingehängtem `build/runner` (das Protokoll sagt „Image neu bauen“, das stimmt für die echte Konfiguration nicht). `deploy.sh` startet ihn neu, **nur wenn sich `runner/` in der Deploy-Spanne ändert** und die Warteschlange leer ist. `ZEITEN_PUBLIC_BASE` nicht setzen: die Vorgabe `https://project.webklar.com/embed/` stimmt. **Nach dem Push prüfen:** `docker inspect -f '{{.State.StartedAt}}' webklar-preview-runner` muss nach dem Push liegen (vorher 2026-10-02T16:17Z); sonst von Hand `docker restart webklar-preview-runner` (das Deploy-Log im Container `webklar-kundenbereich-build` wird täglich um 00:00 rotiert und ist als Beleg unzuverlässig; „naechster Deploy versucht es erneut“ stimmt nicht, ohne Änderung an `runner/` kommt kein Neustart). Ein alter Runner verwirft das neue Feld `zeiten` stillschweigend, der Einbau meldete dann „angestoßen“, setzt aber nichts ein.
9. **Pushen, in dieser Reihenfolge** (du pushst, ich nicht). Die Ziel-Ref steht immer dabei (`<lokaler Branch>:<Ziel>`), damit nichts unter dem falschen Namen ankommt:
1. Portal samt Runner: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-zeiten && git push origin feature/oeffnungszeiten:main`. **Der Portal-Webhook deployt jeden gepushten Branch** (`webhook-server.js` setzt `DEPLOY_BRANCH` aus `payload.ref`, `deploy.sh` macht `reset --hard origin/<branch>`): den Feature-Branch nie unter seinem eigenen Namen pushen (`git push origin feature/oeffnungszeiten`), das würde ihn auf die Produktion bringen.
2. WOMS erst **nach** dem Portal, sonst antwortet das Portal auf die neuen Zeiten-Aktionen mit 404: `cd /home/webklar/apps/ticket.webklar.com/dev-zeiten && git push origin feature/oeffnungszeiten:test`. Der WOMS-Webhook deployt nur `test` (`TICKET_DEPLOY_BRANCH`, Vorgabe `test`; Pushes auf andere Branches ignoriert er); `test` also nie vor dem Portal. Danach `setup-legal.mjs` (legt die Rechtliches-Karte `leistungsbeschreibung-zeiten` an, Schlüssel ≤ 36 Zeichen): **ungetestet gegen die Produktion**, zuerst `node scripts/setup-legal.mjs --dry-run` im WOMS-Worktree, dann `APPWRITE_API_KEY=… APPWRITE_ENDPOINT=… node scripts/setup-legal.mjs`. Der Schlüssel (`APPWRITE_API_KEY`) ist nötig; der Standard-Endpoint des Skripts ist `https://appwrite.webklar.com/v1`, das WOMS-Frontend nutzt in der Vorgabe `https://ticket.webklar.com/v1`: den Endpoint vorher gegen den ausliefernden Container prüfen und ausdrücklich setzen.
3. Relaunch: `cd /home/webklar/apps/webklar.com/dev-zeiten-station && git push origin feature/zeiten-station:main`. Die Station „Öffnungszeiten live“ steht dort auf `bald: true` („Bald verfügbar“) und bleibt so bis Schritt C.
10. **Live-Checks nach dem Deploy** (Portal und WOMS sind da):
- Öffentliche Schnittstelle kennt einen unbekannten Schlüssel nicht: `curl -s https://project.webklar.com/api/public/zeiten/v1/aaaaaaaaaaaaaaaa` antwortet `{"aktiv":false}`.
- Skript: `curl -sI https://project.webklar.com/embed/zeiten.js` ist 200, mit `Access-Control-Allow-Origin` und `Cache-Control`.
- Leistungsbeschreibung: `curl -sI https://project.webklar.com/vertragsunterlagen/zeiten-leistungsbeschreibung.html` ist 200.
- WOMS: Kunde → Reiter „Funktionen“ lädt ohne Fehler (auch für ein Projekt ohne Zeiten).
- Portal-Log: `docker logs --since 15m webklar-kundenbereich-server 2>&1 | grep -i "unknown attribute"` bleibt leer (sonst fehlt ein Schema-Feld, Schritt 5).
- Stripe: Webhook-Adresse und Ereignisliste im Dashboard sind unverändert (die Funktion nutzt dieselben Ereignisse wie News und Terminplaner; es kommt nichts Neues dazu).
- **Rückweg**, falls etwas hakt: `ZEITEN_ENABLED=off` in `.env`, dann `docker compose -f /home/webklar/apps/docker-compose.yml up -d --no-deps kundenbereich-server`. Die Kachel verschwindet, die Schnittstelle antwortet `aktiv: false`, die Platzhalter zeigen ihr Ersatz-HTML, der Abo-Abgleich läuft weiter. Das Schema ist rein additiv (neue Collections und Felder), es gibt nichts zurückzubauen; ein Code-Rollback ist bei `off` nicht nötig.
11. **Pilot-Probelauf** (Abschnitt B), danach Rechtsrunde abschließen (Abschnitt D), dann Abschnitt C.
## B. Probelauf mit `ZEITEN_ENABLED=pilot` auf `test-website`
@@ -48,14 +63,14 @@ Nur dein Konto sieht die Kachel. Beachte: Das Portal prüft nur „Leistungsbesc
4. Preview der `test-website` nach dem Neubau: im Quelltext `<script data-wk-zeiten … data-site="…">`; Hinweis-Leiste oben (schiebt den Header?), Kontakt-Leiste auf dem Handy, JSON-LD im Quelltext (kein Duplikat), Platzhalter mit Status, Tabelle und Badge; Ersatz-HTML aus WOMS („Ersatz-HTML kopieren“) in die Platzhalter. Dazu der Datenschutz-Absatz aus `docs/funktionen/website-datenschutz-absatz.md` (Entwurf) auf der Test-Seite.
5. WOMS: Panel (Zeiten-Einstellungen `navSelektor`, Leisten aus), „Aktivieren ohne Zahlung“ und „Abschalten“ am Testprojekt; Ticket schließen ⇒ nach spätestens 5 Minuten `active` und Mail K16; Abschalten ⇒ Ticket „Ersatz-Tabelle aktualisieren“ (nur wenn der Wochenplan vom Ersatz-HTML abweicht).
6. Mails K24 bis K27 ansehen (K25 über einen Feiertag in 1–7 Tagen, K26/K27 nur mit einem Abo-Ende; sonst über die Tests bewertet).
7. Aufräumen: Funktion am Testprojekt abschalten, Testdaten und Merker löschen lassen, Ticket schließen.
7. Aufräumen: Funktion am Testprojekt abschalten, Ticket schließen. Das Abschalten durch das Team löscht **nie** Daten; die Testdaten löschst du von Hand in der Appwrite-Konsole (Datenbank des Portals): im Dokument der Funktion in `portalFeatures` (`featureKey` = `zeiten`, `projectId` = das Testprojekt) die ID notieren; in der Collection `oeffnungszeiten` das Dokument mit genau dieser ID löschen (es trägt die Eingaben, das Team-Feld und den Ersatz-Hash); in `zeitenErinnerungen` alle Dokumente mit `featureId` = dieser ID löschen (die Erinnerungs-Merker). Danach ist die Schnittstelle des Testprojekts nach spätestens 60 Sekunden (Zwischenspeicher) wieder leer. (Automatisch gelöscht wird nur beim Ende des Abos nach 30 Tagen und beim Löschen des Kundenkontos.)
## C. Schalter `on` (letzter Schritt)
Erst wenn Abschnitt B ohne offene Fehler und Abschnitt D vollständig erledigt ist:
1. `ZEITEN_ENABLED=on` in `.env`, `docker compose up -d kundenbereich-server`.
2. Relaunch: in `src/components/home/Kundenportal.tsx` den Eintrag `oeffnungszeiten` auf `bald: false` (die Marke „Bald verfügbar“ verschwindet), Tests und Build, du pushst. Das Handy-Bild `phone-12-zeiten` enthält den Einleitungstext des Editors: nach der Textrunde neu aufnehmen, wenn er sich geändert hat.
2. Relaunch: in `src/components/home/Kundenportal.tsx` den Eintrag `oeffnungszeiten` auf `bald: false` (die Marke „Bald verfügbar“ verschwindet). **Dabei den Test anpassen**: `relaunch-kundenportal.test.tsx` (um Zeile 142 bis 156) pinnt genau zwei „Bald verfügbar“; nach dem Umschalten ist es eines weniger. Dann Tests und Build, du pushst. Das Handy-Bild `phone-12-zeiten` zeigt den Einleitungstext des Editors (und einen laufenden Urlaubs-Hinweis): nach der Textrunde **neu aufnehmen**, bevor `bald: false` live geht.
3. Logs beobachten (`[zeiten]`, `[zeiten-jobs]`): Abo-Abgleich täglich, Feiertags-Erinnerung stündlich zwischen 08:00 und 20:00.
4. Zurück auf `off`: Kachel verschwindet, die öffentliche Schnittstelle antwortet `aktiv: false`, die Platzhalter zeigen ihr Ersatz-HTML; der Abo-Abgleich läuft weiter.
@@ -64,15 +79,15 @@ Erst wenn Abschnitt B ohne offene Fehler und Abschnitt D vollständig erledigt i
Reihenfolge laut Spec Abschnitt 9: gemeinsame TP2-Bausteine, Leistungsbeschreibung, Datenschutz, Oberflächen- und Mailtexte, Fragen an die Kanzlei, Uploads, `on`.
1. **Gemeinsame TP2-Bausteine** (Block F: Widerrufsbelehrung, AVV) zuerst.
2. **Leistungsbeschreibung** `public/vertragsunterlagen/zeiten-leistungsbeschreibung.html`: Entwurf („ENTWURF – vor Livegang Rechtsrunde“ im Kopf). Wortlaut-Punkte der Gesamtprüfung: Nr. 4 Abs. 2 „sobald das Wartungs-Abo aktiv ist“; Nr. 1 Abs. 1 (wann die Vereinbarung bei Team-Aktivierung entsteht, siehe ZQ9); Nr. 3 Abs. 2 „oder stehen sollen“ streichen; Nr. 3 Abs. 3 „bestätigt den Einbau als abgeschlossen“ und „Tag, der nicht geschlossen ist“; Nr. 8 Abs. 7 zweiter Satz unklar; „Behalten nach Abschaltung“. Danach Fassung festlegen (Register `leistungsbeschreibung.version` und `consent.version` hochzählen, Tests nachziehen).
2. **Leistungsbeschreibung** `public/vertragsunterlagen/zeiten-leistungsbeschreibung.html`: Entwurf („ENTWURF – vor Livegang Rechtsrunde“ im Kopf). Wortlaut-Punkte der Gesamtprüfung: Nr. 4 Abs. 2 „sobald das Wartungs-Abo aktiv ist“; Nr. 1 Abs. 1 (wann die Vereinbarung bei Team-Aktivierung entsteht, siehe ZQ11); Nr. 3 Abs. 2 „oder stehen sollen“ streichen; Nr. 3 Abs. 3 „bestätigt den Einbau als abgeschlossen“ und „Tag, der nicht geschlossen ist“; Nr. 8 Abs. 7 zweiter Satz unklar; „Behalten nach Abschaltung“. Danach Fassung festlegen (Register `leistungsbeschreibung.version` und `consent.version` hochzählen, Tests nachziehen).
3. **Datenschutz Portal § 6d** (`docs/rechtliches/zeiten-datenschutz-6d-entwurf.md`) in `public/datenschutz.html` einbauen, mit dem Anker `id="zeiten"`, plus der Satz in § 6 (Öffnungszeiten im Kundenauftrag). **Er muss live sein, bevor der Link der Leistungsbeschreibung funktioniert.** Danach `scripts/build-legal-pdfs.sh datenschutz` (nicht `all`), Prüfsumme nachziehen, `test/legalTexts.test.js` und `test/zeitenRecht.test.js` anpassen. Wortlaut-Punkte: „Ihre Eingaben“ gegen „der Kunde“, „keine Werbung“ gegen ZQ6.
4. **Widerrufsbelehrung für das 30-€-Produkt freigeben.** Die Bestellseite muss genau `featureHaken('zeiten').belehrung()` vor dem Knopf zeigen (Ruling R32), sonst belegt der Nachweis einen Text, den der Kunde nie sah.
5. **Wertersatz-Satz** (§ 357a Abs. 2 BGB) im Beginn-Haken der bezahlten Bestellung, wie bei News und Terminplaner (ZQ3); Fassung hochzählen. → Code, siehe Abschnitt E, N1.
6. **Widerruf-Seite:** `public/widerrufen.html` und `server/routes/abo.js` (`FEATURE_OF_ORDER`) kennen „Öffnungszeiten live“ nicht; ein zahlender Verbraucher, der widerruft, würde sonst der News-Pinnwand zugeordnet. → Code, siehe N2.
7. **Fragen an die Kanzlei:** `docs/rechtliches/zeiten-kanzlei-fragen.md` (ZQ1 bis ZQ10) ins Kanzlei-Paket des Terminplaners als Q27 ff. übernehmen (AVV Funktionen per Haken, Behalten-Haken, Wertersatz, Haftung ohne Abo, Ende mit dem Abo, Mails, Einstellung bei Unternehmern, Website-Absatz, Vertragsrahmen ohne Abo, Preisangaben in der Hinweis-Leiste).
5. **Wertersatz-Satz** (§ 357a Abs. 2 BGB) im Beginn-Haken der bezahlten Bestellung, wie bei News und Terminplaner (ZQ3): im Code ergänzt (Fixrunde, Fassung `2026-10-05`); der Wortlaut bleibt Entwurf bis zur Rechtsrunde, danach die Fassung auf den Tag der Freigabe setzen (Pins in `test/zeitenRegister.test.js`).
6. **Widerruf-Seite:** in der Fixrunde ergänzt (`public/widerrufen.html`, `server/routes/abo.js` mit `ORDERS`, `FEATURE_OF_ORDER`, `orderLabel`); ein zahlender Verbraucher, der widerruft, wird jetzt den Öffnungszeiten zugeordnet. Nichts mehr zu tun, außer beim Livegang die Seite einmal zu öffnen.
7. **Fragen an die Kanzlei:** `docs/rechtliches/zeiten-kanzlei-fragen.md` (ZQ1 bis ZQ12) ins Kanzlei-Paket des Terminplaners als Q27 ff. übernehmen (AVV Funktionen per Haken, Behalten-Haken, Wertersatz, Haftung ohne Abo, Ende mit dem Abo, Mails, Einstellung bei Unternehmern, Website-Absatz, Vertragsrahmen ohne Abo, Preisangaben in der Hinweis-Leiste, Aktivierung durch das Team ohne Zustimmung, Pflichtinformationen als Haken und in K15).
8. **K02/K03:** Entwurf für den Block `funktionen` in `docs/rechtliches/zeiten-k02-k03-entwurf.md`. Nach der Freigabe die Vorlagen ändern und die Pins nachziehen (`test/mailTemplates.test.js`, `test/publicForms.test.js` `FUNKTIONEN_SATZ`).
9. **Website-Absatz** `docs/funktionen/website-datenschutz-absatz.md` (Varianten A/B, Baustein Route und WhatsApp nur wenn die Leiste die Knöpfe zeigt): freigeben; der Einbau auf einer Kundenseite erst, wenn er in deren Datenschutzerklärung steht.
10. **K15 für bezahlte Öffnungszeiten** trägt keine Vertragsunterlagen und keine Belehrung als Anhang (dauerhafter Datenträger): entscheiden, ob nötig.
10. **K15 für bezahlte Öffnungszeiten** trägt keine Vertragsunterlagen und keine Belehrung als Anhang (dauerhafter Datenträger): entscheiden, ob nötig (Frage ZQ12 zusammen mit dem Haken `information`).
11. **Mailtexte K24 bis K27** und die Texte im Editor in deiner Textrunde: K25 sagt „deine Zeiten gelten normal“, obwohl die Seite an gesetzlichen Feiertagen „geschlossen“ zeigt, wenn du nichts änderst; K24-Variante „wieder eingeschaltet“ (ohne „per Ticket“ / „von Hand“); T19-Text spricht von „Einrichtung“; `disabledMessage` schreibt „du“ klein; eigene Texte des Editors (Preishinweis, Vorschau); Betreffzeile K26 nennt „Wartungs-Abo“ (so gewollt).
12. **Uploads:** die Fassungen (Leistungsbeschreibung, Datenschutzerklärung) in WOMS „Rechtliches“ hochladen (Karte `leistungsbeschreibung-zeiten`).
@@ -82,17 +97,17 @@ Mit ☐ markiert, was noch im Code offen ist; die Fixrunde des Controllers kann
**Vor `on` nötig**
- [ ] N1 Wertersatz-Satz im Beginn-Haken `features.zeiten.consent` (`server/config.js`) und Fassung hochzählen; Pins in `test/featureConsentTexte.test.js`, `test/zeitenRegister.test.js`, `test/zeitenRecht.test.js`.
- [ ] N2 „Öffnungszeiten live“ in `ORDERS`, `FEATURE_OF_ORDER` und im Bestell-Label von `server/routes/abo.js`, im Radioknopf von `public/widerrufen.html`, in `public/legal-form.js` und den Pins (`widerrufen_html_radio`).
- [x] N1 Wertersatz-Satz im Beginn-Haken `features.zeiten.consent` (`server/config.js`), Fassung `2026-10-05`, Pins in `test/zeitenRegister.test.js` (Fixrunde `ecf67d6`). Der Wortlaut bleibt Entwurf (ZQ3).
- [x] N2 „Öffnungszeiten live“ in `ORDERS`, `FEATURE_OF_ORDER` und im Bestell-Label von `server/routes/abo.js`, im Radioknopf von `public/widerrufen.html` und in den Pins (`widerrufen_html_radio`, neuer Test `widerruf_zeiten`); `public/legal-form.js` braucht keine Änderung, es liest `data-text` (Fixrunde `ecf67d6`).
- [ ] N3 Rechtstexte aus den Entwürfen übernehmen (Abschnitt D, Nr. 2 und 3) und den Entwurfskommentar der Leistungsbeschreibung entfernen.
- [ ] N4 Entscheidung zu Anhängen in K15 für Zeiten (D, Nr. 10); K02/K03-Block (D, Nr. 8).
**Härtung (vor `on` empfohlen, für den Pilot nicht nötig)**
- [ ] N5 Website-Skript: im JSON-LD `<` als `<` schreiben (`public/embed/zeiten.js`, plus Test); Kopf-Elemente behalten bei Scroll ≥ Leistenhöhe ihr Inline-`top` (bei klassengesteuertem Verstecken beim Scrollen wiederherstellen).
- [ ] N6 Abo-Abgleich: wenn `disable()` gelingt, aber das Leeren von `aboEndeMailAt` scheitert, laufen K27 und die Ersatz-Tabelle nie (K27 als offen werten, solange `aboEndeMailAt` < `disabledAt`, oder beides in einem Schreibzugriff); `nachAbschaltung` „fehler“ wiederholen oder melden; `activatedAt` bei der Löschung leeren.
- [x] N5 Website-Skript: im JSON-LD `<` als `\u003c` schreiben (`jsonLdText`, Test); Kopf-Elemente bekommen bei Scroll ≥ Leistenhöhe ihren eigenen Inline-`top` zurück (UI-Prüfung `fester_header_versteckt_beim_scrollen`); dazu späte Platzhalter (Einzelseiten-Apps) über einen entprellten MutationObserver (Fixrunde `aa9ee35`).
- [x] N6 Abo-Abgleich: K27 gilt als offen, solange `aboEndeMailAt` vor `disabledAt` liegt (das Zurücksetzen nach `disable()` entfällt); K27 geht erst nach dem Ersatz-Ticket raus, scheitert es, wiederholt der nächste Lauf beides (K27 so höchstens einmal); `activatedAt` wird bei der Löschung geleert (Fixrunde `ecf67d6`).
- [ ] N7 Team: Sperre `zeiten-ersatz:<docId>` gegen zwei Ersatz-Tickets (paralleles Abschalten und Abgleich); `PATCH …/zeiten/team` nur für `setup`/`active`/`disabled` (409 sonst); `createFeatureTicket` mit `fallbackDetails` für das lange Ersatz-Ticket.
- [ ] N8 WOMS „Ticket nachlegen“ bei Abo- oder Team-Zeiten ohne `ticketId`: Portal über `holeZeitenNach` routen (heute 409 bis zum nächsten 5-Minuten-Lauf); Texte „keine Zahlung vermerkt“ und der 409 der Abnahme („noch keine Öffnungszeiten eingetragen“).
- [x] N8 WOMS „Ticket nachlegen“ bei Abo- oder Team-Zeiten ohne `ticketId`: das Portal geht über `holeZeitenNach` (Fixrunde `ecf67d6`). Offen bleiben die Texte „keine Zahlung vermerkt“ (bei gekauften Zeiten ohne Zahlungsvermerk) und der 409 der Abnahme („noch keine Öffnungszeiten eingetragen“).
- [ ] N9 Löschen: in `loescheFunktion` erst die Merker, dann das Dokument; fehlgeschlagene Löschung in `retention.js` beim nächsten Lauf wiederholen.
- [ ] N10 `heileZeiten` in `try/finally`, wenn `activateClosedTickets` wirft.
@@ -101,6 +116,14 @@ Mit ☐ markiert, was noch im Code offen ist; die Fixrunde des Controllers kann
- [ ] N11 Zeitlogik: `wechselListe` um Schwellen (Ende minus 24 h und minus 7 Tage) ergänzen (Texte wie „bis Sonntag Mitternacht“ bleiben sonst den ganzen Sonntag stehen); Mitternachts-Einträge nicht zusammenfassen („heute“ hinkt sonst bis zu 10 Minuten nach).
- [ ] N12 Oberfläche: Portal-Knopf auf dem Desktop 40 px hoch (Ziel 44 px); Relaunch-Stationsliste kann bei 721–780 px Fensterhöhe überlaufen (Schwelle prüfen).
**Weitere Punkte der Fixrunde nach der Abschlussprüfung (erledigt)**
- [x] `activate()` schreibt die Zeiten-Felder nur noch über `extra` (Abo-Rückkehr und Team-Aktivierung der Zeiten): News und Terminplaner brechen auch vor dem Schema-Skript nicht mehr mit „Unknown attribute“.
- [x] Die Einschalten-Route (`/api/funktionen/zeiten/einschalten`) nimmt nur `{ zustimmung }`, nicht die Altform `{ consent: true }` ohne Fassungsprüfung.
- [x] Die Team-Aktivierung behält `kauf` und `unlockedByPurchase`, wenn das Dokument vorher gekauft war (die Widerrufs-Zuordnung bleibt richtig; `einbau` bleibt dabei stehen, weil für gekaufte Dokumente das Nachholen nicht neu baut).
- [x] WOMS: „Leisten live – Einbau läuft“ nur bei `einbau` = `angestossen`, sonst „Einbau läuft (Team setzt die Skript-Zeile von Hand)“ (`919d96f`).
- [x] Editor: die Bestätigung klebt unter der Kopfleiste und bleibt nach dem Speichern eines unteren Abschnitts sichtbar; Website-Skript: Links mit Zugangsdaten werden verworfen, die Adresse der Schnittstelle bestimmt nur das eigene Script-Tag.
## F. Bewusste Abweichungen und Hinweise
- Schema der neuen `portalFeatures`-Felder liegt in `scripts/zeiten-setup.mjs` (nicht in `feature-setup.mjs`), weil dessen Tests genau drei Attribute festschreiben.