118 KiB
Kundenansicht neu, Paket P8: Rechtsrunde Fassung 6 Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Die Rechtstexte der Kundenansicht neu liegen als Entwurf zur Freigabe vor (Abo-Bedingungen Fassung 6, Leistungsbeschreibungen und AVV je Funktion ohne Preise, Datenschutz-Prüfpunkte, Entscheidungsliste mit Kanzlei-Fragen). Der Server unterscheidet die Fassungen je Sitzung (Fassungstabelle, aktuelleAboFassung, Archiv v5, Wortlaute für die Bestellseiten), und zwei Skripte bauen die PDFs und laden die Fassungen nach der Freigabe in WOMS „Rechtliches“. Für Kunden mit alter Ansicht ändert sich nichts.
Architecture: Die Texte sind Entwürfe: Markdown unter docs/rechtliches/*-v6.md, daraus erzeugt der HTML-Generator (neuer Satz funktionen6, neuer Stil) die Seiten unter public/neu/rechtliches/. Die Abo-Bedingungen sind wie Fassung 5 handgepflegtes HTML im neuen Stil, mit gleichen Paragrafen und Ankern. products.js bekommt die Fassungstabelle, die Wortlaute und die Unterlagen-Tabelle der Funktionen. Alle Tests prüfen Texte über feste Pflichtsätze, die in diesem Plan stehen; PDF-Bau und Hochladen laufen nie in diesem Paket.
Tech Stack: Node 20 (Host 24), node --test, der vorhandene HTML-Generator scripts/build-legal-html.mjs, scripts/build-legal-pdfs.sh (headless Chrome, nur beschrieben), Appwrite-REST für den Upload (nur geschrieben, nie ausgeführt).
Spec: docs/superpowers/specs/2026-10-15-kundenansicht-neu-design.md (5.9 komplett; E6, E7, E8, E9, E10, E17, E20, E21; C4, C5, C6, C8, C10, C11, C12, C14, C15; 5.5 und 5.6 für die Wortlaute)
Übersicht/Verträge: /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht/docs/superpowers/plans/2026-10-16-kundenansicht-00-uebersicht.md (gilt wörtlich; dieses Paket liefert V12 Teil P8 und die Wortlaute für V5)
Worktree und Branch: /home/webklar/apps/kundenbereich.webklar.com/dev-kn-recht, Branch kn/recht (von Tag kn-p0). Am Ende Tag kn-p8. Alle Pfade unten sind relativ zu diesem Worktree.
Global Constraints
Es gelten alle Global Constraints der Übersicht wörtlich (Schalter, Aussehen, du-Anrede in Oberflächentexten, keine Funktionspreise, Hashes, Kundenart nur vom Server, Nachweise, Testbefehle, Commits, Sicherheitsblock, Prod-Daten). Paketeigene Zusätze:
- Vorbereitung (einmalig, kein Commit):
git -C /home/webklar/apps/kundenbereich.webklar.com/dev-kn-recht log -1 --format=%Dnenntkn-p0undkn/recht;ls server/services/kundenansicht/schalter.js node_modulesexistiert (Symlinkln -s ../build/node_modules node_modules, niegit add -A);free -mzeigt mindestens 2000 MB verfügbar. - Eingefroren (C8), nur lesen:
public/rechtliches/*,public/vertragsunterlagen/v3/*,public/vertragsunterlagen/v4/*,public/vertragsunterlagen/*.html,public/abo-bedingungen.html,public/WEBklar-Abo-Vertragsunterlagen.pdf,public/datenschutz.html, die bestehendendocs/rechtliches/*.mdohne-v6. Neue Fassungen entstehen nur als neue Dateien. Die Kopie unterpublic/vertragsunterlagen/v5/entsteht mitcpeinzeln, nie mit Wildcards. - Entwurfsstatus: Jede neue Rechtsdatei trägt
Entwurf – Prüfung durch Kanzlei offen(Markdown-Dateien in der Fassungszeile, die Abo-Seite in einem Absatz unter der Kopfzeile). In den Test-Dateien steht dafür die KonstanteENTWURF = true; der Ablauf nach der Freigabe (Task 11) setzt sie auffalseund entfernt die Marken. - Fassungen: Abo
2026-10-16-v6(config.abo.termsVersionNeu), Funktionsunterlagen2026-10-16(FUNKTIONEN_FASSUNG_V6). Das Datum ist der Entwurfstag; bei der Freigabe wird es auf den Freigabetag gesetzt (Task 11, Ablauf). - Pfade der Entwürfe (Vertragsfrage 1): Seite
public/neu/rechtliches/abo-bedingungen.html(Adresse/neu/rechtliches/abo-bedingungen.html), PDFpublic/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf, Funktionsunterlagenpublic/neu/rechtliches/<slug>-2026-10-16.html|pdf. - Keine Funktionspreise in
*-v6.mdund in den Funktionsseiten: kein€. Die Abo-Seite behält die Abo-Preise (20,00 €, 50,00 €, 1,00 €, 100 € pro Stunde) unverändert. - Anrede der Rechtsdokumente: wie bisher (3. Person „der Kunde“, in der Übersichtsbox der Abo-Seite „Ihr“); nur Wortlaute für die Oberfläche (Task 3) sind „du“. Entscheidung R12.
- Nie ausführen in diesem Paket:
scripts/build-legal-pdfs.sh(nurbash -n),scripts/rechtliches-hochladen.mjs(auch nicht mit--probe,--karten-anlegenoder--pruefen; die Tests rufenmainnur mit Nachbau-depsauf), Chrome, Netzwerk. Erlaubt:node scripts/build-legal-html.mjs funktionen6 …(schreibt nur unterpublic/neu/rechtliches/). - Pins, die unverändert bleiben und in Task 1, 2 und 12 mitlaufen:
test/products.test.js:117-137und:187-200,test/mailTemplates.test.js:551-562,test/legalTexts.test.js:104-122(darin:120, dieall-Zeile vonbuild-legal-pdfs.sh),test/buildLegalHtml.test.js:336-368(die Usage-Zeile:367) und:418-448(die Usage-Meldung vonbuild-legal-html.mjs,:436). Angepasst wird keine bestehende Zeile außer dem Import-Blocktest/products.test.js:6-17. - Testbefehle: wie in der Übersicht. Mehrere Dateien nur mit
--test-concurrency=1. Die ganze Suite läuft erst in Phase E.
Review Focus
Die fünf wahrscheinlichsten Fehlerfälle dieses Pakets, häufigster zuerst:
- Die Fassungsweiche trifft die falschen Kunden:
-v6zählt nur als Endung (nicht-v60,-v66,v6-…), Zustimmungen v3 bis v5 behalten Seite, PDF undendParagraph(Mails K02/K03/T05/T07), und v6 gilt nur bei neuer Ansicht (offmit*, leere Liste, E-Mail mit Großschreibung und Leerzeichen, Mitarbeiter-Kundenansicht). Tests: Task 1. - Eingefrorene Fassungen werden verändert oder das Archiv weicht ab (C8): Archiv-Kopie nicht bytegleich, PDF-Link doppelt, Originale angefasst. Tests: Task 2 (Pins), Task 14 (
git diffgegenkn-p0). - Preis, Alt-Wortlaut oder Lücke bei der Vorschuss-Regel in den neuen Texten:
€, „per Haken“, „Zahlungspflichtig“, „Hosting-Abo nicht enthalten“ bei der Analyse, „Vertrag über den Betrieb der Website“ bei Webshop und Buchungskalender, ein Dokument ohne „Vorschuss“ und ohne „zugunsten“ (E17), verschobene Nummern, auf die die AVV verweisen. Tests: Tasks 5, 7, 8, 9, 10. - Wortlaut-Drift zwischen den Rechtstexten, den Konstanten und den Paketen P5/P7: Platzhalter
{fassung}ungefüllt, P5 zeigt noch den Entwurfstext oder verlinkt die Alt-Unterlagen mit Preisen. Tests: Task 3 (inklusive Verdrahtungstest, der nach dem Merge von P5 rot wird, bis verdrahtet ist). - Hochladen ohne Freigabe, mehrere Dokumente in einem Aufruf, ein nicht committeter Stand oder eine fehlende WOMS-Karte: ohne
--freigabe=JJJJ-MM-TTund ohne genau einendocumentKeybricht das Skript ab, ebenso beim Hochladen ohne Karte;--probemeldet eine fehlende Karte als Stopp (drei der elf Dokumente haben heute keine); Import des Moduls liest weder Schlüssel noch Netz. Tests: Task 13.
Dateiübersicht
| Datei | Aufgabe | Task |
|---|---|---|
server/services/products.js |
FASSUNGEN, aktuelleAboFassung, isTermsV4OrNewer, termsDocFor; WORTLAUT_V6, mitFassung, FUNKTIONEN_FASSUNG_V6, FUNKTIONEN_UNTERLAGEN_V6 |
1, 3 |
server/config.js (nur abo.termsVersionNeu, Zeile bei :855) |
Fassung 6 | 1 |
public/vertragsunterlagen/v5/abo-bedingungen.html, …/WEBklar-Abo-Vertragsunterlagen.pdf (neu) |
Archiv Fassung 5, erst P10 aktiv | 2 |
docs/rechtliches/abo-bedingungen-v6.md (neu) |
Entwurf: alle Änderungen gegenüber Fassung 5 im Wortlaut | 4 |
public/neu/rechtliches/abo-bedingungen.html (neu) |
Abo-Bedingungen Fassung 6, neuer Stil | 5 |
scripts/build-legal-html.mjs |
stil: 'neu', Satz funktionen6, --nur= |
6 |
docs/rechtliches/leistungsbeschreibung-{terminplaner,news,zeiten,webshop,unterkunft,website-analyse}-v6.md (neu) |
Leistungsbeschreibungen ohne Preise | 7, 8, 9 |
docs/rechtliches/avv-{website-analyse,terminplaner,webshop,unterkunft}-v6.md (neu) |
AVV mit neuer Vorbemerkung | 9, 10 |
public/neu/rechtliches/<slug>-2026-10-16.html (10 Seiten, erzeugt) |
Funktionsunterlagen | 7 bis 10 |
docs/rechtliches/datenschutz-kundenansicht-pruefpunkte.md, docs/rechtliches/kundenansicht-entscheidungen.md (neu) |
Prüfpunkte, Entscheidungen R1 bis R15, Kanzlei-Fragen KQ1 bis KQ9, Ablauf nach der Freigabe | 11 |
scripts/build-legal-pdfs.sh |
Modi abo6, funktionen6 |
12 |
scripts/rechtliches-hochladen.mjs (neu) |
Upload nach WOMS „Rechtliches“, fehlende Karten anlegen, Nachkontrolle | 13 |
test/products.test.js (Import :6-17, neue Tests nach :137), test/legalTexts.test.js (neuer Test nach :448) |
Fassungstabelle, Archiv v5 | 1, 2 |
test/aboWortlautV6.test.js, test/aboBedingungenV6.test.js, test/legalV6Build.test.js, test/funktionenRechtV6.test.js, test/kundenansichtEntscheidungen.test.js, test/rechtlichesHochladen.test.js (alle neu) |
je Task | 3, 4/5, 6/12, 7 bis 10, 11, 13 |
Task 1: Fassungstabelle, Weiche und termsVersionNeu
Files:
- Modify:
server/services/products.js:28-68(isCurrentTerms,isTermsV4OrNewer,termsDocFor) und der ImportneueAnsichtam Dateianfang - Modify:
server/config.js:853-857(eine Zeile im Abschnittabo; Kommentar: gilt nur für Sitzungen mit neuer Ansicht, Datum bei Freigabe setzen) - Test:
test/products.test.js(Import-Block:6-17umFASSUNGEN, aktuelleAboFassung, isTermsV4OrNewererweitern; neue Tests nach:137)
Interfaces:
-
Consumes:
neueAnsicht(req)ausserver/services/kundenansicht/schalter.js(V1);config.kundenansichtNeu={ mode, pilotEmails }. -
Produces (V12):
export const FASSUNGEN // Object.freeze([{ muster: /-v6$/, nummer: 6, url, pdfFile, endParagraph }, { muster: /-v5$/, nummer: 5, …, archiv: { url, pdfFile } }, { muster: /-v4$/, nummer: 4, … }, { sonst: true, nummer: 3, … }]) export function aktuelleAboFassung(req) // string: neueAnsicht(req) ? config.abo.termsVersionNeu : config.abo.termsVersion export function isTermsV4OrNewer(termsVersion) // v4, v5, v6 export function termsDocFor(termsVersion) // Form unverändert: { version, url, pdfFile, endParagraph }config.abo.termsVersionNeu = '2026-10-16-v6';config.abo.termsVersionbleibt'2026-09-30-v5'. -
Step 1: Failing tests (an
test/products.test.jsanhängen)
test('Fassungstabelle: Reihenfolge, Muster nur am Ende, v6 im Vorschaupfad', () => {
assert.deepEqual(FASSUNGEN.map((f) => f.muster?.source ?? 'sonst'), ['-v6$', '-v5$', '-v4$', 'sonst'])
assert.deepEqual(termsDocFor('2026-10-16-v6'), {
version: '2026-10-16-v6', url: '/neu/rechtliches/abo-bedingungen.html',
pdfFile: 'neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf', endParagraph: '§ 11',
})
for (const v of ['2026-10-16-v60', '2026-10-16-v6x', 'v6-2026-10-16', '2026-10-16-v66']) {
assert.equal(termsDocFor(v).url, '/vertragsunterlagen/v3/abo-bedingungen.html', v)
}
const a = contractAttachmentFor('2026-10-16-v6')
assert.equal(a.filename, 'WEBklar-Abo-Vertragsunterlagen.pdf')
assert.ok(path.isAbsolute(a.path) && a.path.endsWith(path.join('public', 'neu', 'rechtliches', 'WEBklar-Abo-Vertragsunterlagen.pdf')))
})
test('Fassung 5 bleibt bis P10 an den heutigen Adressen, das Archiv ist vorbereitet', () => {
const v5 = FASSUNGEN.find((f) => f.muster?.source === '-v5$')
assert.equal(termsDocFor('2026-09-30-v5').url, '/abo-bedingungen.html')
assert.deepEqual(Object.keys(termsDocFor('2026-09-30-v5')).sort(), ['endParagraph', 'pdfFile', 'url', 'version'])
assert.deepEqual(v5.archiv, { url: '/vertragsunterlagen/v5/abo-bedingungen.html', pdfFile: 'vertragsunterlagen/v5/WEBklar-Abo-Vertragsunterlagen.pdf' })
})
test('isTermsV4OrNewer: v4, v5, v6 ja; v3, fehlende und fremde Endungen nein', () => {
for (const v of ['2026-09-30-v4', '2026-09-30-v5', '2026-10-16-v6']) assert.equal(isTermsV4OrNewer(v), true, v)
for (const v of ['2026-09-25-v3', '2026-09-25', null, undefined, '', '2026-10-16-v60', 7]) assert.equal(isTermsV4OrNewer(v), false, String(v))
})
test('aktuelleAboFassung: v6 nur mit neuer Ansicht', () => {
const k = (email, extra = {}) => ({ session: { customerId: 'c1', email, ...extra } })
const vorher = { ...config.kundenansichtNeu }
try {
Object.assign(config.kundenansichtNeu, { mode: 'off', pilotEmails: ['*'] })
assert.equal(aktuelleAboFassung(k('a@example.invalid')), '2026-09-30-v5', 'off schlägt die Liste')
Object.assign(config.kundenansichtNeu, { mode: 'pilot', pilotEmails: [] })
assert.equal(aktuelleAboFassung(k('a@example.invalid')), '2026-09-30-v5', 'leere Liste lässt niemanden durch')
config.kundenansichtNeu.pilotEmails = ['a@example.invalid']
assert.equal(aktuelleAboFassung(k(' A@Example.INVALID ')), '2026-10-16-v6', 'Großschreibung und Leerraum')
assert.equal(aktuelleAboFassung(k('b@example.invalid')), '2026-09-30-v5')
assert.equal(aktuelleAboFassung(k('b@example.invalid', { viewAs: true, viewId: 'v1' })), '2026-10-16-v6', 'Mitarbeiter-Kundenansicht')
Object.assign(config.kundenansichtNeu, { mode: 'on', pilotEmails: [] })
assert.equal(aktuelleAboFassung(k('b@example.invalid')), '2026-10-16-v6')
} finally { Object.assign(config.kundenansichtNeu, vorher) }
assert.deepEqual([config.abo.termsVersion, config.abo.termsVersionNeu], ['2026-09-30-v5', '2026-10-16-v6'])
})
- Step 2: Rot
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-recht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/products.test.js
Expected: FAIL (FASSUNGEN und aktuelleAboFassung fehlen); die bisherigen Tests der Datei laufen grün.
- Step 3: Umsetzen
config.abo.termsVersionNeu ergänzen. In products.js die Tabelle FASSUNGEN einführen (Werte: v6 url '/neu/rechtliches/abo-bedingungen.html', pdfFile 'neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf', endParagraph '§ 11'; v5 und v4 mit den heutigen Werten aus products.js:51-60; sonst mit den Werten von v3 aus :62-66). termsDocFor sucht den ersten Eintrag, dessen muster auf einen String passt, sonst den Eintrag sonst; version ist die Eingabe, bei leerer oder fehlender Eingabe TERMS_VERSION_V3; die Rückgabe enthält nur die vier Felder (kein archiv, muster, nummer). isTermsV4OrNewer ist wahr, wenn ein Eintrag mit muster passt. isCurrentTerms bleibt unverändert exportiert.
- Step 4: Grün, alte Pins laufen mit
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-recht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test --test-concurrency=1 test/products.test.js test/mailTemplates.test.js test/legalTexts.test.js
Expected: PASS.
- Step 5: Commit
git add server/services/products.js server/config.js test/products.test.js
git commit -m "Abo-Fassung 6: Fassungstabelle, aktuelleAboFassung, termsVersionNeu" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 2: Archiv Fassung 5 vorbereiten
Files:
- Create:
public/vertragsunterlagen/v5/abo-bedingungen.html(Kopie vonpublic/abo-bedingungen.html, nur der PDF-Link zeigt aufs Archiv-PDF) - Create:
public/vertragsunterlagen/v5/WEBklar-Abo-Vertragsunterlagen.pdf(bytegleiche Kopie vonpublic/WEBklar-Abo-Vertragsunterlagen.pdf) - Test:
test/legalTexts.test.js(neuer Test nach:448, Muster wie der Test zu Fassung 4 bei:439-448)
Interfaces:
-
Consumes:
FASSUNGEN[v5].archiv(Task 1). -
Produces: das Archiv, das P10 aktiviert; die Originale bleiben unberührt.
-
Step 1: Failing test
// = `git show 7ae0ffd:public/abo-bedingungen.html | sha256sum` bzw. des PDFs (Fassung 5, Stand 30.09.2026)
const V5 = 'public/vertragsunterlagen/v5/abo-bedingungen.html'
const V5_SHA256 = '3b570eb2daed76d20fc5b8b9aea36a447f0481b21ac950452a82e454db45291e'
const V5_PDF_SHA256 = 'a8344e83c4d8f5ed2610048a5ea9ca6b8d2082b5648e706ea1ef01c10087e37b'
const V5_PDF_LINK = 'href="/WEBklar-Abo-Vertragsunterlagen.pdf"'
const V5_ARCHIVE_PDF_LINK = 'href="/vertragsunterlagen/v5/WEBklar-Abo-Vertragsunterlagen.pdf"'
test('Fassung 5 archiviert (vorbereitet für P10): HTML bis auf den PDF-Link bytegleich, PDF bytegleich', () => {
const archived = read(V5)
assert.equal(archived.split(V5_ARCHIVE_PDF_LINK).length, 2, 'genau ein Archiv-Link')
assert.ok(!archived.includes(V5_PDF_LINK))
assert.equal(createHash('sha256').update(archived.split(V5_ARCHIVE_PDF_LINK).join(V5_PDF_LINK)).digest('hex'), V5_SHA256)
assert.equal(sha256('public/vertragsunterlagen/v5/WEBklar-Abo-Vertragsunterlagen.pdf'), V5_PDF_SHA256)
assert.ok(archived.includes('Version 5 · Stand 30.09.2026'))
})
- Step 2: Rot
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-recht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/legalTexts.test.js
Expected: FAIL (ENOENT auf v5/abo-bedingungen.html).
- Step 3: Archiv anlegen
mkdir -p public/vertragsunterlagen/v5; beide Dateien einzeln mit cp kopieren; in der Kopie die eine Stelle href="/WEBklar-Abo-Vertragsunterlagen.pdf" (Zeile 62) mit dem Edit-Werkzeug auf V5_ARCHIVE_PDF_LINK ändern. Die Originale nicht anfassen.
- Step 4: Grün
Run: wie Step 2. Expected: PASS (der neue Test und alle bisherigen Tests der Datei).
- Step 5: Commit
git add test/legalTexts.test.js public/vertragsunterlagen/v5/abo-bedingungen.html public/vertragsunterlagen/v5/WEBklar-Abo-Vertragsunterlagen.pdf
git commit -m "Abo-Bedingungen Fassung 5: Archiv vorbereitet (erst mit P10 aktiv)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 3: Wortlaute und Unterlagen-Tabelle für die Bestellseiten
Files:
- Modify:
server/services/products.js(anhängen) - Test:
test/aboWortlautV6.test.js(neu; Kopf wietest/products.test.js:1-18)
Interfaces:
-
Consumes: nichts.
-
Produces (für P5, P7, P3):
export const FUNKTIONEN_FASSUNG_V6 // '2026-10-16' export const WORTLAUT_V6 // Object.freeze, tief // .linkzeile { bestellen, einschaltenMitAvv, einschaltenOhneAvv } Text mit genau einem Platzhalter {fassung}; bestellen gilt für Abo, Wechsel und Zusatzspeicher // .beginn { abo, wechsel, einrichtung } das Häkchen nur für privat in der Frist // .hinweis { shop, unterkunft, termine } Pflichten aus den alten Häkchen, ein Satz auf der Einschalt-Seite export function mitFassung(text, fassung) // ersetzt {fassung}; wirft bei leerer Fassung oder fehlendem Platzhalter export const FUNKTIONEN_UNTERLAGEN_V6 // { [key]: { lb: Dok, avv: Dok | null } }, Reihenfolge news, termine, shop, unterkunft, zeiten, analyse // Dok = { version, url, pdfFile, anhangName } -
Step 1: Failing tests (Kopf wie
test/products.test.js:1-5:test,assert,config)
const { WORTLAUT_V6, FUNKTIONEN_FASSUNG_V6, FUNKTIONEN_UNTERLAGEN_V6, mitFassung } = await import('../server/services/products.js')
test('Linkzeilen: Wortlaut, Platzhalter wird gefüllt', () => {
assert.equal(mitFassung(WORTLAUT_V6.linkzeile.bestellen, '2026-10-16-v6'), 'Es gelten die Vertragsbedingungen mit Leistungsbeschreibung, Auftragsverarbeitung und Widerrufsbelehrung (Fassung 2026-10-16-v6). Alles kommt danach auch als PDF per Mail.')
assert.equal(mitFassung(WORTLAUT_V6.linkzeile.einschaltenMitAvv, '2026-10-16'), 'Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16).')
assert.equal(mitFassung(WORTLAUT_V6.linkzeile.einschaltenOhneAvv, '2026-10-16'), 'Es gilt die Leistungsbeschreibung (Fassung 2026-10-16).')
assert.throws(() => mitFassung(WORTLAUT_V6.linkzeile.bestellen, ''), /Fassung/)
assert.throws(() => mitFassung('ohne Platzhalter', '2026-10-16'), /Platzhalter/)
})
test('Häkchen beginn: Wortlaut je Fall (nur privat in der Frist)', () => {
assert.deepEqual(WORTLAUT_V6.beginn, {
abo: 'Bitte beginnt sofort mit dem Abo. Mir ist klar: Widerrufe ich innerhalb von 14 Tagen, zahle ich den bis dahin erbrachten Teil.',
wechsel: 'Bitte beginnt sofort mit dem Wechsel. Mir ist klar: Widerrufe ich innerhalb von 14 Tagen, zahle ich den bis dahin erbrachten Teil.',
einrichtung: 'Bitte beginnt sofort mit der Einrichtung. Mir ist klar: Mit der vollständigen Einrichtung erlischt mein Widerrufsrecht, und wenn ich vorher widerrufe, zahle ich den bis dahin erbrachten Teil.',
})
})
test('Pflichten aus den alten Häkchen verantwortung, stripe, gesundheit', () => {
assert.deepEqual(WORTLAUT_V6.hinweis, {
shop: 'Du verkaufst in eigenem Namen und bist für deine Produkte, Preise und Rechtstexte verantwortlich. Die Zahlungen deiner Käufer laufen über dein eigenes Stripe-Konto.',
unterkunft: 'Du vermietest in eigenem Namen und bist für Preise, Stornobedingungen, Rechnungen und Meldepflichten verantwortlich. Online-Zahlungen deiner Gäste laufen über dein eigenes Stripe-Konto.',
termine: 'Der Terminplaner ist nicht für den Gesundheitsbereich gedacht. Das fragen wir im Fragebogen ab.',
})
})
test('alle Wortlaute: du-Form, kein Preis, nur {fassung} als Platzhalter, tief eingefroren', () => {
const alle = JSON.stringify(WORTLAUT_V6)
assert.doesNotMatch(alle, /€|\bSie\b|\bIhr(e[nmrs]?)?\b/)
assert.deepEqual([...alle.matchAll(/\{[a-z]+\}/g)].map((m) => m[0]).filter((x) => x !== '{fassung}'), [])
for (const t of Object.values(WORTLAUT_V6.linkzeile)) assert.equal((t.match(/\{fassung\}/g) ?? []).length, 1)
for (const o of [WORTLAUT_V6, WORTLAUT_V6.linkzeile, WORTLAUT_V6.beginn, WORTLAUT_V6.hinweis]) assert.ok(Object.isFrozen(o))
})
test('Funktionsunterlagen Fassung 6: Reihenfolge, Vorschaupfad, AVV nur wo Daten im Auftrag verarbeitet werden', () => {
assert.equal(FUNKTIONEN_FASSUNG_V6, '2026-10-16')
assert.deepEqual(Object.keys(FUNKTIONEN_UNTERLAGEN_V6), ['news', 'termine', 'shop', 'unterkunft', 'zeiten', 'analyse'])
assert.deepEqual(FUNKTIONEN_UNTERLAGEN_V6.termine.lb, { version: '2026-10-16', url: '/neu/rechtliches/leistungsbeschreibung-terminplaner-2026-10-16.html', pdfFile: 'neu/rechtliches/leistungsbeschreibung-terminplaner-2026-10-16.pdf', anhangName: 'Leistungsbeschreibung-Terminplaner-2026-10-16.pdf' })
const slugs = Object.values(FUNKTIONEN_UNTERLAGEN_V6).flatMap((u) => [u.lb, u.avv]).filter(Boolean).map((d) => d.url.replace(/^\/neu\/rechtliches\/|-2026-10-16\.html$/g, ''))
assert.deepEqual(slugs, ['leistungsbeschreibung-news', 'leistungsbeschreibung-terminplaner', 'avv-terminplaner', 'leistungsbeschreibung-webshop', 'avv-webshop', 'leistungsbeschreibung-unterkunft', 'avv-unterkunft', 'leistungsbeschreibung-zeiten', 'leistungsbeschreibung-website-analyse', 'avv-website-analyse'])
assert.deepEqual([FUNKTIONEN_UNTERLAGEN_V6.news.avv, FUNKTIONEN_UNTERLAGEN_V6.zeiten.avv], [null, null])
assert.deepEqual(Object.values(FUNKTIONEN_UNTERLAGEN_V6).flatMap((u) => [u.lb, u.avv]).filter(Boolean).map((d) => d.anhangName.replace('-2026-10-16.pdf', '')), ['Leistungsbeschreibung-News-Pinnwand', 'Leistungsbeschreibung-Terminplaner', 'AVV-Terminplaner', 'Leistungsbeschreibung-Webshop', 'AVV-Webshop', 'Leistungsbeschreibung-Buchungskalender', 'AVV-Buchungskalender', 'Leistungsbeschreibung-Oeffnungszeiten-live', 'Leistungsbeschreibung-Website-Analyse', 'AVV-Website-Analyse'])
})
test('Verdrahtung mit P5 (läuft erst nach dem Merge von kn/server; rot, bis der Controller verdrahtet hat)', { skip: config.aboFunktionen === undefined && 'config.aboFunktionen fehlt: P5 noch nicht gemerged' }, async () => {
const reg = await import('../server/services/featureRegistry.js')
assert.equal(config.aboFunktionen.fassung, FUNKTIONEN_FASSUNG_V6)
assert.equal(config.aboFunktionen.texte.beginn, WORTLAUT_V6.beginn.einrichtung)
for (const key of ['termine', 'shop', 'unterkunft']) assert.equal(reg.consentAbo(key).items[0].text, mitFassung(WORTLAUT_V6.linkzeile.einschaltenMitAvv, FUNKTIONEN_FASSUNG_V6), key)
for (const key of ['news', 'zeiten']) assert.equal(reg.consentAbo(key).items[0].text, mitFassung(WORTLAUT_V6.linkzeile.einschaltenOhneAvv, FUNKTIONEN_FASSUNG_V6), key)
})
- Step 2: Rot
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-recht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/aboWortlautV6.test.js
Expected: FAIL (Exporte fehlen).
- Step 3: Umsetzen
Konstanten in products.js wörtlich wie in den Tests; WORTLAUT_V6.linkzeile.bestellen ist der Satz aus Test 1 mit {fassung} statt der Fassung. FUNKTIONEN_UNTERLAGEN_V6 über einen lokalen Helfer dok(slug, anhang): url = /neu/rechtliches/<slug>-<Fassung>.html, pdfFile = neu/rechtliches/<slug>-<Fassung>.pdf, anhangName = <anhang>-<Fassung>.pdf. Alles tief mit Object.freeze.
- Step 4: Grün
Run: wie Step 2. Expected: PASS; der Verdrahtungstest erscheint als übersprungen.
- Step 5: Commit
git add server/services/products.js test/aboWortlautV6.test.js
git commit -m "Abo-Fassung 6: Wortlaute für Linkzeile und Häkchen, Unterlagen-Tabelle der Funktionen" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 4: Abo-Bedingungen Fassung 6, Entwurf der Änderungen
Files:
- Create:
docs/rechtliches/abo-bedingungen-v6.md - Test:
test/aboBedingungenV6.test.js(neu)
Interfaces:
-
Consumes: nichts. Basis ist die Fassung 5 (
public/vertragsunterlagen/v5/abo-bedingungen.html). Die Paragrafenfolge § 1 bis § 16 und alle Anker bleiben (Querverweise in Mails,endParagraph '§ 11'). -
Produces: die Pflichtsätze (Konstante
PFLICHTim Test), die Task 5 in die Seite übernimmt. Aufbau der Datei:# Abo-Bedingungen Fassung 6 (Entwurf), eine Kopfzeile (Stand, „Entwurf zur Freigabe, keine Rechtsberatung“, Basis Fassung 5), je Stelle ein## <Stelle>mit dem Pflichtsatz als Absatz und einer Zeile**Begründung.**mit Spec- oder Entscheidungsnummer, zuletzt## Was gleich bleibt(Preise, Laufzeiten, Paragrafenfolge, Widerrufsbelehrung und Muster-Formular, § 10, Anlagen 1 und 2). -
Step 1: Failing test (Kopf:
import { test } from 'node:test',assert,readFileSync;const { config } = await import('../server/config.js');const ENTWURF = true;const lies = (rel) => readFileSync(new URL(../${rel}, import.meta.url), 'utf8'))
const PFLICHT = [ // stelle = Überschrift im Entwurf; von/bis = Anker-Ids der Seite (null = Dokumentanfang bzw. -ende)
{ stelle: 'Das Wichtigste auf einen Blick', von: null, bis: 'p1', texte: ['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).'] },
{ stelle: '§ 1 Abs. 2', von: 'p1', bis: 'p2', texte: ['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.'] },
{ stelle: '§ 2 Abs. 2 c', von: 'p2', bis: 'p3', texte: ['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.'] },
{ stelle: '§ 2 Abs. 6', von: 'p2', bis: 'p3', texte: ['Wir speichern außerdem den Wortlaut der angezeigten Hinweise und Kästchen, die hinterlegte Kundenart sowie Zeit, IP-Adresse und Browserkennung des Klicks.'] },
{ stelle: '§ 3 Abs. 8', von: 'p3', bis: 'p4', texte: [
'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.'] },
{ stelle: '§ 4 Abs. 2', von: 'p4', bis: 'p5', texte: ['Einen Vorschuss nach Absatz 8 gleichen die folgenden Kontingente zuerst aus.'] },
{ stelle: '§ 4 Abs. 8', von: 'p4', bis: 'p5', texte: ['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.'] },
{ stelle: '§ 4 Abs. 9', von: 'p4', bis: 'p5', texte: ['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.'] },
{ stelle: '§ 6 Abs. 6', von: 'p6', bis: 'p7', texte: ['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“.'] },
{ stelle: '§ 9 Abs. 6', von: 'wechsel', bis: 'zusatzspeicher', texte: ['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.'] },
{ stelle: '§ 11 Abs. 4', von: 'p11', bis: 'p12', texte: ['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.'] },
{ stelle: '§ 14 Abs. 2', von: 'p14', bis: 'p15', texte: ['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.'] },
{ stelle: '§ 15', von: 'p15', bis: 'p16', texte: ['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.'] },
{ stelle: 'Leistungsbeschreibung A Nr. 1', von: 'hosting', bis: 'wartung', texte: ['Website-Analyse im Kundenportal (§ 3 Abs. 8)'] },
{ stelle: 'Leistungsbeschreibung B Nr. 1', von: 'wartung', bis: 'widerruf', texte: ['Funktionen im Kundenportal: News-Pinnwand, Terminplaner, Webshop, Buchungskalender, Öffnungszeiten live und Website-Analyse (§ 3 Abs. 8)'] },
{ stelle: 'Widerruf: Hinweise', von: 'widerruf-hinweise', bis: 'avv', texte: ['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).'] },
{ stelle: 'Anlage 3', von: 'sub', bis: null, texte: ['Server für die Website-Analyse (Messdienst, nur bei eingeschalteter Funktion)', 'Deutschland (Rechenzentrum Falkenstein)'] },
]
test('Entwurf Fassung 6: Stellen in Reihenfolge, jeder Pflichtsatz im Wortlaut unter seiner Stelle', () => {
const md = lies('docs/rechtliches/abo-bedingungen-v6.md')
assert.match(md, /^# Abo-Bedingungen Fassung 6 \(Entwurf\)\n/)
assert.deepEqual([...md.matchAll(/^## (.+)$/gm)].map((m) => m[1]), [...PFLICHT.map((p) => p.stelle), 'Was gleich bleibt'])
assert.equal(md.includes('Entwurf zur Freigabe, keine Rechtsberatung'), ENTWURF)
for (const p of PFLICHT) {
const teil = md.slice(md.indexOf(`## ${p.stelle}\n`)).split(/\n## /)[0]
for (const t of p.texte) assert.ok(teil.includes(t), `${p.stelle}: ${t.slice(0, 50)}`)
assert.match(teil, /\*\*Begründung\.\*\*/, p.stelle)
}
assert.doesNotMatch(md.slice(0, md.indexOf('## Was gleich bleibt')), /€/, 'die Pflichtsätze nennen keinen Preis')
})
- Step 2: Rot
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-recht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/aboBedingungenV6.test.js
Expected: FAIL (ENOENT auf den Entwurf).
- Step 3: Entwurf schreiben
docs/rechtliches/abo-bedingungen-v6.md nach dem Aufbau oben. Die Pflichtsätze wörtlich aus PFLICHT; die Begründung nennt Spec-Abschnitt oder Entscheidung (E6, E9, E10, E17, E21, C4, C5, C10, C11, C12) und bei den offenen Punkten die Nummer aus der Entscheidungsliste (R1 bis R15, Task 11).
- Step 4: Grün
Run: wie Step 2. Expected: PASS.
- Step 5: Commit
git add docs/rechtliches/abo-bedingungen-v6.md test/aboBedingungenV6.test.js
git commit -m "Abo-Bedingungen Fassung 6: Entwurf der Änderungen im Wortlaut" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 5: Abo-Bedingungen Fassung 6 als Seite im neuen Stil
Files:
- Create:
public/neu/rechtliches/abo-bedingungen.html(Basis:cp public/abo-bedingungen.html …, das Original bleibt unberührt) - Test:
test/aboBedingungenV6.test.js(erweitern)
Interfaces:
-
Consumes:
PFLICHT(Task 4),FASSUNGEN(Task 1: Adresse und PDF-Pfad der v6), das Archivpublic/vertragsunterlagen/v5/abo-bedingungen.html(Task 2) als Vergleichsbasis. -
Produces: die Seite, die
termsDocFor('2026-10-16-v6').urlliefert. Neue Absätze stehen als<li>…</li>(vier Leerzeichen, eine Zeile); der neue Absatz (8) in § 3 als<h3>(8) Funktionen</h3>mit zwei<p>; die Übersichtszeile als<tr><td>Funktionen</td><td>…</td></tr>nach der Zeile Wartungs-Abo. -
Step 1: Failing tests (an die Datei aus Task 4 anhängen; Hilfen
stripundbetweenaustest/legalTexts.test.js:17-36kopieren, kein Import möglich)
const V6 = 'public/neu/rechtliches/abo-bedingungen.html', V5A = 'public/vertragsunterlagen/v5/abo-bedingungen.html'
const abschnitt = (html, von, bis) => {
const a = von ? html.indexOf(`id="${von}"`) : 0
assert.ok(a >= 0, `Anker #${von}`)
const b = bis ? html.indexOf(`id="${bis}"`, a + 1) : html.length
assert.ok(b > a, `Anker #${bis}`)
return strip(html.slice(a, b))
}
test('v6-Seite: gleiche Anker wie Fassung 5, Absatzzahlen der geänderten Paragrafen', () => {
const h = lies(V6), ids = (x) => [...x.matchAll(/ id="([^"]+)"/g)].map((m) => m[1])
assert.deepEqual(ids(h), ids(lies(V5A)))
const n = (von, bis) => [...h.slice(h.indexOf(`id="${von}"`), h.indexOf(`id="${bis}"`)).matchAll(/^ <li>([\s\S]*?)<\/li>$/gm)].length
assert.deepEqual([['p1', 'p2'], ['p2', 'p3'], ['p4', 'p5'], ['p6', 'p7'], ['wechsel', 'zusatzspeicher'], ['p11', 'p12'], ['p14', 'p15']].map(([a, b]) => n(a, b)), [4, 6, 9, 6, 6, 4, 2])
assert.equal((h.slice(h.indexOf('id="p3"'), h.indexOf('id="p4"')).match(/<h3>\(\d\)/g) ?? []).length, 8)
})
test('v6-Seite: Version, Stand, Entwurfsmarke und PDF-Link passen zu termsVersionNeu', () => {
const m = /^(\d{4})-(\d{2})-(\d{2})-v6$/.exec(config.abo.termsVersionNeu); assert.ok(m)
const h = lies(V6), [, y, mo, d] = m
assert.ok(strip(h).includes(`Version 6 · Stand ${d}.${mo}.${y}`))
assert.ok(h.includes(`content: "Version 6 · Stand ${d}.${mo}.${y}"`), 'Kopfzeile im Druck')
assert.ok(!strip(h).includes('Version 5'))
assert.equal(strip(h).includes('Entwurf – Prüfung durch Kanzlei offen'), ENTWURF)
assert.ok(h.includes('href="/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf"') && !h.includes('href="/WEBklar-Abo-Vertragsunterlagen.pdf"'))
})
test('v6-Seite: jede Pflichtstelle steht im Wortlaut an ihrem Platz', () => {
const h = lies(V6)
for (const p of PFLICHT) for (const t of p.texte) assert.ok(abschnitt(h, p.von, p.bis).includes(t), `${p.stelle}: ${t.slice(0, 50)}`)
assert.ok(strip(h).includes(`Funktionen ${PFLICHT[0].texte[0]}`), 'Zeile „Funktionen“ in der Übersichtstabelle')
})
test('v6-Seite: Widerrufsbelehrung und Muster-Formular wortgleich mit Fassung 5, Abo-Preise unverändert, Funktionen ohne Preis', () => {
const h = lies(V6), v5 = lies(V5A)
const muster = (x) => between(abschnitt(x, 'widerruf', 'formular'), 'Widerrufsrecht Sie haben das Recht', 'Ende der Widerrufsbelehrung')
assert.equal(muster(h), muster(v5))
const formular = (x) => between(abschnitt(x, 'formular', 'widerruf-hinweise'), 'Muster-Widerrufsformular', '(*) Unzutreffendes streichen.')
assert.equal(formular(h), formular(v5))
for (const s of ['20,00 €', '50,00 €', '1,00 €', '100 € pro Stunde', 'zwölf Monaten']) assert.ok(strip(h).includes(s), s)
assert.doesNotMatch(abschnitt(h, 'p3', 'p4').split('(8) Funktionen')[1], /€/)
})
test('v6-Seite: neues Aussehen, keine alten Farben, Druckkopf', () => {
const style = lies(V6).match(/<style>([\s\S]*?)<\/style>/)[1]
for (const t of ['#1a1f36', '#5e6678', '#ebeef1', '#d8dde3', '#f8f9fb', '#111', '-apple-system', 'color-scheme: light']) assert.ok(style.includes(t), t)
assert.doesNotMatch(style, /#6d1fd1|#9d3dfe|gradient|prefers-color-scheme|backdrop-filter|#f7f6f3/)
})
Ein weiterer Test querverweise übernimmt die Logik von test/legalTexts.test.js:227-262 (Absatzzahlen je Paragraf, § n Abs. m, Leistungsbeschreibung A/B Nr. N) mit lies(V6); erwartet 16 Paragrafen, mindestens 15 Verweise, alle treffen.
- Step 2: Rot
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-recht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/aboBedingungenV6.test.js
Expected: FAIL (ENOENT auf die v6-Seite).
- Step 3: Seite erzeugen
mkdir -p public/neu/rechtliches, cp public/abo-bedingungen.html public/neu/rechtliches/abo-bedingungen.html, dann in der Kopie: (a) den <style>-Block durch den neuen Stil ersetzen (Werte aus den Global Constraints der Übersicht: Fläche #fff, Text #1a1f36, Zweittext #5e6678, Linien #ebeef1/#d8dde3, Fläche #f8f9fb, Akzent #111, Systemschrift, Radien 6 und 12 px, .box als Abschnitt mit Haarlinien statt Karte, .offen als orange Pille #fcedb9/#a82c00, Druckregeln wie Fassung 5 mit Kopfzeile Version 6 · Stand 16.10.2026); (b) Meta-Zeile auf „Version 6 · Stand 16.10.2026“, darunter <p class="hinweis offen">Entwurf – Prüfung durch Kanzlei offen</p>, PDF-Link auf /neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf; (c) die Pflichtsätze aus PFLICHT an ihre Stellen (Abs. 2 c ersetzt den bisherigen Buchstaben c, § 2 Abs. 6 hängt an den bestehenden Absatz an, die Zeile der Anlage 3 kopiert die Hetzner-Zeile der Postfächer und ändert die zwei Zellen). Sonst keine Textänderung.
- Step 4: Grün
Run: wie Step 2. Expected: PASS. Danach git diff --no-index --stat public/abo-bedingungen.html public/neu/rechtliches/abo-bedingungen.html ansehen: nur die genannten Stellen weichen ab.
- Step 5: Commit
git add public/neu/rechtliches/abo-bedingungen.html test/aboBedingungenV6.test.js
git commit -m "Abo-Bedingungen Fassung 6: Seite im neuen Stil (Entwurf)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 6: Generator: neuer Stil und Satz funktionen6
Files:
- Modify:
scripts/build-legal-html.mjs(neue HülleSTYLE_NEU,renderliestmeta.stil,funktionen6Dokumente, Eintrag inSAETZE, Option--nur=,mkdirSyncdes Zielordners, dritte Zeile der Usage-Meldung; die ersten zwei Zeilen der Meldung bleiben, Pintest/buildLegalHtml.test.js:436) - Test:
test/legalV6Build.test.js(neu; Kopf wietest/buildLegalHtml.test.js:1-30)
Interfaces:
-
Consumes:
FUNKTIONEN_FASSUNG_V6,FUNKTIONEN_UNTERLAGEN_V6(Task 3). -
Produces:
export function funktionen6Dokumente() // 10 × { doc: '<key>-lb|<key>-avv', md: 'docs/rechtliches/<slug>-v6.md', html: 'public/neu/rechtliches/<slug>-2026-10-16.html', meta: { fassung, titel, seitenkopf, nav, stil: 'neu' } } render(md, { …, stil: 'neu' }) // ohne stil bleibt die Ausgabe bytegleich zu heute lauf(['funktionen6', '--check'|'--nur=<doc,doc>'], { root })Reihenfolge der
doc:news-lb, termine-lb, termine-avv, shop-lb, shop-avv, unterkunft-lb, unterkunft-avv, zeiten-lb, analyse-lb, analyse-avv. SeitentitelLeistungsbeschreibung <Name>bzw.Auftragsverarbeitungsvertrag <Name>, KopfzeileLeistungsbeschreibung <Name>bzw.AVV <Name>; Namen: News-Pinnwand, Terminplaner, Webshop, Buchungskalender, Öffnungszeiten live, Website-Analyse. Navigation:← Kundenportal(/dashboard.html),Abo-Bedingungen(/neu/rechtliches/abo-bedingungen.html), die Dokumente der Funktion,Datenschutzerklärung(/datenschutz.html),Impressum(https://webklar.com/impressum). -
Step 1: Failing tests (Kopf wie
test/buildLegalHtml.test.js:1-30mituseTestEnv; dazuconst { render, lauf, funktionen6Dokumente } = await import('../scripts/build-legal-html.mjs'))
const F = '2026-10-16'
const SLUGS = ['leistungsbeschreibung-news', 'leistungsbeschreibung-terminplaner', 'avv-terminplaner', 'leistungsbeschreibung-webshop', 'avv-webshop', 'leistungsbeschreibung-unterkunft', 'avv-unterkunft', 'leistungsbeschreibung-zeiten', 'leistungsbeschreibung-website-analyse', 'avv-website-analyse']
test('funktionen6Dokumente: zehn Seiten unter public/neu/rechtliches, Quellen *-v6.md, Navigation im Vorschaupfad', () => {
const d = funktionen6Dokumente()
assert.deepEqual(d.map((x) => x.doc), ['news-lb', 'termine-lb', 'termine-avv', 'shop-lb', 'shop-avv', 'unterkunft-lb', 'unterkunft-avv', 'zeiten-lb', 'analyse-lb', 'analyse-avv'])
assert.deepEqual(d.map((x) => [x.md, x.html]), SLUGS.map((s) => [`docs/rechtliches/${s}-v6.md`, `public/neu/rechtliches/${s}-${F}.html`]))
for (const x of d) {
assert.deepEqual([x.meta.stil, x.meta.fassung], ['neu', F])
const hrefs = x.meta.nav.map((l) => l.href)
assert.ok(hrefs.includes('/neu/rechtliches/abo-bedingungen.html') && hrefs.includes(`/${x.html.slice('public/'.length)}`), x.doc)
assert.ok(hrefs.every((h) => !h.startsWith('/rechtliches/')), 'keine Verweise auf eingefrorene Seiten')
}
})
test('render mit stil neu: weiß und schwarz ohne Lila und Verlauf; ohne stil bleibt die alte Hülle', () => {
const meta = { fassung: F, titel: 'Prüfdokument', seitenkopf: 'Prüfdokument', nav: [{ href: '/dashboard.html', text: 'Portal' }] }
const md = `# Titel\n\n**Fassung ${F}** · Entwurf – Prüfung durch Kanzlei offen\n\n## 1. Geltung\n\n(1) Text.\n`
const neu = render(md, { ...meta, stil: 'neu' }), alt = render(md, meta)
for (const t of ['#1a1f36', '#ebeef1', '#f8f9fb', '#111', '-apple-system']) assert.ok(neu.includes(t), t)
assert.doesNotMatch(neu, /#6d1fd1|gradient|#f7f6f3/)
assert.ok(alt.includes('#6d1fd1') && alt.includes('#f7f6f3'))
assert.equal(neu.replace(/<style>[\s\S]*?<\/style>/, ''), alt.replace(/<style>[\s\S]*?<\/style>/, ''), 'nur die Hülle unterscheidet sich')
assert.throws(() => render(md, { ...meta, stil: 'dunkel' }), /stil/)
})
test('lauf funktionen6: schreibt in neuen Ordner, --check, --nur, falscher Aufruf', () => {
const dir = mkdtempSync(join(tmpdir(), 'legalv6-'))
try {
mkdirSync(join(dir, 'docs/rechtliches'), { recursive: true })
for (const s of SLUGS) writeFileSync(join(dir, `docs/rechtliches/${s}-v6.md`), `# T\n\n**Fassung ${F}** · Entwurf\n\nText.\n`)
const z = []; const ruf = (...a) => lauf(a, { root: dir, log: (x) => z.push(x), fehler: (x) => z.push(x) })
assert.equal(ruf('funktionen6', '--check'), 1)
assert.equal(ruf('funktionen6', '--nur=avv-webshop,news-lb'), 0)
assert.deepEqual(z.filter((x) => x.startsWith('->')), [`-> public/neu/rechtliches/leistungsbeschreibung-news-${F}.html`, `-> public/neu/rechtliches/avv-webshop-${F}.html`])
assert.equal(ruf('funktionen6'), 0); assert.equal(ruf('funktionen6', '--check'), 0)
writeFileSync(join(dir, 'docs/rechtliches/avv-webshop-v6.md'), `# T\n\n**Fassung ${F}** · Entwurf\n\nGeändert.\n`)
z.length = 0; assert.equal(ruf('funktionen6', '--check'), 1)
assert.deepEqual(z.filter((x) => x.startsWith('ABWEICHT')), [`ABWEICHT public/neu/rechtliches/avv-webshop-${F}.html`])
assert.equal(ruf('funktionen6', '--schreiben'), 2); assert.equal(ruf('shop', '--nur=x'), 2); assert.equal(ruf('funktionen6', '--nur=gibt-es-nicht'), 2)
} finally { rmSync(dir, { recursive: true, force: true }) }
})
- Step 2: Rot
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-recht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/legalV6Build.test.js
Expected: FAIL (funktionen6Dokumente fehlt).
- Step 3: Umsetzen
STYLE_NEU(seitenkopf) mit denselben Tokens wie die Abo-Seite aus Task 5 (Markup der Seite bleibt gleich). render prüft meta.stil (undefined oder 'neu', sonst Fehler mit dem Wort „stil“). funktionen6Dokumente baut die Liste aus FUNKTIONEN_UNTERLAGEN_V6 (slug aus der Adresse). lauf: Option --nur= nur mit funktionen6 (sonst Exit 2), unbekannte Doc-Namen Exit 2; vor dem Schreiben mkdirSync(dirname(ziel), { recursive: true }); Usage um eine dritte Zeile node scripts/build-legal-html.mjs funktionen6 [--check] [--nur=<doc,doc>] ergänzen.
- Step 4: Grün, bestehende Generator-Tests laufen mit
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-recht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test --test-concurrency=1 test/legalV6Build.test.js test/buildLegalHtml.test.js
Expected: PASS.
Dann einmal die Kommandozeile ohne Test-Umgebung: node --max-old-space-size=256 scripts/build-legal-html.mjs funktionen6 --check. Erwartet ist Exit 1 mit einer Meldung ENOENT … -v6.md (die Markdown-Quellen fehlen noch), nicht mit einem Import-Fehler. Bricht der Import ab, weil products.js über schalter.js die Sitzungs-Module nachzieht, ist das ein Fehler dieses Tasks und vor dem Commit zu beheben, ohne den Vertrag V1 zu ändern.
- Step 5: Commit
git add scripts/build-legal-html.mjs test/legalV6Build.test.js
git commit -m "Rechtstexte: Generator-Satz funktionen6 im neuen Stil" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 7: Leistungsbeschreibungen Terminplaner, News-Pinnwand, Öffnungszeiten live
Files:
- Create:
docs/rechtliches/leistungsbeschreibung-terminplaner-v6.md(Basisleistungsbeschreibung-terminplaner.md),leistungsbeschreibung-news-v6.md(Basispublic/vertragsunterlagen/funktionen-leistungsbeschreibung.html),leistungsbeschreibung-zeiten-v6.md(Basispublic/vertragsunterlagen/zeiten-leistungsbeschreibung.html), alle unterdocs/rechtliches/ - Create (erzeugt):
public/neu/rechtliches/leistungsbeschreibung-{terminplaner,news,zeiten}-2026-10-16.html - Test:
test/funktionenRechtV6.test.js(neu)
Interfaces:
-
Consumes:
funktionen6Dokumente,render(Task 6);FUNKTIONEN_UNTERLAGEN_V6(Task 3). -
Produces: die Bausteine
Bund die Hilfen des Testgerüsts, die Tasks 8 bis 10 erweitern (LISTE,LB,abschnitte). Fassungszeile jeder Datei:**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo. Nummern der Abschnitte bleiben wie in der Bestandsfassung, wo ein AVV auf sie verweist; Absätze, auf die verwiesen wird, behalten ihre Nummer. -
Step 1: Failing tests (Kopf:
useTestEnv,readFileSync,fileURLToPath,spawnSyncund die Importe ausscripts/build-legal-html.mjswie intest/legalV6Build.test.js;const ROOT = fileURLToPath(new URL('..', import.meta.url));const lies = (rel) => readFileSync(join(ROOT, rel), 'utf8');const ENTWURF = true)
const B = { // Bausteine; {N} = Nominativ, {G} = Genitiv der Partei (Kunde/Kunden, Händler/Händlers, Vermieter/Vermieters)
geltung: 'Diese Leistungsbeschreibung gilt, wenn der {N} 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 {N} die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat.',
voraussetzung: 'Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht.',
einrichtung: '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 {G}.',
projekt: 'Hat WEBklar die Funktion auf Wunsch des {G} 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.',
mehraufwand: 'Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des {G} in Textform aus; es gilt § 4 der Abo-Bedingungen.',
widerruf: ['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 {N} die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat.'],
ende: '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.', // alle Funktionen außer Öffnungszeiten live (R8: Tagesjob)
endeZeiten: '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.', // nur Öffnungszeiten live: wie das bestehende Zeiten-Modul, P6 (Mail K40) und P4 (Text Z2); kein Behalten gegen Entgelt (R8, mit P4/P6 abgestimmt)
}
const PARTEI = { kunde: { N: 'Kunde', G: 'Kunden' }, haendler: { N: 'Händler', G: 'Händlers' }, vermieter: { N: 'Vermieter', G: 'Vermieters' } }
const bau = (t, p) => (Array.isArray(t) ? t.map((x) => bau(x, p)) : t.replaceAll('{N}', PARTEI[p].N).replaceAll('{G}', PARTEI[p].G))
// „Haken“ der Gäste im Widget (Terminplaner) und „Widerrufsbelehrung für Gäste“ bleiben erlaubt; verboten sind die Wörter der alten Bestellung
const VERBOTEN = /€|per Haken|Haken diesem|Zahlungspflichtig|Einrichtungspreis|Gesamtpreis|Bestellung der Einrichtung|im Kundenportal bestellt|Einrichtung von Funktionen|ohne Wartungs-Abo|auch im Hosting-Abo|Hosting-Abo, Wartungs-Abo oder|Vertrag über den Betrieb (der|seiner) Website|solange WEBklar die Website|[a-zäöüß,;:] Sie\b|\bIhr(e[nmrs]?)?\b/ // „Sie“ am Satzanfang ist das Pronoun für die Funktion („Sie setzt ein laufendes Wartungs-Abo voraus“), erlaubt
const LISTE = ['termine', 'news', 'zeiten'] // Task 8 ergänzt shop, unterkunft; Task 9 analyse
const LB = { // frist = Satzteil, der die Löschfrist nennt (C12); pflicht = funktionsspezifischer Pflichtsatz
termine: { slug: 'leistungsbeschreibung-terminplaner', partei: 'kunde', alt: 'docs/rechtliches/leistungsbeschreibung-terminplaner.md', frist: '30 Tage nach dem Abschalten',
pflicht: 'Der Fragebogen fragt die Branche ab; wählt der Kunde den Gesundheitsbereich, ist der Terminplaner für ihn nicht verfügbar.' },
news: { slug: 'leistungsbeschreibung-news', partei: 'kunde', frist: '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.',
pflicht: '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.',
nr: ['1. Geltung', '2. Leistung', '3. Einrichtung', '4. Pflichten des Kunden', '5. Ende der Leistung', '6. Widerruf'] },
zeiten: { slug: 'leistungsbeschreibung-zeiten', partei: 'kunde', frist: '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.',
nr: ['1. Geltung', '2. Leistung', '3. Einbau und Freischaltung', '4. Einrichtung', '5. Nicht enthalten', '6. Pflichten des Kunden', '7. Verfügbarkeit und Aktualität', '8. Haftung', '9. Ende der Leistung', '10. Änderungen und Einstellung', '11. Widerruf', '12. Hinweise'] },
}
const abschnitte = (md) => md.split(/^## /m).slice(1).map((s) => ({ kopf: s.split('\n')[0], absaetze: s.split(/\n\n/).filter((b) => /^\(\d+\) /.test(b)) }))
test('Leistungsbeschreibungen v6: Fassungszeile, Bausteine, Pflichtsatz, Frist, keine Preise', () => {
for (const key of LISTE) {
const e = LB[key], md = lies(`docs/rechtliches/${e.slug}-v6.md`)
assert.match(md.split('\n')[2], new RegExp(`^\\*\\*Fassung 2026-10-16\\*\\*${ENTWURF ? ' · Entwurf – Prüfung durch Kanzlei offen' : ''} · Zusatzbedingungen zum Wartungs-Abo$`), key)
assert.doesNotMatch(md, VERBOTEN, key)
for (const t of ['geltung', 'voraussetzung', 'einrichtung', 'projekt', 'mehraufwand', key === 'zeiten' ? 'endeZeiten' : 'ende']) assert.ok(md.includes(bau(B[t], e.partei)), `${key}: Baustein ${t}`)
if (key === 'zeiten') assert.doesNotMatch(md, /Karenz|Behalten|spätestens am folgenden Tag/, 'Zeiten: 30 Tage Weiterlauf wie das bestehende Modul, kein Behalten, nicht die Abschaltung am Folgetag')
for (const t of bau(B.widerruf, e.partei)) assert.ok(md.includes(t), `${key}: Baustein widerruf`)
if (e.pflicht) assert.ok(md.includes(e.pflicht), `${key}: Pflichtsatz`)
assert.ok(md.includes(e.frist), `${key}: Frist`)
if (e.nr) assert.deepEqual(abschnitte(md).map((a) => a.kopf), e.nr, key)
}
})
test('Terminplaner v6: Überschriften wie Fassung 5 (Nr. 5 heißt „Einrichtung“), Absatzzahlen, auf die der AVV verweist', () => {
const v5 = abschnitte(lies(LB.termine.alt)), v6 = abschnitte(lies('docs/rechtliches/leistungsbeschreibung-terminplaner-v6.md'))
assert.deepEqual(v6.map((a) => a.kopf), v5.map((a) => (a.kopf === '5. Einrichtung und Preis' ? '5. Einrichtung' : a.kopf)))
assert.deepEqual(v6.map((a) => a.absaetze.length), v5.map((a) => (a.kopf.startsWith('10. ') ? 2 : a.absaetze.length)), 'Absatzzahlen wie Fassung 5, Nr. 10 hat die zwei Absätze des Bausteins widerruf')
assert.ok(v6[7].absaetze[0].includes(bau(B.ende, 'kunde')), 'Nr. 8 Abs. 1 ist das Ende (AVV Terminplaner Nr. 1 Abs. 2)')
})
test('Seiten der Gruppe sind genau render(Markdown)', () => {
for (const key of LISTE) {
const d = funktionen6Dokumente().find((x) => x.html.includes(`/${LB[key].slug}-2026-10-16.html`))
assert.equal(lies(d.html), render(lies(d.md), d.meta), `${d.html}: node scripts/build-legal-html.mjs funktionen6 --nur=${d.doc}`)
}
})
- Step 2: Rot
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-recht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/funktionenRechtV6.test.js
Expected: FAIL (ENOENT auf leistungsbeschreibung-terminplaner-v6.md).
- Step 3: Drei Dokumente schreiben
Gemeinsam: Fassungszeile wie oben, Anrede „der Kunde“, keine Preise, keine Links im Markdown (der Generator lehnt sie ab). Ersetzungen gegenüber der Basis:
- Terminplaner (aus
leistungsbeschreibung-terminplaner.md): Nr. 1 Abs. 1 =geltung+voraussetzung+ der Satz „Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Terminplaner.“; Nr. 4 Abs. 3 bekommt den Pflichtsatz angehängt; Nr. 5 heißt „Einrichtung“: Abs. 2 =einrichtung, Abs. 3 Dauer „meist 1–2 Wochen“ ohne „Bestellseite“, Abs. 4 =projekt, Abs. 5 =mehraufwand; Nr. 8 Abs. 1 =endeplus „WEBklar informiert den Kunden per E-Mail.“, Abs. 5 „… bricht WEBklar die Einrichtung ab; ein Vorschuss nach Nr. 5 Abs. 2 verfällt.“, Abs. 6 ohne Erstattung des Einrichtungspreises; Nr. 10 = die zwei Absätzewiderruf. Rest wortgleich. - News-Pinnwand (neu aus der HTML-Datei in dritter Person): Nr. 1 Geltung (
geltung,voraussetzung), Nr. 2 Leistung (Pinnwand, sechs Beiträge je Seite, Zettelfarbe und Anzeigedauer, Fotos verkleinert, Metadaten einschließlich GPS entfernt, Änderungen über den Einbau hinaus nicht enthalten), Nr. 3 Einrichtung (einrichtung, Dauer „meist 1–2 Wochen“,projekt,mehraufwand), Nr. 4 Pflichten (Pflichtsatz), Nr. 5 Ende (ende,frist, Löschen durch den Kunden jederzeit), Nr. 6 Widerruf (widerruf). - Öffnungszeiten live (aus der HTML-Datei, gleiche Nummern): Nr. 1 Abs. 1 =
geltung+voraussetzung(statt „Kunden ohne Wartungs-Abo, die sie für 30 € bestellen“), Abs. 2 „Die Funktion ist im Wartungs-Abo ohne Aufpreis enthalten.“ mit dem Satz zu Leistungsbeschreibung B Nr. 3 und dem Datenschutz-Absatz nach Nr. 6 Abs. 4 (statt „freiwilliges Zusatzangebot“), Abs. 3 ohne „Für Kunden ohne Wartungs-Abo gilt sie allein“ (die Haftungsregeln in Nr. 8 bleiben ausgeschrieben, damit sie auch für Abos früherer Fassungen gelten); Nr. 4 „Einrichtung“ statt „Preis“ (einrichtung,projekt,mehraufwand), Nr. 9 Abs. 2 =endeZeiten+frist(nichtende: Öffnungszeiten live läuft nach dem Abo-Ende 30 Tage unverändert weiter wie im bestehenden Modul, in P4 (Text Z2) und P6 (Mail K40); der Satz der Basis über das Behalten für 30 € entfällt ersatzlos; Abs. 4 verliert ersatzlos „und die Funktion für einmalig 30 € wieder einschalten“ und den Satz „Nach der Löschung ist eine neue Bestellung nach Nr. 4 nötig.“, Abs. 3 verweist weiter auf Abs. 4), Nr. 10 Abs. 5 ohne Preisbezug, Nr. 11 =widerruf; Haftung (Nr. 8) und Pflichten (Nr. 6) wortgleich in dritter Person.
Dann node --max-old-space-size=256 scripts/build-legal-html.mjs funktionen6 --nur=termine-lb,news-lb,zeiten-lb.
- Step 4: Grün
Run: wie Step 2. Expected: PASS.
- Step 5: Commit
git add test/funktionenRechtV6.test.js docs/rechtliches/leistungsbeschreibung-terminplaner-v6.md docs/rechtliches/leistungsbeschreibung-news-v6.md docs/rechtliches/leistungsbeschreibung-zeiten-v6.md
git add public/neu/rechtliches/leistungsbeschreibung-terminplaner-2026-10-16.html public/neu/rechtliches/leistungsbeschreibung-news-2026-10-16.html public/neu/rechtliches/leistungsbeschreibung-zeiten-2026-10-16.html
git commit -m "Leistungsbeschreibungen Fassung 6: Terminplaner, News-Pinnwand, Öffnungszeiten live (Entwurf)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 8: Leistungsbeschreibungen Webshop und Buchungskalender
Files:
- Create:
docs/rechtliches/leistungsbeschreibung-webshop-v6.md(Basisleistungsbeschreibung-webshop.md),leistungsbeschreibung-unterkunft-v6.md(Basisleistungsbeschreibung-unterkunft.md) - Create (erzeugt):
public/neu/rechtliches/leistungsbeschreibung-{webshop,unterkunft}-2026-10-16.html - Test:
test/funktionenRechtV6.test.js(erweitern)
Interfaces:
-
Consumes:
B,bau,LISTE,LB,abschnitte,VERBOTEN(Task 7). -
Produces: dieselbe Abschnittsfolge wie die Bestandsfassung (die AVV verweisen auf Nr. 10 Abs. 1 und Nr. 5 Abs. 6 und 9 des Webshops); Nr. 9 heißt „Einrichtung, Abrechnung und Widerruf“.
-
Step 1: Failing tests (
LISTEum'shop', 'unterkunft'erweitern,LBergänzen, ein Test anhängen)
LB.shop = { slug: 'leistungsbeschreibung-webshop', partei: 'haendler', alt: 'docs/rechtliches/leistungsbeschreibung-webshop.md', frist: '90 Tage nach dem Abschalten',
pflicht: '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.',
nurBetrieb: 'Der Webshop steht nur Kunden zur Verfügung, deren Kundenart „Betrieb“ ist.' }
LB.unterkunft = { slug: 'leistungsbeschreibung-unterkunft', partei: 'vermieter', alt: 'docs/rechtliches/leistungsbeschreibung-unterkunft.md', frist: '90 Tage nach dem Abschalten',
pflicht: '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.' }
test('Webshop und Buchungskalender v6: Überschriften wie Fassung 5, Nr. 9 neu benannt, nur Wartungs-Abo, Ende mit dem Abo', () => {
for (const key of ['shop', 'unterkunft']) {
const e = LB[key], v5 = abschnitte(lies(e.alt)), md = lies(`docs/rechtliches/${e.slug}-v6.md`), v6 = abschnitte(md)
assert.deepEqual(v6.map((a) => a.kopf), v5.map((a) => (a.kopf.startsWith('9. Preis') ? '9. Einrichtung, Abrechnung und Widerruf' : a.kopf)), key)
const n9 = v6.find((a) => a.kopf.startsWith('9. '))
assert.equal(n9.absaetze.length, 5, `${key}: Nr. 9 hat fünf Absätze (1 Einrichtung, 2 Dauer, 3 Projektstart, 4 Mehraufwand, 5 Widerruf)`)
assert.ok(n9.absaetze[2].includes(bau(B.projekt, e.partei)) && n9.absaetze[4].includes(bau(B.widerruf, e.partei)[0]), key)
assert.ok(n9.absaetze[0].includes('keine laufende Gebühr'), key)
assert.ok([...md.matchAll(/Nr\. 9 Abs\. (\d+)/g)].every((m) => Number(m[1]) <= 5), `${key}: Verweise auf Nr. 9 treffen`)
}
assert.ok(lies(`docs/rechtliches/${LB.shop.slug}-v6.md`).includes(LB.shop.nurBetrieb))
})
Die allgemeine Prüfung aus Task 7 (Bausteine, Pflichtsatz, Frist, keine Preise) läuft über die erweiterte LISTE mit; für shop und unterkunft gilt sie mit partei Händler bzw. Vermieter. Der Test „Seiten sind genau render(Markdown)“ ebenfalls.
- Step 2: Rot
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-recht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/funktionenRechtV6.test.js
Expected: FAIL (ENOENT auf leistungsbeschreibung-webshop-v6.md).
- Step 3: Zwei Dokumente schreiben
Basis wie in Task 7, Fassungszeile · Zusatzbedingungen zum Wartungs-Abo. Ersetzungen: Nr. 1 Abs. 1: der Satz „Die Leistungsbeschreibung ist eine Zusatzleistung zum Vertrag über den Betrieb der Website: …“ wird durch voraussetzung ersetzt; Nr. 1 Abs. 2 (Vereinbarung durch Bestellung und Haken) = geltung, dazu „Einschalten kann der Händler erst, nachdem WEBklar seine Anfrage … geprüft und freigegeben hat (Nr. 2 Abs. 1)“ (Buchungskalender: „angenommen“, mit dem Satz zum Projektstart des Vermieters aus der Basis); Nr. 1 Abs. 4 ohne den Satz „Das gilt auch im Hosting-Abo.“; Nr. 5 Abs. 1 beginnt mit „Der Händler hat ein laufendes Wartungs-Abo“ bzw. „Der Vermieter …“, danach der Pflichtsatz (Webshop zusätzlich nurBetrieb); Nr. 9: (1) einrichtung plus „Für den Betrieb … berechnet WEBklar nichts weiter; es gibt keine laufende Gebühr und keine Beteiligung am Umsatz“, (2) Dauer meist 2 bis 3 Wochen ab dem Einschalten (Buchungskalender 1 bis 2, wie in der Basis), (3) projekt, (4) mehraufwand, (5) widerruf als ein Absatz aus beiden Teilen; Nr. 10: Abs. 1 = ende plus „WEBklar informiert den Händler vorher per E-Mail“, Abs. 5 „Beginnt innerhalb der 90 Tage wieder ein Wartungs-Abo …“, Abs. 6 „… bricht WEBklar die Einrichtung ab; ein Vorschuss nach Nr. 9 Abs. 1 verfällt“, Abs. 7 verweist auf „Nr. 9 Abs. 5“; Verweise auf „Nr. 9 Abs. 6“ im Text auf Abs. 5 ändern. Dann node --max-old-space-size=256 scripts/build-legal-html.mjs funktionen6 --nur=shop-lb,unterkunft-lb.
- Step 4: Grün
Run: wie Step 2. Expected: PASS.
- Step 5: Commit
git add test/funktionenRechtV6.test.js docs/rechtliches/leistungsbeschreibung-webshop-v6.md docs/rechtliches/leistungsbeschreibung-unterkunft-v6.md public/neu/rechtliches/leistungsbeschreibung-webshop-2026-10-16.html public/neu/rechtliches/leistungsbeschreibung-unterkunft-2026-10-16.html
git commit -m "Leistungsbeschreibungen Fassung 6: Webshop und Buchungskalender (Entwurf)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 9: Website-Analyse im Hosting-Abo: Leistungsbeschreibung und AVV
Files:
- Create:
docs/rechtliches/leistungsbeschreibung-website-analyse-v6.md(Basisleistungsbeschreibung-website-analyse.md),docs/rechtliches/avv-website-analyse-v6.md(Basisavv-website-analyse.md) - Create (erzeugt):
public/neu/rechtliches/leistungsbeschreibung-website-analyse-2026-10-16.html,public/neu/rechtliches/avv-website-analyse-2026-10-16.html - Test:
test/funktionenRechtV6.test.js(erweitern)
Interfaces:
-
Consumes:
abschnitte,lies,VERBOTEN(Task 7). Die Analyse hat eigene BausteineBA, weil sie in beiden Abos steckt und keine Einrichtung berechnet (C6, C10). -
Produces: Texte, die dem Verhalten von P5 (Eignungsprüfung
analyse.js:45,60) und P6 (Abgleich, Mail K19) entsprechen. Die AVV-ErsetzungstabelleAVVdieses Tasks erweitert Task 10. -
Step 1: Failing tests
const BA = {
voraussetzung: 'Die Funktion ist in beiden Abos enthalten. Sie setzt ein laufendes Hosting-Abo oder Wartungs-Abo voraus.',
einrichtung: 'Die Einrichtung der Website-Analyse ist für den Kunden kostenlos. WEBklar rechnet sie weder auf ein Stundenkontingent an noch berechnet WEBklar sie gesondert.',
ende: '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.',
widerruf: 'Das Einschalten der Website-Analyse ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen.',
}
test('Website-Analyse v6 (Leistungsbeschreibung): in beiden Abos, Einrichtung kostenlos, Ende mit dem Abo, Zielgruppenregel bleibt', () => {
const md = lies('docs/rechtliches/leistungsbeschreibung-website-analyse-v6.md')
assert.match(md.split('\n')[2], /^\*\*Fassung 2026-10-16\*\* · (Entwurf – Prüfung durch Kanzlei offen · )?Zusatzbedingungen zum Hosting-Abo und zum Wartungs-Abo$/)
assert.doesNotMatch(md, /€|per Haken|[a-zäöüß,;:] Sie\b|Im Hosting-Abo ist die Website-Analyse nicht enthalten|Sie ist im Wartungs-Abo ohne Aufpreis|im Kundenportal bestellt/)
assert.ok(md.includes(B.geltung.replaceAll('{N}', 'Kunde')))
for (const t of Object.values(BA)) assert.ok(md.includes(t), t.slice(0, 40))
for (const s of ['Sie ist im Hosting-Abo und im Wartungs-Abo ohne Aufpreis enthalten.', 'Der Kunde hat ein laufendes Hosting-Abo oder Wartungs-Abo.',
'Der Kunde kann das Abo dann innerhalb von 30 Tagen nach der Mitteilung zum Zeitpunkt der Einstellung kündigen',
'Weil für die Website-Analyse kein Aufpreis berechnet wird, ändert ihre Einstellung den Preis des Abos nicht.',
'Richtet sich die Website auch gezielt an Kinder unter 16 Jahren']) assert.ok(md.includes(s), s) // letzter Satz: Zielgruppenregel (Nr. 3 Abs. 4) bleibt
assert.deepEqual(abschnitte(md).map((a) => a.kopf), abschnitte(lies('docs/rechtliches/leistungsbeschreibung-website-analyse.md')).map((a) => a.kopf))
})
LB.analyse = { slug: 'leistungsbeschreibung-website-analyse', partei: 'kunde' } // nur für die Querverweise in Task 10
const AVV = { // [alt, neu] je Datei; der Rest ab "## 1." ist sonst wortgleich mit der Bestandsfassung
analyse: [['spätestens mit dem Ende des Wartungs-Abos; den Messdienst schaltet WEBklar dann spätestens am folgenden Tag ab', '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'],
['(Ausschalten oder Ende des Wartungs-Abos)', '(Ausschalten oder Ende des Abos, das sie enthält)'],
['Das Wartungs-Abo bleibt davon unberührt.', 'Das Abo bleibt davon unberührt.']],
}
const KOPF_AVV = ENTWURF ? '**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen' : '**Fassung 2026-10-16**'
const AVV_FUNKTION = { termine: 'Terminplaner', shop: 'Webshop', unterkunft: 'Buchungskalender', analyse: 'Website-Analyse' }
const avvVorbemerkung = (funktion, fassung) => `Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „${funktion}“ einschaltet und die dort angezeigte Zeile „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung ${fassung})“ mit dem Klick auf „Einschalten“ bestätigt; diesem Vertrag und der Leistungsbeschreibung „${funktion}“ („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.`
function pruefeAvv(key, slug) {
const v5 = lies(`docs/rechtliches/${slug}.md`), v6 = lies(`docs/rechtliches/${slug}-v6.md`)
assert.equal(v6.split('\n')[2], KOPF_AVV)
assert.ok(v6.includes(`(1) ${avvVorbemerkung(AVV_FUNKTION[key], '2026-10-16')}`))
assert.doesNotMatch(v6, /per Haken|Mit der Bestellung weist|Vertrag über den Betrieb der Website|keine Bestellung geworden/)
let erwartet = v5.replace(/^\*\*Fassung \d{4}-\d{2}-\d{2}\*\*.*$/m, KOPF_AVV)
erwartet = erwartet.replace(/^\(1\) Der Kunde schließt diesen Vertrag[^\n]*$/m, `(1) ${avvVorbemerkung(AVV_FUNKTION[key], '2026-10-16')}`)
for (const [alt, neu] of AVV[key]) { assert.ok(erwartet.includes(alt), `Bestand enthält: ${alt.slice(0, 40)}`); erwartet = erwartet.replaceAll(alt, neu) }
assert.equal(v6, erwartet, `${slug}-v6.md = Bestandsfassung mit genau diesen Ersetzungen`)
}
test('AVV Website-Analyse v6', () => { pruefeAvv('analyse', 'avv-website-analyse') })
test('Analyse-Seiten sind genau render(Markdown)', () => {
for (const doc of ['analyse-lb', 'analyse-avv']) {
const d = funktionen6Dokumente().find((x) => x.doc === doc)
assert.equal(lies(d.html), render(lies(d.md), d.meta), d.html)
}
})
Die Fassungszeile der Bestandsdatei wird im Test durch KOPF_AVV ersetzt, weil nur sie die Entwurfsmarke trägt; sonst gilt Gleichheit bis auf die Tabelle AVV.
- Step 2: Rot
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-recht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/funktionenRechtV6.test.js
Expected: FAIL (ENOENT auf leistungsbeschreibung-website-analyse-v6.md).
- Step 3: Zwei Dokumente schreiben
Leistungsbeschreibung (Fassungszeile · Zusatzbedingungen zum Hosting-Abo und zum Wartungs-Abo): Nr. 1 Abs. 1 = geltung mit BA.voraussetzung, Abs. 2 und 3 unverändert, neuer Abs. 4 = BA.widerruf; Nr. 2 Abs. 1 „Sie ist im Hosting-Abo und im Wartungs-Abo ohne Aufpreis enthalten.“ plus BA.einrichtung; Nr. 3 Abs. 1 „Der Kunde hat ein laufendes Hosting-Abo oder Wartungs-Abo.“; Nr. 7 Abs. 1 = BA.ende plus Ausschalten durch den Kunden, Nr. 8 Abs. 4 „Der Kunde kann das Abo dann …“ und Abs. 5 „… ändert ihre Einstellung den Preis des Abos nicht.“ (die Sätze der Tests). AVV: Bestandsdatei kopieren, Fassungszeile KOPF_AVV, die drei Ersetzungen aus AVV.analyse, Vorbemerkung Abs. 1 aus avvVorbemerkung. Dann node --max-old-space-size=256 scripts/build-legal-html.mjs funktionen6 --nur=analyse-lb,analyse-avv.
- Step 4: Grün
Run: wie Step 2. Expected: PASS.
- Step 5: Commit
git add test/funktionenRechtV6.test.js docs/rechtliches/leistungsbeschreibung-website-analyse-v6.md docs/rechtliches/avv-website-analyse-v6.md public/neu/rechtliches/leistungsbeschreibung-website-analyse-2026-10-16.html public/neu/rechtliches/avv-website-analyse-2026-10-16.html
git commit -m "Website-Analyse Fassung 6: Hosting- und Wartungs-Abo, Leistungsbeschreibung und AVV (Entwurf)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 10: AVV Terminplaner, Webshop und Buchungskalender
Files:
- Create:
docs/rechtliches/avv-terminplaner-v6.md,avv-webshop-v6.md,avv-unterkunft-v6.md(Basis: die gleichnamigen Bestandsdateien) - Create (erzeugt):
public/neu/rechtliches/avv-{terminplaner,webshop,unterkunft}-2026-10-16.html - Test:
test/funktionenRechtV6.test.js(erweitern)
Interfaces:
-
Consumes:
pruefeAvv,AVV,AVV_FUNKTION,avvVorbemerkung(Task 9),abschnitte. -
Produces: die AVV-Struktur Option A (Entscheidung R10): eigene Verträge je Funktion, Anlage 1 bleibt der Rahmen. Querverweise zwischen AVV und Leistungsbeschreibung treffen.
-
Step 1: Failing tests
AVV.termine = [['Mit der Bestellung weist der Kunde WEBklar an', 'Mit dem Einschalten weist der Kunde WEBklar an'], ['aus denen keine Bestellung geworden ist', 'aus denen die Funktion nicht eingeschaltet wurde']]
for (const [key, name] of [['shop', 'den Webshop'], ['unterkunft', 'den Buchungskalender']]) {
AVV[key] = [['Mit der Bestellung weist der Kunde WEBklar an', 'Mit dem Einschalten weist der Kunde WEBklar an'],
[`spätestens mit dem Ende des Vertrags, aufgrund dessen WEBklar die Website des Kunden betreibt; ${name} schaltet WEBklar dann zum Vertragsende ab`, `spätestens mit dem Ende des Wartungs-Abos; ${name} schaltet WEBklar dann spätestens am folgenden Tag ab`],
['oder der Vertrag über den Betrieb der Website beendet', 'oder das Wartungs-Abo beendet']]
}
test('AVV Terminplaner, Webshop, Buchungskalender v6: Bestandsfassung mit genau den Ersetzungen', () => {
pruefeAvv('termine', 'avv-terminplaner'); pruefeAvv('shop', 'avv-webshop'); pruefeAvv('unterkunft', 'avv-unterkunft')
})
test('Querverweise zwischen AVV und Leistungsbeschreibung treffen', () => {
for (const [key, avv] of [['termine', 'avv-terminplaner'], ['shop', 'avv-webshop'], ['unterkunft', 'avv-unterkunft'], ['analyse', 'avv-website-analyse']]) {
const lb = abschnitte(lies(`docs/rechtliches/${LB[key].slug}-v6.md`)), text = lies(`docs/rechtliches/${avv}-v6.md`)
for (const [ref, n, a] of text.matchAll(/Nr\. (\d+)(?: Abs\. (\d+))? der Leistungsbeschreibung/g)) {
const abschnitt = lb.find((x) => x.kopf.startsWith(`${n}. `)); assert.ok(abschnitt, `${avv}: ${ref}`)
if (a) assert.ok(Number(a) <= abschnitt.absaetze.length, `${avv}: ${ref}`)
}
}
})
test('alle zehn Seiten sind genau render(Markdown), --check meldet nichts', () => {
const lauf1 = spawnSync(process.execPath, ['--max-old-space-size=256', 'scripts/build-legal-html.mjs', 'funktionen6', '--check'], { cwd: ROOT, encoding: 'utf8', timeout: 60000 })
assert.equal(lauf1.status, 0, lauf1.stdout + lauf1.stderr)
assert.equal([...lauf1.stdout.matchAll(/^ok /gm)].length, 10)
})
- Step 2: Rot
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-recht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/funktionenRechtV6.test.js
Expected: FAIL (ENOENT auf avv-terminplaner-v6.md).
- Step 3: Drei AVV schreiben
Bestandsdatei kopieren, Fassungszeile **Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen, Vorbemerkung Abs. 1 nach avvVorbemerkung, dazu genau die Ersetzungen aus AVV[key]. Sonst nichts ändern. Dann node --max-old-space-size=256 scripts/build-legal-html.mjs funktionen6 --nur=termine-avv,shop-avv,unterkunft-avv.
- Step 4: Grün
Run: wie Step 2. Expected: PASS (alle Tests der Datei, einschließlich --check über zehn Seiten).
- Step 5: Commit
git add test/funktionenRechtV6.test.js docs/rechtliches/avv-terminplaner-v6.md docs/rechtliches/avv-webshop-v6.md docs/rechtliches/avv-unterkunft-v6.md public/neu/rechtliches/avv-terminplaner-2026-10-16.html public/neu/rechtliches/avv-webshop-2026-10-16.html public/neu/rechtliches/avv-unterkunft-2026-10-16.html
git commit -m "AVV Fassung 6: Terminplaner, Webshop, Buchungskalender mit Klick auf die Linkzeile (Entwurf)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 11: Datenschutz-Prüfpunkte, Entscheidungsliste, Kanzlei-Fragen und Ablauf
Files:
- Create:
docs/rechtliches/datenschutz-kundenansicht-pruefpunkte.md,docs/rechtliches/kundenansicht-entscheidungen.md - Test:
test/kundenansichtEntscheidungen.test.js(neu)
Interfaces:
- Consumes: Pflichtsätze und Dateien aus Tasks 3 bis 10.
- Produces: das Blatt, das der Controller mit dem Inhaber durchgeht (E20). Aufbau der Entscheidungsdatei: Kopf (Stand, „Entwurf zur Freigabe, keine Rechtsberatung“, Verweis auf
/root/reports/kundenansicht-neu-2026-10-15/bestand/recht.md§ 4),## Zu entscheidenmit Tabelle| Nr. | Punkt | Vorschlag | Alternative | Kanzlei-Frage |,## Fragen an die Kanzleimit je Frage### KQn: …und den vier Teilen**Frage.**,**Fundstelle im Text.**,**Unsere Annahme.**,**Alternative.**(Musterdocs/rechtliches/zeiten-kanzlei-fragen.md),## AVV-Struktur,## Ablauf nach der Freigabe. Prüfpunkte-Datei:D1bisD8mit Fundstellepublic/datenschutz.html:<Zeile>.
Inhalt der Tabelle (Vorschlag und Alternative ausformulieren, Kurzfassung hier):
| Nr. | Punkt | Vorschlag | Alternative | KQ |
|---|---|---|---|---|
| R1 | Fassungsnummer und Datum | Abo 2026-10-16-v6, Funktionen 2026-10-16; bei Freigabe auf den Freigabetag setzen |
Entwurfstag bleibt | – |
| R2 | Geltung für laufende Abos v3 bis v5 (C11) | nur neue Abschlüsse (§ 15); Funktionen über Linkzeile und Klick, auch mit Stundenkonto- und Vorschuss-Regel; keine Pflicht-Info-Mail, freiwillige Mail später | Info-Mail 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, Rest 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“: Projektkosten, Funktion läuft nur mit Abo | nur in den Leistungsbeschreibungen | KQ8 |
| R5 | Kundenart als Grundlage (E3, E4) | § 1 Abs. 2: hinterlegt nach Angabe, leer gilt als privat, maßgeblich ist der Zweck | Selbstauskunft des Kunden in den Bedingungen | KQ4 |
| R6 | Häkchen-Wortlaut Abo, Wechsel, Einrichtung | Texte aus WORTLAUT_V6.beginn ohne Paragrafenzitate; Betrieb setzt 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 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; 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 | KQ8 |
| R10 | AVV-Struktur | Option A: AVV je Funktion bleiben eigene Verträge, neue Vorbemerkung, Anlage 1 bleibt Rahmen (§ 14 Abs. 2) | B: Anlagen 4 bis 7 zu den Abo-Bedingungen; C: Rahmen-AVV mit einer Anlage je Funktion | KQ7 |
| R11 | Widerrufsbelehrung Funktionen | bleibt als Datei für den Bestand, die neuen Texte verlinken sie nicht | bleibt Anlage der Leistungsbeschreibung | KQ6 |
| R12 | Anrede der Rechtsdokumente (E15, 5.7) | bleiben im bisherigen Stil; nur Oberflächentexte „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) | Häkchen bleibt | KQ5 |
Fragen KQ1 bis KQ9: KQ1 Geltung der Fassung 6 und Info an laufende Abos (§ 15; Klick als Einbeziehung der Vorschuss-Regel); KQ2 Vorschuss (Transparenz, Verfall zugunsten des Kunden, Kontingent als Vorleistung); KQ3 Einrichtung in der Widerrufsfrist (eigener Vertrag oder Teil des Abos, Erlöschen nach § 356 Abs. 5 Nr. 2 bei einem Dauerschuldverhältnis, Bemessung des Wertersatzes); KQ4 Belastbarkeit der Kundenart (Frage 5 in recht.md § 4); KQ5 Häkchentexte ohne Normzitat (Frage 2), Wechsel nur mit Häkchen; KQ6 Belehrung per Link und PDF (Frage 4); KQ7 AVV per Klick auf die Linkzeile (Frage 3) und Struktur A/B/C; KQ8 Funktionen als Teil des Abos, Kopplung an das Abo-Ende, Fristen, Rückkehr ohne neue Zustimmung; KQ9 Ausschlüsse als Vertragsklausel (Gesundheitsbereich, ausgeschlossene Waren, Webshop nur Betrieb).
Prüfpunkte D1 bis D8 (je mit public/datenschutz.html:<Zeile> und kurzer Frage): D1 :120 Nachweis nennt „Klick auf ‚Zahlungspflichtig bestellen‘“ und die Stripe-Sitzung, Funktionsliste nur News und Terminplaner; D2 :119 Nachweis beim Abo: künftig Wortlaut der Zeilen und Kundenart; D3 :81 Kundenart als neues Datum im Kundenkonto (Zweck, Grundlage, Quelle, Dauer); D4 :121 Stripe-Satz gilt für Abo-Zahlungen, Einschalten löst keine Zahlung aus (:108, :114 Stripe Connect bleibt); D5 :95 Analyse „Kunden mit Wartungs-Abo“; D6 :98, :106, :112 Funktionsabschnitte „Kunden mit Wartungs-Abo“ und Ende mit dem Abo; D7 :131 Stundenkonto und Vorschuss als Abrechnungsdaten; D8 :123 Fristen-Sammelangabe (GET /api/fristen) ist nur Lesen vorhandener Daten.
- Step 1: Failing tests (Kopf:
test,assert,readFileSync;const lies = (rel) => readFileSync(new URL(../${rel}, import.meta.url), 'utf8');const ENTWURF = true)
const md = lies('docs/rechtliches/kundenansicht-entscheidungen.md'), dsg = lies('docs/rechtliches/datenschutz-kundenansicht-pruefpunkte.md')
test('Entscheidungsliste: R1 bis R15, je Punkt Vorschlag, Alternative und Kanzlei-Frage', () => {
const z = md.split('\n').filter((l) => /^\| R\d+ \|/.test(l)).map((l) => l.split('|').slice(1, -1).map((s) => s.trim()))
assert.deepEqual(z.map((r) => r[0]), Array.from({ length: 15 }, (_, i) => `R${i + 1}`))
for (const r of z) { assert.equal(r.length, 5); assert.ok(r.every(Boolean), r[0]); assert.match(r[4], /^(–|KQ\d+(, KQ\d+)*)$/, r[0]) }
const kq = [...md.matchAll(/^### (KQ\d+): .+$/gm)].map((m) => m[1])
assert.deepEqual(kq, Array.from({ length: 9 }, (_, i) => `KQ${i + 1}`))
for (const r of z) for (const id of r[4].match(/KQ\d+/g) ?? []) assert.ok(kq.includes(id), `${r[0]} nennt ${id}`)
assert.equal(md.includes('Entwurf zur Freigabe, keine Rechtsberatung'), ENTWURF)
})
test('R8 ist mit P4/P6 abgestimmt: Zeiten laufen 30 Tage weiter, die übrigen schalten am Folgetag ab, kein Behalten', () => {
const [, punkt, vorschlag, alternative] = md.split('\n').find((l) => l.startsWith('| R8 |')).split('|').slice(1, -1).map((s) => s.trim())
assert.match(punkt, /mit P4\/P6 abgestimmt/)
for (const s of ['spätestens am Folgetag', 'Öffnungszeiten live läuft nach dem Ende noch 30 Tage unverändert weiter', 'kein Behalten']) assert.ok(vorschlag.includes(s), s)
assert.doesNotMatch(vorschlag, /Karenz|entfällt/)
assert.ok(alternative.includes('Öffnungszeiten live schalten wie die übrigen spätestens am Folgetag ab'))
})
test('Kanzlei-Fragen haben vier Teile', () => {
for (const teil of md.split(/\n(?=### KQ\d+:)/).slice(1)) for (const k of ['**Frage.**', '**Fundstelle im Text.**', '**Unsere Annahme.**', '**Alternative.**']) assert.ok(teil.includes(k), `${teil.slice(0, 8)} ${k}`)
})
test('AVV-Struktur nennt A, B und C; Ablauf nach der Freigabe in fester Reihenfolge mit Schutzbefehlen', () => {
const avv = md.slice(md.indexOf('## AVV-Struktur'), md.indexOf('## Ablauf nach der Freigabe'))
for (const s of ['Option A', 'Option B', 'Option C', 'Anlage 1']) assert.ok(avv.includes(s), s)
const a = md.slice(md.indexOf('## Ablauf nach der Freigabe'))
const reihenfolge = ['Freigabe des Inhabers', 'ENTWURF = false', 'scripts/build-legal-html.mjs funktionen6', 'free -m', 'nice -n 10 scripts/build-legal-pdfs.sh abo6', 'nice -n 10 scripts/build-legal-pdfs.sh funktionen6', 'rechtliches-hochladen.mjs --probe', '--karten-anlegen', 'ein Aufruf je Dokument', '--freigabe=', '--pruefen']
const pos = reihenfolge.map((s) => { const i = a.indexOf(s); assert.ok(i >= 0, s); return i })
assert.deepEqual([...pos].sort((x, y) => x - y), pos, 'Reihenfolge')
assert.ok(a.includes('Datum der Fassungen') && a.includes('Entwurfsmarke'), 'Datum setzen und Marken entfernen')
})
test('Datenschutz-Prüfpunkte D1 bis D8 mit gültiger Fundstelle in public/datenschutz.html', () => {
assert.deepEqual([...dsg.matchAll(/^\*\*(D\d)\*\*/gm)].map((m) => m[1]), ['D1', 'D2', 'D3', 'D4', 'D5', 'D6', 'D7', 'D8'])
const zeilen = lies('public/datenschutz.html').split('\n')
const refs = [...dsg.matchAll(/public\/datenschutz\.html:(\d+)/g)].map((m) => Number(m[1]))
assert.ok(refs.length >= 8 && refs.every((n) => n >= 1 && n <= zeilen.length))
assert.ok(zeilen[119].includes('Zahlungspflichtig bestellen') && zeilen[118].includes('Leistungsbeginn') && zeilen[94].includes('Wartungs-Abo'), 'D1, D2, D5 zitieren die richtigen Zeilen')
})
- Step 2: Rot
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-recht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtEntscheidungen.test.js
Expected: FAIL (ENOENT).
- Step 3: Beide Dateien schreiben
Tabelle und Fragen nach dem Inhalt oben. ## AVV-Struktur: Optionen A, B, C mit Vorteil und Nachteil in je zwei Sätzen, Empfehlung A. ## Ablauf nach der Freigabe als nummerierte Schritte: 1. Freigabe des Inhabers je Punkt (R1 bis R15). 2. Datum der Fassungen auf den Freigabetag setzen (config.abo.termsVersionNeu, FUNKTIONEN_FASSUNG_V6, die Datei- und Testnamen mit dem Datum, die Fassungszeilen, Version 6 · Stand … in der Abo-Seite). 3. Entwurfsmarke aus allen Dateien entfernen und ENTWURF = false in test/aboBedingungenV6.test.js, test/funktionenRechtV6.test.js, test/kundenansichtEntscheidungen.test.js setzen. 4. node --max-old-space-size=256 scripts/build-legal-html.mjs funktionen6. 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, nie parallel, FORCE=1 nur nach Textänderung); die PDFs und die .sha256 committen. 6. node scripts/rechtliches-hochladen.mjs --probe; meldet er STOPP: Karte fehlt: … (laut setup-legal.mjs des WOMS fehlen drei von elf Karten: News, Website-Analyse und AVV Website-Analyse), zuerst node scripts/rechtliches-hochladen.mjs --karten-anlegen, dann --probe wiederholen. Ist der Vorflug grün, 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). Der Ablauf zitiert die Entwurfsmarken nicht wörtlich (der Test prüft, dass die Marke der Datei verschwindet).
- Step 4: Grün
Run: wie Step 2. Expected: PASS.
- Step 5: Commit
git add test/kundenansichtEntscheidungen.test.js docs/rechtliches/kundenansicht-entscheidungen.md docs/rechtliches/datenschutz-kundenansicht-pruefpunkte.md
git commit -m "Rechtsrunde Fassung 6: Entscheidungsliste, Kanzlei-Fragen, Datenschutz-Prüfpunkte, Ablauf" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 12: PDF-Bau: Modi abo6 und funktionen6 (nur geschrieben, nicht ausgeführt)
Files:
- Modify:
scripts/build-legal-pdfs.sh(Kopfkommentar, zwei Modi vorall), eine zusätzlicheecho-Zeile in der Usage; die erste Usage-Zeile und der Blockall)bleiben, Pinstest/buildLegalHtml.test.js:336-368undtest/legalTexts.test.js:120) - Test:
test/legalV6Build.test.js(erweitern)
Interfaces:
-
Consumes: Slugs und Fassung aus Task 3 und 6.
-
Produces:
abo6drucktpublic/neu/rechtliches/abo-bedingungen.htmlnachpublic/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf;funktionen6druckt die zehn Seiten nachpublic/neu/rechtliches/<slug>-<Fassung>.pdf; beide legen<PDF>.sha256(SHA-256 der Seite) daneben und drucken ein PDF mit passender Prüfsumme nicht neu (FORCE=1erzwingt). Die Fassung der Funktionen liest ein Aufruf wie bei den anderen Modi:timeout 60 node --max-old-space-size=256 --input-type=module -e "const { FUNKTIONEN_FASSUNG_V6 } = await import('./server/services/products.js'); console.log(FUNKTIONEN_FASSUNG_V6)", geprüft gegen^[0-9]{4}-[0-9]{2}-[0-9]{2}$. Fehlt eine Seite: MeldungHTML fehlt: … (zuerst node scripts/build-legal-html.mjs funktionen6). -
Step 1: Failing test
test('build-legal-pdfs.sh: Modi abo6 und funktionen6, all und erste Usage-Zeile unverändert, nie parallel', () => {
const sh = readFileSync(join(ROOT, 'scripts/build-legal-pdfs.sh'), 'utf8')
assert.match(sh, /^abo6\)$/m); assert.match(sh, /^funktionen6\)$/m)
const abo6 = sh.slice(sh.indexOf('\nabo6)'), sh.indexOf('\nfunktionen6)'))
assert.ok(abo6.includes('public/neu/rechtliches/abo-bedingungen.html') && abo6.includes('public/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf') && abo6.includes('.sha256'))
for (const s of SLUGS) assert.ok(sh.includes(s), s)
assert.ok(sh.includes('FUNKTIONEN_FASSUNG_V6') && sh.includes('zuerst node scripts/build-legal-html.mjs funktionen6'))
const all = sh.slice(sh.indexOf('\nall)'), sh.indexOf('\n*)'))
assert.ok(!all.includes('neu/rechtliches') && all.includes(' pdf public/abo-bedingungen.html public/WEBklar-Abo-Vertragsunterlagen.pdf\n'), 'all druckt die Entwürfe nicht')
assert.ok(sh.includes('Usage: $0 [all|analyse|termine|shop|unterkunft|datenschutz]') && sh.includes('abo6|funktionen6'))
assert.match(sh, /^ {2}timeout 120 "\$CHROME" /m)
assert.ok(!/ &$|^\s*wait\b|xargs -P/m.test(sh))
assert.equal(spawnSync('bash', ['-n', join(ROOT, 'scripts/build-legal-pdfs.sh')], { encoding: 'utf8' }).status, 0)
})
test('PDFs der Fassung 6: erst nach der Freigabe vorhanden; wenn vorhanden, PDF, Prüfsumme der Seite daneben', { skip: !existsSync(join(ROOT, 'public/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf')) && 'PDFs entstehen erst nach der Freigabe (Task 11, Ablauf)' }, () => {
for (const p of ['abo-bedingungen.html', ...SLUGS.map((s) => `${s}-2026-10-16.html`)]) {
const pdf = join(ROOT, 'public/neu/rechtliches', p === 'abo-bedingungen.html' ? 'WEBklar-Abo-Vertragsunterlagen.pdf' : p.replace('.html', '.pdf'))
assert.equal(readFileSync(pdf).subarray(0, 5).toString(), '%PDF-', pdf)
assert.equal(readFileSync(`${pdf}.sha256`, 'utf8').trim(), createHash('sha256').update(readFileSync(join(ROOT, 'public/neu/rechtliches', p))).digest('hex'), p)
}
})
Der Kopf der Datei importiert dafür zusätzlich createHash, spawnSync, existsSync, fileURLToPath und setzt const ROOT = fileURLToPath(new URL('..', import.meta.url)).
- Step 2: Rot
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-recht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/legalV6Build.test.js
Expected: FAIL (Modus abo6 fehlt); der zweite Test erscheint als übersprungen.
- Step 3: Modi ergänzen
Beide Modi direkt vor all), in der Reihenfolge abo6), funktionen6), im Stil von shop_pdfs und auftraege_pdfs (Prüfsumme, pdf()-Helfer, FORCE), in den Kopfkommentar eine Zeile je Modus mit dem Hinweis „Entwurf; erst nach der Freigabe drucken, vorher free -m, mit nice“. Das Skript wird in diesem Paket nicht ausgeführt.
- Step 4: Grün, Pins laufen mit
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-recht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test --test-concurrency=1 test/legalV6Build.test.js test/buildLegalHtml.test.js test/legalTexts.test.js
Expected: PASS.
- Step 5: Commit
git add scripts/build-legal-pdfs.sh test/legalV6Build.test.js
git commit -m "Rechtstexte: PDF-Bau für Fassung 6 (Modi abo6 und funktionen6, nach Freigabe)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 13: Hochladen nach WOMS „Rechtliches“ (nur geschrieben, nicht ausgeführt)
Files:
- Create:
scripts/rechtliches-hochladen.mjs(Vorlage/root/reports/webshop-2026-10-05/rechtliches-hochladen.mjs, nur lesen) - Test:
test/rechtlichesHochladen.test.js(neu)
Interfaces:
-
Consumes:
FUNKTIONEN_UNTERLAGEN_V6(Task 3), die Dateien aus Tasks 5 bis 10 und die PDFs aus Task 12 (erst nach der Freigabe). -
Produces:
export const DOKUMENTE_V6 // [{ documentKey, dateien: [pdfPfad, htmlPfad], titel, quelle }]: abo-bedingungen, dann je Funktion lb und avv wie in FUNKTIONEN_UNTERLAGEN_V6 (documentKey = slug); quelle = HTML-Pfad, den `source` beim Hochladen trägt (heute = dateien[1]; verschiebt P10 die Abo-Seite, ändert er dateien, quelle bleibt) export function dokumentFuer(documentKey) // Eintrag oder Error('Unbekanntes Dokument: …') export function parseArgs(argv) // { modus: 'probe'|'karten'|'pruefen'|'hochladen', documentKey, freigabe } export function tagVorher(iso) // 'JJJJ-MM-TT' einen Tag zurück export function planVersion({ alle, source, validFrom }) // { schon, nummer: 'vN', aktuellId, validToAltes } export function kartenId(key) // Dokument-ID der Karte: key, wenn höchstens 36 Zeichen, sonst 27 Zeichen von key + '-' + 8 Hexzeichen von sha256(key) export function kartenPlan({ vorhandeneKeys, hoechsteSort }) // fehlende Karten in Tabellenreihenfolge: [{ id, daten: { key, title, liveUrl, sort } }] export function pruefeHochgeladen(versionenJe) // { [documentKey]: Version[] } → { fehlt: documentKey[] } export async function main(argv, deps) // → Exit-Code (0|1); deps = { fetch, schluessel, log } mit Standard globales fetch, Datei /root/.appwrite-rechtliches.key, console.log; der einzige Weg zu Schlüssel und Netz; läuft als Prozess nur beim direkten Aufruf der DateiAufrufe (alle über
main): (1)node scripts/rechtliches-hochladen.mjs --probe: nur lesen, Schlüssel, Bucketlegal-files, vorhandene Versionen und die Karten inlegalDocuments; jede fehlende Karte ist ein Stopp: ZeileSTOPP: Karte fehlt: <documentKey>und Exit 1. Lautscripts/setup-legal.mjsdes WOMS fehlen drei der elf Karten (leistungsbeschreibung-news,leistungsbeschreibung-website-analyse,avv-website-analyse). (2)--karten-anlegen: legt genau die Karten auskartenPlanan und fasst vorhandene nie an; läuft einmal vor dem ersten Hochladen. (3)node scripts/rechtliches-hochladen.mjs <documentKey> --freigabe=JJJJ-MM-TT(genau ein Dokument;validFrom= Freigabedatum); fehlt die Karte, bricht der Aufruf ab, bevor etwas geschrieben wird. (4)--pruefen: nur lesen, die Nachkontrolle nach den Uploads; Exit 0 nur, wenn jedes der elf Dokumente eine Version mitstatus: 'current'undsource=kundenbereich@<Hash>:<quelle>hat, sonst ZeileFEHLT <documentKey>und Exit 1 (dafür ist P10 Task 9 Step 2 vorgesehen, Vertragsfrage 9). Schlüssel/root/.appwrite-rechtliches.key, Endpunkthttps://ticket.webklar.com/v1, Projekt6a1058610003c5a13a05, Datenbankwoms-database. Vor dem Hochladen:git status --porcelain -- <Dateien>leer, beide Dateien vorhanden,source=kundenbereich@<git rev-parse --short HEAD>:<HTML-Pfad>; sonst Abbruch. Danach wie die Vorlage (Dateien inlegal-files, neue Versioncurrent, bisherigesupersededmitvalidTo). -
Step 1: Failing tests (Kopf:
test,assert;const { FUNKTIONEN_UNTERLAGEN_V6, FUNKTIONEN_FASSUNG_V6 } = await import('../server/services/products.js'))
const m = await import('../scripts/rechtliches-hochladen.mjs') // der Import selbst darf weder Schlüssel lesen noch das Netz nutzen
test('parseArgs: ein Dokument je Aufruf, Hochladen nur mit --freigabe=Datum, --probe und --pruefen lesen nur, --karten-anlegen legt nur Karten an', () => {
assert.deepEqual(m.parseArgs(['--probe']), { modus: 'probe', documentKey: null, freigabe: null })
assert.deepEqual(m.parseArgs(['--karten-anlegen']), { modus: 'karten', documentKey: null, freigabe: null })
assert.deepEqual(m.parseArgs(['--pruefen']), { modus: 'pruefen', documentKey: null, freigabe: null })
assert.deepEqual(m.parseArgs(['avv-webshop', '--freigabe=2026-10-20']), { modus: 'hochladen', documentKey: 'avv-webshop', freigabe: '2026-10-20' })
for (const bad of [[], ['avv-webshop'], ['--freigabe=2026-10-20'], ['avv-webshop', 'avv-terminplaner', '--freigabe=2026-10-20'], ['avv-webshop', '--freigabe=20.10.2026'], ['avv-webshop', '--freigabe=2026-13-40'], ['datenschutz', '--freigabe=2026-10-20'], ['--probe', 'avv-webshop'], ['--probe', '--freigabe=2026-10-20'], ['--karten-anlegen', 'avv-webshop'], ['--karten-anlegen', '--freigabe=2026-10-20'], ['--pruefen', '--probe'], ['--pruefen', 'avv-webshop']]) {
assert.throws(() => m.parseArgs(bad), /Aufruf|Freigabe|Dokument/, JSON.stringify(bad))
}
assert.equal(typeof m.main, 'function')
})
test('Tabelle: elf Dokumente, Dateien im Vorschaupfad, passend zu FUNKTIONEN_UNTERLAGEN_V6', () => {
assert.deepEqual(m.DOKUMENTE_V6.map((d) => d.documentKey), ['abo-bedingungen', 'leistungsbeschreibung-news', 'leistungsbeschreibung-terminplaner', 'avv-terminplaner', 'leistungsbeschreibung-webshop', 'avv-webshop', 'leistungsbeschreibung-unterkunft', 'avv-unterkunft', 'leistungsbeschreibung-zeiten', 'leistungsbeschreibung-website-analyse', 'avv-website-analyse'])
assert.deepEqual(m.dokumentFuer('abo-bedingungen').dateien, ['public/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf', 'public/neu/rechtliches/abo-bedingungen.html'])
const unterlagen = Object.values(FUNKTIONEN_UNTERLAGEN_V6).flatMap((u) => [u.lb, u.avv]).filter(Boolean)
assert.deepEqual(m.DOKUMENTE_V6.slice(1).map((d) => d.dateien), unterlagen.map((u) => [`public/${u.pdfFile}`, `public${u.url}`]))
assert.deepEqual(m.DOKUMENTE_V6.map((d) => d.quelle), m.DOKUMENTE_V6.map((d) => d.dateien[1]), 'quelle = HTML-Pfad, der in source steht')
assert.throws(() => m.dokumentFuer('datenschutz'), /Unbekanntes Dokument/)
})
test('planVersion: neue Fassung als vN, bisherige gültige wird abgelöst, doppelter Upload wird erkannt', () => {
const alle = [{ $id: 'a', version: 'v1', validFrom: '2026-09-30', status: 'current', source: 'x' }, { $id: 'b', version: 'v0', validFrom: '2026-09-25', status: 'superseded', source: 'y' }]
assert.deepEqual(m.planVersion({ alle, source: 's', validFrom: '2026-10-20' }), { schon: false, nummer: 'v3', aktuellId: 'a', validToAltes: '2026-10-19' })
assert.equal(m.planVersion({ alle, source: 'x', validFrom: '2026-10-20' }).schon, true)
assert.deepEqual(m.planVersion({ alle: [], source: 's', validFrom: '2026-10-20' }), { schon: false, nummer: 'v1', aktuellId: null, validToAltes: null })
assert.equal(m.planVersion({ alle, source: 's', validFrom: '2026-09-30' }).validToAltes, '2026-09-30', 'gleicher Tag: validTo = validFrom wie in der Vorlage')
assert.equal(m.tagVorher('2026-03-01'), '2026-02-28')
})
// Nachbau des WOMS-Backends für main(): Karten als Dokumente { $id, key, sort }, Versionen und Dateien leer; zählt alle Aufrufe
function backend(karten) {
const aufrufe = [], log = [], antwort = (obj) => ({ ok: true, status: 200, text: async () => JSON.stringify(obj) })
const fetch = async (url, opt = {}) => {
const methode = opt.method ?? 'GET', pfad = new URL(url).pathname.replace(/^\/v1/, ''); aufrufe.push(`${methode} ${pfad}`)
if (methode === 'GET' && pfad.endsWith('/collections/legalDocuments/documents')) return antwort({ total: karten.length, documents: karten })
if (methode === 'GET' && pfad.endsWith('/collections/legalVersions/documents')) return antwort({ total: 0, documents: [] })
if (methode === 'GET' && pfad.startsWith('/storage/buckets/legal-files/files')) return antwort({ total: 0, files: [] })
if (methode === 'POST' && pfad.endsWith('/collections/legalDocuments/documents')) { const b = JSON.parse(opt.body); karten.push({ $id: b.documentId, ...b.data }); return antwort({ $id: b.documentId }) }
throw new Error(`unerwartet: ${methode} ${pfad}`)
}
return { karten, aufrufe, log, deps: { fetch, schluessel: () => 'test-schluessel', log: (z) => log.push(z) } }
}
const NUR_LESEN = (b) => b.aufrufe.every((a) => a.startsWith('GET '))
const FEHLEN = ['leistungsbeschreibung-news', 'leistungsbeschreibung-website-analyse', 'avv-website-analyse'] // „leistungsbeschreibung-website-analyse“ hat 37 Zeichen, eine Dokument-ID darf höchstens 36 haben
const kartenDoc = (key, i) => ({ $id: key.slice(0, 36), key, sort: i + 1 })
const VORHANDEN = m.DOKUMENTE_V6.map((d) => d.documentKey).filter((k) => !FEHLEN.includes(k)) // die Karten laut WOMS scripts/setup-legal.mjs
test('kartenPlan: genau die drei fehlenden Karten in Tabellenreihenfolge, mit zulässiger Dokument-ID', () => {
const plan = m.kartenPlan({ vorhandeneKeys: VORHANDEN, hoechsteSort: 14 })
assert.deepEqual(plan.map((k) => [k.daten.key, k.daten.sort]), [[FEHLEN[0], 15], [FEHLEN[1], 16], [FEHLEN[2], 17]])
assert.deepEqual(plan.map((k) => k.daten.title), ['Leistungsbeschreibung News-Pinnwand', 'Leistungsbeschreibung Website-Analyse', 'AVV Website-Analyse'])
assert.ok(plan.every((k) => k.daten.liveUrl.startsWith('https://project.webklar.com/neu/rechtliches/') && k.daten.liveUrl.endsWith(`-${FUNKTIONEN_FASSUNG_V6}.html`)))
assert.ok(plan.every((k) => /^[A-Za-z0-9][A-Za-z0-9._-]{0,35}$/.test(k.id)), 'Dokument-ID höchstens 36 Zeichen')
assert.equal(plan[0].id, FEHLEN[0], 'kurze Schlüssel bleiben die ID, wie in setup-legal.mjs')
assert.notEqual(plan[1].id, FEHLEN[1]); assert.equal(m.kartenId(FEHLEN[1]), plan[1].id, 'lange Schlüssel: abgeleitete ID, immer dieselbe')
assert.deepEqual(m.kartenPlan({ vorhandeneKeys: m.DOKUMENTE_V6.map((d) => d.documentKey), hoechsteSort: 20 }), [])
})
test('--probe: eine fehlende Karte ist ein Stopp (Exit 1), mit allen Karten Exit 0; schreibt nie', async () => {
const b = backend(VORHANDEN.map(kartenDoc))
assert.equal(await m.main(['--probe'], b.deps), 1)
for (const k of FEHLEN) assert.ok(b.log.includes(`STOPP: Karte fehlt: ${k}`), k)
assert.ok(NUR_LESEN(b), b.aufrufe.join(', '))
assert.equal(await m.main(['--probe'], backend(m.DOKUMENTE_V6.map((d, i) => kartenDoc(d.documentKey, i))).deps), 0)
})
test('--karten-anlegen: legt nur die drei fehlenden Karten an, ein zweiter Lauf tut nichts, danach ist --probe grün', async () => {
const b = backend(VORHANDEN.map(kartenDoc))
assert.equal(await m.main(['--karten-anlegen'], b.deps), 0)
assert.deepEqual(b.aufrufe.filter((a) => !a.startsWith('GET ')), FEHLEN.map(() => 'POST /databases/woms-database/collections/legalDocuments/documents'))
assert.deepEqual(b.karten.slice(VORHANDEN.length).map((k) => k.key), FEHLEN)
b.aufrufe.length = 0
assert.equal(await m.main(['--karten-anlegen'], b.deps), 0); assert.ok(NUR_LESEN(b))
assert.equal(await m.main(['--probe'], b.deps), 0)
})
test('Hochladen ohne Karte bricht ab, bevor etwas geschrieben wird', async () => {
const b = backend(VORHANDEN.map(kartenDoc))
assert.equal(await m.main([FEHLEN[0], '--freigabe=2026-10-20'], b.deps), 1)
assert.ok(b.log.some((z) => z.includes(`Karte fehlt: ${FEHLEN[0]}`)) && NUR_LESEN(b))
})
test('pruefeHochgeladen und --pruefen: alle elf Dokumente brauchen eine aktuelle Version mit passender Quelle', async () => {
const ok = (d) => [{ $id: 'v', version: 'v2', status: 'current', source: `kundenbereich@abc1234:${d.quelle}` }]
const alle = Object.fromEntries(m.DOKUMENTE_V6.map((d) => [d.documentKey, ok(d)]))
assert.deepEqual(m.pruefeHochgeladen(alle), { fehlt: [] })
assert.deepEqual(m.pruefeHochgeladen({ ...alle, 'avv-website-analyse': [] }).fehlt, ['avv-website-analyse'])
assert.deepEqual(m.pruefeHochgeladen({ ...alle, 'abo-bedingungen': [{ ...alle['abo-bedingungen'][0], source: 'kundenbereich@7ae0ffd:public/abo-bedingungen.html' }] }).fehlt, ['abo-bedingungen'], 'Fassung 5 zählt nicht')
assert.deepEqual(m.pruefeHochgeladen({ ...alle, 'leistungsbeschreibung-news': [{ ...alle['leistungsbeschreibung-news'][0], status: 'superseded' }] }).fehlt, ['leistungsbeschreibung-news'])
assert.equal(m.pruefeHochgeladen({}).fehlt.length, 11)
const b = backend([]) // Versionen leer: alle elf fehlen
assert.equal(await m.main(['--pruefen'], b.deps), 1)
assert.equal(b.log.filter((z) => z.startsWith('FEHLT ')).length, 11); assert.ok(NUR_LESEN(b))
})
- Step 2: Rot
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-recht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/rechtlichesHochladen.test.js
Expected: FAIL (ERR_MODULE_NOT_FOUND für das Skript).
- Step 3: Skript schreiben
Reine Funktionen wie oben exportieren; main liest erst dort den Schlüssel (deps.schluessel), ruft deps.fetch, schreibt über deps.log und gibt den Exit-Code zurück; ein Einstiegsblock ruft main nur auf, wenn process.argv[1] diese Datei ist, und beendet den Prozess mit dem Code. parseArgs wirft Error('Aufruf: …') bzw. Error('Freigabe fehlt …') bzw. Error('Unbekanntes Dokument: …'); das Datum muss ein gültiger Kalendertag sein. planVersion und tagVorher wie in der Vorlage (nummer = Zahl der Versionen mit ^v\d+$ plus eins; validTo des Alten = Tag vor validFrom, bei gleichem oder früherem validFrom = validFrom). Karten: Die Liste von legalDocuments wird über das Attribut key verglichen (WOMS liest die Karten so, nicht über die ID); kartenPlan setzt title aus titel, liveUrl aus https://project.webklar.com/ plus dateien[1] ohne public/ und sort ab hoechsteSort + 1; angelegt wird mit POST auf legalDocuments (documentId = id). --probe schreibt je fehlender Karte STOPP: Karte fehlt: <documentKey> und gibt dann 1 zurück; --pruefen liest die Versionen aller elf Dokumente, schreibt ok <documentKey> bzw. FEHLT <documentKey> und gibt 1 zurück, wenn pruefeHochgeladen(...).fehlt nicht leer ist (ein Dokument zählt, wenn eine Version status: 'current' hat und source zu kundenbereich@<7 bis 40 Hexzeichen>:<quelle> passt). Beim Hochladen kommt nach dem Lesen des Schlüssels als Erstes die Kartenprüfung (Karte fehlt: <documentKey> (zuerst --karten-anlegen), Rückgabe 1), erst danach die Git- und Dateiprüfung und der Upload. Das Skript wird in diesem Paket nicht ausgeführt, auch nicht mit --probe, --karten-anlegen oder --pruefen.
- Step 4: Grün
Run: wie Step 2. Expected: PASS.
- Step 5: Commit
git add scripts/rechtliches-hochladen.mjs test/rechtlichesHochladen.test.js
git commit -m "Rechtstexte: Hochladen-Skript für WOMS Rechtliches (ein Dokument je Aufruf, nur mit Freigabe)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 14: Selbstprüfung
Files: keine Änderungen, außer einer Korrektur dessen, was die Prüfung findet.
- Step 1: Speicher prüfen
Run: free -m
Expected: mindestens 2000 MB verfügbar; sonst warten und den Controller melden.
- Step 2: Alle eigenen und alle berührten Testdateien
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-recht && timeout 1800 nice -n 10 node --max-old-space-size=1024 --test --test-concurrency=1 test/products.test.js test/legalTexts.test.js test/mailTemplates.test.js test/buildLegalHtml.test.js test/legalDocs.test.js test/aboWortlautV6.test.js test/aboBedingungenV6.test.js test/legalV6Build.test.js test/funktionenRechtV6.test.js test/kundenansichtEntscheidungen.test.js test/rechtlichesHochladen.test.js
Expected: alle PASS; erwartet übersprungen sind genau zwei Tests: der Verdrahtungstest in aboWortlautV6.test.js (bis P5 gemerged ist) und der PDF-Test in legalV6Build.test.js (bis zur Freigabe).
- Step 3: Generator über alle zehn Seiten
Run: node --max-old-space-size=256 scripts/build-legal-html.mjs funktionen6 --check
Expected: zehn Zeilen ok …, Exit 0.
- Step 4: Eingefrorenes unverändert (C8)
Run: git diff --quiet kn-p0 -- public/rechtliches public/vertragsunterlagen/v3 public/vertragsunterlagen/v4 public/vertragsunterlagen/funktionen-leistungsbeschreibung.html public/vertragsunterlagen/funktionen-leistungsbeschreibung-2026-09-30.html public/vertragsunterlagen/zeiten-leistungsbeschreibung.html public/abo-bedingungen.html public/WEBklar-Abo-Vertragsunterlagen.pdf public/datenschutz.html; echo $?
Expected: 0. Dazu git diff --name-only kn-p0 | sort: nur Dateien aus der Dateiübersicht; git diff --name-only kn-p0 -- build ist leer.
- Step 5: Verträge abhaken
V12: FASSUNGEN, aktuelleAboFassung, isTermsV4OrNewer, config.abo.termsVersionNeu, Pfad der Seite (Vertragsfrage 1), Archiv v5 vorbereitet. Wortlaute für V5 (beginn Abo, Wechsel, Einrichtung; Linkzeilen): Task 3. Keine Datei außerhalb der Zuständigkeitstabelle geändert. Kein Skript mit Netz oder Chrome gelaufen.
- Step 6: Aufräumen prüfen, Tag setzen
Run: ls -l /dev/null (zeigt crw-rw-rw-), git status --short (sauber), dann:
git tag kn-p8
Vertragsfragen an den Controller
- Pfad der Fassung-6-Seite (V12). Vorschlag, so gebaut: Seite
/neu/rechtliches/abo-bedingungen.html, PDF/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf, Funktionsunterlagen/neu/rechtliches/<slug>-2026-10-16.html|pdf. Bitte in V12 nachtragen. P10 verschiebt Seite und PDF nachpublic/, stellt inFASSUNGENv5 auf diearchiv-Adressen und v6 auf die Wurzel-Adressen, passtDOKUMENTE_V6im Hochladen-Skript, den Modusabo6und die Navigation des Generators an und zieht die festen Links auf/abo-bedingungen.htmlnach (Karte abo-server § 3 Punkt 6:app.js:961,985,1093,1098,1307,1308,1476,dashboard.html:83,331,login.html:85,datenschutz.html:43,stripeBilling.js:148,162). - Wortlaute und Unterlagen: Besitz und Verdrahtung. Der Plan P5 (Vertragsfrage 7) sagt, P8 ändere
config.aboFunktionen.texte.beginnund.fassung. Das widerspricht der Zuständigkeitstabelle (P8 nurabo.termsVersionNeu), undconfig.aboFunktionengibt es inkn/rechtnicht. Vorschlag: P8 liefertWORTLAUT_V6,FUNKTIONEN_FASSUNG_V6,FUNKTIONEN_UNTERLAGEN_V6,mitFassunginproducts.js. Nach dem Merge von P5 und P8 setzt der Controller (oder P7) inconfig.aboFunktionenfassung: '2026-10-16'undtexte.beginnals Literal (config.js importiert keine Dienste) und stelltconsentAboundaboKatalog.unterlagenaufFUNKTIONEN_UNTERLAGEN_V6um: P5 verlinkt heuteconfig.features[key].leistungsbeschreibungund für die AnalyselegalPaths(config.analyse.avvVersion), also die Alt-Unterlagen mit Preisen. Der Verdrahtungstest intest/aboWortlautV6.test.jswird nach dem Merge rot, bis das erledigt ist. - Wo erscheinen die Sätze der alten Häkchen
verantwortung,stripe,gesundheit?WORTLAUT_V6.hinweis.{shop,unterkunft,termine}sind fertig. Vorschlag: P5 liefert sie alskatalog[].hinweis(Server, nie Client), P3 zeigt sie über dem Knopf der Einschalt-Seite, der Nachweis speichert sie als Texthinweis. P5 hat laut seiner Vertragsfrage 2 darauf gewartet. - Fassung in den Bestellrouten (P7).
stripeBilling.js:606-607schreibtconfig.abo.termsVersionfest in die Stripe-Metadaten; die Datei steht in keiner Zeile der Zuständigkeitstabelle. Für Fassung 6 brauchtcreateAboCheckoutdie Fassung als Parameter. Dasselbe gilt fürabo.js:236,328,388(Prüfung) undabo.js:201(GET /api/abolieferttermsVersion). Vorschlag: P7 übernimmt alle vier Stellen mitaktuelleAboFassung(req). - Abo-Ende-Verhalten, das die Texte zusagen (P6; mit P4/P6 abgestimmt, R8). Terminplaner, News, Webshop und Buchungskalender schalten spätestens am Folgetag ab (Tagesjob). Öffnungszeiten live läuft nach dem Ende des Abos noch 30 Tage unverändert weiter und wird dann abgeschaltet (bestehendes Modul
zeiten/aboAbgleich.js, P6 mit den Mails K40 und K41, P4 mit Text Z2, Spec 5.5 „bisherige Lösch- und Behaltefristen“); nur das 30-€-Behalten entfällt. Löschfristen: Terminplaner, News und Öffnungszeiten 30 Tage nach der Abschaltung, Webshop und Buchungskalender 90, Analyse-Messdaten 60; ein neues passendes Abo in der Frist holt die Funktion zurück; für News braucht es einen Löschjob (heute keiner, Karte funktionen-server § 2; P6 plant ihn). Die Texte sagen genau das: Abo-Bedingungen § 3 Abs. 8 und § 11 Abs. 4 (Task 4) und der BausteinendeZeitender Leistungsbeschreibung Zeiten (Task 7). Zwei Punkte für P6, die dieser Plan nicht ändert: (a) der Satz der Mails K02 und K03 „… enden mit dem Abo. Ihre Eingaben bleiben danach noch 30 Tage gespeichert“ stimmt für Öffnungszeiten live nur mit Zusatz (30 Tage Weiterlauf, dann 30 Tage Eingaben); (b) P6 schaltet die Zeiten beim Widerruf des Wechsels sofort ab, die Basisfassung ließ sie dann 30 Tage weiterlaufen, der neue Text sagt dafür nur „endet“ (Bausteinwiderruf). Wählt der Inhaber in R8 die Alternative (auch Zeiten am Folgetag), müssen Task 4, Task 7 und P4/P6 nachziehen. - WOMS-Karten fehlen. Für
leistungsbeschreibung-news,leistungsbeschreibung-website-analyseundavv-website-analysegibt es keine Karte inlegalDocuments(setup-legal.mjs:92-109). So gebaut (Task 13, Ablauf in Task 11 Schritt 6):--probemeldet fehlende Karten als Stopp,--karten-anlegenlegt genau sie vor dem ersten Hochladen an, das Hochladen ohne Karte bricht ab; P9 muss nichts tun. Achtung:leistungsbeschreibung-website-analysehat 37 Zeichen, eine Dokument-ID darf höchstens 36 haben. Die Karte bekommt deshalb eine abgeleitete ID (kartenId); WOMS liest Karten über das Attributkey. Trüge jemand die Karten insetup-legal.mjsein, das den Schlüssel als ID nimmt, scheiterte diese eine. - Sichtbarkeit der Entwurfsseiten.
public/neu/rechtliches/*.htmlsind nach dem Push überexpress.staticöffentlich, auch bei Schalteroff, mit Entwurfsmarke. Vorschlag: so lassen wie frühere Runden, oder die zehn Funktionsseiten und die Abo-Seite in einen letzten Commit legen, den der Inhaber erst mit der Freigabe pusht. - Annahme zu P0.
neueAnsicht(req)liestreq.session(V1) und kenntconfig.kundenansichtNeu = { mode, pilotEmails }.aktuelleAboFassungund der Test in Task 1 bauen darauf (wie P5 Frage 6). - P10 Task 9 Step 2: alle elf Dokumente prüfen. Dieser Plan liefert
scripts/rechtliches-hochladen.mjs --pruefen(nur lesen): Exit 0 nur, wenn alle elfdocumentKeyausDOKUMENTE_V6eine aktuelle Version mit passendersourcehaben (E20, Spec 5.10: Fassung 6 ist voronhochgeladen, nicht nur die Abo-Bedingungen).quelleim Eintrag bleibt der Entwurfspfad, auch wenn P10 die Abo-Seite verschiebt. P10 Task 9 Step 2 prüft heute nurabo-bedingungenüber--probeund sollte auf--pruefenumgestellt werden; dieser Plan ändert P10 nicht.