# Kundenansicht neu, P10 Umschalten und Aufräumen: 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:** Alle Kunden bekommen die neue Kundenansicht (Schalter `on`), Fassung 6 wird die geltende Abo-Fassung, und die alte Oberfläche samt Kaufwegen für Funktionen verschwindet, ohne dass Bestand (Webhooks, Rechnungen, Widerruf) leidet. **Architecture:** - **Teil A (Tasks 1 bis 6, Code):** alles, was das Umschalten braucht, hinter dem Schalter. Ein Bestandsskript für die Kundenart (E18), eine Weiche, die erst bei `on` die öffentlichen Seiten, das Manifest, `/dashboard.html` ohne Sitzung und die Fassung-6-Adresse neu ausliefert, zwei lesende Prüfskripte und eine Checkliste. Bis zum Umschalten verhält sich alles wie heute. - **Teil B (Tasks 7 bis 13, live):** Auslieferung, Kundenart setzen, Vorflug, Schalter `on`, Nachprüfung, Offline-Cache. Jeder Live-Schritt ist ein eigener Task mit „Stop: Inhaber-OK nötig“. **Rückweg bis Task 13: `KUNDENANSICHT_NEU=off`.** - **Teil C (Tasks 14 bis 25):** Fassung 6 festschreiben, Login von `app.js` lösen, alte Oberfläche löschen, Kaufwege zurückbauen, am Ende den Schalter vereinfachen. **Ab Task 14 gibt es keinen Rückweg per Schalter**, Fehler werden vorwärts korrigiert. Grund: `off` würde die alten Bestellseiten (Wortlaut Fassung 5) mit Fassung 6 als einziger geltender Fassung kombinieren. **Tech Stack:** Node 20 (Prod-Container) und 24 (Host), Express 4, Appwrite-REST (`server/services/appwriteAdmin.js`), `node --test` mit `appwriteMock`, `testApp`, `fakeStripe`; Vanilla-ES-Module ohne Build; Sichtprüfung nur über das Werkzeug aus P0; Docker Compose auf dem Host für den Schalter. **Spec:** `/home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht/docs/superpowers/specs/2026-10-15-kundenansicht-neu-design.md` (5.10 „Umschalten für alle“, E18, E19, E20, C8, C11, C14) **Übersicht/Verträge:** `/home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht/docs/superpowers/plans/2026-10-16-kundenansicht-00-uebersicht.md` (gilt wörtlich) **Worktree und Branch:** `/home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht`, Branch `feature/kundenansicht-neu` (Hauptzweig, erst nach Phase E und ausdrücklichem OK des Inhabers). Zwischentags `kn-p10-a`, `kn-p10-b`, `kn-p10-c1` bis `kn-p10-c4`, Endtag `kn-p10`. Kürzel: `KW` = `/root/reports/kundenansicht-neu-2026-10-15/werkzeug`, `RB` = `/root/reports/kundenansicht-neu-2026-10-15/umschalten` (Protokolle), `APPS` = `/home/webklar/apps`. Container `webklar-kundenbereich-server`, Live-Adresse `https://project.webklar.com`. Befehle nennen den Worktree-Pfad ausgeschrieben. ## Global Constraints Alle Global Constraints der Übersicht gelten wörtlich (alte Ansicht bis zum Umschalten unverändert, Schalter `off | pilot | on`, Anrede „du“ in neuen Oberflächentexten, Hashes, rechtlich fest, Kundenart nur vom Server, Testbefehle mit Speicherbremse, Playwright nur über das Werkzeug, Commits mit Dateien einzeln und `Co-Authored-By: Claude Opus 5.5 ` als zweitem `-m`, Sicherheitsblock, kein Push, nichts in `build/`). Paketeigene Zusätze: - **Live-Schritte** (Push, Skripte gegen Produktion, `.env`, Compose, Dienst neu starten) tragen den Satz „Stop: Inhaber-OK nötig“. Ein Agent führt sie nie von sich aus aus, er meldet dem Controller, was bereitliegt. Pushen tut nur der Inhaber: `git push origin '^{commit}:refs/heads/main'`, nur fast-forward, nie `--force`. - **Geheimnisse:** `APPS/.env` wird nie ausgegeben. Erlaubt sind nur `command grep -a -n '^=' APPS/.env` für `KUNDENANSICHT_NEU`, `KUNDENANSICHT_NEU_PILOT_EMAILS`, `ABO_PILOT_EMAILS` und die `*_ENABLED`-Schalter sowie `sed -i` auf genau eine Zeile. Vor jeder Änderung an `.env` oder `docker-compose.yml` entsteht eine Sicherung `.bak--kundenansicht`. - **Dienst neu starten** nur mit `cd /home/webklar/apps && docker compose up -d --no-deps kundenbereich-server`. Nie `preview-portal`, nie andere Dienste, nie ohne `--no-deps`. - **Löschen** nur mit `git rm ` für Dateien, die der Plan einzeln nennt. Kein `rm -rf`. - **Rechtstexte** ändert dieses Paket nie inhaltlich. Schlägt ein Test auf den Wortlaut der Fassung 6 fehl, stoppt der Agent und meldet die Assertion dem Controller (der Inhaber entscheidet). - **Tests löschen** nur, wenn ihr Gegenstand verschwindet (Datei, Zweig, Schalter). Der Commit nennt Test und Grund. Was weiter gilt, wird umgestellt. - **Skriptausgaben** nennen nur IDs und Zahlen, nie Namen, Adressen oder Beträge einzelner Kunden. - **Schwere Läufe:** die ganze Suite nur in Task 1 und Task 25 (vorher `free -m`, nachher `ls -l /dev/null`). Dazwischen Einzeldateien, mehrere nur mit `--test-concurrency=1`. ## Review Focus 1. **Weiche liefert zur falschen Zeit oder gar nichts:** In `off` und `pilot` (auch mit `*`) darf sich keine öffentliche Seite ändern. In `on` bekommt auch ein Besucher ohne Sitzung die neue Seite (sonst blitzt `/dashboard.html` alt und dunkel auf, die PWA startet dort). Fehlt eine neue Datei, fällt die Weiche auf die alte zurück statt 500. Die Fassung-6-Adresse verlässt `public/` nie. Tests: Task 3, 4. 2. **Kundenart-Skript überschreibt Falsches:** bereits gesetztes `privat`, unbekannte Werte, Leerzeichen, ein zwischen Prüfen und Schreiben gesetzter Wert, falsche Bestätigungszahl, zweiter Lauf, Schreibfehler mittendrin. Erwartet: nur leere Felder werden `unternehmen`, nichts ohne passende Bestätigungszeile, keine Personendaten in der Ausgabe. Test: Task 2. 3. **Festschreiben macht Fassung 6 tot oder Altverträge unauffindbar:** Bei gleichen Werten von `termsVersion` und `termsVersionNeu` meldet eine „alt zuerst“-Prüfung (`fassungDerAnfrage`) immer `alt`, und der Weg der Fassung 6 läuft nie. Das Archiv v5 weicht von der geprüften Seite ab, das PDF für die Bestätigungsmail fehlt. Test: Task 14. 4. **Löschen reißt Gebrauchtes mit:** `app.js` trägt Login, Service-Worker-Registrierung, iOS-Fokuszoom und den Hash-Pin der Gutschrift-Bedingungen. Zwei CSS-Dateien lädt `auftrag.html` bzw. `analyse.js` noch. Die öffentlichen Seiten verschwinden nicht, wenn jemand den Schalter zurückstellt. Tests: Task 16, 17. 5. **Rückbau trifft den Bestand:** Zahlung einer vor dem Umschalten gestarteten Bestellung (SEPA braucht Tage), Widerruf gekaufter Funktionen innerhalb der Frist, Rechnungen. Erwartet: Webhook und Widerruf funktionieren weiter, neue Käufe sind 404. Tests: Task 5, 19, 20. ## Dateiübersicht Besitz laut Zuständigkeitstabelle: `scripts/kundenart-bestand.mjs` und „Umschalten, Aufräumen alter Dateien“. Was darüber hinausgeht, trägt `VF` mit der Nummer der Vertragsfrage am Ende. | Datei | Aufgabe | Task | |---|---|---| | `scripts/kundenart-bestand.mjs`, `test/kundenartBestand.test.js` | E18: Kundenart der Bestandseinträge | 2 | | `server/routes/umschaltSeiten.js`, `test/knUmschaltSeiten.test.js`, `server/index.js` (eine Zeile direkt vor `app.use(express.static(publicDir))`, geteilt mit P0) (VF1) | Weiche | 3 | | `scripts/umschalten-nachpruefung.mjs`, `test/knUmschaltNachpruefung.test.js` (VF1) | HTTP-Nachprüfung alt/neu | 4 | | `scripts/kauf-bestand-pruefen.mjs`, `test/kaufBestandPruefen.test.js` (VF1) | Käufe lesen (nur lesend) | 5 | | `docs/umschalten-kundenansicht.md`, `test/knUmschaltCheckliste.test.js` (VF1) | Checkliste in der Reihenfolge der Spec 5.10 | 6 | | `public/sw.js:5` (`CACHE`), `test/knOfflineCache.test.js` | Offline-Seite in installierten Apps erneuern | 12 | | `public/abo-bedingungen.html`, `public/WEBklar-Abo-Vertragsunterlagen.pdf` (+ `.sha256`; werden Fassung 6, verschoben aus `public/neu/rechtliches/`), `public/vertragsunterlagen/v5/*` (nur geprüft), `server/config.js:855`, `server/services/products.js` (Zeilen v6 und v5), `server/services/vertragsNachweis.js` (`fassungDerAnfrage`), `scripts/build-legal-pdfs.sh` (Modus `abo6`), `scripts/rechtliches-hochladen.mjs` (`DOKUMENTE_V6`), `test/knFassung6Fest.test.js` (VF3, VF4) | Fassung 6 festschreiben | 14 | | Pins: `test/legalTexts.test.js:43,104-121`, `test/products.test.js:120,190`, `test/aboRoutes.test.js:87`, P8s `test/aboBedingungenV6.test.js`, `legalV6Build.test.js`, `rechtlichesHochladen.test.js` | Fassung 6 | 14 | | `public/neu/oeffentlich/login.js` (neu), `public/neu/oeffentlich/login.html`, `test/knLoginModul.test.js` (VF5) | Login löst sich von `app.js` | 16 | | gelöscht: `public/dashboard.html`, `app.js`, `login.html`, `kuendigen.html`, `widerrufen.html`, `offline.html`, `manifest.webmanifest`, `kundenbereich.css`, `login.css`, `legal-form.css`, `termine.css`, `shop.css`, `buchungen.css`, `zeiten.css`, `funktionen.css`, `analyse.css`; `test/knAufraeumen.test.js` (neu) | alte Oberfläche | 17 | | zwölf Pin-Zeilen mit Datei:Zeile (Tabelle in Task 17) | Pins an der alten Oberfläche | 17 | | `server/routes/funktionen.js:292-385,436`, `server/services/stripeBilling.js:666`, `server/services/siteFeatures.js:297`, `server/services/zeiten/mails.js` (`k29Mail`, `k30Mail`), `server/services/zeiten/aboAbgleich.js`, `test/knKaufRueckbau.test.js` | Kaufpfade zurückbauen | 20 | | `public/termine.js`, `termine-fragebogen.js`, `shop.js`, `shop-anfrage.js`, `buchungen.js`, `buchungen-anfrage.js`, `zeiten.js`, `zeiten-bestellen.js`, `funktionen.js`, `analyse.js`, `funktion-bestellen.js`, `test/knAltZweigeWeg.test.js` (VF6) | Alt-Zweige der Module | 21 | | `server/services/kundenansicht/schalter.js`, `server/config.js` (Abschnitt `kundenansichtNeu`), `server/index.js` (toter Handler), `test/knSchalterWeg.test.js` | Schalter vereinfachen | 23 | --- ### Task 1: Vorbedingungen und Basislinie **Files:** keine Änderung, kein Commit. **Interfaces:** - Consumes: Hauptzweig nach Phase E mit P0 bis P8 gemergt (Tags `kn-p0` bis `kn-p8`, je mit `-haupt`); WOMS-Tag `kn-p9` im Repo `/home/webklar/apps/ticket.webklar.com/build`; die Verträge V1, V4, V12. Der Inhaber hat Vorschau, Pilot und Fassung 6 freigegeben und ausdrücklich „umschalten“ gesagt (Übersicht, Phase F). - Produces: die Zahl `N0` (grüne Tests der ganzen Suite) als Vergleich für Task 25. - [ ] **Step 1: Zustand prüfen** Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht && git branch --show-current && git status --short && git tag --list 'kn-p*'` Expected: `feature/kundenansicht-neu`, Status leer (außer untracked Plan-Dateien unter `docs/superpowers/plans/`), Tags `kn-p0` bis `kn-p8`. - [ ] **Step 2: Dateien der Verträge vorhanden** Run: ```bash cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht ls server/services/kundenansicht/schalter.js server/services/kundenansicht/dashboardWeiche.js server/services/kundenart.js server/services/vertragsNachweis.js server/services/bestelltexte.js scripts/kundenansicht-setup.mjs scripts/rechtliches-hochladen.mjs public/neu/dashboard.html public/neu/manifest.webmanifest public/neu/oeffentlich/login.html public/neu/oeffentlich/kuendigen.html public/neu/oeffentlich/widerrufen.html public/neu/oeffentlich/offline.html public/vertragsunterlagen/v5/abo-bedingungen.html public/vertragsunterlagen/v5/WEBklar-Abo-Vertragsunterlagen.pdf public/neu/rechtliches/abo-bedingungen.html public/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf public/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf.sha256 command grep -a -n "termsVersionNeu" server/config.js command grep -a -n "FASSUNGEN" server/services/products.js command grep -a -n "WORTLAUT_STATUS" server/services/bestelltexte.js command grep -a -c "Entwurf – Prüfung durch Kanzlei offen" public/neu/rechtliches/abo-bedingungen.html ``` Expected: `ls` ohne Fehler, die drei `grep` mit Treffern, `WORTLAUT_STATUS` ist `'freigegeben'` (Wortlaut vom Inhaber abgenommen, E20; P7 VF9), die Zahl der Entwurfsmarken ist `0` (P8 hat die Marken nach der Freigabe entfernt und die PDFs gebaut). Fehlt etwas oder steht noch `entwurf` da, stoppt der Agent und meldet es (VF3, VF4), er baut nichts nach. Die Fassung-6-Seite liegt laut P8 unter `public/neu/rechtliches/abo-bedingungen.html` mit dem PDF daneben. - [ ] **Step 3: Basislinie der ganzen Suite** Run: `free -m` (mindestens 2000 MB verfügbar, sonst warten und melden), dann `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht && timeout 2400 nice -n 10 systemd-run --scope -q -p MemoryMax=2500M -p MemorySwapMax=0 env NODE_OPTIONS=--max-old-space-size=1024 node --test --test-concurrency=2 test/*.test.js` Expected: `fail 0`. Notiere `pass` als `N0`. Danach `ls -l /dev/null` mit `crw-rw-rw-`. --- ### Task 2: Kundenart der Bestandseinträge (`scripts/kundenart-bestand.mjs`, E18) **Files:** - Create: `scripts/kundenart-bestand.mjs` - Test: `test/kundenartBestand.test.js` **Interfaces:** - Consumes: `listAllDocuments(collectionId)`, `getDocument(collectionId, id)`, `updateDocument(collectionId, id, data)` aus `server/services/appwriteAdmin.js`; `config.collections.customers`; `KUNDENARTEN` (V4) aus `server/services/kundenart.js`. - Produces: ```js export function parseArgs(argv) // { modus: 'pruefen' } | { modus: 'ausfuehren', bestaetigung }; sonst Error "Aufruf: …" export function bestaetigungsZeile(anzahl) // `Kundenart unternehmen setzen: ${anzahl}` export async function pruefen({ log }) // { gesamt, mitKundenart: { unternehmen, privat }, ohne: string[], ohneNachStatus: { [customerStatus]: string[] }, unbekannt: string[] }, nur IDs export async function ausfuehren({ bestaetigung, log }) // { gesetzt, uebersprungen, fehler: [{ id, grund }] } ``` - [ ] **Step 1: Failing test schreiben** (`test/kundenartBestand.test.js`; Kopf wie `test/featureConsentFelder.test.js:1-12`, aber mit `startAppwriteMock({ schema: { customers: ['name', 'email', 'companyName', 'customerStatus', 'kundenart'] } })`) ```js const { parseArgs, bestaetigungsZeile, pruefen, ausfuehren } = await import('../scripts/kundenart-bestand.mjs') const kunde = (id, customerStatus, kundenart) => aw.seed('customers', { $id: id, name: `Name ${id}`, email: `${id}@example.invalid`, companyName: `Firma ${id}`, customerStatus, ...(kundenart === undefined ? {} : { kundenart }) }) kunde('k_leer', 'customer', ''); kunde('k_fehlt', 'lead'); kunde('k_blank', 'lost', ' '); kunde('k_archiv', 'archived', '') kunde('k_unt', 'customer', 'unternehmen'); kunde('k_priv', 'lead', 'privat'); kunde('k_fremd', 'customer', 'firma') const OHNE = ['k_archiv', 'k_blank', 'k_fehlt', 'k_leer'] const stand = () => JSON.stringify([...aw.docs('customers')]) const sammler = () => { const zeilen = []; return { log: (z) => zeilen.push(z), text: () => zeilen.join('\n') } } test('parseArgs: nur --pruefen oder --ausfuehren mit Bestätigung, alles andere wirft', () => { assert.deepEqual(parseArgs(['--pruefen']), { modus: 'pruefen' }) assert.deepEqual(parseArgs(['--ausfuehren', '--bestaetigung=Kundenart unternehmen setzen: 4']), { modus: 'ausfuehren', bestaetigung: 'Kundenart unternehmen setzen: 4' }) for (const schlecht of [[], ['--ausfuehren'], ['--pruefen', '--ausfuehren'], ['--force'], ['--pruefen', '--bestaetigung=x']]) assert.throws(() => parseArgs(schlecht), /Aufruf/) assert.equal(bestaetigungsZeile(42), 'Kundenart unternehmen setzen: 42') }) test('pruefen zählt und nennt IDs, schreibt nichts, gibt keine Personendaten aus', async () => { const vorher = stand(); const out = sammler() const e = await pruefen({ log: out.log }) assert.deepEqual([e.gesamt, e.mitKundenart, [...e.ohne].sort(), e.unbekannt], [7, { unternehmen: 1, privat: 1 }, OHNE, ['k_fremd']]) assert.deepEqual(e.ohneNachStatus, { customer: ['k_leer'], lead: ['k_fehlt'], lost: ['k_blank'], archived: ['k_archiv'] }) assert.equal(stand(), vorher) assert.match(out.text(), /Ohne Kundenart: 4/); assert.match(out.text(), /Unbekannte Werte: 1/); assert.match(out.text(), /Kundenart unternehmen setzen: 4/) assert.doesNotMatch(out.text(), /Name |Firma |@example/) }) test('ausfuehren ohne passende Bestätigungszeile schreibt nichts', async () => { const vorher = stand() for (const b of ['', 'ja', 'Kundenart unternehmen setzen: 3', 'Kundenart unternehmen setzen: 5', 'kundenart unternehmen setzen: 4']) await assert.rejects(ausfuehren({ bestaetigung: b, log: () => {} }), /Kundenart unternehmen setzen: 4/, b) assert.equal(stand(), vorher) }) test('ausfuehren setzt nur leere Felder, lässt alles andere stehen und ist wiederholbar', async () => { const out = sammler() const e = await ausfuehren({ bestaetigung: bestaetigungsZeile(4), log: out.log }) assert.deepEqual([e.gesetzt, e.uebersprungen, e.fehler], [4, 0, []]) const art = (id) => aw.docs('customers').get(id).kundenart assert.deepEqual(OHNE.map(art), Array(4).fill('unternehmen')); assert.deepEqual(['k_unt', 'k_priv', 'k_fremd'].map(art), ['unternehmen', 'privat', 'firma']) assert.equal(aw.docs('customers').get('k_leer').name, 'Name k_leer') for (const id of OHNE) assert.match(out.text(), new RegExp(`\\t${id}\\t`)); assert.doesNotMatch(out.text(), /Name |Firma |@example/) assert.equal((await ausfuehren({ bestaetigung: bestaetigungsZeile(0), log: () => {} })).gesetzt, 0) }) test('Wert, der zwischen Prüfen und Schreiben gesetzt wurde, bleibt', async () => { kunde('k_r1', 'lead', ''); kunde('k_r2', 'lead', '') const e = await ausfuehren({ bestaetigung: bestaetigungsZeile(2), log: (z) => { if (/^Schreibe 2 /.test(z)) kunde('k_r1', 'lead', 'privat') } }) assert.deepEqual([e.gesetzt, e.uebersprungen], [1, 1]) assert.deepEqual(['k_r1', 'k_r2'].map((id) => aw.docs('customers').get(id).kundenart), ['privat', 'unternehmen']) }) test('Schreibfehler stehen im Ergebnis, die übrigen Einträge werden trotzdem gesetzt', async () => { kunde('k_e1', 'lead', ''); kunde('k_e2', 'lead', '') aw.failNext({ collection: 'customers', method: 'PATCH', status: 500, message: 'kaputt' }) const e = await ausfuehren({ bestaetigung: bestaetigungsZeile(2), log: () => {} }) assert.deepEqual([e.gesetzt, e.fehler.length], [1, 1]); assert.match(e.fehler[0].grund, /kaputt/) }) ``` - [ ] **Step 2: Lauf, muss scheitern** Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenartBestand.test.js` Expected: FAIL mit `Cannot find module` auf `kundenart-bestand.mjs`. - [ ] **Step 3: Implementieren** in `scripts/kundenart-bestand.mjs` (Vorlage für Aufbau und Einstieg: `scripts/feature-consent-felder-leeren.mjs`) Regeln, die der Test nicht allein bestimmt: 1. Leer heißt `String(doc.kundenart ?? '').trim() === ''`, gültig ist ein Wert aus `KUNDENARTEN`, alles andere ist `unbekannt` und wird nie geschrieben. 2. `ausfuehren` ruft `pruefen`. Bei 0 leeren Einträgen kehrt es sofort zurück. Sonst muss `bestaetigung` exakt `bestaetigungsZeile(ohne.length)` sein, sonst `Error` mit dieser Zeile in der Meldung, vor jedem Schreiben. 3. Dann `log('Schreibe ${n} Einträge …')`, je ID `getDocument` neu lesen, nur bei noch leerem Feld `updateDocument(config.collections.customers, id, { kundenart: 'unternehmen' })` mit genau diesem einen Feld, sonst `uebersprungen`. Ein Schreibfehler landet in `fehler`, die Schleife läuft weiter. 4. Protokollzeile je Eintrag `\t\t\t->\tunternehmen`, am Ende `Summe gesetzt=… uebersprungen=… fehler=…`. Die Protokolldatei entsteht per `tee` nach `RB` (Task 8), nicht im Container. 5. `pruefen` druckt `Einträge gesamt: n`, `Ohne Kundenart: n`, `Unbekannte Werte: n`, je `customerStatus` eine Zeile `: ` und zuletzt die Bestätigungszeile `Kundenart unternehmen setzen: n` (Task 8 liest diese Zeilen). 6. Einstieg nur bei direktem Start. Exit 0 bei Erfolg, 1 bei Schreibfehlern, 2 bei falschem Aufruf oder falscher Bestätigung. - [ ] **Step 4: Lauf grün** Run: wie Step 2. Expected: PASS, 6 Tests. - [ ] **Step 5: Commit** ```bash cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht git add scripts/kundenart-bestand.mjs test/kundenartBestand.test.js git commit -m "Kundenansicht neu P10: Skript setzt die Kundenart der Bestandseinträge auf Unternehmen (E18)" -m "Co-Authored-By: Claude Opus 5.5 " ``` --- ### Task 3: Umschalt-Weiche (`server/routes/umschaltSeiten.js`) **Files:** - Create: `server/routes/umschaltSeiten.js` - Modify: `server/index.js` (Import und eine Zeile `app.use(umschaltSeitenRouter({ publicDir }))` hinter P0s zwei Weichen-Zeilen und vor `app.use(express.static(publicDir))`) - Test: `test/knUmschaltSeiten.test.js` **Interfaces:** - Consumes: `neueAnsichtModus(): 'off'|'pilot'|'on'` (V1), `termsDocFor(version)` aus `server/services/products.js` (liefert `{ url, pdfFile }`), `config.abo.termsVersionNeu`. - Produces: ```js export const UMSCHALT_SEITEN = Object.freeze({ // Adresse → Datei unter public/ '/dashboard.html': 'neu/dashboard.html', // greift nur, wo P0s Weiche nicht gegriffen hat, also ohne Sitzung '/login.html': 'neu/oeffentlich/login.html', '/kuendigen.html': 'neu/oeffentlich/kuendigen.html', '/widerrufen.html': 'neu/oeffentlich/widerrufen.html', '/offline.html': 'neu/oeffentlich/offline.html', '/manifest.webmanifest': 'neu/manifest.webmanifest', }) export function umschaltSeitenRouter({ publicDir, modus = neueAnsichtModus, fassung6 = fassung6Aus }): express.Router // fassung6Aus() = config.abo.termsVersionNeu ? termsDocFor(config.abo.termsVersionNeu) : null → { url, pdfFile } | null ``` Regeln: nur GET und HEAD. Ist `modus() !== 'on'`, ruft jede Zeile `next()`. Bei `on` liefert `res.sendFile` die Datei mit `Cache-Control: no-cache`. Zusätzliche Zeilen, nur bei `on` und gesetztem `fassung6()`: `/abo-bedingungen.html` liefert `fassung6().url`, `/WEBklar-Abo-Vertragsunterlagen.pdf` liefert `fassung6().pdfFile`. Grund: Stripe-Checkout und die neuen Seiten verlinken `/abo-bedingungen.html` (`stripeBilling.js:148,162`) und müssen im Fassung-6-Weg deren Wortlaut zeigen. Jeder Pfad wird gegen `publicDir` geschützt (aufgelöster Pfad muss darunter liegen). Fehlt die Datei (`ENOENT`), ruft die Zeile `next()`. - [ ] **Step 1: Failing test schreiben** (`test/knUmschaltSeiten.test.js`; Kopf wie `test/kundenansichtEnde.test.js:1-19` für Mock, Stripe, `useTestEnv`; dann dynamisch `startTestApp`, `umschaltSeitenRouter`, `UMSCHALT_SEITEN`; dazu `express`, `node:fs`, `node:os`, `node:path`, `node:url`) ```js const publicDir = fileURLToPath(new URL('../public/', import.meta.url)) let modus = 'off'; let fassung6 = null const router = express.Router() router.use(umschaltSeitenRouter({ publicDir, modus: () => modus, fassung6: () => fassung6 })) router.use(express.static(publicDir)) const app = await startTestApp({ '/': router }) const holen = async (pfad, basis = app.url) => { const r = await fetch(basis + pfad, { redirect: 'manual' }); return { status: r.status, bytes: Buffer.from(await r.arrayBuffer()), headers: r.headers } } const datei = (rel) => readFileSync(path.join(publicDir, rel)) for (const [pfad, neu] of Object.entries(UMSCHALT_SEITEN)) test(`${pfad}: off und pilot liefern die Datei von heute, on die neue (no-cache)`, async () => { for (const m of ['off', 'pilot']) { modus = m; const r = await holen(pfad); assert.ok(r.status === 200 && r.bytes.equals(datei(pfad.slice(1))), `${m} ${pfad}`) } modus = 'on'; const r = await holen(pfad) assert.ok(r.status === 200 && r.bytes.equals(datei(neu)), pfad); assert.equal(r.headers.get('cache-control'), 'no-cache') }) test('Manifest bei on ist JSON', async () => { modus = 'on'; assert.match((await holen('/manifest.webmanifest')).headers.get('content-type'), /json/) }) test('Fassung 6: Seite und PDF kommen bei on aus der Neu-Fassung, sonst bleibt die Datei von heute', async () => { fassung6 = { url: '/neu/oeffentlich/offline.html', pdfFile: 'WEBklar-Wartungs-Abo-Vertragsunterlagen.pdf' } // Stellvertreter-Dateien modus = 'pilot'; assert.ok((await holen('/abo-bedingungen.html')).bytes.equals(datei('abo-bedingungen.html'))) modus = 'on' assert.ok((await holen('/abo-bedingungen.html')).bytes.equals(datei('neu/oeffentlich/offline.html'))) assert.ok((await holen('/WEBklar-Abo-Vertragsunterlagen.pdf')).bytes.equals(datei('WEBklar-Wartungs-Abo-Vertragsunterlagen.pdf'))) fassung6 = null; assert.ok((await holen('/abo-bedingungen.html')).bytes.equals(datei('abo-bedingungen.html'))) }) test('Pfadwächter: eine Adresse außerhalb von public/ wird nie geliefert', async () => { modus = 'on'; fassung6 = { url: '/../../package.json', pdfFile: '../../server/config.js' } for (const p of ['/abo-bedingungen.html', '/WEBklar-Abo-Vertragsunterlagen.pdf']) assert.ok((await holen(p)).bytes.equals(datei(p.slice(1))), p) }) test('fehlende neue Datei: Rückfall auf die alte statt 500', async () => { const tmp = mkdtempSync(path.join(tmpdir(), 'kn-weiche-')); writeFileSync(path.join(tmp, 'login.html'), 'ALT') const a2 = express(); a2.use(umschaltSeitenRouter({ publicDir: tmp, modus: () => 'on', fassung6: () => null })); a2.use(express.static(tmp)) const srv = await new Promise((res) => { const s = a2.listen(0, '127.0.0.1', () => res(s)) }) try { const r = await holen('/login.html', `http://127.0.0.1:${srv.address().port}`); assert.deepEqual([r.status, r.bytes.toString()], [200, 'ALT']) } finally { srv.close() } }) test('server/index.js hängt die Weiche hinter P0s Weichen und vor express.static ein', () => { const index = readFileSync(new URL('../server/index.js', import.meta.url), 'utf8') const reihe = ["app.get('/dashboard.html', dashboardWeiche(", 'app.use(umschaltSeitenRouter(', 'app.use(express.static(publicDir))'].map((s) => index.indexOf(s)) assert.ok(reihe[0] > -1 && reihe.every((n, i) => i === 0 || n > reihe[i - 1]), reihe.join(',')) }) ``` - [ ] **Step 2: Lauf, muss scheitern** Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/knUmschaltSeiten.test.js` Expected: FAIL mit `Cannot find module` auf `umschaltSeiten.js`. - [ ] **Step 3: Implementieren** nach den Regeln oben. Eine Hilfsfunktion `unterPublic(publicDir, rel): string | null` löst `'.' + path.posix.normalize('/' + rel)` gegen `publicDir` auf und gibt `null` zurück, wenn das Ergebnis nicht unter `publicDir` liegt. - [ ] **Step 4: Lauf grün, dazu die Pins am Einhängen** Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test --test-concurrency=1 test/knUmschaltSeiten.test.js test/kundenansichtDashboard.test.js test/adminPanelGone.test.js test/legalDocs.test.js` Expected: PASS, `fail 0`. - [ ] **Step 5: Commit** ```bash cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht git add server/routes/umschaltSeiten.js server/index.js test/knUmschaltSeiten.test.js git commit -m "Kundenansicht neu P10: Umschalt-Weiche liefert öffentliche Seiten, Manifest und Fassung-6-Adresse bei on" -m "Co-Authored-By: Claude Opus 5.5 " ``` --- ### Task 4: Nachprüf-Skript (`scripts/umschalten-nachpruefung.mjs`) **Files:** - Create: `scripts/umschalten-nachpruefung.mjs` - Test: `test/knUmschaltNachpruefung.test.js` **Interfaces:** - Consumes: nur HTTP (`fetch`), keine Cookies, kein Appwrite. - Produces: ```js export const PRUEFUNGEN = { neu: Pruefung[], alt: Pruefung[] } // Pruefung = { pfad, typ: 'html'|'json'|'pdf', enthaelt: string[], enthaeltNicht: string[] } export function parseArgs(argv) // [basis, '--erwartet=neu'|'--erwartet=alt'] → { basis, erwartet }; basis nur http(s)://host[:port] ohne Pfad, sonst Error "Aufruf: …" export async function nachpruefen(basis, erwartet, { fetchFn = fetch, ohne = [] } = {}) // → { ok, zeilen: [{ pfad, ok, grund }] } ``` Marker (fest vorgegeben; jede Zeile verlangt Status 200): | Pfad | `neu`: enthält | `neu`: enthält nicht | `alt`: enthält | |---|---|---|---| | `/dashboard.html` (ohne Cookie) | `` | `kb-page` | `class="portal-page dashboard-page kb-page"` | | `/login.html` | `content="#ffffff"`, `/neu/tokens.css` | `#9d3dfe`, `/login.css` | `/login.css`, `content="#09090b"` | | `/kuendigen.html`, `/widerrufen.html` | `/neu/oeffentlich/oeffentlich.css`, `/legal-form.js` | `/legal-form.css` | `/legal-form.css` | | `/offline.html` | `content="#ffffff"` | `#9d3dfe`, ` p.typ === 'json' ? { status: 200, type: 'application/manifest+json', body: JSON.stringify(erwartet === 'neu' ? MANIFEST : { ...MANIFEST, background_color: '#000000', theme_color: '#000000' }) } : p.typ === 'pdf' ? { status: 200, type: 'application/pdf', body: '%PDF-1.7 …' } : { status: 200, type: 'text/html', body: p.enthaelt.join(' ') } const stub = (erwartet, aendern = () => null) => async (url) => { const pfad = new URL(url).pathname; const a = aendern(pfad) ?? antwort(PRUEFUNGEN[erwartet].find((x) => x.pfad === pfad), erwartet) if (a instanceof Error) throw a return new Response(a.body, { status: a.status, headers: { 'content-type': a.type } }) } test('neu und alt: alle Zeilen grün, eine Zeile je Prüfung', async () => { for (const e of ['neu', 'alt']) { const r = await nachpruefen('https://stub.invalid', e, { fetchFn: stub(e) }); assert.deepEqual([r.ok, r.zeilen.length], [true, PRUEFUNGEN[e].length], e) } }) test('neu gegen alte Seiten ist rot und nennt die Seiten', async () => { const r = await nachpruefen('https://stub.invalid', 'neu', { fetchFn: async (u) => stub('alt')(u).catch(() => new Response('', { status: 404 })) }) assert.equal(r.ok, false); assert.ok(r.zeilen.filter((z) => !z.ok).map((z) => z.pfad).includes('/login.html')) }) test('Status 500, Netzfehler und verbotener Marker färben genau ihre Zeile rot', async () => { const r = await nachpruefen('https://stub.invalid', 'neu', { fetchFn: stub('neu', (p) => p === '/offline.html' ? { status: 500, type: 'text/html', body: '' } : p === '/kuendigen.html' ? new Error('ECONNRESET') : p === '/login.html' ? { status: 200, type: 'text/html', body: 'content="#ffffff" /neu/tokens.css #9d3dfe' } : null) }) assert.deepEqual(r.zeilen.filter((z) => !z.ok).map((z) => z.pfad).sort(), ['/kuendigen.html', '/login.html', '/offline.html']) assert.match(r.zeilen.find((z) => z.pfad === '/offline.html').grund, /500/) }) test('gegen die echten Dateien der Weiche: alt bei off, neu bei on (ohne die Fassung-6-Zeilen)', async () => { let modus = 'off'; const router = express.Router() router.use(umschaltSeitenRouter({ publicDir, modus: () => modus, fassung6: () => null })); router.use(express.static(publicDir)) const app = await startTestApp({ '/': router }); const OHNE = ['/abo-bedingungen.html', '/WEBklar-Abo-Vertragsunterlagen.pdf'] assert.deepEqual((await nachpruefen(app.url, 'alt', { ohne: OHNE })).zeilen.filter((z) => !z.ok), []) modus = 'on'; const r = await nachpruefen(app.url, 'neu', { ohne: OHNE }); assert.deepEqual(r.zeilen.filter((z) => !z.ok), []); assert.equal(r.zeilen.length, PRUEFUNGEN.neu.length - 2) }) test('parseArgs', () => { assert.deepEqual(parseArgs(['https://project.webklar.com', '--erwartet=neu']), { basis: 'https://project.webklar.com', erwartet: 'neu' }) for (const schlecht of [[], ['https://x.invalid'], ['ftp://x.invalid', '--erwartet=neu'], ['https://x.invalid/pfad', '--erwartet=neu'], ['https://x.invalid', '--erwartet=beide']]) assert.throws(() => parseArgs(schlecht), /Aufruf/) }) ``` - [ ] **Step 2: Lauf, muss scheitern** Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/knUmschaltNachpruefung.test.js` Expected: FAIL mit `Cannot find module` auf `umschalten-nachpruefung.mjs`. - [ ] **Step 3: Implementieren.** Je Prüfung ein `fetch` mit `redirect: 'manual'` und Zeitlimit 15 s. `grund` nennt Status, fehlenden Marker, verbotenen Marker oder `Netzfehler`. Der Einstieg druckt je Zeile `ok` bzw. `FEHLER ` und setzt Exit 0 bzw. 1 (2 bei falschem Aufruf). - [ ] **Step 4: Lauf grün.** Run: wie Step 2. Expected: PASS, 5 Tests. Passt ein Marker nicht zu den echten Dateien (Test „echten Dateien“ rot), ändert der Agent nur die Marker-Tabelle, nie die Seiten. - [ ] **Step 5: Commit** ```bash cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht git add scripts/umschalten-nachpruefung.mjs test/knUmschaltNachpruefung.test.js git commit -m "Kundenansicht neu P10: Nachprüf-Skript prüft die ausgelieferten Seiten gegen alt oder neu" -m "Co-Authored-By: Claude Opus 5.5 " ``` --- ### Task 5: Käufe lesen (`scripts/kauf-bestand-pruefen.mjs`) **Files:** - Create: `scripts/kauf-bestand-pruefen.mjs` - Test: `test/kaufBestandPruefen.test.js` **Interfaces:** - Consumes: `listAllDocuments` aus `appwriteAdmin.js`, `config.collections.portalFeatures`, `withdrawalDeadline(contractAt) → { lastDay, endsAt }` aus `server/services/deadlines.js:99`. - Produces: `export async function pruefeKaeufe({ log, jetzt = new Date() })` → `{ offeneBestellungen: string[], fristLaeuft: string[], gekauftGesamt: number }` (nur Dokument-IDs). Rein lesend. Der Einstieg setzt Exit 1, wenn `offeneBestellungen` nicht leer ist. - [ ] **Step 1: Failing test schreiben** (Kopf und Hilfen `feature(id, over)` und `sammler()` wie `test/featureConsentFelder.test.js:1-30`, Mock mit `portalFeatures: schemaKeys('portalFeatures')`; `jetzt = new Date('2026-11-10T10:00:00Z')`) ```js feature('f_offen', { state: 'ordered', purchaseStatus: 'ordered' }) // wartet auf Zahlung feature('f_frist', { state: 'setup', purchaseStatus: 'paid', unlockedByPurchase: true, orderedAt: '2026-11-08T09:00:00.000Z' }) // Frist läuft feature('f_alt', { state: 'active', purchaseStatus: 'paid', unlockedByPurchase: true, orderedAt: '2026-09-20T09:00:00.000Z' }) // Frist vorbei feature('f_abo', { state: 'active', unlockedBy: 'abo' }); feature('f_aus', { state: 'off' }) // nicht gekauft test('zählt offene Bestellungen und laufende Fristen, schreibt nichts, nennt nur IDs', async () => { const vorher = JSON.stringify([...aw.docs('portalFeatures')]); const out = sammler() const e = await pruefeKaeufe({ log: out.log, jetzt }) assert.deepEqual([e.offeneBestellungen, e.fristLaeuft, e.gekauftGesamt], [['f_offen'], ['f_frist'], 2]) assert.equal(JSON.stringify([...aw.docs('portalFeatures')]), vorher) assert.match(out.text(), /Offene Bestellungen: 1/); assert.match(out.text(), /Fristen laufen: 1/) }) test('ohne Käufe: alles leer', async () => { aw.docs('portalFeatures').clear(); assert.deepEqual(await pruefeKaeufe({ log: () => {}, jetzt }), { offeneBestellungen: [], fristLaeuft: [], gekauftGesamt: 0 }) }) ``` - [ ] **Step 2: Lauf, muss scheitern.** Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kaufBestandPruefen.test.js`. Expected: FAIL (`Cannot find module`). - [ ] **Step 3: Implementieren.** „Gekauft“ heißt `state` in `setup | active | disabled` und `unlockedByPurchase`; „Frist läuft“ heißt `withdrawalDeadline(consentAt || orderedAt).endsAt > jetzt` (Näherung, nur zur Information); offen heißt `state === 'ordered'`. Ausgabezeilen `Offene Bestellungen: n (id, …)` und `Fristen laufen: n (id, …)`. - [ ] **Step 4: Lauf grün.** Run: wie Step 2. Expected: PASS, 2 Tests. - [ ] **Step 5: Commit** ```bash cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht git add scripts/kauf-bestand-pruefen.mjs test/kaufBestandPruefen.test.js git commit -m "Kundenansicht neu P10: Skript zählt offene Bestellungen und Widerrufsfristen gekaufter Funktionen" -m "Co-Authored-By: Claude Opus 5.5 " ``` --- ### Task 6: Umschalt-Checkliste (`docs/umschalten-kundenansicht.md`) **Files:** - Create: `docs/umschalten-kundenansicht.md` - Test: `test/knUmschaltCheckliste.test.js` **Interfaces:** - Produces: eine Anleitung für Inhaber und Controller. Reihenfolge laut Spec 5.10: Kundenart setzen, Fassung 6 hochgeladen, WOMS-Pflichtfeld live, dann `KUNDENANSICHT_NEU=on`, Rückweg `off`. - [ ] **Step 1: Failing test schreiben** ```js const doc = readFileSync(new URL('../docs/umschalten-kundenansicht.md', import.meta.url), 'utf8') const KOEPFE = ['## 1. Voraussetzungen', '## 2. Kundenart der Bestandseinträge setzen', '## 3. Fassung 6 hochgeladen', '## 4. WOMS-Pflichtfeld live', '## 5. Schalter auf on', '## 6. Nachprüfung', '## 7. Rückweg', '## 8. Danach: festschreiben und aufräumen'] test('Überschriften in der Reihenfolge der Spec 5.10', () => { const pos = KOEPFE.map((k) => doc.indexOf(k)); assert.ok(pos.every((p, i) => p > -1 && (i === 0 || p > pos[i - 1])), pos.join(',')) }) test('nennt die Befehle und die Stopps', () => { for (const s of ['kundenart-bestand.mjs --pruefen', '--ausfuehren', 'KUNDENANSICHT_NEU=on', 'KUNDENANSICHT_NEU=off', 'docker compose up -d --no-deps kundenbereich-server', '--erwartet=neu', 'docker-compose.yml', 'Stop: Inhaber-OK nötig', 'kein Rückweg per Schalter']) assert.ok(doc.includes(s), s) }) ``` - [ ] **Step 2: Lauf, muss scheitern.** Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/knUmschaltCheckliste.test.js`. Expected: FAIL mit `ENOENT`. - [ ] **Step 3: Schreiben.** Die acht Abschnitte in du-Form, je höchstens zehn Zeilen. Inhalt aus den Tasks 8 bis 11 und 14 (Befehle wörtlich übernehmen, `` aus `--pruefen`). Abschnitt 7 nennt den Rückweg (`sed` auf `off`, derselbe Compose-Befehl, `--erwartet=alt`) und dass Verträge nach Fassung 6 gültig bleiben. Abschnitt 8 nennt „kein Rückweg per Schalter“ und den Beobachtungszeitraum. - [ ] **Step 4: Lauf grün.** Run: wie Step 2. Expected: PASS, 2 Tests. - [ ] **Step 5: Commit** ```bash cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht git add docs/umschalten-kundenansicht.md test/knUmschaltCheckliste.test.js git commit -m "Kundenansicht neu P10: Checkliste zum Umschalten in der Reihenfolge der Spec" -m "Co-Authored-By: Claude Opus 5.5 " ``` --- ### Task 7: Live, Teil A ausliefern **Files:** keine. **Stop: Inhaber-OK nötig** (Push durch den Inhaber). **Interfaces:** - Consumes: Tasks 2 bis 6 committed. - Produces: Teil A läuft in Produktion (Skripte im Container, Weiche mit Schalter `pilot`); für Kunden ändert sich nichts. - [ ] **Step 1: Teil A als Ganzes prüfen und taggen** Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test --test-concurrency=1 test/kundenartBestand.test.js test/knUmschaltSeiten.test.js test/knUmschaltNachpruefung.test.js test/kaufBestandPruefen.test.js test/knUmschaltCheckliste.test.js test/kundenansichtDashboard.test.js test/adminPanelGone.test.js test/legalDocs.test.js && git status --short && git tag kn-p10-a` Expected: `fail 0`, Status leer. - [ ] **Step 2: Übergabe an den Controller (Text für den Inhaber)** Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht && git fetch origin && git log --oneline origin/main..kn-p10-a` Der Controller schreibt dem Inhaber: „Teil A ändert für Kunden nichts, solange der Schalter nicht `on` ist. Push: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht && git push origin 'kn-p10-a^{commit}:refs/heads/main'`.“ **Stop: Inhaber-OK nötig.** - [ ] **Step 3: Deploy prüfen (nach dem Push des Inhabers)** Run: `git -C /home/webklar/apps/kundenbereich.webklar.com/build rev-parse --short HEAD && git -C /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht rev-parse --short kn-p10-a && docker logs --since 5m webklar-kundenbereich-server 2>&1 | tail -5` Expected: beide Kurzformen gleich (sonst zwei Minuten warten, der Webhook baut), im Log kein `Unbehandelter Fehler`. - [ ] **Step 4: Ruhezustand prüfen** Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht && node scripts/umschalten-nachpruefung.mjs https://project.webklar.com --erwartet=alt` Expected: Exit 0, alle Zeilen `ok`. --- ### Task 8: Live, Kundenart der Bestandseinträge setzen (E18) **Files:** keine. **Stop: Inhaber-OK nötig** vor dem Schreiben. **Interfaces:** - Consumes: `scripts/kundenart-bestand.mjs` im Container (Task 7). - Produces: `Ohne Kundenart: 0` in Produktion; Protokolle in `RB`. - [ ] **Step 1: Prüfen (nur lesend)** Run: ```bash mkdir -p /root/reports/kundenansicht-neu-2026-10-15/umschalten docker exec -w /app webklar-kundenbereich-server node scripts/kundenart-bestand.mjs --pruefen | tee /root/reports/kundenansicht-neu-2026-10-15/umschalten/kundenart-pruefen-$(date -u +%Y%m%d-%H%M%S).log ``` Expected: `Ohne Kundenart: `, `Unbekannte Werte: 0` und die Bestätigungszeile `Kundenart unternehmen setzen: `. Stand der Spec (15.10.): 42 Einträge, nämlich 6 Kunden, 31 Leads, 5 verlorene. Weicht `` ab oder gibt es unbekannte Werte, nennt der Controller dem Inhaber die Zahlen und wartet. - [ ] **Step 2: Freigabe.** Der Controller zeigt dem Inhaber die Zahlen aus Step 1 und fragt, ob alle diese Einträge auf `unternehmen` gesetzt werden sollen. **Stop: Inhaber-OK nötig.** - [ ] **Step 3: Setzen** Run: ```bash set -o pipefail docker exec -w /app webklar-kundenbereich-server node scripts/kundenart-bestand.mjs --ausfuehren --bestaetigung="Kundenart unternehmen setzen: " | tee /root/reports/kundenansicht-neu-2026-10-15/umschalten/kundenart-ausfuehren-$(date -u +%Y%m%d-%H%M%S).log ``` Expected: Exit 0, letzte Zeile `Summe gesetzt= uebersprungen=0 fehler=0`. - [ ] **Step 4: Gegenprobe.** Run: Step 1 noch einmal. Expected: `Ohne Kundenart: 0`. --- ### Task 9: Live, Vorflug (nur lesend) **Files:** keine. **Stop: Inhaber-OK nötig** für den Zeitpunkt des Umschaltens (Step 8). **Interfaces:** - Consumes: Tasks 7 und 8; `scripts/rechtliches-hochladen.mjs --probe` (P8); WOMS-Tag `kn-p9`. - Produces: die Freigabe des Inhabers zum Umschalten, eine Sicherung von `.env`. - [ ] **Step 1: Kundenart.** Run: `docker exec -w /app webklar-kundenbereich-server node scripts/kundenart-bestand.mjs --pruefen | command grep -a "Ohne Kundenart"`. Expected: `Ohne Kundenart: 0`. Sonst Task 8 wiederholen (ein Lead, der seitdem entstand, ist normal). - [ ] **Step 2: Fassung 6 hochgeladen (E20).** Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht && node scripts/rechtliches-hochladen.mjs --probe`. Expected: Die Ausgabe zeigt für `abo-bedingungen` eine aktuelle Version, deren `source` auf `:public/neu/rechtliches/abo-bedingungen.html` endet (Format laut Übergabe von P8; die WOMS-Nummer `vN` zählt Uploads und ist nicht die Fassung). Der Inhaber bestätigt in WOMS unter „Rechtliches“, dass die Karte `abo-bedingungen` die Fassung 6 trägt. Fehlt sie, stoppt der Controller. Hochladen ohne `--probe` nur auf ausdrücklichen Auftrag des Inhabers. - [ ] **Step 3: WOMS-Pflichtfeld live.** Run: `git -C /home/webklar/apps/ticket.webklar.com/build merge-base --is-ancestor kn-p9 HEAD && echo live`. Expected: `live`, und der Inhaber bestätigt: „Neuer Lead“ und „Neuer Kunde“ zeigen die Kundenart ohne Vorgabe. - [ ] **Step 4: Setup und Wortlaut (P7 VF9).** Run: `docker exec -w /app webklar-kundenbereich-server node scripts/kundenansicht-setup.mjs --pruefen; docker exec -w /app webklar-kundenbereich-server grep -n "WORTLAUT_STATUS" server/services/bestelltexte.js`. Expected: Das Setup meldet keine fehlenden Attribute (insbesondere `featureConsents.kundenart`; ohne sie scheitert jede Bestellung der neuen Ansicht mit 500), und die Zeile mit der Definition von `WORTLAUT_STATUS` trägt `'freigegeben'`. Sonst stoppt der Controller; das Setup läuft nur auf ausdrückliche Anweisung nach OK des Inhabers. - [ ] **Step 5: Schalter in Compose und `.env`.** Run: `command grep -a -n 'KUNDENANSICHT_NEU' /home/webklar/apps/docker-compose.yml; command grep -a -n '^KUNDENANSICHT_NEU' /home/webklar/apps/.env`. Expected: in `docker-compose.yml` zwei Zeilen `- KUNDENANSICHT_NEU=${KUNDENANSICHT_NEU:-off}` und `- KUNDENANSICHT_NEU_PILOT_EMAILS=${KUNDENANSICHT_NEU_PILOT_EMAILS:-}` (bei den `ZEITEN_*`-Zeilen um Zeile 1713), in `.env` `KUNDENANSICHT_NEU=pilot` und die Pilot-Liste. Fehlt eine Compose-Zeile, meldet der Controller es (VF2); Task 10 beginnt erst nach der Korrektur. - [ ] **Step 6: Schalter, die den Katalog bestimmen.** Run: `command grep -a -n '^\(ABO_PILOT_EMAILS\|TERMINE_ENABLED\|SHOP_ENABLED\|UNTERKUNFT_ENABLED\|ZEITEN_ENABLED\|ANALYSE_ENABLED\|AUFTRAEGE_ENABLED\)=' /home/webklar/apps/.env`. Der Inhaber bestätigt jeden Wert. Besonders: `ABO_PILOT_EMAILS` muss `*` sein (sonst antwortet jede Abo-Bestellung eines Nicht-Pilot-Kunden mit 403); `ANALYSE_ENABLED` und `AUFTRAEGE_ENABLED` bleiben `off`, bis ihre eigenen Livegänge erledigt sind (ausgeschaltete Funktionen erscheinen nicht unter „Entdecken“, C9). - [ ] **Step 7: Ruhe und Sicherung.** Run: `docker logs --since 72h webklar-kundenbereich-server 2>&1 | command grep -a -c 'Unbehandelter Fehler'` (Expected: 0, sonst die Zeilen ansehen), dann `cp -n /home/webklar/apps/.env /home/webklar/apps/.env.bak-$(date -u +%Y-%m-%d)-kundenansicht-on`. - [ ] **Step 8: Freigabe zum Umschalten.** Der Controller nennt dem Inhaber das Ergebnis der Schritte 1 bis 7 und schlägt eine ruhige Uhrzeit vor. **Stop: Inhaber-OK nötig** („umschalten, jetzt“). --- ### Task 10: Live, Schalter auf `on` (Rückweg `off`) **Files:** keine. **Stop: Inhaber-OK nötig** (Zeitpunkt aus Task 9). **Interfaces:** - Consumes: Freigabe aus Task 9, die Compose-Zeilen. - Produces: `KUNDENANSICHT_NEU=on` in Produktion; Rückweg `off` bis Task 13. - [ ] **Step 1: `.env` ändern** Run: `sed -i 's/^KUNDENANSICHT_NEU=.*/KUNDENANSICHT_NEU=on/' /home/webklar/apps/.env && command grep -a -n '^KUNDENANSICHT_NEU=' /home/webklar/apps/.env` Expected: genau die Zeile `KUNDENANSICHT_NEU=on`. Kommt keine Zeile, stoppt der Controller (der Vorflug war unvollständig). - [ ] **Step 2: Dienst neu starten** Run: `cd /home/webklar/apps && docker compose up -d --no-deps kundenbereich-server && docker ps --filter name=webklar-kundenbereich-server --format '{{.Status}}'` Expected: `Up …`. Antwortet die Seite die ersten Sekunden mit 502, wiederholt der Controller Step 3 nach 10 s. - [ ] **Step 3: Nachprüfen** Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht && node scripts/umschalten-nachpruefung.mjs https://project.webklar.com --erwartet=neu` Expected: Exit 0, alle Zeilen `ok`, einschließlich `/abo-bedingungen.html` mit `Version 6` und des PDFs. - [ ] **Step 4: Rückweg (nur bei Fehler, vorher kurz den Inhaber informieren)** Run: `sed -i 's/^KUNDENANSICHT_NEU=.*/KUNDENANSICHT_NEU=off/' /home/webklar/apps/.env && cd /home/webklar/apps && docker compose up -d --no-deps kundenbereich-server`, dann `node scripts/umschalten-nachpruefung.mjs https://project.webklar.com --erwartet=alt` aus dem Worktree. Expected: Exit 0. Verträge, die unter Fassung 6 abgeschlossen wurden, bleiben gültig (Fassungstabelle und Archiv bleiben). --- ### Task 11: Live, Nachprüfungen (Routen, Login, Pilot-Konten, Mails) **Files:** keine. **Stop: Inhaber-OK nötig** für den optionalen Probekauf. **Interfaces:** - Consumes: Task 10. - Produces: der Beobachtungszeitraum (`RB/beobachtung.txt`) als Voraussetzung für Task 14. - [ ] **Step 1: Routen.** Run: `curl -s https://project.webklar.com/api/auth/me`. Expected: JSON mit `"authenticated":false`; dazu der grüne Lauf aus Task 10 Step 3. - [ ] **Step 2: Login und Oberfläche** (Inhaber mit zwei Konten, einem Betrieb und einem Pilot-Konto, auf Handy und Desktop). Erwartet: (a) `/login.html` ist weiß, die Anmeldung führt auf Website. (b) `#chat`, `#rechnungen`, `#wartung` und `#termine?bestellt=1&projekt=` landen am richtigen Ort mit erhaltener Query. (c) „Verträge kündigen“ und „Vertrag widerrufen“ liegen höchstens zwei Taps entfernt, beide Seiten öffnen weiß. (d) Funktionen zeigt „Meine/Entdecken“ ohne Preise, „Einschalten“ eines Betriebs hat kein Häkchen. (e) Die Kundenansicht der Mitarbeiter (WOMS) zeigt die neue Oberfläche und sperrt Schreibaktionen. - [ ] **Step 3: Mails.** Optional ein Probekauf mit dem Testkonto aus `ABO_TEST_EMAILS` (0,50 €). Erwartet: K01 an den Kunden und T02 ans Team kommen an, der Anhang `WEBklar-Abo-Vertragsunterlagen.pdf` ist Fassung 6, in `featureConsents` steht eine Zeile mit `featureKey` `abo` und der Kundenart. **Stop: Inhaber-OK nötig** für Kauf und Stornieren. - [ ] **Step 4: Logs.** Run: `docker logs --since 30m webklar-kundenbereich-server 2>&1 | command grep -a -c -i 'Unbehandelter Fehler'`. Expected: 0. - [ ] **Step 5: Installierte App.** Android öffnet weiß. iOS liest die Statusleiste beim Installieren: bleibt sie schwarz, wird die App vom Home-Bildschirm entfernt und neu hinzugefügt. - [ ] **Step 6: Beobachtungszeit.** Beginn und Ende (Vorschlag: sieben Tage) stehen in `RB/beobachtung.txt`. Task 12 und 13 dürfen sofort laufen, Task 14 erst nach Ablauf und neuem OK des Inhabers. --- ### Task 12: Offline-Seite in installierten Apps erneuern (Code) **Files:** - Modify: `public/sw.js:5` (`CACHE`) - Test: `test/knOfflineCache.test.js` Hintergrund: Der Service Worker legt `/offline.html` beim Installieren ab (`cache.add(new Request(OFFLINE_URL, { cache: 'reload' }))`). Installieren läuft nur, wenn sich `sw.js` ändert. Die Änderung muss deshalb **nach** dem Umschalten live gehen (Task 13), sonst speichert sie die alte dunkle Seite erneut. **Interfaces:** Produces `CACHE = 'kb-offline-v2'`; `OFFLINE_URL` bleibt `'/offline.html'`. - [ ] **Step 1: Failing test schreiben** ```js const sw = readFileSync(new URL('../public/sw.js', import.meta.url), 'utf8') test('sw.js: Cache v2, Offline-Seite unverändert, alte Caches werden gelöscht', () => { assert.match(sw, /const CACHE = 'kb-offline-v2'/); assert.doesNotMatch(sw, /kb-offline-v1/) assert.match(sw, /const OFFLINE_URL = '\/offline\.html'/); assert.match(sw, /cache\.add\(new Request\(OFFLINE_URL, \{ cache: 'reload' \}\)\)/) assert.match(sw, /if \(key !== CACHE\) await caches\.delete\(key\)/); assert.doesNotMatch(sw, /auftrag/) }) ``` - [ ] **Step 2: Lauf, muss scheitern.** Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/knOfflineCache.test.js`. Expected: FAIL (`kb-offline-v1`). - [ ] **Step 3: `CACHE` auf `'kb-offline-v2'` setzen.** Sonst nichts in `sw.js` ändern. - [ ] **Step 4: Lauf grün, dazu der Pin auf `sw.js`.** Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test --test-concurrency=1 test/knOfflineCache.test.js test/auftraegeKundenportalHuelle.test.js`. Expected: PASS. - [ ] **Step 5: Commit und Tag** ```bash cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht git add public/sw.js test/knOfflineCache.test.js git commit -m "Kundenansicht neu P10: Service Worker legt die weiße Offline-Seite neu ab (Cache v2)" -m "Co-Authored-By: Claude Opus 5.5 " git tag kn-p10-b ``` --- ### Task 13: Live, Offline-Seite ausliefern **Files:** keine. **Stop: Inhaber-OK nötig** (Push). Voraussetzung: Task 10 ist gelaufen. **Interfaces:** - Consumes: Tag `kn-p10-b` (Task 12), `KUNDENANSICHT_NEU=on` (Task 10). - Produces: installierte Apps zeigen offline die weiße Seite. - [ ] **Step 1: Push.** Der Inhaber pusht `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht && git push origin 'kn-p10-b^{commit}:refs/heads/main'`. - [ ] **Step 2: Prüfen.** Run: `curl -s https://project.webklar.com/sw.js | command grep -a -c kb-offline-v2`. Expected: `1`. Dann in einem Browser mit besuchter Seite: Netz aus, Seite neu laden. Expected: weiße Offline-Seite. --- ### Task 14: Fassung 6 festschreiben (kein Rückweg per Schalter ab hier) **Files:** - Move (`git mv -f`): `public/neu/rechtliches/abo-bedingungen.html` nach `public/abo-bedingungen.html`, `public/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf` nach `public/WEBklar-Abo-Vertragsunterlagen.pdf`, dazu `…pdf.sha256` (Prüfsumme der Seite) daneben. Einzige Änderung am Text der Seite: der PDF-Link `/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf` wird `/WEBklar-Abo-Vertragsunterlagen.pdf` (das Druck-PDF bleibt gleich, der Link steht nur am Bildschirm), die Prüfsumme daneben wird neu geschrieben. Die bisherigen Dateien (Fassung 5) liegen schon als Archiv in `public/vertragsunterlagen/v5/` (von P8). - Modify: `server/config.js:855` (eine Konstante für beide Schlüssel), `server/services/products.js` (Zeilen v6 und v5 der Fassungstabelle), `server/services/vertragsNachweis.js` (`fassungDerAnfrage`), `server/routes/umschaltSeiten.js` (Fassung-6-Zeilen und Parameter `fassung6` entfallen, zwei Weiterleitungen kommen dazu), `scripts/build-legal-pdfs.sh` (Modus `abo6` entfällt), `scripts/rechtliches-hochladen.mjs` (`DOKUMENTE_V6`, Eintrag `abo-bedingungen`: neue Pfade). Das ist die Übergabe laut P8-Vertragsfrage 1. - Pins: `test/legalTexts.test.js:43,104-121`, `test/products.test.js:120,190` und P8s Tests `Fassungstabelle`, `Fassung 5 bleibt bis P10`, `aktuelleAboFassung`, `test/aboRoutes.test.js:87`, `test/aboBedingungenV6.test.js` (Konstante `V6`), `test/legalV6Build.test.js` (`abo6` und PDF-Schleife), `test/rechtlichesHochladen.test.js` (Pfade des ersten Eintrags), `test/knUmschaltSeiten.test.js` (die zwei Fassung-6-Tests entfallen, ihr Gegenstand verschwindet) - Test: `test/knFassung6Fest.test.js` **Interfaces:** - Consumes: `termsDocFor`, `contractAttachmentFor` (`products.js`), `fassungDerAnfrage(req, termsVersion)` (P7: `'alt' | 'neu' | null`). - Produces: `config.abo.termsVersion === config.abo.termsVersionNeu` (Muster `^\d{4}-\d{2}-\d{2}-v6$`). `termsDocFor()` liefert `url: '/abo-bedingungen.html'` und `pdfFile: 'WEBklar-Abo-Vertragsunterlagen.pdf'`. `termsDocFor()` liefert die Archivwerte (`url: '/vertragsunterlagen/v5/abo-bedingungen.html'`, `pdfFile: 'vertragsunterlagen/v5/WEBklar-Abo-Vertragsunterlagen.pdf'`); das Feld `archiv` der Tabelle entfällt. `fassungDerAnfrage` prüft die Neu-Fassung zuerst: Ist `termsVersion` gleich der Neu-Fassung, gilt `neueAnsicht(req) ? 'neu' : null`, nie `'alt'`. `umschaltSeitenRouter` leitet `/neu/rechtliches/abo-bedingungen.html` und `/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf` dauerhaft (301, unabhängig vom Schalter) auf `/abo-bedingungen.html` bzw. `/WEBklar-Abo-Vertragsunterlagen.pdf`. Grund: Die zehn Funktionsseiten von P8 verlinken die Abo-Seite unter dem Entwurfspfad, und Kopien in WOMS können das PDF dort verlinken. Die Weiterleitungen halten diese Links gültig, ohne die freigegebenen Funktionsseiten und PDFs anzufassen. - [ ] **Step 1: Beobachtungszeit und OK prüfen, Hashes ermitteln.** Der Inhaber hat nach der Beobachtungszeit (Task 11 Step 6) „festschreiben“ gesagt. Dann: Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht && sha256sum public/WEBklar-Abo-Vertragsunterlagen.pdf public/vertragsunterlagen/v5/WEBklar-Abo-Vertragsunterlagen.pdf public/neu/rechtliches/abo-bedingungen.html public/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf && cat public/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf.sha256 && command grep -a -c 'href="/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf"' public/neu/rechtliches/abo-bedingungen.html && git status --short` Expected: Zeile 1 = Zeile 2 (das PDF im Archiv v5 ist das heutige PDF; die HTML-Datei des Archivs unterscheidet sich nur im PDF-Link, das prüft `test/legalTexts.test.js` von P8), die Prüfsumme neben dem PDF gleicht Zeile 3 (Seite Fassung 6), die Zahl der Entwurfslinks ist `1`, der Status leer. Weicht etwas ab, stoppt der Agent (VF3). Die Werte heißen `V5_PDF_SHA` (Zeile 1), `V6_QUELLE_SHA` (Zeile 3) und `V6_PDF_SHA` (Zeile 4). - [ ] **Step 2: Failing test schreiben** (`test/knFassung6Fest.test.js`; Kopf wie `test/knUmschaltSeiten.test.js` mit `createHash`, `config`, `termsDocFor`, `contractAttachmentFor`, `fassungDerAnfrage`) ```js const V5_PDF_SHA = '<64 Hex aus Step 1>'; const V6_QUELLE_SHA = '<…>'; const V6_PDF_SHA = '<…>' const LINK_ENTWURF = 'href="/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf"'; const LINK_HAUPT = 'href="/WEBklar-Abo-Vertragsunterlagen.pdf"' const sha256 = (b) => createHash('sha256').update(b).digest('hex') const sha = (rel) => sha256(readFileSync(new URL(`../${rel}`, import.meta.url))) const lies = (rel) => readFileSync(new URL(`../${rel}`, import.meta.url), 'utf8') test('Hauptseite ist die Seite von P8 mit genau einem getauschten PDF-Link, ohne Entwurfsmarke; PDF, Prüfsumme und Archiv stimmen; die Entwurfsdateien sind weg', () => { const seite = lies('public/abo-bedingungen.html') assert.equal(seite.split(LINK_HAUPT).length, 2); assert.ok(!seite.includes(LINK_ENTWURF)) assert.equal(sha256(seite.replace(LINK_HAUPT, LINK_ENTWURF)), V6_QUELLE_SHA) assert.equal(sha('public/WEBklar-Abo-Vertragsunterlagen.pdf'), V6_PDF_SHA) assert.equal(lies('public/WEBklar-Abo-Vertragsunterlagen.pdf.sha256').trim(), sha('public/abo-bedingungen.html')) assert.equal(sha('public/vertragsunterlagen/v5/WEBklar-Abo-Vertragsunterlagen.pdf'), V5_PDF_SHA) for (const f of ['abo-bedingungen.html', 'WEBklar-Abo-Vertragsunterlagen.pdf', 'WEBklar-Abo-Vertragsunterlagen.pdf.sha256']) assert.equal(existsSync(new URL(`../public/neu/rechtliches/${f}`, import.meta.url)), false, f) const text = seite.replace(/<[^>]+>/g, ' ') assert.match(text, /Version 6 · Stand/); assert.doesNotMatch(text, /Entwurf – Prüfung durch Kanzlei offen/) }) test('die Entwurfsadressen leiten dauerhaft weiter (Links aus den Funktionsseiten und aus Kopien bleiben gültig)', async () => { const router = express.Router(); router.use(umschaltSeitenRouter({ publicDir, modus: () => 'off' })); router.use(express.static(publicDir)) const app = await startTestApp({ '/': router }) for (const [von, nach] of [['/neu/rechtliches/abo-bedingungen.html', '/abo-bedingungen.html'], ['/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf', '/WEBklar-Abo-Vertragsunterlagen.pdf']]) { const r = await fetch(app.url + von, { redirect: 'manual' }); assert.deepEqual([r.status, new URL(r.headers.get('location'), app.url).pathname], [301, nach], von) } }) test('termsDocFor und Mail-Anhang je Fassung', () => { const v6 = config.abo.termsVersion; const { url, pdfFile } = termsDocFor(v6) assert.deepEqual([url, pdfFile], ['/abo-bedingungen.html', 'WEBklar-Abo-Vertragsunterlagen.pdf']) const v5 = termsDocFor('2026-09-30-v5'); assert.deepEqual([v5.url, v5.pdfFile], ['/vertragsunterlagen/v5/abo-bedingungen.html', 'vertragsunterlagen/v5/WEBklar-Abo-Vertragsunterlagen.pdf']) for (const v of [v6, '2026-09-30-v5', '2026-09-30-v4', '2026-09-25-v3']) assert.equal(readFileSync(contractAttachmentFor(v).path).subarray(0, 5).toString(), '%PDF-', v) }) test('eine Fassung in der Konfiguration', () => { assert.equal(config.abo.termsVersion, config.abo.termsVersionNeu); assert.match(config.abo.termsVersion, /^\d{4}-\d{2}-\d{2}-v6$/) }) test('fassungDerAnfrage: Fassung 6 ist für neue Sitzungen "neu", nie "alt"; v5, v4, leer sind null', () => { const req = { session: { customerId: 'c3', appwriteUserId: 'u3', email: 'k3@example.invalid' } }; const gemerkt = config.kundenansichtNeu.mode try { config.kundenansichtNeu.mode = 'on' assert.equal(fassungDerAnfrage(req, config.abo.termsVersion), 'neu') for (const v of ['2026-09-30-v5', '2026-09-30-v4', '', undefined, 6]) assert.equal(fassungDerAnfrage(req, v), null, String(v)) config.kundenansichtNeu.mode = 'off'; assert.equal(fassungDerAnfrage(req, config.abo.termsVersion), null) } finally { config.kundenansichtNeu.mode = gemerkt } }) test('Build-Skript und Hochladen-Skript kennen nur noch die Hauptadresse', () => { const sh = lies('scripts/build-legal-pdfs.sh') assert.deepEqual(sh.split('\n').filter((l) => l.trim().startsWith('pdf ') && l.includes('abo-bedingungen')), [' pdf public/abo-bedingungen.html public/WEBklar-Abo-Vertragsunterlagen.pdf']) assert.doesNotMatch(sh, /^abo6\)$/m); assert.doesNotMatch(sh, /abo6\|/) assert.doesNotMatch(lies('scripts/rechtliches-hochladen.mjs'), /neu\/rechtliches\/(abo-bedingungen\.html|WEBklar-Abo-Vertragsunterlagen\.pdf)/) }) ``` - [ ] **Step 3: Lauf, muss scheitern.** Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/knFassung6Fest.test.js`. Expected: FAIL (Hauptseite ist noch Fassung 5, `termsVersion` ist v5). - [ ] **Step 4: Umsetzen.** 1. `git mv -f` für die drei Dateien aus „Files“. Dann in der Seite den einen PDF-Link tauschen (Entwurfspfad auf `/WEBklar-Abo-Vertragsunterlagen.pdf`) und die Prüfsumme neben dem PDF neu schreiben: `sha256sum public/abo-bedingungen.html | cut -d' ' -f1 > public/WEBklar-Abo-Vertragsunterlagen.pdf.sha256`. Die Seite sonst nicht bearbeiten, das PDF nicht neu erzeugen. 2. `server/config.js`: eine Konstante mit dem Wert der bisherigen `termsVersionNeu`, gelesen von `termsVersion` und `termsVersionNeu`. 3. `products.js`: Zeile v6 auf `/abo-bedingungen.html` und `WEBklar-Abo-Vertragsunterlagen.pdf`, Zeile v5 auf die Werte ihres Feldes `archiv` (das Feld entfällt). `endParagraph` bleiben. 4. `fassungDerAnfrage`: Reihenfolge wie bei „Produces“. `umschaltSeiten.js`: Fassung-6-Zeilen und die Parameter `fassung6` und `fassung6Aus` entfernen, die zwei Weiterleitungen ergänzen; die zwei Fassung-6-Tests in `knUmschaltSeiten.test.js` entfallen. 5. `scripts/build-legal-pdfs.sh`: den Modus `abo6` samt Kopfkommentar und der Nennung in der Usage-Zeile entfernen (`all)` und `funktionen6)` bleiben). `scripts/rechtliches-hochladen.mjs`: im Eintrag `abo-bedingungen` von `DOKUMENTE_V6` die Pfade `public/WEBklar-Abo-Vertragsunterlagen.pdf` und `public/abo-bedingungen.html`. 6. Pins von P8 und aus der Vorzeit: `legalTexts.test.js:104-111` auf `-v6$` und `Version 6 · Stand` (und `!includes('Version 5')`); `:43` bleibt (`CUR` ist jetzt Fassung 6); `:112-121` bleibt gültig (die Seite trägt nach dem Tausch `href="/WEBklar-Abo-Vertragsunterlagen.pdf"`). `aboBedingungenV6.test.js` verlangt dort den Entwurfslink und nicht den Hauptlink: diese eine Assertion kehrt sich um. `products.test.js:120,190` und P8s drei Tests auf die Werte aus „Produces“ (der Test `aktuelleAboFassung` erwartet nun für jeden Modus `config.abo.termsVersionNeu`). `aboBedingungenV6.test.js`: Konstante `V6` auf `public/abo-bedingungen.html`, `ENTWURF` bleibt `false`. `legalV6Build.test.js`: die `abo6`-Zeilen entfallen, die PDF-Schleife prüft den Eintrag `abo-bedingungen` an den neuen Pfaden. `rechtlichesHochladen.test.js`: Pfade des ersten Eintrags. `aboRoutes.test.js:87` bleibt `'/abo-bedingungen.html'`. - [ ] **Step 5: Lauf grün, mit allen Pins auf die Seite** Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht && timeout 900 nice -n 10 node --max-old-space-size=1024 --test --test-concurrency=1 test/knFassung6Fest.test.js test/knUmschaltSeiten.test.js test/legalTexts.test.js test/products.test.js test/aboRoutes.test.js test/buildLegalHtml.test.js test/aboBedingungenV6.test.js test/legalV6Build.test.js test/rechtlichesHochladen.test.js test/funktionenRechtV6.test.js test/aboWortlautV6.test.js test/kundenansichtEntscheidungen.test.js test/termineKanzleiPaket.test.js test/termineRechtstexte.test.js test/termineVertragsunterlagen.test.js test/zeitenRecht.test.js test/auftraegeRechtstexte.test.js test/knVertragsNachweis.test.js test/knAboFassung6.test.js test/knCheckoutFassung6.test.js test/knWechselFassung6.test.js test/knSpeicherFassung6.test.js test/knGutschriftFassung6.test.js test/knBestaetigungsmails.test.js test/planSwitch.test.js test/mailStorage.test.js` Expected: `fail 0`. Schlägt eine Zeile auf den Wortlaut der Seite fehl (zum Beispiel `termineVertragsunterlagen.test.js:775-786`, `zeitenRecht.test.js:153`), stoppt der Agent und meldet die Assertion. Er ändert weder Seite noch Pin. - [ ] **Step 6: Commit und Tag** ```bash cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht git add server/config.js server/services/products.js server/services/vertragsNachweis.js server/routes/umschaltSeiten.js scripts/build-legal-pdfs.sh scripts/rechtliches-hochladen.mjs test/knFassung6Fest.test.js test/knUmschaltSeiten.test.js test/legalTexts.test.js test/products.test.js test/aboRoutes.test.js test/aboBedingungenV6.test.js test/legalV6Build.test.js test/rechtlichesHochladen.test.js git commit -m "Kundenansicht neu P10: Fassung 6 ist die geltende Abo-Fassung, Fassung 5 liegt im Archiv" -m "Co-Authored-By: Claude Opus 5.5 " git tag kn-p10-c1 ``` (Die `git mv`-Verschiebungen stehen schon im Index.) --- ### Task 15: Live, Fassung 6 ausliefern **Files:** keine. **Stop: Inhaber-OK nötig** (Push). Ab hier kein Rückweg per Schalter. **Interfaces:** - Consumes: Tag `kn-p10-c1`. - Produces: `/abo-bedingungen.html` ist Fassung 6, das Archiv v5 ist erreichbar. - [ ] **Step 1: Push.** Der Inhaber pusht `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht && git push origin 'kn-p10-c1^{commit}:refs/heads/main'`. - [ ] **Step 2: Deploy und Seiten prüfen.** Run: `git -C /home/webklar/apps/kundenbereich.webklar.com/build rev-parse --short HEAD && cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht && git rev-parse --short kn-p10-c1 && node scripts/umschalten-nachpruefung.mjs https://project.webklar.com --erwartet=neu && curl -s -o /dev/null -w '%{http_code}\n' https://project.webklar.com/vertragsunterlagen/v5/abo-bedingungen.html` Expected: gleiche Kurzformen, Exit 0, `200`. Dazu `curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' https://project.webklar.com/neu/rechtliches/abo-bedingungen.html`. Expected: `301 https://project.webklar.com/abo-bedingungen.html`. - [ ] **Step 3: Altvertrag.** Ein Kunde mit Fassung 5 öffnet in der neuen Ansicht Verträge → Abo → „Bedingungen“. Expected: Das Archiv v5 öffnet (nicht Fassung 6). Der Controller prüft es mit einem Abo, dessen `termsVersion` auf `-v5` endet, oder lässt es den Inhaber mit seinem Konto prüfen. - [ ] **Step 4: Logs.** Run: `docker logs --since 20m webklar-kundenbereich-server 2>&1 | command grep -a -c -i 'Unbehandelter Fehler'`. Expected: 0. --- ### Task 16: Login von `app.js` lösen (`public/neu/oeffentlich/login.js`) **Files:** - Create: `public/neu/oeffentlich/login.js` - Modify: `public/neu/oeffentlich/login.html` (`