110 KiB
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
ondie öffentlichen Seiten, das Manifest,/dashboard.htmlohne 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.jslö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:offwü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 <noreply@anthropic.com> 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 '<tag>^{commit}:refs/heads/main', nur fast-forward, nie--force. - Geheimnisse:
APPS/.envwird nie ausgegeben. Erlaubt sind nurcommand grep -a -n '^<SCHLÜSSEL>=' APPS/.envfürKUNDENANSICHT_NEU,KUNDENANSICHT_NEU_PILOT_EMAILS,ABO_PILOT_EMAILSund die*_ENABLED-Schalter sowiesed -iauf genau eine Zeile. Vor jeder Änderung an.envoderdocker-compose.ymlentsteht eine Sicherung<Datei>.bak-<JJJJ-MM-TT>-kundenansicht. - Dienst neu starten nur mit
cd /home/webklar/apps && docker compose up -d --no-deps kundenbereich-server. Niepreview-portal, nie andere Dienste, nie ohne--no-deps. - Löschen nur mit
git rm <Datei>für Dateien, die der Plan einzeln nennt. Keinrm -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, nachherls -l /dev/null). Dazwischen Einzeldateien, mehrere nur mit--test-concurrency=1.
Review Focus
- Weiche liefert zur falschen Zeit oder gar nichts: In
offundpilot(auch mit*) darf sich keine öffentliche Seite ändern. Inonbekommt auch ein Besucher ohne Sitzung die neue Seite (sonst blitzt/dashboard.htmlalt 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ässtpublic/nie. Tests: Task 3, 4. - 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 werdenunternehmen, nichts ohne passende Bestätigungszeile, keine Personendaten in der Ausgabe. Test: Task 2. - Festschreiben macht Fassung 6 tot oder Altverträge unauffindbar: Bei gleichen Werten von
termsVersionundtermsVersionNeumeldet eine „alt zuerst“-Prüfung (fassungDerAnfrage) immeralt, 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. - Löschen reißt Gebrauchtes mit:
app.jsträgt Login, Service-Worker-Registrierung, iOS-Fokuszoom und den Hash-Pin der Gutschrift-Bedingungen. Zwei CSS-Dateien lädtauftrag.htmlbzw.analyse.jsnoch. Die öffentlichen Seiten verschwinden nicht, wenn jemand den Schalter zurückstellt. Tests: Task 16, 17. - 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-p0biskn-p8, je mit-haupt); WOMS-Tagkn-p9im 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:
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)ausserver/services/appwriteAdmin.js;config.collections.customers;KUNDENARTEN(V4) ausserver/services/kundenart.js. - Produces:
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 wietest/featureConsentFelder.test.js:1-12, aber mitstartAppwriteMock({ schema: { customers: ['name', 'email', 'companyName', 'customerStatus', 'kundenart'] } }))
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:
- Leer heißt
String(doc.kundenart ?? '').trim() === '', gültig ist ein Wert ausKUNDENARTEN, alles andere istunbekanntund wird nie geschrieben. ausfuehrenruftpruefen. Bei 0 leeren Einträgen kehrt es sofort zurück. Sonst mussbestaetigungexaktbestaetigungsZeile(ohne.length)sein, sonstErrormit dieser Zeile in der Meldung, vor jedem Schreiben.- Dann
log('Schreibe ${n} Einträge …'), je IDgetDocumentneu lesen, nur bei noch leerem FeldupdateDocument(config.collections.customers, id, { kundenart: 'unternehmen' })mit genau diesem einen Feld, sonstuebersprungen. Ein Schreibfehler landet infehler, die Schleife läuft weiter. - Protokollzeile je Eintrag
<ISO-Zeit>\t<id>\t<vorher>\t->\tunternehmen, am EndeSumme gesetzt=… uebersprungen=… fehler=…. Die Protokolldatei entsteht perteenachRB(Task 8), nicht im Container. pruefendrucktEinträge gesamt: n,Ohne Kundenart: n,Unbekannte Werte: n, jecustomerStatuseine Zeile<status>: <ids>und zuletzt die BestätigungszeileKundenart unternehmen setzen: n(Task 8 liest diese Zeilen).- 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
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 <noreply@anthropic.com>"
Task 3: Umschalt-Weiche (server/routes/umschaltSeiten.js)
Files:
- Create:
server/routes/umschaltSeiten.js - Modify:
server/index.js(Import und eine Zeileapp.use(umschaltSeitenRouter({ publicDir }))hinter P0s zwei Weichen-Zeilen und vorapp.use(express.static(publicDir))) - Test:
test/knUmschaltSeiten.test.js
Interfaces:
- Consumes:
neueAnsichtModus(): 'off'|'pilot'|'on'(V1),termsDocFor(version)ausserver/services/products.js(liefert{ url, pdfFile }),config.abo.termsVersionNeu. - Produces:
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 wietest/kundenansichtEnde.test.js:1-19für Mock, Stripe,useTestEnv; dann dynamischstartTestApp,umschaltSeitenRouter,UMSCHALT_SEITEN; dazuexpress,node:fs,node:os,node:path,node:url)
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 | nulllöst'.' + path.posix.normalize('/' + rel)gegenpublicDirauf und gibtnullzurück, wenn das Ergebnis nicht unterpublicDirliegt. -
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
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 <noreply@anthropic.com>"
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:
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) |
<meta name="kn-seite" content="neu"> |
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, <link |
#9d3dfe |
/manifest.webmanifest (JSON) |
id und start_url /dashboard.html, scope /, background_color und theme_color #ffffff, Shortcut-Namen Chat, Rechnungen, Verträge |
background_color #000000 |
|
/abo-bedingungen.html |
Version 6 |
Version 5 · Stand, Entwurf – Prüfung durch Kanzlei offen |
Version 5 · Stand |
/WEBklar-Abo-Vertragsunterlagen.pdf |
application/pdf, Beginn %PDF- |
- Step 1: Failing test schreiben (Kopf:
express,startTestApp,umschaltSeitenRouter,publicDirwie in Task 3)
// Stellvertreter-Antworten werden aus den Markern der Tabelle gebaut (Zeichenketten aus `enthaelt`, Manifest als JSON)
const MANIFEST = { id: '/dashboard.html', start_url: '/dashboard.html', scope: '/', background_color: '#ffffff', theme_color: '#ffffff', shortcuts: [{ name: 'Chat' }, { name: 'Rechnungen' }, { name: 'Verträge' }] }
const antwort = (p, erwartet) => 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
fetchmitredirect: 'manual'und Zeitlimit 15 s.grundnennt Status, fehlenden Marker, verbotenen Marker oderNetzfehler. Der Einstieg druckt je Zeileokbzw.FEHLER <pfad> <grund>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
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 <noreply@anthropic.com>"
Task 5: Käufe lesen (scripts/kauf-bestand-pruefen.mjs)
Files:
- Create:
scripts/kauf-bestand-pruefen.mjs - Test:
test/kaufBestandPruefen.test.js
Interfaces:
-
Consumes:
listAllDocumentsausappwriteAdmin.js,config.collections.portalFeatures,withdrawalDeadline(contractAt) → { lastDay, endsAt }ausserver/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, wennoffeneBestellungennicht leer ist. -
Step 1: Failing test schreiben (Kopf und Hilfen
feature(id, over)undsammler()wietest/featureConsentFelder.test.js:1-30, Mock mitportalFeatures: schemaKeys('portalFeatures');jetzt = new Date('2026-11-10T10:00:00Z'))
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
stateinsetup | active | disabledundunlockedByPurchase; „Frist läuft“ heißtwithdrawalDeadline(consentAt || orderedAt).endsAt > jetzt(Näherung, nur zur Information); offen heißtstate === 'ordered'. AusgabezeilenOffene Bestellungen: n (id, …)undFristen laufen: n (id, …). - Step 4: Lauf grün. Run: wie Step 2. Expected: PASS, 2 Tests.
- Step 5: Commit
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 <noreply@anthropic.com>"
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ückwegoff. -
Step 1: Failing test schreiben
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 mitENOENT. - 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,
<N>aus--pruefen). Abschnitt 7 nennt den Rückweg (sedaufoff, 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
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 <noreply@anthropic.com>"
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.mjsim Container (Task 7). -
Produces:
Ohne Kundenart: 0in Produktion; Protokolle inRB. -
Step 1: Prüfen (nur lesend)
Run:
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: <N>, Unbekannte Werte: 0 und die Bestätigungszeile Kundenart unternehmen setzen: <N>. Stand der Spec (15.10.): 42 Einträge, nämlich 6 Kunden, 31 Leads, 5 verlorene. Weicht <N> 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
unternehmengesetzt werden sollen. Stop: Inhaber-OK nötig. -
Step 3: Setzen
Run:
set -o pipefail
docker exec -w /app webklar-kundenbereich-server node scripts/kundenart-bestand.mjs --ausfuehren --bestaetigung="Kundenart unternehmen setzen: <N>" | 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=<N> 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-Tagkn-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ürabo-bedingungeneine aktuelle Version, derensourceauf:public/neu/rechtliches/abo-bedingungen.htmlendet (Format laut Übergabe von P8; die WOMS-NummervNzählt Uploads und ist nicht die Fassung). Der Inhaber bestätigt in WOMS unter „Rechtliches“, dass die Karteabo-bedingungendie Fassung 6 trägt. Fehlt sie, stoppt der Controller. Hochladen ohne--probenur 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 (insbesonderefeatureConsents.kundenart; ohne sie scheitert jede Bestellung der neuen Ansicht mit 500), und die Zeile mit der Definition vonWORTLAUT_STATUSträ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: indocker-compose.ymlzwei Zeilen- KUNDENANSICHT_NEU=${KUNDENANSICHT_NEU:-off}und- KUNDENANSICHT_NEU_PILOT_EMAILS=${KUNDENANSICHT_NEU_PILOT_EMAILS:-}(bei denZEITEN_*-Zeilen um Zeile 1713), in.envKUNDENANSICHT_NEU=pilotund 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_EMAILSmuss*sein (sonst antwortet jede Abo-Bestellung eines Nicht-Pilot-Kunden mit 403);ANALYSE_ENABLEDundAUFTRAEGE_ENABLEDbleibenoff, 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), danncp -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=onin Produktion; Rückwegoffbis 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.htmlist weiß, die Anmeldung führt auf Website. (b)#chat,#rechnungen,#wartungund#termine?bestellt=1&projekt=<id>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 AnhangWEBklar-Abo-Vertragsunterlagen.pdfist Fassung 6, infeatureConsentssteht eine Zeile mitfeatureKeyabound 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
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:
CACHEauf'kb-offline-v2'setzen. Sonst nichts insw.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
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 <noreply@anthropic.com>"
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.htmlnachpublic/abo-bedingungen.html,public/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdfnachpublic/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.pdfwird/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 inpublic/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 Parameterfassung6entfallen, zwei Weiterleitungen kommen dazu),scripts/build-legal-pdfs.sh(Modusabo6entfällt),scripts/rechtliches-hochladen.mjs(DOKUMENTE_V6, Eintragabo-bedingungen: neue Pfade). Das ist die Übergabe laut P8-Vertragsfrage 1. - Pins:
test/legalTexts.test.js:43,104-121,test/products.test.js:120,190und P8s TestsFassungstabelle,Fassung 5 bleibt bis P10,aktuelleAboFassung,test/aboRoutes.test.js:87,test/aboBedingungenV6.test.js(KonstanteV6),test/legalV6Build.test.js(abo6und 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(<Fassung 6>)lieferturl: '/abo-bedingungen.html'undpdfFile: 'WEBklar-Abo-Vertragsunterlagen.pdf'.termsDocFor(<Fassung 5>)liefert die Archivwerte (url: '/vertragsunterlagen/v5/abo-bedingungen.html',pdfFile: 'vertragsunterlagen/v5/WEBklar-Abo-Vertragsunterlagen.pdf'); das Feldarchivder Tabelle entfällt.fassungDerAnfrageprüft die Neu-Fassung zuerst: IsttermsVersiongleich der Neu-Fassung, giltneueAnsicht(req) ? 'neu' : null, nie'alt'.umschaltSeitenRouterleitet/neu/rechtliches/abo-bedingungen.htmlund/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdfdauerhaft (301, unabhängig vom Schalter) auf/abo-bedingungen.htmlbzw./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 wietest/knUmschaltSeiten.test.jsmitcreateHash,config,termsDocFor,contractAttachmentFor,fassungDerAnfrage)
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,termsVersionist v5). - Step 4: Umsetzen.
git mv -ffü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.server/config.js: eine Konstante mit dem Wert der bisherigentermsVersionNeu, gelesen vontermsVersionundtermsVersionNeu.products.js: Zeile v6 auf/abo-bedingungen.htmlundWEBklar-Abo-Vertragsunterlagen.pdf, Zeile v5 auf die Werte ihres Feldesarchiv(das Feld entfällt).endParagraphbleiben.fassungDerAnfrage: Reihenfolge wie bei „Produces“.umschaltSeiten.js: Fassung-6-Zeilen und die Parameterfassung6undfassung6Ausentfernen, die zwei Weiterleitungen ergänzen; die zwei Fassung-6-Tests inknUmschaltSeiten.test.jsentfallen.scripts/build-legal-pdfs.sh: den Modusabo6samt Kopfkommentar und der Nennung in der Usage-Zeile entfernen (all)undfunktionen6)bleiben).scripts/rechtliches-hochladen.mjs: im Eintragabo-bedingungenvonDOKUMENTE_V6die Pfadepublic/WEBklar-Abo-Vertragsunterlagen.pdfundpublic/abo-bedingungen.html.- Pins von P8 und aus der Vorzeit:
legalTexts.test.js:104-111auf-v6$undVersion 6 · Stand(und!includes('Version 5'));:43bleibt (CURist jetzt Fassung 6);:112-121bleibt gültig (die Seite trägt nach dem Tauschhref="/WEBklar-Abo-Vertragsunterlagen.pdf").aboBedingungenV6.test.jsverlangt dort den Entwurfslink und nicht den Hauptlink: diese eine Assertion kehrt sich um.products.test.js:120,190und P8s drei Tests auf die Werte aus „Produces“ (der TestaktuelleAboFassungerwartet nun für jeden Modusconfig.abo.termsVersionNeu).aboBedingungenV6.test.js: KonstanteV6aufpublic/abo-bedingungen.html,ENTWURFbleibtfalse.legalV6Build.test.js: dieabo6-Zeilen entfallen, die PDF-Schleife prüft den Eintragabo-bedingungenan den neuen Pfaden.rechtlichesHochladen.test.js: Pfade des ersten Eintrags.aboRoutes.test.js:87bleibt'/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
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 <noreply@anthropic.com>"
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.htmlist 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.htmlExpected: gleiche Kurzformen, Exit 0,200. Dazucurl -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
termsVersionauf-v5endet, 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(<script src="/app.js">wird<script src="/neu/oeffentlich/login.js">, der Inline-AufrufinitLoginPage();bleibt), bei Bedarfpublic/neu/huelle.js(Service-Worker-Registrierung und iOS-Fokuszoom, VF5) - Test:
test/knLoginModul.test.js
Interfaces:
-
Consumes: der Login-Teil von
public/app.js(Stand der Basis, Zeilen mitcommand grep -a -nbestätigen):apisamtsessionEndRedirected(1-27),showError,hideError(32-40),getQueryParam(54),safeRedirectUrlbisinitLoginPage(rund 421-568),isIosDevice(2157),preventIosFocusZoomsamt Aufruf (2549-2554) und die Registrierungnavigator.serviceWorker.register('/sw.js')beiload(2555-2560). -
Produces: ein klassisches Skript (kein Modul), das diese Funktionen Zeichen für Zeichen übernimmt,
window.initLoginPage = initLoginPagesetzt und beimload/sw.jsregistriert (nur wenn'serviceWorker' in navigator && window.isSecureContext). Es enthält nichts vom Dashboard. -
Step 1: Failing test schreiben (
test/knLoginModul.test.js,vm.createContextwietest/kundenansichtClient.test.js:60-91)
const SRC = readFileSync(new URL('../public/neu/oeffentlich/login.js', import.meta.url), 'utf8')
function kontext({ search = '', zugriff = { allowed: false }, secure = true } = {}) {
const geladen = []; const registriert = []
const ctx = vm.createContext({
window: { location: { search, href: '' }, isSecureContext: secure, addEventListener: (n, f) => { if (n === 'load') geladen.push(f) } },
document: { getElementById: () => null, body: { classList: { contains: () => false } }, querySelector: () => null },
navigator: { serviceWorker: { register: (u) => { registriert.push(u); return Promise.resolve() } }, userAgent: '', platform: '', maxTouchPoints: 0 },
fetch: async () => ({ ok: true, json: async () => zugriff }), URLSearchParams, URL, console, encodeURIComponent, setInterval, clearInterval,
})
vm.runInContext(SRC, ctx); return { ctx, geladen, registriert }
}
const ziel = (search, zugriff) => vm.runInContext('resolvePostLoginTarget()', kontext({ search, zugriff }).ctx)
test('next: nur /dashboard.html mit Hash aus Kleinbuchstaben, Fremdes fällt auf /dashboard.html', async () => {
for (const [next, soll] of [['/dashboard.html#chat', '/dashboard.html#chat'], ['/dashboard.html#Chat', '/dashboard.html'], ['//evil.example/x', '/dashboard.html'], ['https://evil.example/', '/dashboard.html'], ['/dashboard.html?x=1', '/dashboard.html'], ['', '/dashboard.html']]) assert.equal(await ziel(`?next=${encodeURIComponent(next)}`), soll, next)
})
test('Vorschau-Adresse nur mit Zugriff und nur unter *.project.webklar.com', async () => {
const v = 'https://kundenansicht-neu.project.webklar.com/'
assert.equal(await ziel(`?next=${encodeURIComponent(v)}`, { allowed: true }), v); assert.equal(await ziel(`?next=${encodeURIComponent(v)}`, { allowed: false }), '/dashboard.html')
assert.equal(await ziel(`?next=${encodeURIComponent('https://x.project.webklar.com.evil.example/')}`, { allowed: true }), '/dashboard.html')
})
test('Service Worker wird beim Laden registriert, nur im sicheren Kontext; initLoginPage ist global', () => {
const a = kontext(); a.geladen[0](); assert.deepEqual(a.registriert, ['/sw.js']); assert.equal(typeof a.ctx.window.initLoginPage, 'function')
const b = kontext({ secure: false }); b.geladen.forEach((f) => f()); assert.deepEqual(b.registriert, [])
})
test('login.html lädt login.js statt app.js, und die neue Hülle registriert den Service Worker und verhindert den iOS-Fokuszoom ebenfalls', () => {
const html = readFileSync(new URL('../public/neu/oeffentlich/login.html', import.meta.url), 'utf8')
assert.match(html, /<script src="\/neu\/oeffentlich\/login\.js"><\/script>/); assert.doesNotMatch(html, /\/app\.js/); assert.match(html, /initLoginPage\(\)/)
const huelle = readFileSync(new URL('../public/neu/huelle.js', import.meta.url), 'utf8')
assert.match(huelle, /serviceWorker\.register\('\/sw\.js'\)/); assert.match(huelle, /maximum-scale/)
})
- 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/knLoginModul.test.js. Expected: FAIL (ENOENTauflogin.js). - Step 3: Umsetzen.
login.jsaus den genannten Stellen zusammensetzen (kopieren, nicht umschreiben). Fehlt inpublic/neu/huelle.jsdie Registrierung oder der iOS-Fokuszoom (command grep -a -n 'serviceWorker\|maximum-scale' public/neu/huelle.js), ergänzt der Agent dort denload-Block mit derselben Bedingung undisIosDevicesamtpreventIosFocusZoomausapp.js:2157und:2549-2554, ohne neue Importe (P1-Pins verbieten sie), und nennt es im Commit. Im Testtest/knAnsichtOeffentlich.test.js(P2, Task 17) gilt der Skriptvergleich fürlogin.htmljetzt mitlogin.jsan Stelle vonapp.js: diese eine Zeile anpassen. - Step 4: Lauf grün, dazu die Seiten-Tests. 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/knLoginModul.test.js test/knAnsichtOeffentlich.test.js test/knHuelleQuelltext.test.js test/knHuelleStart.test.js. Expected: PASS. - Step 5: Sichtprüfung über das Werkzeug. Run:
free -m, danncd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht && /root/reports/kundenansicht-neu-2026-10-15/werkzeug/run-browser.sh test/ui/kundenansicht-neu.ui.mjs /root/reports/kundenansicht-neu-2026-10-15/werkzeug/tmp/shots-p10 --only o-login --gate p2. Expected: Exit 0;390x844/o-login.pngzeigt das weiße Formular wie vorher. - Step 6: Commit
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht
git add public/neu/oeffentlich/login.js public/neu/oeffentlich/login.html test/knLoginModul.test.js test/knAnsichtOeffentlich.test.js
git commit -m "Kundenansicht neu P10: Login-Logik und Service-Worker-Registrierung liegen in login.js statt in app.js" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
(Hat der Agent public/neu/huelle.js ändern müssen, kommt diese Datei als eigenes git add mit in den Commit.)
Task 17: Alte Oberfläche löschen
Files:
- Delete (
git rm, einzeln):public/dashboard.html,public/app.js,public/login.html,public/kuendigen.html,public/widerrufen.html,public/offline.html,public/manifest.webmanifest,public/kundenbereich.css,public/login.css,public/legal-form.css,public/termine.css,public/shop.css,public/buchungen.css,public/zeiten.css,public/funktionen.css,public/analyse.css - Bleiben (werden noch geladen):
public/auftrag.css(auftrag.html),public/geraete.css(analyse.js:342),public/chat-composer.cssund.js,public/kundenansicht.js,public/style.css(unbenutzt, aber nicht Teil der alten Oberfläche) - Modify:
server/routes/umschaltSeiten.js(Parametermodusentfällt, die Zeilen liefern immer),test/knUmschaltSeiten.test.js(Matrix wird „immer die neue Datei“) - Test:
test/knAufraeumen.test.js; Pins: Tabelle in Step 4
Interfaces: Produces umschaltSeitenRouter({ publicDir }): liefert die sechs Zeilen von UMSCHALT_SEITEN unbedingt. Ab jetzt ist die neue Oberfläche auch ohne on die einzige.
-
Step 1: Alte Zusicherungen einfrieren (zwei Wegwerf-Skripte im Scratch-Ordner, nicht committen)
test/support/modulKlassen.json: die Klassenliste je Präfix, die der Abdeckungstesttest/kundenansichtNeuModulCss.test.js(P4, Task 9) heute aus den alten CSS-Dateien liest (Funktionklassen(...)des Tests aufrufen). Der Test liest danach diese Datei.test/support/oeffentlichStand.json: sichtbarer Text, Feldattribute und Skripte der vier alten Seiten, berechnet mit den FunktionensichtbarerText,attribute,skripteaustest/knAnsichtOeffentlich.test.js(P2, Task 17). Der Test vergleicht danach die neuen Seiten damit. Fürlogin.htmlsteht dort/neu/oeffentlich/login.jsstatt/app.js.
-
Step 2: Failing test schreiben (
test/knAufraeumen.test.js)
const ROOT = fileURLToPath(new URL('../', import.meta.url))
const GELOESCHT = ['public/dashboard.html', 'public/app.js', 'public/login.html', 'public/kuendigen.html', 'public/widerrufen.html', 'public/offline.html', 'public/manifest.webmanifest', 'public/kundenbereich.css', 'public/login.css', 'public/legal-form.css', 'public/termine.css', 'public/shop.css', 'public/buchungen.css', 'public/zeiten.css', 'public/funktionen.css', 'public/analyse.css']
const ALS_ADRESSE_GUELTIG = new Set(['dashboard.html', 'login.html', 'kuendigen.html', 'widerrufen.html', 'offline.html', 'manifest.webmanifest']) // bleiben Adressen der Weiche
const dateien = (dir, aus = []) => readdirSync(path.join(ROOT, dir), { withFileTypes: true }).flatMap((e) => { const rel = `${dir}/${e.name}`; return aus.some((a) => rel.startsWith(a)) ? [] : e.isDirectory() ? dateien(rel, aus) : /\.(m?js|html|css|json|sh)$/.test(e.name) ? [rel] : [] })
const ALLE = [...dateien('public', ['public/vertragsunterlagen', 'public/rechtliches']), ...dateien('server'), ...dateien('scripts'), ...dateien('test').filter((f) => f !== 'test/knAufraeumen.test.js')]
const text = (f) => readFileSync(path.join(ROOT, f), 'utf8')
test('gelöschte Dateien sind weg, die noch gebrauchten bleiben', () => {
for (const f of GELOESCHT) assert.equal(existsSync(path.join(ROOT, f)), false, f)
for (const f of ['public/auftrag.css', 'public/geraete.css', 'public/chat-composer.css', 'public/chat-composer.js', 'public/kundenansicht.js', 'public/neu/manifest.webmanifest']) assert.equal(existsSync(path.join(ROOT, f)), true, f)
})
test('nichts liest oder lädt eine gelöschte Datei', () => {
for (const f of ALLE) { const t = text(f)
for (const g of GELOESCHT) { const n = path.basename(g)
assert.ok(!t.includes(`public/${n}`), `${f}: Dateiform public/${n}`)
if (!ALS_ADRESSE_GUELTIG.has(n)) assert.doesNotMatch(t, new RegExp(`["'(=]/${n.replace('.', '\\.')}(?![\\w.-])`), `${f}: Adresse /${n}`) } }
})
test('jede CSS-Datei in public/ wird noch geladen (kein Waisenkind), ausgenommen style.css', () => {
for (const css of readdirSync(path.join(ROOT, 'public')).filter((n) => n.endsWith('.css') && n !== 'style.css')) assert.ok(ALLE.some((f) => text(f).includes(`/${css}`)), css)
})
test('Service Worker und iOS-Fokuszoom überleben app.js, in der Hülle und auf der Login-Seite', () => {
for (const f of ['public/neu/huelle.js', 'public/neu/oeffentlich/login.js']) { assert.ok(text(f).includes("serviceWorker.register('/sw.js')"), `${f}: Registrierung`); assert.ok(text(f).includes('maximum-scale'), `${f}: iOS-Fokuszoom`) }
})
test('Manifest: Identität unverändert, weiß, Shortcuts Chat, Rechnungen, Verträge', () => {
const m = JSON.parse(text('public/neu/manifest.webmanifest'))
assert.deepEqual([m.id, m.start_url, m.scope, m.display, m.background_color, m.theme_color], ['/dashboard.html', '/dashboard.html', '/', 'standalone', '#ffffff', '#ffffff'])
assert.deepEqual(m.shortcuts.map((s) => s.name), ['Chat', 'Rechnungen', 'Verträge'])
})
- 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/knAufraeumen.test.js. Expected: FAIL im ersten Test (die Dateien existieren) und im zweiten (Dateiform-Treffer in den alten Tests). Der Lauf nennt die Dateien, die noch auf Altes zeigen. - Step 4: Umsetzen. Zuerst die Zeilen der Pin-Liste, dann
git rm, dann die Weiche:
| Datei:Zeile | Pin | Aktion |
|---|---|---|
test/shopWiring.test.js:9-27 |
dashboard.html-Links, view-webshop, app.js VIEW_HASHES, FEATURE_LABELS, viewRefreshers, APP_PARENT |
löschen (Gegenstück: test/knHuelleRouten.test.js, knHuelleSeite.test.js) |
test/termineView.test.js:305-316 |
wie oben für termine |
nur die Zeilen mit app.js/dashboard.html löschen, Zeilen 314-316 (funktionen.js, zwei absolute Importe) bleiben |
test/unterkunftWiring.test.js:120-130 |
wie oben für buchungen |
die app.js/dashboard.html-Zeilen löschen, die Zeile zu initBuchungen bleibt |
test/zeitenBestellen.test.js:826-836 |
wie oben für zeiten, dazu zeiten.css-Link |
die app.js/dashboard.html-Zeilen löschen |
test/analyseView.test.js:56-66 |
Analyse verdrahtet | die app.js/dashboard.html-Zeilen löschen, ab const an = bleibt es |
test/funktionenRoutes.test.js:502-507 |
FEATURE_LABELS, view-news aria-live |
diese Zeilen löschen, die Embed-Zeilen (embed/news.js) bleiben |
test/legalTexts.test.js:401-425 |
Hash von TAKEOVER_CONDITIONS in app.js |
auf public/neu/ansicht-gutschrift.js umstellen; Hash 2b7a745c… und conditionsVersion bleiben. Die Zusatzzeile „sobald Ihr Abo läuft“ liest die Datei, die den Satz trägt (command grep -a -rn 'sobald Ihr Abo läuft' public/neu) |
test/zeitenEditor.test.js:1944-1950, test/buchungenUi.test.js, .sh-box[hidden]- und .bk-personenpreise-Pins |
Regeln in den alten Modul-CSS | auf public/neu/module.css umstellen, wenn die Regel dort steht (Farben nach Palette), sonst Pin löschen |
test/auftraegeKundenportalDashboard.test.js, auftraegeKundenportalPortal.test.js, termineFragebogenView.test.js |
Quelltext-Pins und vm-Ausschnitte aus app.js/dashboard.html |
umstellen, wenn ein Gegenstück in public/neu/ existiert; löschen, wenn ein P2/P3-Test dieselbe Aussage trägt |
test/auftraegeKundenportalHuelle.test.js:49-52, test/publicForms.test.js:1439-1450, test/auftraegeAblauf.test.js:288, test/auftraegeKundenportalWiderruf.test.js:9 |
lesen manifest.webmanifest, kuendigen.html, widerrufen.html |
auf public/neu/manifest.webmanifest bzw. public/neu/oeffentlich/… umstellen |
test/knAnsichtOeffentlich.test.js, test/kundenansichtNeuModulCss.test.js |
Paarvergleich alt/neu, alte CSS als Klassenquelle | auf die Dateien aus Step 1 umstellen |
test/support/knWortlautOrakel.js, test/knAnsichtOrakel.test.js, test/ui/*.ui.mjs, die dashboard.html oder app.js laden (command grep -a -l 'dashboard\.html|/app\.js' test/ui/*.mjs) |
Orakel liest app.js; Playwright-Skripte der alten Seite (nicht Teil von npm test) |
git rm (Orakel nur, wenn command grep -a -ln knWortlautOrakel test sonst nichts findet); Server-Hilfen test/ui/*TestServer.mjs bleiben, nur ihre Verweise auf die alte Seite entfallen; Abdeckung tragen Unit-Tests und Werkzeug-Gates |
Danach git rm der sechzehn Dateien aus „Files“, dann umschaltSeitenRouter ohne modus. In test/knUmschaltSeiten.test.js entfällt die Variable modus (auch in den Übergaben an den Router), die Matrix prüft nur noch, dass jede der sechs Zeilen die neue Datei mit Cache-Control: no-cache liefert; der Test „fehlende neue Datei“ bleibt, er nutzt ein eigenes Verzeichnis (ohne modus-Übergabe).
- Step 5: Lauf grün, alle berührten Dateien
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/knAufraeumen.test.js test/knUmschaltSeiten.test.js test/shopWiring.test.js test/termineView.test.js test/unterkunftWiring.test.js test/zeitenBestellen.test.js test/analyseView.test.js test/funktionenRoutes.test.js test/legalTexts.test.js test/zeitenEditor.test.js test/buchungenUi.test.js test/auftraegeKundenportalDashboard.test.js test/auftraegeKundenportalPortal.test.js test/termineFragebogenView.test.js test/auftraegeKundenportalHuelle.test.js test/publicForms.test.js test/auftraegeAblauf.test.js test/auftraegeKundenportalWiderruf.test.js test/knAnsichtOeffentlich.test.js test/kundenansichtNeuModulCss.test.js test/kundenansichtClient.test.js test/kundenansichtGuard.test.js test/kundenansicht.test.js
Expected: fail 0. Meldet ein Test eine Datei, die dieser Plan nicht kennt, wird sie nach der Regel der Pin-Liste behandelt und im Commit genannt.
- Step 6: Sichtprüfung über das Werkzeug, alle Gates, eine Instanz nach der anderen. Run:
free -m, danncd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht && for g in p1 p2 p3 p4 p7; do /root/reports/kundenansicht-neu-2026-10-15/werkzeug/run-browser.sh test/ui/kundenansicht-neu.ui.mjs /root/reports/kundenansicht-neu-2026-10-15/werkzeug/tmp/shots-p10-$g --gate $g || break; done; ls -l /dev/null. Expected: jedes Gate Exit 0,/dev/nullmitcrw-rw-rw-. - Step 7: Commit und Tag
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht
git add server/routes/umschaltSeiten.js test/knUmschaltSeiten.test.js test/knAufraeumen.test.js test/support/modulKlassen.json test/support/oeffentlichStand.json
git add <jede geänderte Testdatei aus der Pin-Liste einzeln>
git commit -m "Kundenansicht neu P10: alte Oberfläche gelöscht (dashboard.html, app.js, alte Seiten und CSS), Pins umgestellt" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
git tag kn-p10-c2
(Die git rm-Löschungen stehen schon im Index.)
Task 18: Live, neue Oberfläche als einzige ausliefern
Files: keine. Stop: Inhaber-OK nötig (Push).
Interfaces:
-
Consumes: Tag
kn-p10-c2;KUNDENANSICHT_NEU=on. -
Produces: Die alte Oberfläche ist in Produktion nicht mehr erreichbar.
-
Step 1: Vorbedingung. Run:
command grep -a -n '^KUNDENANSICHT_NEU=' /home/webklar/apps/.env. Expected:KUNDENANSICHT_NEU=on. Sonst stoppt der Controller: Ab diesem Stand zeigt die Oberfläche auch beipilotoderoffdie neue Seite, die Server-Pfade aber nicht (Task 23 räumt das auf). -
Step 2: Push. Der Inhaber pusht
git push origin 'kn-p10-c2^{commit}:refs/heads/main'. -
Step 3: Prüfen. Run:
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht && node scripts/umschalten-nachpruefung.mjs https://project.webklar.com --erwartet=neu; for p in app.js kundenbereich.css login.css; do curl -s -o /dev/null -w "$p %{http_code}\n" https://project.webklar.com/$p; done. Expected: Exit 0 und dreimal404. -
Step 4: Menschen prüfen (Inhaber, Handy und Desktop): Anmelden,
#chat,#rechnungen, Konto → Kündigen und Widerrufen, Abmelden. Installierte App im Flugmodus öffnen: weiße Offline-Seite. Expected: wie vor dem Push. -
Step 5: Logs. Run:
docker logs --since 20m webklar-kundenbereich-server 2>&1 | command grep -a -c -i 'Unbehandelter Fehler'. Expected: 0.
Task 19: Live, Bestand an Käufen prüfen (nur lesend)
Files: keine. Stop: Inhaber-OK nötig (Lauf gegen Produktion, nur lesend).
Interfaces:
-
Consumes:
scripts/kauf-bestand-pruefen.mjsim Container (Task 7). -
Produces: die Zahl der offenen Bestellungen als Tor für Task 20.
-
Step 1: Lauf. Run:
docker exec -w /app webklar-kundenbereich-server node scripts/kauf-bestand-pruefen.mjs | tee /root/reports/kundenansicht-neu-2026-10-15/umschalten/kaeufe-$(date -u +%Y%m%d-%H%M%S).log. Expected:Offene Bestellungen: 0, Exit 0.Fristen laufen: nist nur eine Information (der Widerruf bleibt erhalten, Task 20 baut ihn nicht zurück). -
Step 2: Entscheidung. Ist
Offene Bestellungengrößer als 0, wartet der Controller (Checkout-Sitzungen gelten 24 Stunden, SEPA bis zu einer Woche) und wiederholt Step 1. Task 20 beginnt erst bei 0. Der Controller nennt dem Inhaber das Ergebnis.
Task 20: Server-Kaufpfade zurückbauen
Files:
- Modify:
server/routes/funktionen.js(POST /:key/checkoutrund:292-385,POST /zeiten/behaltenrund:436), danach jede Funktion, die nur diese Routen brauchte (Kandidaten:createFeatureCheckoutstripeBilling.js:666,markOrderedsiteFeatures.js:297,pruefeAbo,pruefeInklusiveinfeatureOrder.js, der Startteil vonserver/services/zeiten/behalten.js) - Modify:
server/services/zeiten/mails.js(k29Mailrund:100,k30Mailrund:113) und ihre Aufrufer inserver/services/zeiten/aboAbgleich.js - Test:
test/knKaufRueckbau.test.js; Reparatur oder Entfernen intest/funktionenCheckout.test.js,test/zeitenCheckout.test.js,test/zeitenBehalten.test.js,test/featureCheckout.test.js,test/zeitenMails.test.js,test/aboFunktionenMails.test.js(P6)
Interfaces: Produces: POST /api/funktionen/:key/checkout und POST /api/funktionen/zeiten/behalten antworten 404. k29Mail und k30Mail (Abo-Ende der Öffnungszeiten) liefern immer die Vorlagen K40 und K41 ohne Preis; die Parameter modell und preis (P6) entfallen, weil der 30-€-Weg „Behalten“ nicht mehr existiert (Spec 5.5). Die Vorlagen-Dateien K29 und K30 bleiben liegen (der Pin test/mailTemplates.test.js führt sie). Bleiben, wie sie sind: markPaid, markCheckoutFailed, isFeatureCheckoutEvent, handleFeatureCheckoutEvent, onFeaturePaid, paidFeatureOrders, withdrawFeature, die Nachweis-Schreibwege (speichereZustimmung), backfillInvoiceLinks, runActivation, POST /api/funktionen/zeiten/einschalten, die Mails K15 und T16, Rechnungen und POST /api/abo/public/withdraw. Preise in config.js, oeffentlicheAnsicht und die Antwortfelder von GET /api/funktionen bleiben (Pins, T19 und K15 für Bestand).
- Step 1: Failing test schreiben (
test/knKaufRueckbau.test.js; Kopf wietest/featurePaid.test.js, dort den Aufbau der Fixture für eine bezahlte Funktion und den Webhook-Aufruf übernehmen)
test('neue Käufe: die beiden Routen gibt es nicht mehr', async () => {
for (const key of ['news', 'termine', 'shop', 'unterkunft', 'zeiten']) assert.equal((await post(`/api/funktionen/${key}/checkout`, { projectId: 'p1' }, { cookie: customerCookie('c1') })).status, 404, key)
assert.equal((await post('/api/funktionen/zeiten/behalten', { projectId: 'p1' }, { cookie: customerCookie('c1') })).status, 404)
})
test('Bestand: Zahlung einer vor dem Umschalten gestarteten Bestellung wird weiter gebucht', async () => {
// Zustand ordered per aw.seed herstellen (nicht über die entfernte Route), dann checkout.session.completed über postWebhook
// Erwartet: state 'setup', unlockedByPurchase true, purchaseStatus 'paid', Einrichtungs-Ticket angelegt, K15 an den Kunden
})
test('Bestand: abgebrochene Sitzung setzt ordered auf off zurück (checkout.session.expired)', async () => { /* markCheckoutFailed über den Webhook */ })
test('Bestand: Widerruf einer gekauften Funktion wird angenommen', async () => {
// POST /api/abo/public/withdraw mit order 'funktion' für ein Konto mit gekaufter Funktion in der Frist → 200 { ok: true, receivedAt }
})
test('Öffnungszeiten-Abo-Ende: k29Mail und k30Mail liefern immer K40 und K41, ohne Preis', () => {
const frist = new Date('2026-11-16T12:00:00.000Z')
const a = k29Mail({ kunde: 'K', projekt: 'P', art: 'beendet', frist }); assert.equal(a.template, 'K40'); assert.equal('preis' in a.values, false)
assert.equal(k30Mail({ kunde: 'K', projekt: 'P', loeschdatum: frist }).template, 'K41')
})
test('Quelltext: createFeatureCheckout und markOrdered gibt es nicht mehr, die Webhook-Wege schon', () => {
const billing = readFileSync(new URL('../server/services/stripeBilling.js', import.meta.url), 'utf8'); const sf = readFileSync(new URL('../server/services/siteFeatures.js', import.meta.url), 'utf8')
assert.doesNotMatch(billing, /export (async )?function createFeatureCheckout/); assert.doesNotMatch(sf, /export (async )?function markOrdered/)
for (const n of ['markPaid', 'markCheckoutFailed', 'handleFeatureCheckoutEvent', 'onFeaturePaid', 'paidFeatureOrders']) assert.match(sf, new RegExp(`function ${n}\\b`), n)
})
(Die drei Kommentar-Rümpfe schreibt der Agent aus den Vorlagen in featurePaid.test.js und publicForms.test.js; die erwarteten Werte stehen in den Kommentaren.)
- 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/knKaufRueckbau.test.js. Expected: FAIL (die Routen antworten noch,createFeatureCheckoutexistiert). - Step 3: Umsetzen. Beide Routen entfernen, dann für jeden Kandidaten
command grep -a -rn '<name>' server scripts public. Eine Funktion ohne Aufrufer außerhalb ihrer Definition fällt, eine mit Aufrufer bleibt. Die Liste „Bleiben“ oben wird nie angefasst. - Step 4: Alte Tests nachziehen. Test für Test: Ist der Gegenstand eine entfernte Route, löschen. Sind es Webhook, Nachweis, Fristen oder Widerruf, den Ausgangszustand per
aw.seedherstellen statt über die Route. Der Commit nennt jede gelöschte Datei und jeden gelöschten Testnamen. - Step 5: Lauf grün. 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/knKaufRueckbau.test.js test/featurePaid.test.js test/funktionenRoutes.test.js test/funktionenRecht.test.js test/rechtePrivatArchiv.test.js test/aboRoutes.test.js test/featureFreischaltung.test.js test/zeitenEinschalten.test.js test/featureTicket.test.js test/featureMails.test.js test/featureConsentTexte.test.js test/zeitenMails.test.js test/aboFunktionenMails.test.js test/zeitenAboAbgleich.test.js test/mailTemplates.test.js test/kundenansichtGuard.test.js. Expected:fail 0. - Step 6: Commit
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht
git add server/routes/funktionen.js server/services/stripeBilling.js server/services/siteFeatures.js server/services/zeiten/mails.js server/services/zeiten/aboAbgleich.js test/knKaufRueckbau.test.js
git add <jede geänderte oder gelöschte Datei aus Step 3 und 4 einzeln>
git commit -m "Kundenansicht neu P10: Kaufwege für Funktionen entfernt, Webhook, Rechnungen und Widerruf bleiben" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 21: Alt-Zweige der Funktionsmodule entfernen
Files:
- Modify:
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(Fundstellen von P4:termine.js:49-56,100-106,322-394,shop.js:64-74,195,253,295-360,buchungen.js:32-41,124-130,155-205,zeiten.js:55,62-73,132,161,218-226,funktionen.js:474-515,analyse.js:22-49,76-112; percommand grep -a -n modell public/*.jsneu bestimmen) - Test:
test/knAltZweigeWeg.test.js; die Pins aus P4 (Importlisten und Verbote) auf den neuen Stand
Interfaces: Produces: Die Module lesen modell nie mehr, es gibt nur den Abo-Zweig. renderEinschaltSeite (funktion-bestellen.js) und renderZeitenEinschalten (zeiten-bestellen.js) bleiben, ihre Bestell- und Behalten-Gegenstücke entfallen.
- Step 1: Failing test schreiben (
test/knAltZweigeWeg.test.js;KEIN_PREISaustest/support/modulAbo.js, P4)
const lies = (f) => readFileSync(new URL(`../public/${f}`, import.meta.url), 'utf8')
const MODULE = ['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']
const BESTELL = ['funktion-bestellen.js', 'zeiten-bestellen.js', 'shop-anfrage.js', 'buchungen-anfrage.js']
test('kein Modul liest mehr `modell`', () => { for (const f of MODULE) assert.doesNotMatch(lies(f), /\bmodell\b/, f) })
test('Bestell- und Anfrageseiten: kein Preis, keine Zahlung, kein Behalten, keine Fortsetzung', () => {
for (const f of BESTELL) { assert.doesNotMatch(lies(f), KEIN_PREIS, f); assert.doesNotMatch(lies(f), /fortsetzen|wunschWeg|priceLabel|ustHinweis|unternehmer/, f) }
})
test('alle relativen und absoluten Importe der Module lösen sich auf', () => {
for (const f of MODULE) for (const m of lies(f).matchAll(/from '([./][^']+)'/g)) assert.ok(existsSync(new URL(m[1].startsWith('/') ? `../public${m[1]}` : `../public/${m[1]}`, import.meta.url)), `${f}: ${m[1]}`)
})
- 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/knAltZweigeWeg.test.js. Expected: FAIL (modellkommt vor). - Step 3: Umsetzen. Je
modell-Stelle: Abo-Zweig behalten,elselöschen, die Variable entfernen, wo sie konstant wird. Danach nicht mehr aufgerufene Funktionen und Importe löschen. Ein Treffer vonKEIN_PREISim Alt-Zweig wird gelöscht. Steht er in einem Text des Abo-Zweigs (P4, Abschnitt „Texte“), ist das ein Fehler von P4 und geht an den Controller. - Step 4: Pins und Tests der Module. Die Importlisten-Pins aus P4 (
termineView.test.js:373-394,450-453,termineFragebogenView.test.js:994-1034,3072-3086,zeitenBestellen.test.js:841-865,zeitenView.test.js:390-399,shopWiring.test.js:40-42) werden auf die kleineren Listen gesetzt (nur Löschungen, keine neuen Importe). Die P4-Fälle „ohnemodell“ (alte Bestellseite, „Behalten“, Unternehmer-Haken, lange Reiterleiste) entfallen, ihr Gegenstand ist weg. - Step 5: Lauf grün, dazu die Modul-Tests und die Gates. 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/knAltZweigeWeg.test.js test/termineView.test.js test/termineFragebogenView.test.js test/shopView.test.js test/shopWiring.test.js test/buchungenUi.test.js test/unterkunftWiring.test.js test/zeitenView.test.js test/zeitenBestellen.test.js test/analyseView.test.js test/funktionenTiles.test.js test/funktionBestellenKey.test.js test/kundenansichtNeuModulCss.test.js, dannfree -mundfor g in p3 p4; do /root/reports/kundenansicht-neu-2026-10-15/werkzeug/run-browser.sh test/ui/kundenansicht-neu.ui.mjs /root/reports/kundenansicht-neu-2026-10-15/werkzeug/tmp/shots-p10-alt-$g --gate $g || break; done. Expected:fail 0und beide Gates Exit 0. - Step 6: Commit
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht
git add test/knAltZweigeWeg.test.js public/termine.js public/termine-fragebogen.js public/shop.js public/shop-anfrage.js public/buchungen.js public/buchungen-anfrage.js public/zeiten.js public/zeiten-bestellen.js public/funktionen.js public/analyse.js public/funktion-bestellen.js
git add <jede angepasste Testdatei einzeln>
git commit -m "Kundenansicht neu P10: Alt-Zweige der Funktionsmodule (Bestellseiten, Behalten, lange Reiter) entfernt" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
git tag kn-p10-c3
Task 22: Live, Rückbau ausliefern
Files: keine. Stop: Inhaber-OK nötig (Push). Voraussetzung: Task 19 meldete 0 offene Bestellungen.
Interfaces:
-
Consumes: Tag
kn-p10-c3. -
Produces: Kaufwege und Alt-Zweige sind in Produktion weg.
-
Step 1: Push. Der Inhaber pusht
git push origin 'kn-p10-c3^{commit}:refs/heads/main'. -
Step 2: Prüfen. Run:
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht && node scripts/umschalten-nachpruefung.mjs https://project.webklar.com --erwartet=neu; curl -s -o /dev/null -w '%{http_code}\n' -X POST https://project.webklar.com/api/funktionen/termine/checkout. Expected: Exit 0 und404. -
Step 3: Menschen prüfen (Inhaber): jede Funktion unter „Meine“ öffnen (News, Terminplaner, Webshop, Buchungskalender, Öffnungszeiten, Analyse falls an), eine Einschalten-Seite ansehen. Expected: wie vor dem Push, keine leeren Flächen, keine Fehlerkarte.
-
Step 4: Logs. Run:
docker logs --since 20m webklar-kundenbereich-server 2>&1 | command grep -a -c -i 'Unbehandelter Fehler'. Expected: 0.
Task 23: Schalter-Code vereinfachen (neueAnsicht immer wahr)
Files:
- Modify:
server/services/kundenansicht/schalter.js(drei Konstanten),server/config.js(AbschnittkundenansichtNeuundkundenansichtNeuEnabledentfallen),server/index.js(der tote Handlerapp.get('/dashboard.html', (req, res, next) => { … redirect('/login.html') … })hinterexpress.staticentfällt) - Test:
test/knSchalterWeg.test.js,test/support/dateiListe.js(die Hilfendateienundtextaustest/knAufraeumen.test.js, das sie danach von dort importiert); entfernen oder umstellen:test/kundenansichtSchalter.test.js,test/kundenansichtDashboard.test.js,test/kundenansichtMe.test.js(P0) und jede Zeile intest/mitKUNDENANSICHT_NEUoderconfig.kundenansichtNeu
Interfaces: Produces:
export const neueAnsichtModus = () => 'on'
export const neueAnsichtFuerSession = () => true
export const neueAnsicht = () => true
Die Aufrufer ändern sich nicht. Die Alt-Zweige hinter neueAnsicht(req) bleiben als toter Code stehen und werden in Task 25 gezählt (sie zu entfernen hat keinen Nutzen und Risiko, der Inhaber entscheidet). config.abo.termsVersionNeu bleibt als gleichwertiger Alias. GET /api/auth/me liefert dadurch ansicht: 'neu' für jeden (das Werkzeug braucht keine Änderung).
- Step 1: Failing test schreiben (
test/knSchalterWeg.test.js)
test('schalter.js: drei Konstanten, jede Sitzung (auch keine) ist neu', async () => {
const { neueAnsichtModus, neueAnsichtFuerSession, neueAnsicht } = await import('../server/services/kundenansicht/schalter.js')
assert.equal(neueAnsichtModus(), 'on')
for (const s of [null, undefined, {}, { role: 'admin' }, { customerId: 'c1', appwriteUserId: 'u1', email: 'k@example.invalid' }]) assert.equal(neueAnsichtFuerSession(s), true)
assert.equal(neueAnsicht({}), true)
})
test('nichts in server/, scripts/, public/ kennt den Schalter noch', () => {
for (const f of [...dateien('server'), ...dateien('scripts'), ...dateien('public', ['public/vertragsunterlagen', 'public/rechtliches'])]) assert.doesNotMatch(text(f), /kundenansichtNeu|KUNDENANSICHT_NEU/, f)
})
test('Tests und Testhilfen setzen den Schalter nicht mehr', () => {
for (const f of dateien('test').filter((x) => x !== 'test/knSchalterWeg.test.js')) assert.doesNotMatch(text(f), /kundenansichtNeu|KUNDENANSICHT_NEU/, f)
})
test('der tote Dashboard-Handler hinter express.static ist weg', () => {
const index = text('server/index.js'); assert.ok(index.includes('dashboardWeiche(')); assert.doesNotMatch(index, /app\.get\('\/dashboard\.html', \(req, res, next\)/)
})
- 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/knSchalterWeg.test.js. Expected: FAIL. - Step 3: Umsetzen.
schalter.jsundconfig.jswie oben. Danncommand grep -a -rln 'KUNDENANSICHT_NEU\|kundenansichtNeu' testabarbeiten: Zeilen, die die Umgebung oderconfig.kundenansichtNeusetzen, löschen; Fälle, deren Ausgangslage „alte Ansicht“ ist (404 ohne neue Ansicht, unveränderte alte Antwort, Nicht-Pilot-Kunde), löschen; die übrigen bleiben unverändert.kundenansichtSchalter.test.jsund die Modus-Matrix inkundenansichtDashboard.test.jsentfallen,kundenansichtMe.test.jserwartetansicht: 'neu'. - Step 4: Lauf grün, mit allen berührten Dateien. 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/knSchalterWeg.test.js $(command grep -a -rl 'neueAnsicht\|dashboardWeiche' test/*.test.js | sort -u | tr '\n' ' '). Expected:fail 0. - Step 5: Commit und Tag
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht
git add server/services/kundenansicht/schalter.js server/config.js server/index.js test/knSchalterWeg.test.js test/support/dateiListe.js test/knAufraeumen.test.js
git add <jede geänderte Testdatei einzeln; gelöschte stehen per git rm im Index>
git commit -m "Kundenansicht neu P10: Schalter vereinfacht, neueAnsicht ist immer wahr, Konfiguration und toter Handler entfernt" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
git tag kn-p10-c4
Task 24: Live, Schalter-Code ausliefern und Umgebung bereinigen
Files: keine. Stop: Inhaber-OK nötig (Push, dann .env und Compose).
Interfaces:
-
Consumes: Tag
kn-p10-c4. -
Produces: Der Schalter existiert nirgends mehr; Compose und
.envsind sauber (Step 3, optional). -
Step 1: Push. Der Inhaber pusht
git push origin 'kn-p10-c4^{commit}:refs/heads/main'. Prüfen wie Task 22 Step 2 (--erwartet=neu, Exit 0). -
Step 2: Anmelden prüfen. Der Inhaber meldet sich auf dem Handy an und öffnet
#chat. Expected: unverändert. Den Schalter gibt es serverseitig nicht mehr, Compose und.envsind jetzt wirkungslos. -
Step 3: Umgebung bereinigen (optional, nur auf Wunsch des Inhabers). Sicherungen:
cp -n /home/webklar/apps/.env /home/webklar/apps/.env.bak-$(date -u +%Y-%m-%d)-kundenansicht-endeundcp -n /home/webklar/apps/docker-compose.yml /home/webklar/apps/docker-compose.yml.bak-$(date -u +%Y-%m-%d)-kundenansicht-ende. Dannsed -i '/^KUNDENANSICHT_NEU/d' /home/webklar/apps/.envundsed -i '/KUNDENANSICHT_NEU/d' /home/webklar/apps/docker-compose.yml, danachcd /home/webklar/apps && docker compose up -d --no-deps kundenbereich-server. Expected: DienstUp,command grep -a -c KUNDENANSICHT /home/webklar/apps/docker-compose.ymlgibt0.
Task 25: Selbstprüfung
Files: keine.
Interfaces:
-
Produces: Tag
kn-p10und der Bericht an den Controller. -
Step 1: Eigene Testdateien grün. Run:
free -m, danncd /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/knOfflineCache.test.js test/knFassung6Fest.test.js test/knLoginModul.test.js test/knAufraeumen.test.js test/knKaufRueckbau.test.js test/knAltZweigeWeg.test.js test/knSchalterWeg.test.js. Expected:fail 0. -
Step 2: Die ganze Suite. Run:
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; ls -l /dev/null. Expected:fail 0. Diepass-Zahl liegt um die neuen Dateien über und um die entfernten Fälle (Schalter, Orakel, Kaufwege, Alt-Zweige) unterN0; die Differenz steht im Bericht./dev/nullzeigtcrw-rw-rw-. -
Step 3: Verträge abhaken. Run:
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht && command grep -a -rc 'neueAnsicht(' server scripts | command grep -a -v ':0$'; ls public/vertragsunterlagen/v5; command grep -a -n 'Version 6' public/abo-bedingungen.html | head -2; git status --short. Expected: Die Liste der Dateien mit toten Alt-Zweigen (neueAnsicht() geht in den Bericht, das Archiv v5 enthält HTML und PDF, die Hauptseite nenntVersion 6, der Status ist leer. Abgehakt: V1 (Schalter ist konstanton,/dashboard.htmlkommt vorexpress.staticaus der Weiche), V2 (die öffentlichen Seiten liefertumschaltSeitenRouterauspublic/neu/oeffentlich/), V4 (das Skript nimmt die Kundenart nie vom Client, es nutztKUNDENARTEN), V12 (Fassung 6 ist/abo-bedingungen.html, Fassung 5 liegt im Archiv,config.abo.termsVersionist Fassung 6). -
Step 4: Tag und Bericht. Run:
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht && git tag kn-p10 && git rev-parse --short kn-p10. Letzte Nachricht des Agenten: Tag mit Commit-Hash,N0und die Endzahl der grünen Tests,Summe gesetzt=aus Task 8, die Dateien mit totenneueAnsicht(-Zweigen, alle gelöschten Testdateien aus den Tasks 17, 20, 21 und 23 mit Grund, die offenen Vertragsfragen.
Vertragsfragen an den Controller
Alle Vorschläge sind im Plan umgesetzt; bitte bestätigen oder korrigieren, bevor P10 startet.
- VF1 Dateien außerhalb der Zuständigkeitstabelle. P10 braucht zusätzlich
server/routes/umschaltSeiten.js,scripts/umschalten-nachpruefung.mjs,scripts/kauf-bestand-pruefen.mjs,docs/umschalten-kundenansicht.md,test/support/dateiListe.js,test/support/modulKlassen.json,test/support/oeffentlichStand.jsonund die TestskundenartBestand,knUmschaltSeiten,knUmschaltNachpruefung,kaufBestandPruefen,knUmschaltCheckliste,knOfflineCache,knFassung6Fest,knLoginModul,knAufraeumen,knKaufRueckbau,knAltZweigeWeg,knSchalterWeg. Dazu eine Zeile inserver/index.js(geteilt mit P0, hinter dessen Weiche). Vorschlag: in die Zeile von P10 aufnehmen. - VF2 Compose-Zeilen für den Schalter. P0 beschreibt das Einschalten „in der Compose-Umgebung“, aber kein Paket legt die Zeilen
KUNDENANSICHT_NEUundKUNDENANSICHT_NEU_PILOT_EMAILSin/home/webklar/apps/docker-compose.ymlan (Datei außerhalb des Repos). Vorschlag: Der Controller legt sie in Phase E vor dem Pilot an; Task 9 Step 4 prüft sie. - VF3 Fassung 6 und Archiv v5 (P8). P10 baut auf dem Plan von P8 auf: Seite
public/neu/rechtliches/abo-bedingungen.htmlmit PDF und.sha256daneben (die PDFs entstehen nach der Freigabe, P8 Task 11 Ablauf), Archiv v5 unterpublic/vertragsunterlagen/v5/(HTML bis auf den PDF-Link bytegleich, PDF bytegleich),FASSUNGENmit dem Feldarchivan der Zeile v5. P10 führt die Übergabe aus P8-Vertragsfrage 1 aus: Verschieben, die Zeilen v6 und v5 der Tabelle,abo6im Build-Skript,DOKUMENTE_V6im Hochladen-Skript. Die Funktionsseiten von P8 bleiben unberührt, ihre Verweise auf den Entwurfspfad der Abo-Seite lösen 301-Weiterleitungen auf (Task 14). Bitte V12 um die Pfade ergänzen. - VF4
fassungDerAnfrage(P7). SindtermsVersionundtermsVersionNeugleich, meldet P7s Reihenfolge („alt“ zuerst) immeralt, und der Weg der Fassung 6 liefe nie. Vorschlag: P7 baut die Reihenfolge „Neu-Fassung zuerst“ schon jetzt und prüft sie mit gleichen Werten; P10 änderte dann in Task 14 nichts. - VF5 Service-Worker-Registrierung und iOS-Fokuszoom. Beides steht heute nur in
public/app.js. Die neue Hülle (P1,huelle.js) muss die Registrierung enthalten, sonst fügt P10 sie in Task 16 ein. Vorschlag: P1 ergänzt sie, damit P10 nur prüft. - VF6 Alt-Zweige in P4-Dateien. P4 überlässt P10 das Entfernen der Alt-Zweige (VF6 von P4). Task 21 ändert dafür elf P4-Dateien und ihre Importlisten-Pins. Bitte bestätigen, dass das in P10 liegt.
- VF7 V1 und
/dashboard.htmlohne Sitzung. V1 sagt „ohne Sitzung → false“. Für die Seite braucht P10 beiondie neue Seite auch ohne Sitzung (sonst startet die PWA auf der alten dunklen Seite). Task 3 löst das in der Weiche (Zeile/dashboard.htmlinUMSCHALT_SEITEN), nicht inneueAnsichtFuerSession. Vorschlag: V1 um diesen Satz ergänzen. - VF8 Playwright-Skripte der alten Seite. Task 17 löscht die
test/ui/*.ui.mjs, diedashboard.htmloderapp.jsladen. Sie gehören nicht zunpm test; die Abdeckung tragen Unit-Tests und die Werkzeug-Gates. Bitte bestätigen. - VF9 Prüfung „Fassung 6 hochgeladen“. Es gibt kein lesendes Werkzeug für die WOMS-Collection
legalVersions. Task 9 stützt sich aufrechtliches-hochladen.mjs --probe(P8, listet Versionen mitsourceund fehlende Karten) und die Bestätigung des Inhabers. Die WOMS-NummervNzählt Uploads und ist nicht die Fassungsnummer, deshalb prüft der Plan diesource. Bitte bestätigen. - VF10 Beobachtungszeit und Rückweg. Zwischen Task 11 und Task 14 liegt eine Beobachtungszeit (Vorschlag: sieben Tage). Task 14 ist der Punkt ohne Rückweg per Schalter und braucht ein eigenes OK des Inhabers.
- VF11 Umfang des Rückbaus. Task 20 baut nur die zwei Routen und ihre Helfer zurück. Preise in
config.js, die Antwortfelder vonGET /api/funktionenundoeffentlicheAnsichtbleiben (Pins, Mails T19 und K15 für den Bestand). Task 23 lässt die Alt-Zweige hinterneueAnsicht(als toten Code stehen und zählt sie; ihr Entfernen ist eine eigene Entscheidung des Inhabers. - VF12 Reste des 30-€-Behaltens außerhalb von P10. P6 überlässt P10 die Route und die Texte K29/K30 (Task 20 stellt
k29Mailundk30Mailauf K40 und K41 um, die Vorlagen-Dateien bleiben). P9 verweistzeitenStatusText(„falls der Kunde die Funktion nicht behält“) und die Bestätigungstexte des Knopfs „Aktivieren (ohne Zahlung)“ ebenfalls an P10; beides liegt im WOMS-Repo. Vorschlag: eine eigene kleine WOMS-Änderung auftestmit Push durch den Inhaber, nicht in diesem Plan. - VF13 Wortlaut
freigegebenund Setup voron(P7 VF9). Task 1 und Task 9 prüfen, dassWORTLAUT_STATUSauf'freigegeben'steht undscripts/kundenansicht-setup.mjsgelaufen ist (--pruefen). Das Setup selbst läuft nicht in P10, sondern auf Anweisung des Controllers nach OK des Inhabers.