109 KiB
Kundenansicht neu, P4 Funktionsmodule: Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Die gemeinsamen Funktionsmodule (Terminplaner, News, Webshop, Buchungskalender, Öffnungszeiten live, Analyse) bekommen ihr weißes Aussehen unter .kn (public/neu/module.css) und verhalten sich bei modell === 'abo' ohne Preise und Bestellseiten: Einschalten statt Kaufen, Reiter in Etage 2. Die alte Ansicht verhält sich weiter exakt wie heute.
Architecture: Jede Verzweigung hängt an modell === 'abo' in der Antwort von GET /api/funktionen, die alle sechs Module beim Start laden. Den Einschalt-Ablauf von P3 (zeigeEinschalten) bekommen die Module als Init-Option einschalten; die gemeinsame Seite renderEinschaltSeite in funktion-bestellen.js zeigt Zusammenfassung und offene Punkte und mountet ihn. Die Reiter von Terminplaner, Webshop, Buchungskalender und Analyse zeichnet im Abo-Modell die Etage von P1; die Module melden sie per kn:reiter (Kurzlabels, wo nötig). module.css ersetzt termine.css, shop.css, buchungen.css, zeiten.css, funktionen.css (News) und analyse.css vollständig; ein Prüftest sichert Klassen-Abdeckung, Scope .kn, Palette und Verbote.
Tech Stack: Vanilla-JS-ES-Module ohne Build (public/), Tests mit node --test und test/support/fakeDom.js (nur h()/textContent), CSS, Sichtprüfung nur mit dem Werkzeug aus P0.
Spec: /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht/docs/superpowers/specs/2026-10-15-kundenansicht-neu-design.md (Abschnitte 5.1, 5.4–5.6, C2, C9, C12, C15)
Ü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-kn-ui, Branch kn/ui. Start nach P3 (Tag kn-p3), Ende mit Tag kn-p4. Alle Pfade unten sind relativ zu diesem Worktree.
Global Constraints
Alle Global Constraints und Verträge V1–V13 der Übersicht gelten (Aussehen, „du“, keine Preise für Funktionen, Hashes, Rechtlich fest, Testbefehle mit Speicherbremse, Commits mit Zeile Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>, Sicherheitsblock, kein Push). Paketeigene Zusätze:
- Alte Ansicht unverändert. Verzweigt wird nur mit
antwort.modell === 'abo'(strikt, ohne trim oder lowercase).public/dashboard.html,app.jsund alle alten CSS-Dateien bleiben unberührt. Bestehende Testdateien ändert dieses Paket nur an den zwei Pins der Dateiübersicht. - Quelle von
modell. Alle sechs Module laden beim StartGET /api/funktionenund lesenmodellundkundenartdort. Die Unter-Reiter-Module (termine-tab-*,shop-tab-*,buchungen-tab-*,zeiten-editor.js,analyse-cards.js) brauchen keinmodellund bleiben unverändert. - Pins, die neue Importzeilen verbieten (Erweiterung nur über benannte Importe einer bestehenden Importzeile):
funktionen.js:termineView.test.js:315-320,shopWiring.test.js:40-42,termineFragebogenView.test.js:1033-1034(genau zwei absolute Importe, ein relativer, keinimport(, kein Wortfunktion-bestellen).funktion-bestellen.js:termineFragebogenView.test.js:994-1034(nur./termine-dom.jsund./termine-view.js, keinimport(, kein Wort „zahlungspflichtig“, „details“, „summary“, keininnerHTML,hrefnur übersicherLink).termine-fragebogen.js:termineFragebogenView.test.js:3072-3086(acht Importe fest, keinhref, keinPATCH).zeiten.js,zeiten-bestellen.js:zeitenBestellen.test.js:841-865,zeitenView.test.js:390-399(Importlisten fest, keinimport().termine.js:termineView.test.js:373-394, 450-453(nur relative statische Importe, je einmallocation.hashundhistory.replaceState, einmalapi: {, kein Text „Wartungs-Abo“, „gekündigt“,'vertraege'). Neuetermine*.js-Dateien sind verboten (termineView.test.js:324-329).- Die
history.replaceState-Aufrufe der Rückkehr-Query intermine.js,shop.js,zeiten.js,funktionen.jsbleiben (P1 stempelt den Verlauf danach neu).
- Init-Optionen.
initTermine,initNews,initShop,initBuchungen,initZeitennehmen zusätzlicheinschalten(VF1). News und Analyse lesen die Projektwahl auch ausroot.dataset.wunschProjekt(V2) und entfernen das Attribut. - Reiter (P1, Vertragsfrage 5). Im Abo-Modell zeichnen Terminplaner, Webshop, Buchungskalender und Analyse keine eigene Leiste, sondern melden
kn:reiter(Spec 5.2 und 5.4: gemeinsame Mechanik „statt der vier eigenen Kopien“, Unterseiten-Reiter immer unten in Etage 2). Die Analyse meldet Übersicht, Heatmap und Einstellungen aus der ClosuremountCockpitinanalyse.js(VF2). - Neue Dateien außerhalb der Zuständigkeitstabelle:
public/fn-reiter.js,test/support/modulAbo.js,test/kundenansichtNeuModul*.test.js.public/neu/module.csslegt P1 als Stub an; ab P4 gehört die Datei P4. - Texte im Abo-Modell stehen wörtlich im Abschnitt „Texte“. Kein Preis, kein „Zahlungspflichtig“, kein „Behalten“, Du-Form.
- CSS: jede Regel unter
.kn; Farben nur aus der Palette der Übersicht plus die drei Zettelfarben der News; Fokusoutline: 2px solid #111; keine Verläufe, kein Glas, kein Dunkelmodus, keine Animationen außertransitionbis 150 ms;geraete.cssbleibt unberührt (Geräterahmen der Heatmap sind Abbildungen). Inhaltsfarben der Kunden (Widget-Akzentfarbe, Banner-Farbwahl der Analyse, Zettelfarben) bleiben. - Die Annahmen der Vertragsfragen VF1–VF6 (Ende des Dokuments) gelten, bis der Controller anders entscheidet.
Review Focus
- Alte Ansicht bricht:
modellfehlt, ist leer oder falsch geschrieben ('Abo','abo '): Bestellseite mit Preis, „Behalten“, Unternehmer-Haken, eigene Reiterleiste mit langen Labels bleiben wie heute. Tests: Task 1 und 3 bis 8 (je „ohne modell“), Task 15 (Bestand-Tests). - Preis- und Zahlungswörter im Abo-Modell: Reste aus alten Pfaden (
priceLabel,ustHinweis, „Zahlung fortsetzen“, „Für 30 € behalten“, „299 €“) auf Einschalt-, Anfrage- und Statusflächen. Tests:KEIN_PREISin Task 2 und 4 bis 8. - Reiterleiste doppelt oder fehlend: Im Abo-Modell erscheint sie in Modul und Etage zugleich oder in keinem von beiden; eine Ansicht ohne Reiter lässt die Reiter der vorigen stehen;
bei(key)wechselt nicht. Tests: Task 1 (alle drei Module), Task 8 (Analyse, auch leere Liste), Task 3 und 5 (leere Liste). - Privatkunde oder leere Kundenart im Webshop: das Formular erscheint trotzdem, oder ein aktiver Bestands-Shop verschwindet. Privat und leer bekommen Hinweis plus Chat, nie das Formular;
state !== 'off'bleibt bedienbar. Test: Task 5. - Dunkle Reste im CSS: eine vergessene Klasse behält Weiß-auf-Dunkel und wird auf Weiß unleserlich. Tests: Abdeckung je Präfix in Task 9–13, Gates in Task 14.
Dateiübersicht
| Datei | Aufgabe | Task |
|---|---|---|
public/fn-reiter.js (neu) |
reiterEintraege, meldeReiter, KURZLABELS |
1 |
public/termine.js, public/shop.js, public/buchungen.js |
modell lesen, im Abo-Modell Reiter melden (:49-56, :100-106, :322-394; :295-360; :39-41, :155-205) |
1 |
public/funktion-bestellen.js |
renderEinschaltSeite (nach :123), Blöcke aus baueSeite (:146-170) herausziehen, statusKarte (:306, :315) mit modell |
2 |
public/termine.js, public/termine-fragebogen.js |
einschalten, fertig im Kontext, letzter Schritt (:27-29, :80, :90, :98, :101-104) |
3 |
public/funktionen.js |
News: Einschalt-Seite (:474-515), Projektwahl aus dataset |
4 |
public/shop.js, public/shop-anfrage.js |
Anfrage ohne Unternehmer, Satz, Privat-Hinweis, Einschalten (:64-74, :195, :253; :37, :67-68, :115-120) |
5 |
public/buchungen.js, public/buchungen-anfrage.js |
Anfrage ohne Preis, Einschalten (:32-41, :124-130; :11, :20, :94) |
6 |
public/zeiten.js, public/zeiten-bestellen.js |
Einschalten, kein Behalten (:55, :62-73, :132, :161, :218-226; :105, :289) |
7 |
public/analyse.js |
Aktivierung mit Linkzeile und Zielgruppen-Häkchen (:22-49, :76-112); Reiter als kn:reiter melden (:22-49, mountCockpit :174-328) |
8 |
public/neu/module.css (Stub von P1) |
alle Modul-Klassen unter .kn |
9–13 |
public/neu/bilder/analyse.webp (P3, nur dieses Bild neu erzeugt) |
Analyse in neuer Optik (P3 VF2) | 14 |
test/support/modulAbo.js (neu) |
Testhilfe | 1 |
test/kundenansichtNeuModul{Reiter,Einschalten,Termine,News,Shop,Buchungen,Zeiten,Analyse,Css}.test.js (neu) |
je Task der Namen | 1–9 |
test/shopWiring.test.js:31 (Pin) |
Regex der Signatur initShop(root, { projectId, einschalten } = {}) |
5 |
test/unterkunftWiring.test.js:130 (Pin) |
Regex der Signatur initBuchungen(root, { projectId, einschalten } = {}) |
6 |
Werkzeug /root/reports/kundenansicht-neu-2026-10-15/werkzeug/ (eigenes Git): views.mjs, gates.mjs, fixtures/module.mjs (neu, 17 Fälle), fixtures/endpunkte-p4.mjs (neu, Analyse-Endpunkte), Einträge in fixtures/faelle.mjs, fixtures/index.mjs, fixtures/endpunkte.mjs, vertrag-endpunkte.mjs |
Ansichten, Gate p4, Fälle |
14 |
Texte (Abo-Modell, wörtlich)
| Kürzel | Datei | Wortlaut |
|---|---|---|
| E1 | funktion-bestellen.js, termine-fragebogen.js |
Überschrift der Seite mit Zusammenfassung und letzter Eintrag der Fortschrittsleiste: Prüfen und einschalten |
| E2 | funktion-bestellen.js |
bei offenen Punkten: Du kannst einschalten, sobald die offenen Punkte erledigt sind. |
| E4 | funktion-bestellen.js, funktionen.js |
ohne Funktion einschalten: Das Einschalten ist gerade nicht verfügbar. Bitte lade die Seite neu oder schreib uns im Chat. |
| E5 | funktion-bestellen.js, zeiten-bestellen.js |
Faktenzeile der Statuskarte: Eingeschaltet am (statt Bestellt am) |
| F1 | termine-fragebogen.js |
Du siehst den Fragebogen in der Kundenansicht. Angaben kannst du ändern, Schritte bestätigen und einschalten kann nur der Kunde selbst. |
| S1 | shop-anfrage.js, buchungen-anfrage.js |
erster Stichpunkt statt der Preiszeile: Im Wartungs-Abo enthalten.; letzter Stichpunkt: Wir sehen uns deine Anfrage kurz an. Erst nach unserer Zusage kannst du einschalten. |
| S2 | shop-anfrage.js |
Satz über „Anfrage senden“: Mit „Anfrage senden“ bestätigst du, dass du keine ausgeschlossenen Waren verkaufst ( + AUSGESCHLOSSEN + ). |
| S3 | shop.js |
Privat: Titel Der Webshop ist für Betriebe, Text Für Privatpersonen gibt es den Webshop leider nicht. Stimmt das nicht? Schreib uns im Chat., dazu der Chat-Knopf |
| B1 | shop.js, buchungen.js |
Gruppe der Zusammenfassung: Titel Deine Anfrage (in shop.js schon so) |
| Z2 | zeiten-bestellen.js |
Abo-Ende in setup/active: Dein Wartungs-Abo ist beendet. Öffnungszeiten live läuft noch bis <TT.MM.JJJJ> und schaltet sich dann ab. |
| Z3 | zeiten-bestellen.js |
abgeschaltet mit Daten: Deine Eingaben bleiben[ bis <TT.MM.JJJJ>] gespeichert. Mit einem neuen Wartungs-Abo schaltet sich Öffnungszeiten live wieder ein. |
| A1 | analyse.js |
Linkzeile: Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung <avvVersion>)., beide Wörter sind Links (legal.lb, legal.avv) |
| A2 | analyse.js |
Zielgruppen-Zeile mit Häkchen: Meine Website richtet sich nicht gezielt an Kinder unter 16 Jahren. |
| A3 | analyse.js |
bei reason === 'kein_abo': Die Analyse ist im Hosting- und im Wartungs-Abo enthalten. plus der Link ZU_VERTRAEGE |
Tasks
Task 1: Reiter im Abo-Modell (kn:reiter, C2) und Testhilfe
Files:
- Create:
public/fn-reiter.js,test/support/modulAbo.js - Modify:
public/termine.js(initTermine:49-56,ladeFunktionen:100-106,zeichneAnsicht,zeigeReiter:322-394),public/shop.js(zeichneAnsicht,zeigeReiter:295-360),public/buchungen.js(starte:39-41,zeichneAnsicht,zeigeReiter:155-205) - Test:
test/kundenansichtNeuModulReiter.test.js
Interfaces:
-
Consumes: Ereignis
kn:reiterder Etage (P1):CustomEventmitbubbles: trueunddetail: { eintraege: [{ key, label, kurz? }], aktiv, bei(key) }; eine leere Liste heißt „keine Reiter“, dann füllt „Zurück“ die Zeile. -
Produces
public/fn-reiter.js(keine Importe):export const KURZLABELS // { termine: { team: 'Team' }, shop: { uebersicht: 'Start', rechtliches: 'Recht' }, buchungen: { unterkuenfte: 'Preise' } }; die Analyse (Übersicht, Heatmap, Einstellungen) braucht keine Kurzlabels und hat deshalb keinen Eintrag, läuft aber durch dieselbe Funktion export function reiterEintraege(modul, reiterListe) // [{ key, label, … }] → [{ key, label, kurz? }]; kurz nur mit Eintrag in KURZLABELS, andere Felder fallen weg export function meldeReiter(el, { eintraege, aktiv = '', bei = () => {} }) // sendet kn:reiter aus el; ohne CustomEvent oder ohne el.dispatchEvent: false, sonst true -
Produces
test/support/modulAbo.js(alle späteren Tasks nutzen es):export const mitDom // (fn) => async () => { fx = installFakeDom(); setzt globalThis.CustomEvent auf eine fakeDom-taugliche Klasse (type, detail, bubbles, target, ergebnisse, preventDefault, stopPropagation); try { await fn(fx); await fx.ruhe() } finally { fx.restore(); CustomEvent zurück } } export function hoereAufKn(fx) // { reiter: [detail …] } – lauscht am document.body auf kn:reiter export function routen(fx, tabelle) // ersetzt fetch; Schlüssel 'METHODE /pfad?query'; Wert: { status?, body } | Array (der Reihe nach, letzter gilt weiter) | (aufruf) => Wert; unbekannt: 404 und Eintrag in fx.unbekannt; jeder Aufruf in fx.fetchLog export function einschaltenAttrappe() // { fn, aufrufe: [{ wurzel, key, projectId, viewAs, onFertig }], fertig(i = 0, ergebnis = {}) → ruft aufrufe[i].onFertig }; fn zeichnet nichts export const KEIN_PREIS // /€|einmalig|Preis|Zahlungspflichtig|Bestellseite|Behalten/ export function htmlWurzel(stageSelektor) // für innerHTML-Module: { root, stage, kn }; root.dataset ist {}; root.querySelector(stageSelektor) liefert stage, root.querySelector('.an') die Attrappe `an` (addEventListener merkt die Handler in an.handlers, querySelector → null, querySelectorAll → []), sonst null; stage.els['#id'] erzeugt Attrappen { innerHTML, disabled, checked, dataset, handlers, textContent }; stage.setAttribute/getAttribute speichern in stage.attribute; root.dispatchEvent(ev) legt ev.detail der kn:reiter-Ereignisse in kn.reiter ab und gibt true zurück (die Analyse meldet ihre Reiter an root) export async function ladeFunktionenModul() // public/funktionen.js laden, alle statischen Importe auf die Dateien in public/ umbiegen (Muster funktionenZeitenTile.test.js:12-18) -
Verhalten der drei Module bei
modell === 'abo': keine eigene Leiste (keinrole="tablist"), das Panel trägtaria-labelstattaria-labelledby; jeder Reiterwechsel meldetmeldeReiter(root, { eintraege: reiterEintraege(modul, REITER), aktiv: key, bei: waehleReiter }); jedes Zeichnen ohne Reiter (Fragebogen, Info-, Anfrage- und Statuskarten, Vorlagenwahl) meldet die leere Liste vor dem Zeichnen. Termine und Buchungen lesen dazumodellins.modell(Shop:s.funktionen.modell). Ohnemodelländert sich nichts. Die Analyse (Task 8) meldet ebenso aus der ClosuremountCockpitinanalyse.js, mit denselben Hilfen und Regeln (leere Liste vor jedem Zeichnen ohne Reiter,bei=pickTab); Verdrahtung und Test stehen dort, weil siemodellerst dort liest und mithtmlWurzelarbeitet. -
Step 1: Voraussetzungen prüfen
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git tag --list 'kn-p*' && git status --short && free -m && ls public/neu
Expected: Tags kn-p0 bis kn-p3, git status leer, „available“ mindestens 2000 MB, public/neu enthält dashboard.html, reiter.js, etage.js, funktion-einschalten.js und den Stub module.css. Fehlt etwas davon: stoppen und dem Controller melden.
- Step 2: Failing test
test/kundenansichtNeuModulReiter.test.js
test('KURZLABELS: genau diese Einträge (C2)', () => {
assert.deepEqual(KURZLABELS, { termine: { team: 'Team' }, shop: { uebersicht: 'Start', rechtliches: 'Recht' }, buchungen: { unterkuenfte: 'Preise' } })
})
test('reiterEintraege: Kurzlabel nur mit Eintrag, fremde Felder fallen weg', () => {
const liste = [{ key: 'termine', label: 'Termine', render() {} }, { key: 'team', label: 'Team & Plätze', render() {} }]
assert.deepEqual(reiterEintraege('termine', liste), [{ key: 'termine', label: 'Termine' }, { key: 'team', label: 'Team & Plätze', kurz: 'Team' }])
assert.deepEqual(reiterEintraege('unbekannt', liste.slice(1)), [{ key: 'team', label: 'Team & Plätze' }])
assert.deepEqual(reiterEintraege('analyse', [{ key: 'heatmap', label: 'Heatmap' }]), [{ key: 'heatmap', label: 'Heatmap' }], 'Analyse: kein Kurzlabel nötig')
})
test('meldeReiter: das Ereignis steigt auf und trägt Einträge, aktiv und bei; ohne Element tut es nichts', mitDom(async (fx) => {
const k = hoereAufKn(fx); const el = h('div'); document.body.append(el); const bei = () => {}
assert.equal(meldeReiter(el, { eintraege: [{ key: 'a', label: 'A' }], aktiv: 'a', bei }), true)
assert.deepEqual(k.reiter, [{ eintraege: [{ key: 'a', label: 'A' }], aktiv: 'a', bei }])
assert.equal(meldeReiter(null, { eintraege: [] }), false)
}))
Dazu eine Tabelle FAELLE (je Modul init, optional vorher = vergissAuswahl, tabelle(modell) für den Zustand active, erwartete Labels alt und Kurzlabels kurz) und zwei Tests je Modul. Fixtures: ein Projekt p1 mit features.<schlüssel>.state = 'active', modell nur im Körper, wenn gesetzt.
termine:GET /api/funktionenmittermine: { verfuegbar: true },GET /api/termine/einstellungen?projectId=p1mit{ state: 'active', einstellung: {}, fehlt: [] };alt['Termine', 'Leistungen', 'Team & Plätze', 'Einstellungen'],kurz['Termine', 'Leistungen', 'Team', 'Einstellungen']shop:shop: { verfuegbar: true },features: { shop: {} },GET /api/shop/status?projectId=p1mit{ verfuegbar: true, state: 'active', anfrage: null };alt['Übersicht', 'Artikel', 'Bestellungen', 'Einstellungen', 'Rechtliches'],kurz['Start', 'Artikel', 'Bestellungen', 'Einstellungen', 'Recht']buchungen:unterkunft: { verfuegbar: true },GET /api/unterkunft/status?projectId=p1mit{ state: 'active' };alt['Anfragen', 'Kalender', 'Unterkünfte & Preise', 'Rechnungen', 'Einstellungen'],kurz['Anfragen', 'Kalender', 'Preise', 'Rechnungen', 'Einstellungen']
for (const f of FAELLE) {
test(`${f.name}, Abo: keine eigene Leiste, kn:reiter mit Kurzlabels, bei() wechselt`, mitDom(async (fx) => {
const k = hoereAufKn(fx); const root = await oeffne(fx, f, 'abo')
assert.equal(fx.finde(root, '[role="tablist"]'), null)
const erst = k.reiter.at(-1)
assert.deepEqual(erst.eintraege.map((e) => e.label), f.alt)
assert.deepEqual(erst.eintraege.map((e) => e.kurz ?? e.label), f.kurz)
const andere = erst.eintraege.find((e) => e.key !== erst.aktiv).key
erst.bei(andere); await fx.ruhe()
assert.equal(k.reiter.at(-1).aktiv, andere)
}))
test(`${f.name}, ohne modell (fehlt, „Abo“): eigene Leiste mit langen Labels, kein Ereignis`, mitDom(async (fx) => {
for (const modell of [undefined, 'Abo']) {
const k = hoereAufKn(fx); const root = await oeffne(fx, f, modell)
assert.deepEqual(fx.alle(root, '[role="tab"]').map((t) => fx.text(t)), f.alt)
assert.equal(k.reiter.length, 0)
}
}))
}
oeffne(fx, f, modell) ruft f.vorher?.(), routen(fx, f.tabelle(modell)), hängt eine Wurzel an den Body, ruft f.init(root, { projectId: 'p1' }) und fx.ruhe(). Zwei weitere Tests: (a) REITER.map((r) => r.label) von shop.js und buchungen.js bleiben unverändert (die Pins buchungenUi.test.js:53, shopView.test.js:373); (b) Shop im Abo-Modell im Zustand off (features.shop.state und status.state 'off', kundenart: 'unternehmen') meldet zuletzt die leere Liste: assert.deepEqual(k.reiter.at(-1).eintraege, []).
- Step 3: Run test to verify it fails
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulReiter.test.js
Expected: FAIL mit „Cannot find module“ (fn-reiter.js und modulAbo.js fehlen).
- Step 4: Implement
public/fn-reiter.jsundtest/support/modulAbo.jsnach den Signaturen oben
routen folgt dem Helfer in termineFragebogenView.test.js:3115-3127. htmlWurzel und ladeFunktionenModul folgen funktionenTiles.test.js:84-104 und funktionenZeitenTile.test.js:12-18; die Attrappen an, setAttribute und dispatchEvent für die Analyse erweitern fakeEl und fakeStage aus funktionenTiles.test.js:405-415. meldeReiter baut new CustomEvent('kn:reiter', { bubbles: true, detail }).
- Step 5: Reiter in den drei Modulen verdrahten
In termine.js, shop.js, buchungen.js: neue Importzeile import { meldeReiter, reiterEintraege } from './fn-reiter.js'. Die Literale von REITER bleiben unverändert (Pin termineView.test.js:364). Verhalten wie im Abschnitt „Verhalten der drei Module“; im Abo-Modell werden Knöpfe und Leiste nicht gebaut (in termine.js lässt der vorhandene filter(Boolean) eine fehlende Leiste weg, in shop.js und buchungen.js die Leiste nicht in replaceChildren aufnehmen).
- Step 6: Run test to verify it passes
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test --test-concurrency=1 test/kundenansichtNeuModulReiter.test.js test/termineView.test.js test/shopView.test.js test/buchungenUi.test.js
Expected: PASS (neue und bestehende Tests).
- Step 7: Commit
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git add public/fn-reiter.js test/support/modulAbo.js test/kundenansichtNeuModulReiter.test.js public/termine.js public/shop.js public/buchungen.js && git commit -m "Module: im Abo-Modell Reiter mit Kurzlabels an die Etage melden (C2)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 2: Einschalt-Seite im Abo-Modell (funktion-bestellen.js)
Files:
- Modify:
public/funktion-bestellen.js(neue Funktion nach:123; Gruppen- und Offen-Block ausbaueSeite:146-170in zwei Hilfsfunktionen derselben Datei ziehen;statusKarte:306,:315) - Test:
test/kundenansichtNeuModulEinschalten.test.js
Interfaces:
-
Consumes:
h,fehlerKarteaus./termine-dom.js,zusammenfassungaus./termine-view.js(beide schon importiert);einschalten=zeigeEinschalten(platz, { key, projectId, onFertig, … })auspublic/neu/funktion-einschalten.js(P3, VF1). P3 lädt die Liste selbst, zeichnet Bestätigung und Knopf und ruftonFertig({ feature, ticketWoid })nach 200 undonFertig({ feature: null, ticketWoid: null })nach 409bereits_eingeschaltet; alle anderen Fehler zeigt P3 selbst. -
Produces:
export function renderEinschaltSeite(root, { ctx = {}, daten = {}, zumSchritt, key, zusammenfassung: gruppenFunktion, einschalten, nachEinschalten } = {}) // → Element section.kb-panel.tm-bestellen.tm-einschalten[data-key] // daten: wie bei renderBestellen (einstellung, fragebogen, branchen, offen, kundenansicht): Quelle von Zusammenfassung und offenen Punkten // einschalten(platz, { key, projectId: ctx.projectId, viewAs, onFertig }) mit viewAs = daten.kundenansicht === true || ctx.kundenansicht === true // onFertig ist eine Closure mit Merker: nur der erste Aufruf wirkt und nur, wenn ctx.aktuell?.() nicht false ist; sie ruft nachEinschalten() (die Ansicht zeichnet sich neu) // statusKarte({ ..., modell }): modell === 'abo' → Faktenzeile „Eingeschaltet am“ (E5)Aufbau: gibt es Gruppen (
gruppenFunktion(daten.einstellung, daten.fragebogen, daten.branchen), beikey === 'termine'ohne eigene Funktion diezusammenfassungaustermine-view.js) oder offene Punkte (daten.offen), steht obenh3E1, dann die Gruppen mit „Ändern“ und die offenen Punkte mit „Zum Schritt“. Bei offenen Punkten folgt der Hinweis E2 undeinschaltenwird nicht gerufen. Sonst steht der Platzdiv.tm-einschalten-platz[data-einschalten=key]da, in deneinschaltengenau einmal zeichnet; fehlteinschalten, steht dort diefehlerKartemit E4. -
Step 1: Failing test
test/kundenansichtNeuModulEinschalten.test.js
function seite(optionen) {
const wurzel = h('div'); document.body.append(wurzel)
renderEinschaltSeite(wurzel, { ctx: { projectId: 'p1' }, daten: { offen: [], kundenansicht: false }, key: 'shop', ...optionen })
return wurzel
}
const GRUPPEN = () => [{ schritt: 'anfrage', titel: 'Deine Anfrage', zeilen: ['Was du verkaufst: Kleidung'] }]
test('mountet den Ablauf genau einmal in den Platz, mit key, projectId und viewAs; ohne Haken, Preis und Überschrift', mitDom(async (fx) => {
const a = einschaltenAttrappe(); const wurzel = seite({ einschalten: a.fn })
assert.equal(a.aufrufe.length, 1)
assert.deepEqual([a.aufrufe[0].key, a.aufrufe[0].projectId, a.aufrufe[0].viewAs], ['shop', 'p1', false])
assert.ok(a.aufrufe[0].wurzel === fx.finde(wurzel, '[data-einschalten="shop"]'))
assert.equal(fx.finde(wurzel, 'h3'), null)
assert.equal(fx.alle(wurzel, 'input').length, 0)
assert.doesNotMatch(fx.text(wurzel), KEIN_PREIS)
}))
test('Zusammenfassung mit E1; offene Punkte sperren den Ablauf (E2); „Ändern“ und „Zum Schritt“ rufen zumSchritt', mitDom(async (fx) => {
const a = einschaltenAttrappe(); const schritte = []
const wurzel = seite({ einschalten: a.fn, zusammenfassung: GRUPPEN, zumSchritt: (k) => schritte.push(k), daten: { offen: [{ schritt: 'zeiten', text: 'Bitte wähle das Bundesland.' }], kundenansicht: false } })
assert.equal(fx.text(fx.finde(wurzel, 'h3')), 'Prüfen und einschalten')
assert.equal(a.aufrufe.length, 0)
assert.match(fx.text(wurzel), /Du kannst einschalten, sobald die offenen Punkte erledigt sind\./)
await fx.klick(fx.finde(wurzel, '[data-aktion="zum-schritt"][data-schritt="zeiten"]'))
await fx.klick(fx.finde(wurzel, '[data-aktion="aendern"][data-schritt="anfrage"]'))
assert.deepEqual(schritte, ['zeiten', 'anfrage'])
}))
test('Kundenansicht: der Ablauf bekommt viewAs true', mitDom(async () => {
const a = einschaltenAttrappe(); seite({ einschalten: a.fn, daten: { offen: [], kundenansicht: true } })
assert.equal(a.aufrufe[0].viewAs, true)
}))
test('onFertig: höchstens einmal, nicht nach ctx.aktuell() === false', mitDom(async (fx) => {
let n = 0
const a = einschaltenAttrappe(); seite({ einschalten: a.fn, nachEinschalten: () => { n += 1 } })
a.fertig(0); a.fertig(0); await fx.ruhe()
assert.equal(n, 1)
const b = einschaltenAttrappe(); seite({ einschalten: b.fn, ctx: { projectId: 'p1', aktuell: () => false }, nachEinschalten: () => { n += 1 } })
b.fertig(0); await fx.ruhe()
assert.equal(n, 1)
}))
test('ohne Funktion einschalten: Fehlerkarte E4 statt Absturz', mitDom(async (fx) => {
const wurzel = seite({})
assert.match(fx.text(wurzel), /Das Einschalten ist gerade nicht verfügbar\. Bitte lade die Seite neu oder schreib uns im Chat\./)
assert.ok(fx.finde(wurzel, '[role="alert"]'))
}))
test('statusKarte: Abo-Modell „Eingeschaltet am“, sonst „Bestellt am“', mitDom(async (fx) => {
const args = { projekt: { projectId: 'p1' }, bestellung: { state: 'setup', orderedAt: '2026-10-16T09:00:00.000Z', ticketWoid: '100150' } }
assert.match(fx.text(statusKarte({ ...args, modell: 'abo' })), /Eingeschaltet am\s*16\.10\.2026/)
assert.match(fx.text(statusKarte(args)), /Bestellt am\s*16\.10\.2026/)
}))
- Step 2: Run test to verify it fails
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulEinschalten.test.js
Expected: FAIL mit „renderEinschaltSeite is not a function“.
- Step 3: Gruppen- und Offen-Block herausziehen (Refactor ohne Änderung der Ausgabe)
In baueSeite die Erzeugung von gruppen (:150-159) und offenBlock (:161-170) in zwei Funktionen derselben Datei verschieben, die baueSeite und renderEinschaltSeite benutzen. Danach sofort: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/termineFragebogenView.test.js test/funktionBestellenKey.test.js Expected: PASS wie vor dem Schritt.
- Step 4: Implement
renderEinschaltSeiteundstatusKarte({ modell })
Keine neuen Importe, kein href, keine Wörter aus der Pin-Liste (details, summary, „zahlungspflichtig“).
- Step 5: Run tests to verify they pass
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test --test-concurrency=1 test/kundenansichtNeuModulEinschalten.test.js test/termineFragebogenView.test.js test/funktionBestellenKey.test.js
Expected: PASS (neue und bestehende Tests).
- Step 6: Commit
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git add public/funktion-bestellen.js test/kundenansichtNeuModulEinschalten.test.js && git commit -m "Funktionsmodule: gemeinsame Einschalt-Seite für das Abo-Modell" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 3: Terminplaner im Abo-Modell
Files:
- Modify:
public/termine.js(initTermine:49-56,baueCtx:322-350,statusKarte-Aufruf:245-252),public/termine-fragebogen.js(Import:24,TITEL:27,KUNDENANSICHT:29,:80,:90,:98,bestellseite:101-104) - Test:
test/kundenansichtNeuModulTermine.test.js
Interfaces:
-
Consumes:
renderEinschaltSeite(Task 2),s.modellundmeldeReiter(Task 1), Testhilfen; V5-Feldermodell,kundenart. -
Produces:
initTermine(root, { projectId, einschalten } = {});s.einschaltenin der Sitzung;baueCtxliefert zusätzlicheinschalten: s.einschaltenundfertig: () => ladeNeu(s); der Fragebogen liestctx.s?.modell === 'abo'. -
Step 1: Failing test
test/kundenansichtNeuModulTermine.test.js
Kopf: STAND = bestellbarerStand({ now: new Date('2026-10-16T08:00:00.000Z') }) aus test/support/fragebogenStand.js (alle Schritte bestätigt, der Start ist also der letzte Schritt); daten(extra) = { state: 'off', einstellung: STAND.einstellung, fragebogen: STAND.fragebogen, version: 1, branchen: [], kundenansicht: false, abo: { ok: true }, offen: [], bestellung: null, ...extra }; liste(modell, state = 'off', feature = {}) = Antwort von GET /api/funktionen mit einem Projekt p1 (features.termine = { state, siteKey: 'abcdefghijklmnop', ...feature }), termine: { verfuegbar: true, bestellbar: true, grund: null, preis: '149 €', einrichtung: 'meist in 1–2 Wochen', hinweis: null } und modell nur, wenn gesetzt; BESTELLUNG = { preis: '149 €', preisHinweis: 'einmalig, ohne Umsatzsteuer gemäß § 19 UStG', einrichtung: '', belehrung: null, dokumente: [], consent: { version: 'v-test', knopf: 'Zahlungspflichtig bestellen · 149 €', items: [{ id: 'beginn', text: 'Ich verlange den sofortigen Beginn.', haken: true }] } }; EINSTELLUNGEN = 'GET /api/termine/einstellungen?projectId=p1'; oeffne(fx, tabelle, optionen) hängt eine Wurzel an den Body, ruft routen, initTermine(wurzel, { projectId: 'p1', ...optionen }) und fx.ruhe().
test('Abo, alles bestätigt: letzter Schritt E1 mountet den Ablauf, ohne Haken, Preisblock und Checkout; keine Reiter', mitDom(async (fx) => {
const a = einschaltenAttrappe(); const k = hoereAufKn(fx)
const wurzel = await oeffne(fx, { 'GET /api/funktionen': liste('abo'), [EINSTELLUNGEN]: { body: daten() } }, { einschalten: a.fn })
assert.equal(fx.text(fx.finde(wurzel, '.tm-fb-bestellen h3')), 'Prüfen und einschalten')
assert.match(fx.text(fx.finde(wurzel, '.tm-schritt[aria-current="step"]')), /Prüfen und einschalten/)
assert.deepEqual(a.aufrufe.map((x) => [x.key, x.projectId, x.viewAs]), [['termine', 'p1', false]])
assert.equal(fx.alle(wurzel, 'input[type="checkbox"]').length, 0, 'auch kein Haken „gesundheit“')
for (const sel of ['[data-aktion="bestellen"]', '.tm-bestellen-preis', '.tm-rechtstext', '.tm-bestellen-zustimmung']) assert.equal(fx.finde(wurzel, sel), null, sel)
assert.equal(fx.fetchLog.filter((x) => x.method === 'POST').length, 0)
assert.deepEqual(k.reiter.at(-1).eintraege, [])
}))
test('Abo, offene Punkte: kein Ablauf, Hinweis E2, „Zum Schritt“ führt zurück', mitDom(async (fx) => {
const a = einschaltenAttrappe()
const offen = [{ schritt: 'zeiten', text: 'Bitte wähle das Bundesland deines Betriebs.' }]
const wurzel = await oeffne(fx, { 'GET /api/funktionen': liste('abo'), [EINSTELLUNGEN]: { body: daten({ offen }) } }, { einschalten: a.fn })
assert.equal(a.aufrufe.length, 0)
assert.match(fx.text(wurzel), /Du kannst einschalten, sobald die offenen Punkte erledigt sind\./)
await fx.klick(fx.finde(wurzel, '[data-aktion="zum-schritt"][data-schritt="zeiten"]'))
assert.equal(fx.finde(wurzel, '.tm-schritt[aria-current="step"]').getAttribute('data-schritt'), 'zeiten')
}))
test('Abo: nach dem Einschalten lädt die Ansicht neu und zeigt die Statuskarte „In Einrichtung“ mit „Eingeschaltet am“', mitDom(async (fx) => {
const a = einschaltenAttrappe(); let n = 0
const wurzel = await oeffne(fx, {
'GET /api/funktionen': () => (n === 0 ? liste('abo') : liste('abo', 'setup', { orderedAt: '2026-10-16T09:00:00.000Z', ticketWoid: '100150' })),
[EINSTELLUNGEN]: () => ({ body: n === 0 ? daten() : daten({ state: 'setup' }) }),
}, { einschalten: a.fn })
n = 1; a.fertig(0); await fx.ruhe()
const karte = fx.finde(wurzel, '.tm-statuskarte[data-state="setup"]')
assert.match(fx.text(karte), /Eingeschaltet am\s*16\.10\.2026/)
assert.doesNotMatch(fx.text(karte), /Bestellt am|Zahlung/)
}))
test('Abo: ohne Funktion einschalten steht E4; in der Kundenansicht steht F1 und der Ablauf bekommt viewAs true', mitDom(async (fx) => {
const ohne = await oeffne(fx, { 'GET /api/funktionen': liste('abo'), [EINSTELLUNGEN]: { body: daten() } })
assert.match(fx.text(ohne), /Das Einschalten ist gerade nicht verfügbar\. Bitte lade die Seite neu oder schreib uns im Chat\./)
const a = einschaltenAttrappe()
const ansicht = await oeffne(fx, { 'GET /api/funktionen': liste('abo'), [EINSTELLUNGEN]: { body: daten({ kundenansicht: true }) } }, { einschalten: a.fn })
assert.match(fx.text(ansicht), /Du siehst den Fragebogen in der Kundenansicht\. Angaben kannst du ändern, Schritte bestätigen und einschalten kann nur der Kunde selbst\./)
assert.equal(a.aufrufe[0].viewAs, true)
}))
test('Ohne modell (fehlt, leer, „Abo“, „abo “): unverändert Bestellseite mit Knopf des Registers, Ablauf wird nie gerufen', mitDom(async (fx) => {
for (const modell of [undefined, '', 'Abo', 'abo ']) {
const a = einschaltenAttrappe()
const wurzel = await oeffne(fx, { 'GET /api/funktionen': liste(modell), [EINSTELLUNGEN]: { body: daten({ bestellung: BESTELLUNG }) } }, { einschalten: a.fn })
assert.equal(a.aufrufe.length, 0, JSON.stringify(modell))
assert.equal(fx.text(fx.finde(wurzel, '[data-aktion="bestellen"]')), 'Zahlungspflichtig bestellen · 149 €')
assert.equal(fx.text(fx.finde(wurzel, '.tm-fb-bestellen h3')), 'Prüfen und bestellen')
}
}))
- Step 2: Run test to verify it fails
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulTermine.test.js
Expected: FAIL (der letzte Schritt zeigt die Bestellseite, a.aufrufe ist leer).
- Step 3: Implement
termine.js
initTermine(root, { projectId, einschalten } = {}); einschalten in s.einschalten; baueCtx ergänzt einschalten und fertig: () => ladeNeu(s); der statusKarte-Aufruf übergibt modell: s.modell. Kein neues location.hash, replaceState oder api: {.
- Step 4: Implement
termine-fragebogen.js
Import { renderBestellen, renderEinschaltSeite } aus ./funktion-bestellen.js (nur die benannten Importe erweitern). bestellseite() ruft bei ctx.s?.modell === 'abo' renderEinschaltSeite(seite, { ctx, daten: ctx.daten, zumSchritt: (key) => zeige(key), key: 'termine', einschalten: ctx.einschalten, nachEinschalten: () => ctx.fertig() }), sonst wie bisher renderBestellen. Titel der Leiste und des Zählers (:90, :98) und der Hinweis :80 folgen E1 und F1 über zwei neue Konstanten TITEL_ABO und KUNDENANSICHT_ABO; die Map TITEL bleibt.
- Step 5: Run tests to verify they pass
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test --test-concurrency=1 test/kundenansichtNeuModulTermine.test.js test/termineView.test.js test/termineFragebogenView.test.js
Expected: PASS (neue und bestehende Tests).
- Step 6: Commit
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git add public/termine.js public/termine-fragebogen.js test/kundenansichtNeuModulTermine.test.js && git commit -m "Terminplaner: im Abo-Modell Einschalten statt Bestellen, ohne Häkchen" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 4: News-Pinnwand im Abo-Modell
Files:
- Modify:
public/funktionen.js(initNews:474-512,renderBooking:515) - Test:
test/kundenansichtNeuModulNews.test.js
Interfaces:
-
Consumes:
htmlWurzel('#news-stage'),ladeFunktionenModul,einschaltenAttrappe,KEIN_PREIS(Task 1). -
Produces:
initNews(root, { projectId, einschalten } = {}); im Zustandoffundmodell === 'abo'besteht die Seite nur aus dem Platzdiv#news-einschalten, in deneinschalten(platz, { key: 'news', projectId, viewAs: false, onFertig })zeichnet (die Überschrift „News-Pinnwand einschalten“ kommt von P3);onFertiglädtinitNews(root, { einschalten })einmal neu;root.dataset.wunschProjektwählt das Projekt und wird entfernt. Keine neuen Importe (PinstermineView.test.js:315-320). -
Step 1: Failing test
test/kundenansichtNeuModulNews.test.js
Kopf:
const { initNews } = await ladeFunktionenModul()
globalThis.window = { location: { hash: '#news', pathname: '/dashboard.html', search: '', href: '' }, history: { replaceState() {} } }
after(() => { delete globalThis.window })
const projekt = (id, name) => ({ projectId: id, projectName: name, features: { news: { state: 'off' } } })
async function lauf({ modell, projects = [projekt('p1', 'Muster')], dataset = {}, projectId = 'p1', einschalten } = {}) {
const calls = []
globalThis.fetch = async (url) => { calls.push(String(url)); return { ok: true, status: 200, json: async () => ({ ...(modell ? { modell } : {}), projects, features: { news: { priceLabel: '30 €' } }, consentText: 'Alt', setupEstimate: 'meist in 1–2 Wochen', ustHinweis: 'ohne Umsatzsteuer gemäß § 19 UStG' }) } }
const { root, stage } = htmlWurzel('#news-stage')
Object.assign(root.dataset, dataset)
await initNews(root, { projectId, einschalten })
return { root, stage, calls }
}
test('Abo: Seite ohne Preis, Haken und Checkout; der Ablauf bekommt den Platz mit key news', async () => {
const a = einschaltenAttrappe(); const { stage } = await lauf({ modell: 'abo', einschalten: a.fn })
assert.doesNotMatch(stage.innerHTML, KEIN_PREIS)
assert.doesNotMatch(stage.innerHTML, /news-consent|news-order|30 €/)
assert.deepEqual([a.aufrufe[0].key, a.aufrufe[0].projectId, a.aufrufe[0].viewAs], ['news', 'p1', false])
assert.ok(a.aufrufe[0].wurzel === stage.els['#news-einschalten'])
})
test('Abo: nach onFertig lädt die Pinnwand neu', async () => {
const a = einschaltenAttrappe(); const { calls } = await lauf({ modell: 'abo', einschalten: a.fn })
a.fertig(0); await new Promise((r) => setImmediate(r))
assert.equal(calls.filter((u) => u === '/api/funktionen').length, 2)
})
test('Abo ohne Funktion einschalten: Satz E4 im Platz', async () => {
const { stage } = await lauf({ modell: 'abo' })
assert.match(stage.els['#news-einschalten'].textContent, /Das Einschalten ist gerade nicht verfügbar\. Bitte lade die Seite neu oder schreib uns im Chat\./)
})
test('Ohne modell (fehlt, „Abo“): unverändert Preis, Haken und Bestellknopf; der Ablauf wird nie gerufen', async () => {
for (const modell of [undefined, 'Abo']) {
const a = einschaltenAttrappe(); const { stage } = await lauf({ modell, einschalten: a.fn })
assert.match(stage.innerHTML, /<strong>30 €<\/strong>/)
assert.match(stage.innerHTML, /id="news-order" disabled>Zahlungspflichtig bestellen · 30 €</)
assert.equal(a.aufrufe.length, 0)
}
})
test('wunschProjekt aus dem Behälter wählt das Projekt und wird entfernt', async () => {
const a = einschaltenAttrappe()
const { root } = await lauf({ modell: 'abo', projects: [projekt('p1', 'Muster'), projekt('p2', 'Muster 2')], dataset: { wunschProjekt: 'p2' }, projectId: '', einschalten: a.fn })
assert.equal(a.aufrufe[0].projectId, 'p2')
assert.equal(root.dataset.wunschProjekt, undefined)
})
- Step 2: Run test to verify it fails
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulNews.test.js
Expected: FAIL (die Seite zeigt „30 €“ und news-order).
- Step 3: Implement in
funktionen.js
initNews(root, { projectId, einschalten } = {}): Wunsch = projectId || root.dataset?.wunschProjekt || '', Attribut entfernen (root.dataset kann in Attrappen fehlen), selection.projectId setzen. modell aus der Antwort mitlesen. Bei modell === 'abo' und Zustand off statt renderBooking eine neue Funktion renderBookingAbo(stage, project, { einschalten, neuLaden }): stage.innerHTML = '<div id="news-einschalten"></div>'; ist einschalten eine Funktion, wird sie mit dem Platz und { key: 'news', projectId, viewAs: false, onFertig } aufgerufen (onFertig ruft neuLaden() einmal; neuLaden ist () => initNews(root, { einschalten })), sonst steht E4 im Platz (textContent). Kein Import, kein import(.
- Step 4: Run tests to verify they pass
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test --test-concurrency=1 test/kundenansichtNeuModulNews.test.js test/funktionenTiles.test.js test/funktionenZeitenTile.test.js test/funktionenRecht.test.js test/shopWiring.test.js
Expected: PASS.
- Step 5: Commit
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git add public/funktionen.js test/kundenansichtNeuModulNews.test.js && git commit -m "News: im Abo-Modell Einschalten statt Bestellen, Projektwahl über dataset" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 5: Webshop im Abo-Modell
Files:
- Modify:
public/shop.js(initShop:64,starte:69-74,zeigeAnfrage:195,zeigeBestellseite:253, Import:18),public/shop-anfrage.js(renderAnfrageFormular:37,:53-54,:67-68,:91-92,:115-120) - Test:
test/kundenansichtNeuModulShop.test.js; Pintest/shopWiring.test.js:31
Interfaces:
-
Consumes:
renderEinschaltSeite(Task 2),meldeReiter(Task 1), Testhilfen; V5-Feldermodell,kundenartder Antwort vonGET /api/funktionen. -
Produces:
initShop(root, { projectId, einschalten } = {});renderAnfrageFormular(root, { ..., modell }). Im Abo-Modell sendet der Körperanfrageohneunternehmerund mitkeineAusgeschlossenen: true. Kundenart: nur'unternehmen'darf anfragen, alles andere ('privat', leer, fehlend) gilt als privat (E4). -
Step 1: Pin anpassen und failing test
In test/shopWiring.test.js:31 die Regex auf /export function initShop\(root, \{ projectId, einschalten \} = \{\}\)/ ändern. Test test/kundenansichtNeuModulShop.test.js, Kopf:
const ANFRAGE = { warenart: 'kleidung', beschreibung: 'Handgemachte Mützen und Schals.', artikelzahl: 'bis20', versand: true, abholung: false, unternehmer: true, keineAusgeschlossenen: true, nachricht: '' }
const funk = (state, kopf = {}) => ({ projects: [{ projectId: 'p1', projectName: 'Mein Laden', features: { shop: { state } } }], features: { shop: { priceLabel: '299 €', consent: { version: 'v', knopf: 'Zahlungspflichtig bestellen · 299 €', items: [] } } }, ustHinweis: 'einmalig, ohne Umsatzsteuer gemäß § 19 UStG', legal: { shop: {} }, shop: { verfuegbar: true }, ...kopf })
async function oeffne(fx, { kopf = {}, state = 'off', anfrage = null, einschalten } = {}) {
vergissAuswahl()
routen(fx, { 'GET /api/funktionen': { body: funk(state, kopf) }, 'GET /api/shop/status?projectId=p1': { body: { verfuegbar: true, state, ticketWoid: null, anfrage } }, 'POST /api/shop/anfrage': { body: { anfrage: {} } } })
const root = h('div'); document.body.append(root)
await initShop(root, { projectId: 'p1', einschalten })
await fx.ruhe()
return root
}
const ZUGESAGT = { status: 'angenommen', anfrage: ANFRAGE, at: '2026-10-05T10:30:00.000Z' }
test('Abo, Betrieb: Anfrage ohne Unternehmer-Haken, Satz S2 über dem Knopf, kein Preis; Körper ohne unternehmer', mitDom(async (fx) => {
const root = await oeffne(fx, { kopf: { modell: 'abo', kundenart: 'unternehmen' } })
assert.equal(fx.finde(root, '[data-feld="unternehmer"]'), null)
assert.equal(fx.finde(root, '[data-feld="keineAusgeschlossenen"]'), null)
const satz = fx.finde(root, '[data-satz="ausgeschlossen"]')
assert.equal(fx.text(satz), `Mit „Anfrage senden“ bestätigst du, dass du keine ausgeschlossenen Waren verkaufst (${AUSGESCHLOSSEN}).`)
const knoten = fx.alle(root, '*')
assert.ok(knoten.indexOf(satz) < knoten.indexOf(fx.finde(root, '[data-aktion="anfrage-senden"]')), 'Satz steht über dem Knopf')
assert.match(fx.text(fx.finde(root, '.sh-erklaerung')), /Im Wartungs-Abo enthalten\./)
assert.doesNotMatch(fx.text(fx.finde(root, '.sh-erklaerung')), KEIN_PREIS)
await fx.setze(fx.finde(root, '[data-feld="warenart"]'), 'kleidung')
await fx.setze(fx.finde(root, '[data-feld="beschreibung"]'), 'Handgemachte Mützen und Schals.')
await fx.setze(fx.finde(root, '[data-feld="artikelzahl"]'), 'bis20')
await fx.klick(fx.finde(root, '[data-feld="versand"]'))
await fx.klick(fx.finde(root, '[data-aktion="anfrage-senden"]'))
assert.deepEqual(fx.fetchLog.find((a) => a.method === 'POST').body, { projectId: 'p1', anfrage: { warenart: 'kleidung', beschreibung: 'Handgemachte Mützen und Schals.', artikelzahl: 'bis20', versand: true, abholung: false, keineAusgeschlossenen: true, nachricht: '' } })
}))
test('Abo, privat oder Kundenart leer/fehlend: Hinweis S3 mit Chat, nie das Formular, kein POST, leere Reiterliste', mitDom(async (fx) => {
const k = hoereAufKn(fx)
for (const kundenart of ['privat', '', undefined]) {
const root = await oeffne(fx, { kopf: { modell: 'abo', kundenart } })
assert.equal(fx.finde(root, 'form'), null, String(kundenart))
assert.match(fx.text(root), /Der Webshop ist für Betriebe/)
assert.match(fx.text(root), /Für Privatpersonen gibt es den Webshop leider nicht\. Stimmt das nicht\? Schreib uns im Chat\./)
assert.ok(fx.finde(root, 'button[data-view="chat"]'))
}
assert.equal(fx.fetchLog.filter((a) => a.method === 'POST').length, 0)
assert.deepEqual(k.reiter.at(-1).eintraege, [])
}))
test('Abo, privat mit bestehendem Shop (active): die fünf Reiter bleiben', mitDom(async (fx) => {
const k = hoereAufKn(fx)
await oeffne(fx, { kopf: { modell: 'abo', kundenart: 'privat' }, state: 'active' })
assert.equal(k.reiter.at(-1).eintraege.length, 5)
}))
test('Abo, Anfrage angenommen: Gruppe B1 und Ablauf key shop statt Bestellseite; onFertig lädt neu', mitDom(async (fx) => {
const a = einschaltenAttrappe()
const root = await oeffne(fx, { kopf: { modell: 'abo', kundenart: 'unternehmen' }, anfrage: ZUGESAGT, einschalten: a.fn })
assert.equal(a.aufrufe[0].key, 'shop')
assert.equal(fx.text(fx.finde(root, '.tm-bestellen-gruppe h4')), 'Deine Anfrage')
assert.equal(fx.finde(root, '[data-aktion="bestellen"]'), null)
a.fertig(0); await fx.ruhe()
assert.equal(fx.fetchLog.filter((x) => x.url === '/api/shop/status?projectId=p1').length, 2)
}))
test('Ohne modell (fehlt, „Abo“): Formular mit Unternehmer-Haken und 299 €, nach der Zusage die Bestellseite, der Ablauf wird nie gerufen', mitDom(async (fx) => {
for (const modell of [undefined, 'Abo']) {
const a = einschaltenAttrappe(); const kopf = modell ? { modell } : {}
const formular = await oeffne(fx, { kopf, einschalten: a.fn })
assert.ok(fx.finde(formular, '[data-feld="unternehmer"]'))
assert.match(fx.text(formular), /299 € einmalig/)
const zugesagt = await oeffne(fx, { kopf, anfrage: ZUGESAGT, einschalten: a.fn })
assert.ok(fx.finde(zugesagt, '[data-aktion="bestellen"]'))
assert.equal(a.aufrufe.length, 0)
}
}))
(AUSGESCHLOSSEN kommt aus ../public/shop-anfrage.js.)
- Step 2: Run test to verify it fails
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulShop.test.js
Expected: FAIL ([data-feld="unternehmer"] ist da).
- Step 3: Implement
shop-anfrage.js
Neue Option modell. Bei 'abo': Stichpunkte S1 statt Preiszeile, kein ustHinweis-Absatz, kein Feld unternehmer, statt des Haken-Feldes keineAusgeschlossenen ein p.sh-hinweis[data-satz="ausgeschlossen"] mit S2 direkt vor dem Absende-Knopf; der Körper lässt unternehmer weg und sendet keineAusgeschlossenen: true. Ohne modell bleibt alles wie bisher.
- Step 4: Implement
shop.js
initShop(root, { projectId, einschalten } = {}) reicht einschalten an starte (auch im Wiederholen-Rückruf) und legt es in s.einschalten. zeigeAnfrage: bei s.funktionen?.modell === 'abo' und s.funktionen.kundenart !== 'unternehmen' die infoKarte mit S3 und Chat statt Formular. Das Formular bekommt modell: s.funktionen?.modell. zeigeBestellseite: im Abo-Modell renderEinschaltSeite(wurzel, { ctx, daten, key: 'shop', zusammenfassung: zusammenfassungVon(daten.anfrage), einschalten: s.einschalten, nachEinschalten: () => ladeNeu(s) }) (der Gruppentitel B1 steht in zusammenfassungVon schon). Reiter wie in Task 1. Import renderEinschaltSeite zur bestehenden Importzeile von ./funktion-bestellen.js hinzufügen.
- Step 5: Run tests to verify they pass
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test --test-concurrency=1 test/kundenansichtNeuModulShop.test.js test/shopView.test.js test/shopWiring.test.js
Expected: PASS.
- Step 6: Commit
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git add public/shop.js public/shop-anfrage.js test/kundenansichtNeuModulShop.test.js test/shopWiring.test.js && git commit -m "Webshop: im Abo-Modell Anfrage ohne Unternehmer-Haken, Privat-Hinweis, Einschalten" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 6: Buchungskalender im Abo-Modell
Files:
- Modify:
public/buchungen.js(initBuchungen:32,starte:39-41,zeigeAnfrage:124-130, Import:8),public/buchungen-anfrage.js(anfrageListe:20,renderAnfrageFormular:33,:94-97) - Test:
test/kundenansichtNeuModulBuchungen.test.js; Pintest/unterkunftWiring.test.js:130
Interfaces:
-
Consumes:
renderEinschaltSeite(Task 2),s.modellundmeldeReiter(Task 1), Testhilfen. -
Produces:
initBuchungen(root, { projectId, einschalten } = {});renderAnfrageFormular(root, { ..., modell }); neuer ExportanfrageZeilen(a)inbuchungen-anfrage.js→[{ name, wert }](Zeilen der Liste, wieshop-anfrage.js;[]ohne Objekt);anfrageListenutzt sie. -
Step 1: Pin anpassen und failing test
test/unterkunftWiring.test.js:130 auf /export function initBuchungen\(root, \{ projectId, einschalten \} = \{\}\)/ ändern. Test test/kundenansichtNeuModulBuchungen.test.js, Kopf:
const ANFRAGE = { art: 'ferienwohnung', einheiten: 2, portale: ['booking'], nachricht: '' }
const ZUGESAGT = { status: 'angenommen', anfrage: ANFRAGE, at: '2026-10-05T10:30:00.000Z' }
async function oeffne(fx, { kopf = {}, anfrage = null, einschalten } = {}) {
vergissAuswahl()
routen(fx, {
'GET /api/funktionen': { body: { projects: [{ projectId: 'p1', projectName: 'Haus am See', features: { unterkunft: { state: 'off' } } }], unterkunft: { verfuegbar: true }, ...kopf } },
'GET /api/unterkunft/status?projectId=p1': { body: { state: 'off', anfrage } },
'POST /api/unterkunft/anfrage-funktion?projectId=p1': { status: 201, body: { anfrage: {} } },
})
const root = h('div'); document.body.append(root)
await initBuchungen(root, { projectId: 'p1', einschalten })
await fx.ruhe()
return root
}
test('Abo: Anfrageformular nennt S1, kein Preis, sendet den Körper unverändert', mitDom(async (fx) => {
const root = await oeffne(fx, { kopf: { modell: 'abo' } })
const text = fx.text(fx.finde(root, '.sh-erklaerung'))
assert.match(text, /Im Wartungs-Abo enthalten\./)
assert.match(text, /Erst nach unserer Zusage kannst du einschalten\./)
assert.doesNotMatch(text, KEIN_PREIS)
await fx.setze(fx.finde(root, '[data-feld="art"]'), 'ferienwohnung')
await fx.setze(fx.finde(root, '[data-feld="einheiten"]'), '2')
await fx.klick(fx.finde(root, '[data-feld="portale.booking"]'))
await fx.klick(fx.finde(root, '[data-aktion="anfrage-senden"]'))
assert.deepEqual(fx.fetchLog.find((x) => x.method === 'POST').body, ANFRAGE)
}))
test('Abo, angenommen: Gruppe B1 und Ablauf key unterkunft; kein Haken, kein Wort „Rechnung“; onFertig lädt neu', mitDom(async (fx) => {
const a = einschaltenAttrappe()
const root = await oeffne(fx, { kopf: { modell: 'abo' }, anfrage: ZUGESAGT, einschalten: a.fn })
assert.equal(a.aufrufe[0].key, 'unterkunft')
assert.equal(fx.text(fx.finde(root, '.tm-bestellen-gruppe h4')), 'Deine Anfrage')
assert.match(fx.text(root), /Art der Unterkunft: Ferienwohnung oder Ferienhaus/)
assert.equal(fx.alle(root, 'input[type="checkbox"]').length, 0)
assert.doesNotMatch(fx.text(root), /Rechnung/)
a.fertig(0); await fx.ruhe()
assert.equal(fx.fetchLog.filter((x) => x.url === '/api/unterkunft/status?projectId=p1').length, 2)
}))
test('Ohne modell (fehlt, „Abo“): Formular mit „199 € einmalig“, angenommen zeigt „Wir melden uns bei dir“, der Ablauf wird nie gerufen', mitDom(async (fx) => {
for (const modell of [undefined, 'Abo']) {
const a = einschaltenAttrappe(); const kopf = modell ? { modell } : {}
assert.match(fx.text(await oeffne(fx, { kopf, einschalten: a.fn })), /199 € einmalig für den Einbau in deine Website\./)
assert.match(fx.text(await oeffne(fx, { kopf, anfrage: ZUGESAGT, einschalten: a.fn })), /Wir melden uns bei dir mit den nächsten Schritten zur Bestellung\./)
assert.equal(a.aufrufe.length, 0)
}
}))
- Step 2: Run test to verify it fails
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulBuchungen.test.js
Expected: FAIL.
- Step 3: Implement
buchungen-anfrage.js: anfrageZeilen herausziehen; im Abo-Modell erster und letzter Stichpunkt S1 statt ${preis} einmalig …. buchungen.js: einschalten in die Sitzung; zeigeAnfrage zeigt bei angenommen im Abo-Modell renderEinschaltSeite(wurzel, { ctx: { projectId, aktuell }, daten: { offen: [], kundenansicht: false }, key: 'unterkunft', zusammenfassung: () => [{ schritt: 'anfrage', titel: 'Deine Anfrage', zeilen: anfrageZeilen(ansicht.anfrage).map(({ name, wert }) => ${name}: ${wert}) }], einschalten: s.einschalten, nachEinschalten: () => zeigeProjekt(s) }). Das Rechnungsauftrag-Häkchen bleibt allein in buchungen-tab-einstellungen.js (unverändert). Import renderEinschaltSeite zur bestehenden Importzeile von ./funktion-bestellen.js hinzufügen.
- Step 4: Run tests to verify they pass
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test --test-concurrency=1 test/kundenansichtNeuModulBuchungen.test.js test/buchungenUi.test.js test/unterkunftWiring.test.js
Expected: PASS.
- Step 5: Commit
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git add public/buchungen.js public/buchungen-anfrage.js test/kundenansichtNeuModulBuchungen.test.js test/unterkunftWiring.test.js && git commit -m "Buchungskalender: im Abo-Modell Anfrage ohne Preis und Einschalten nach der Zusage" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 7: Öffnungszeiten live im Abo-Modell
Files:
- Modify:
public/zeiten.js(behaltenMoeglich:55,initZeiten:62-73,ladeListe:132,baueCtx:161,zeichneProjekt:173-232),public/zeiten-bestellen.js(Import:23, neue Funktion nach:105,zeitenStatusKarte:289) - Test:
test/kundenansichtNeuModulZeiten.test.js
Interfaces:
-
Consumes:
renderEinschaltSeite(Task 2; nur der benannte Import in der bestehenden Zeile:23), Testhilfen. -
Produces:
initZeiten(root, { projectId, einschalten } = {});s.modell,s.einschalten(auch inladeListe);baueCtxlieferteinschalten;renderZeitenEinschalten(root, { ctx, projekt })(Export vonzeiten-bestellen.js; ruftrenderEinschaltSeitemitkey: 'zeiten',daten: { offen: [], kundenansicht: ctx.kundenansicht },einschalten: ctx.einschalten,nachEinschalten: () => ctx.fertig({ danke: 'eingeschaltet' }); die Überschrift „Öffnungszeiten live einschalten“ kommt von P3);zeitenStatusKarte({ ..., modell }). Im Abo-Modell gibt es keinen Behalten-Weg: weder Knopf nochwunschWeg='behalten'nochzeigeBehalten. -
Step 1: Failing test
test/kundenansichtNeuModulZeiten.test.js
Kopf:
const EDITOR = { state: 'active', daten: null, version: 0, kundenansicht: false, vorschau: null, feiertage: [], googleText: '' } // Antwort von GET /api/zeiten/p1
const feature = (state, extra = {}) => ({ state, siteKey: null, ticketWoid: null, orderedAt: null, activatedAt: null, invoiceUrl: null, disabledReason: null, disabledAt: null, loeschungAm: null, unlockedBy: state === 'off' ? null : 'abo', aboEndeAt: null, einbau: null, ...extra })
const liste = (modell, state = 'off', extra = {}, angebot = {}) => ({ body: { ...(modell ? { modell } : {}), projects: [{ projectId: 'p1', projectName: 'Website p1', liveUrl: '', features: { news: { state: 'off' }, zeiten: feature(state, extra) }, zeitenStatus: null, zeitenLeer: false }], features: { zeiten: { priceLabel: '30 €', consent: { version: 'v', knopf: 'Zahlungspflichtig bestellen · 30 €', items: [] }, leistungsbeschreibung: { version: '2026-10-05', url: '/vertragsunterlagen/zeiten-leistungsbeschreibung.html' } } }, ustHinweis: 'ohne Umsatzsteuer gemäß § 19 UStG', zeiten: { verfuegbar: true, inklusive: true, grund: null, preis: '30 €', einschalten: { consent: { version: 'v', knopf: 'Einschalten', items: [] }, leistungsbeschreibung: { version: '2026-10-05', url: '/vertragsunterlagen/zeiten-leistungsbeschreibung.html' } }, belehrung: 'Belehrung', ...angebot } } })
async function oeffne(fx, antwort, { einschalten, dataset = {} } = {}) {
routen(fx, { 'GET /api/funktionen': antwort, 'GET /api/zeiten/p1': { body: EDITOR } })
const root = h('div'); document.body.append(root)
Object.assign(root.dataset, dataset)
await initZeiten(root, { projectId: 'p1', einschalten }); await fx.ruhe()
return root
}
const ALT_ENDE = { aboEndeAt: '2026-09-01T00:00:00.000Z' }, KEIN_ABO = { inklusive: false, grund: 'kein_abo' }
test('Abo, off: Einschalt-Seite mit key zeiten, ohne Haken, Preis und Behalten; nach onFertig die Danke-Zeile', mitDom(async (fx) => {
const a = einschaltenAttrappe(); let n = 0
const root = await oeffne(fx, () => (n === 0 ? liste('abo') : liste('abo', 'setup')), { einschalten: a.fn })
assert.deepEqual([a.aufrufe[0].key, a.aufrufe[0].projectId], ['zeiten', 'p1'])
assert.equal(fx.alle(root, 'input[type="checkbox"]').length, 0)
assert.equal(fx.finde(root, '[data-aktion="behalten"], [data-aktion="bestellen"], [data-aktion="einschalten"]'), null)
assert.doesNotMatch(fx.text(root), KEIN_PREIS)
n = 1; a.fertig(0); await fx.ruhe()
assert.match(fx.text(fx.finde(root, '.zt-status')), /Öffnungszeiten live ist eingeschaltet\./)
}))
test('Abo: Abo-Ende in active nennt die Frist (Z2) und „Eingeschaltet am“, ohne Preis und ohne Behalten-Knopf', mitDom(async (fx) => {
const karte = zeitenStatusKarte({ projekt: { projectId: 'p1', zeitenLeer: false }, feature: { state: 'active', orderedAt: '2026-08-01T10:00:00.000Z', ...ALT_ENDE }, angebot: { ...KEIN_ABO, preis: '30 €' }, modell: 'abo' }, {})
assert.match(fx.text(karte), /Dein Wartungs-Abo ist beendet\. Öffnungszeiten live läuft noch bis 01\.10\.2026 und schaltet sich dann ab\./)
assert.match(fx.text(karte), /Eingeschaltet am\s*01\.08\.2026/)
assert.equal(fx.finde(karte, '[data-aktion="behalten"]'), null)
assert.doesNotMatch(fx.text(karte), KEIN_PREIS)
}))
test('Abo: abgeschaltet mit Daten (Z3), ohne Behalten-Knopf', mitDom(async (fx) => {
const karte = zeitenStatusKarte({ projekt: { projectId: 'p1', zeitenLeer: false }, feature: { state: 'disabled', disabledReason: 'abo_ende', loeschungAm: '2099-12-31' }, angebot: { ...KEIN_ABO, preis: '30 €' }, modell: 'abo' }, {})
assert.match(fx.text(karte), /Deine Eingaben bleiben bis 31\.12\.2099 gespeichert\. Mit einem neuen Wartungs-Abo schaltet sich Öffnungszeiten live wieder ein\./)
assert.equal(fx.finde(karte, '[data-aktion="behalten"]'), null)
assert.doesNotMatch(fx.text(karte), KEIN_PREIS)
}))
test('wunschWeg „behalten“: im Abo-Modell ohne Wirkung, ohne modell die Behalten-Seite und der Knopf „Für 30 € behalten“', mitDom(async (fx) => {
const wunsch = { dataset: { wunschWeg: 'behalten' } }
const abo = await oeffne(fx, liste('abo', 'active', ALT_ENDE, KEIN_ABO), wunsch)
assert.equal(fx.finde(abo, '[data-variante="behalten"]'), null)
assert.equal(fx.finde(abo, '[data-aktion="behalten"]'), null)
const alt = await oeffne(fx, liste(undefined, 'active', ALT_ENDE, KEIN_ABO), wunsch)
assert.ok(fx.finde(alt, '[data-variante="behalten"]'))
const karte = zeitenStatusKarte({ projekt: { projectId: 'p1', zeitenLeer: false }, feature: { state: 'active', ...ALT_ENDE }, angebot: { ...KEIN_ABO, preis: '30 €' } }, {})
assert.equal(fx.text(fx.finde(karte, '[data-aktion="behalten"]')), 'Für 30 € behalten')
}))
- Step 2: Run test to verify it fails
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulZeiten.test.js
Expected: FAIL.
- Step 3: Implement
zeiten-bestellen.js
renderZeitenEinschalten nach renderZeitenBestellen. zeitenStatusKarte: bei modell === 'abo' der Block „Abo-Ende“ (frist && lage === 'bezahlen') als Info-Box mit Z2 (Frist aus behaltenFrist), der Block „abgeschaltet mit Daten“ (daten && lage === 'bezahlen') als Info-Box mit Z3 (tagText); die Fälle lage === 'inklusive' | 'abo' bleiben; Faktenzeile E5. Keine Preis-, Behalten- oder Rechnungs-Elemente.
- Step 4: Implement
zeiten.js
initZeiten(root, { projectId, einschalten } = {}); s.modell (auch in ladeListe), s.einschalten; baueCtx ergänzt einschalten; zeichneProjekt: Zustand off im Abo-Modell ruft renderZeitenEinschalten(kopf, { ctx, projekt }); der Aufruf von zeitenStatusKarte übergibt modell; behaltenMoeglich(projekt, angebot, modell) liefert im Abo-Modell false. Keine neue Importzeile: renderZeitenEinschalten kommt in die benannten Importe von ./zeiten-bestellen.js.
- Step 5: Run tests to verify they pass
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test --test-concurrency=1 test/kundenansichtNeuModulZeiten.test.js test/zeitenView.test.js test/zeitenBestellen.test.js test/zeitenEditor.test.js
Expected: PASS.
- Step 6: Commit
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git add public/zeiten.js public/zeiten-bestellen.js test/kundenansichtNeuModulZeiten.test.js && git commit -m "Öffnungszeiten live: im Abo-Modell Einschalten und kein Behalten-Weg" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 8: Analyse im Abo-Modell
Files:
- Modify:
public/analyse.js(initAnalyse:22-49,renderAnalyseActivation:76-112,mountCockpit:174-328; neue relative Importzeile./fn-reiter.js) - Test:
test/kundenansichtNeuModulAnalyse.test.js
Interfaces:
-
Consumes:
htmlWurzel('#an-stage')(mitan,setAttribute,kn.reiter),KEIN_PREIS,meldeReiter,reiterEintraege(Task 1); Antwortfeldmodell. -
Produces:
renderAnalyseActivation(stage, project, reload, { modell } = {})(vierter Parameter optional); im Abo-Modell Linkzeile A1 statt des langen Häkchen-Satzes, ein Häkchen#an-zielgruppemit A2 (sperrt den Knopf#an-onwie#an-avv, zusätzlich beicontroller.ok !== true); Text A3 beikein_abo. Der Körper vonPOST /api/funktionen/analyse/activateist im Abo-Modell{ projectId, accentColor, consent: { accepted: true, avvVersion }, audienceConfirmed: true }(VF4; der heutige Server ignoriert das Feld).root.dataset.wunschProjektwählt das Projekt (V2) und wird entfernt. -
Reiter in der Etage (Spec 5.2 und 5.4, VF2): Im Abo-Modell zeichnet
initAnalysedie LeistetabsHtmlnicht (keinrole="tablist", kein.an-tabbtn); das Panel#an-stageträgtrole="tabpanel"undaria-labelmit dem Label des aktiven Reiters stattaria-labelledby.mountCockpit({ …, abo, root })meldet inshowTab()nachpaintTabs()meldeReiter(root, { eintraege: reiterEintraege('analyse', ANALYSE_REITER), aktiv: tabNow(), bei: pickTab })mitANALYSE_REITER = [{ key: 'uebersicht', label: 'Übersicht' }, { key: 'heatmap', label: 'Heatmap' }, { key: 'einstellungen', label: 'Einstellungen' }](Reihenfolge und Wortlaut wietabsHtml;paintTabssetzt im Abo-Modellaria-labelstattaria-labelledby). Jeder Zweig ohne Cockpit (Einschalten, abgeschaltet, nicht verfügbar, unbekannt, keine Website) meldet vor dem Zeichnen die leere Liste, damit die Etage nicht die Reiter der vorigen Ansicht behält; das Cockpit ersetzt sie gleich danach. Die Kopfleiste (Website wählen, Zeitraum) bleibt im Inhalt,analyse-cards.jsbleibt unverändert. Ohnemodell === 'abo'bleibt alles wie heute: LeistetabsHtml,aria-labelledby, kein Ereignis. -
Step 1: Failing test
test/kundenansichtNeuModulAnalyse.test.js
Kopf (nach funktionenTiles.test.js:405-428):
import { analyseSel } from '../public/analyse-view.js'
const { initAnalyse } = await import('../public/analyse.js')
globalThis.document = { querySelectorAll: () => [] } // pickTab ruft hideChartTip
after(() => { delete globalThis.document })
const LEGAL = { avv: '/rechtliches/avv-website-analyse-2026-10-02.html', lb: '/rechtliches/leistungsbeschreibung-website-analyse-2026-10-02.html' }
const CTRL = { ok: true, company: 'Muster GmbH', address: 'Weg 1, 12345 Ort', email: 'info@muster.de' }
const offProject = (id = 'p1', analyse = {}) => ({ projectId: id, projectName: `Muster ${id}`, analyse: { state: 'off', eligible: true, accentColor: '#111111', avvVersion: '2026-10-02', legal: LEGAL, controller: CTRL, ...analyse } })
const aktivProjekt = (id = 'p1') => offProject(id, { state: 'active' })
const KEIN_ABO = { eligible: false, reason: 'kein_abo', reasonText: 'Alter Satz des Servers' }
async function lauf({ modell, projects = [offProject()], dataset = {}, projectId = 'p1', tab = 'uebersicht' } = {}) {
const posts = []
globalThis.fetch = async (url, opts) => {
if (opts?.method === 'POST') posts.push({ url: String(url), body: JSON.parse(opts.body) })
if (String(url).startsWith('/api/funktionen/analyse/stats')) return { ok: false, status: 503, json: async () => ({}) } // das Cockpit zeigt die Fehlerkarte; mehr braucht der Reitertest nicht
return { ok: true, status: 200, json: async () => ({ ...(modell ? { modell } : {}), projects }) }
}
const { root, stage, kn } = htmlWurzel('#an-stage')
Object.assign(root.dataset, dataset)
analyseSel.tab = tab
await initAnalyse(root, { projectId })
return { root, stage, posts, kn }
}
test('Abo: Linkzeile A1 mit beiden Links, ein Häkchen mit A2, kein langer Zustimmungssatz, keine Preise', async () => {
const { stage } = await lauf({ modell: 'abo' })
const html = stage.innerHTML
assert.match(html, /Es gelten <a href="[^"]*"[^>]*>Leistungsbeschreibung<\/a> und <a href="[^"]*"[^>]*>Auftragsverarbeitung<\/a> \(Fassung 2026-10-02\)\./)
assert.ok(html.includes(`href="${LEGAL.lb}"`) && html.includes(`href="${LEGAL.avv}"`))
assert.match(html, /Meine Website richtet sich nicht gezielt an Kinder unter 16 Jahren\./)
assert.doesNotMatch(html, /Ich beauftrage WEBklar|id="an-avv"/)
assert.doesNotMatch(html, KEIN_PREIS)
})
test('Abo: Knopf gesperrt ohne Zielgruppen-Häkchen und bei unvollständigem Verantwortlichen; Körper mit audienceConfirmed', async () => {
const { stage, posts } = await lauf({ modell: 'abo' })
const box = stage.els['#an-zielgruppe']; const knopf = stage.els['#an-on']
box.checked = true; box.handlers.change(); assert.equal(knopf.disabled, false)
box.checked = false; box.handlers.change(); assert.equal(knopf.disabled, true)
box.checked = true; box.handlers.change(); await knopf.handlers.click()
assert.deepEqual(posts, [{ url: '/api/funktionen/analyse/activate', body: { projectId: 'p1', accentColor: '#111111', consent: { accepted: true, avvVersion: '2026-10-02' }, audienceConfirmed: true } }])
const ohne = await lauf({ modell: 'abo', projects: [offProject('p1', { controller: { ok: false, company: '', address: '', email: '', missing: ['firma'] } })] })
ohne.stage.els['#an-zielgruppe'].checked = true; ohne.stage.els['#an-zielgruppe'].handlers.change()
assert.equal(ohne.stage.els['#an-on'].disabled, true)
})
test('Abo: kein_abo nennt Hosting- und Wartungs-Abo mit Link zu Verträge (A3), nicht den Satz des Servers', async () => {
const { stage } = await lauf({ modell: 'abo', projects: [offProject('p1', KEIN_ABO)] })
assert.match(stage.innerHTML, /Die Analyse ist im Hosting- und im Wartungs-Abo enthalten\./)
assert.match(stage.innerHTML, /data-view="vertraege"/)
assert.doesNotMatch(stage.innerHTML, /Alter Satz des Servers/)
})
test('Ohne modell (fehlt, „Abo“): der lange Zustimmungssatz mit #an-avv und bei kein_abo der Satz des Servers', async () => {
for (const modell of [undefined, 'Abo']) {
const a = await lauf({ modell })
assert.match(a.stage.innerHTML, /id="an-avv"/)
assert.match(a.stage.innerHTML, /Ich beauftrage WEBklar/)
const b = await lauf({ modell, projects: [offProject('p1', KEIN_ABO)] })
assert.match(b.stage.innerHTML, /Alter Satz des Servers/)
}
})
test('wunschProjekt aus dem Behälter wählt das Projekt und wird entfernt', async () => {
const { root, stage } = await lauf({ modell: 'abo', projects: [offProject('p1'), offProject('p2')], dataset: { wunschProjekt: 'p2' }, projectId: '' })
assert.match(stage.innerHTML, /Analyse für Muster p2 einschalten/)
assert.equal(root.dataset.wunschProjekt, undefined)
})
test('Abo, aktive Analyse: keine eigene Leiste, kn:reiter mit den drei Reitern, Panel mit aria-label; bei() wechselt und meldet den neuen aktiven Reiter', async () => {
assert.equal(typeof CustomEvent, 'function', 'meldeReiter braucht CustomEvent (Node 20: global)')
const { root, stage, kn } = await lauf({ modell: 'abo', projects: [aktivProjekt()] })
assert.doesNotMatch(root.innerHTML, /role="tablist"|an-tabbtn|aria-labelledby/)
assert.match(root.innerHTML, /id="an-stage" role="tabpanel" aria-label="Übersicht"/)
const erst = kn.reiter.at(-1)
assert.deepEqual(erst.eintraege, [{ key: 'uebersicht', label: 'Übersicht' }, { key: 'heatmap', label: 'Heatmap' }, { key: 'einstellungen', label: 'Einstellungen' }])
assert.equal(erst.aktiv, 'uebersicht')
erst.bei('heatmap'); await new Promise((r) => setImmediate(r))
assert.equal(kn.reiter.at(-1).aktiv, 'heatmap')
assert.equal(stage.attribute['aria-label'], 'Heatmap')
assert.equal(analyseSel.tab, 'heatmap')
})
test('Abo ohne Cockpit (Einschalten, abgeschaltet, nicht verfügbar, keine Website): leere Reiterliste, „Zurück“ füllt die Zeile', async () => {
for (const projects of [[offProject()], [offProject('p1', { state: 'disabled' })], [offProject('p1', KEIN_ABO)], []]) {
const { kn } = await lauf({ modell: 'abo', projects })
assert.deepEqual(kn.reiter.at(-1)?.eintraege, [], JSON.stringify(projects.map((p) => p.analyse.state)))
}
})
test('Ohne modell (fehlt, „Abo“): eigene Leiste mit denselben drei Labels, aria-labelledby, kein kn:reiter', async () => {
for (const modell of [undefined, 'Abo']) {
const { root, kn } = await lauf({ modell, projects: [aktivProjekt()] })
assert.deepEqual([...root.innerHTML.matchAll(/role="tab"[^>]*>([^<]+)</g)].map((m) => m[1]), ['Übersicht', 'Heatmap', 'Einstellungen'])
assert.match(root.innerHTML, /role="tablist"/)
assert.match(root.innerHTML, /id="an-stage" role="tabpanel" aria-labelledby="an-tab-uebersicht"/)
assert.equal(kn.reiter.length, 0)
}
})
- Step 2: Run test to verify it fails
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulAnalyse.test.js
Expected: FAIL.
- Step 3: Implement in
analyse.js
initAnalyse(root, { projectId } = {}) liest root.dataset?.wunschProjekt und modell und reicht { modell } an renderAnalyseActivation; die Texte A1–A3 über kleine Konstanten; die Abbildung im Abo-Modell ersetzt nur den <label class="fn-consent">-Block, den Körper der Aktivierung und den kein_abo-Satz, alles andere (Farbwahl, Banner-Vorschau, Verantwortlicher) bleibt. a.legal bleibt die Quelle der Links (Pin avvPage.test.js:28-34), kein confirm(.
Reiter: neue Importzeile import { meldeReiter, reiterEintraege } from './fn-reiter.js' und die Konstante ANALYSE_REITER neben TABS. initAnalyse liest abo = modell === 'abo', meldet bei abo vor dem Zeichnen die leere Liste (jeder Zweig, auch die frühe Rückkehr ohne nutzbare Website), lässt tabsHtml weg und setzt das Panel-Attribut. mountCockpit bekommt abo und root; showTab() ruft nach paintTabs() eine Closure melde() (abo && meldeReiter(root, …), bei: pickTab). Klick- und Tastatur-Handler bleiben unverändert: der Teaser „Heatmap“ der Übersicht ruft ebenfalls pickTab, showTab() meldet dann den neuen aktiven Reiter an die Etage.
- Step 4: Run tests to verify they pass
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test --test-concurrency=1 test/kundenansichtNeuModulAnalyse.test.js test/analyseView.test.js test/avvPage.test.js test/funktionenTiles.test.js
Expected: PASS.
- Step 5: Commit
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git add public/analyse.js test/kundenansichtNeuModulAnalyse.test.js && git commit -m "Analyse: im Abo-Modell Linkzeile und Zielgruppen-Häkchen, Hosting berechtigt, Reiter in der Etage" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 9: module.css Gerüst, Prüftest und gemeinsame Bausteine
Files:
- Modify:
public/neu/module.css(Stub von P1) - Test:
test/kundenansichtNeuModulCss.test.js
Interfaces:
- Consumes:
public/neu/tokens.cssund die übrigenpublic/neu/*.css(P1–P3, nur lesen); alte CSS-Dateien inpublic/als Klassenquelle. - Produces:
module.cssmit Abschnitt 0 (Alt-Klassen, Fokus, Handy, reduzierte Bewegung). Der Prüftest hat die ListeFERTIG(Präfixe, für die schon die Abdeckung gilt); Task 10–13 hängen ihre Präfixe an.
Gestaltungsregeln (gelten für Task 9–13; alle Selektoren beginnen mit .kn ):
| Altes Muster | Neu |
|---|---|
Karte (.kb-panel, *-abschnitt, *-info, fn-booking) |
Abschnitt: ohne Fläche, Rahmen, Radius; padding: 20px 0; ab dem zweiten Abschnitt obere Haarlinie #ebeef1; h3 15px/600 |
Hinweis (tm-box, sh-box, -info, -warn) |
Zeile #f8f9fb, Radius 6px, 14px, ohne Rahmen; -warn: Fläche #fcedb9, Schrift #a82c00 |
Meldung (tm-meldung-ok/-fehler, sh-meldung-*) |
grüne Pille #d7f7c2/#006908, rote #ffe7f2/#b3093c; Feldfehler Schrift #c0123c |
Reiterleisten (an-tabs, tm-reiterleiste, sh-reiterleiste) |
Unterstrich-Reiter: Flex, overflow-x: auto, untere Haarlinie; Reiter 14px/600 #5e6678, aktiv #111 mit 2px Unterstrich #111; Handy: 44px hoch. Die Leisten von Terminplaner, Webshop, Buchungskalender und Analyse zeichnet im Abo-Modell die Etage; ihre Klassen bleiben trotzdem gestaltet (Abdeckungstest, die alte Ansicht nutzt sie weiter) |
Segment, Chip (tm-seg, tm-chip) |
Ring-Schatten, aktiv Fläche #111 und Schrift #fff |
.kb-btn-primary, .abo-btn |
Fläche #111, Schrift #fff, Radius 6px, 36px (Handy 44px), :disabled 40 % Deckkraft |
.kb-btn-outline, .kb-btn |
Fläche #fff, Ring-Schatten 0 0 0 1px rgba(60, 66, 87, .16), 0 1px 1px rgba(0, 0, 0, .06), hover #f6f8fa |
Pillen (tm-pille-*, sh-pille-*, fn-pill-*) |
ohne Rand: wait/setup orange (#fcedb9/#a82c00), ok/on grün, off grau (#ebeef1/#414552), bad rot, Info blau (#e0f2fe/#075985) |
| Felder (input, select, textarea) | Fläche #fff, Ring-Schatten, Radius 6px, 36px (Handy 44px); Fokus siehe Abschnitt 0 |
Listen und Kacheln (tm-eintrag, sh-artikel, bk-zeile) |
Zeilen mit Haarlinie, kein Kasten; Hover #f6f8fa |
Rückfrage (tm-frage-box, sh-frage-box) |
Fläche #f8f9fb, Radius 12px, kein Rahmen |
| Belegt/frei/angefragt/gesperrt (Kalender) | belegt #111 mit #fff, angefragt orange Pille, gesperrt grau Pille, frei #fff mit Haarlinie |
- Step 1: Failing test
test/kundenansichtNeuModulCss.test.js
Hilfen im Testfile: lies(datei) (liest aus public/), regeln(css): { selektor, koerper }[] (Kommentare entfernt, verschachtelte @media-Regeln flach, @keyframes ohne Schritte), selektoren(css): string[] (Kommalisten aufgespalten, Klammer-bewusst für :is() und :not()), klassen(css, praefix): Set<string> (aus den Selektoren), neuCss() = Inhalt aller public/neu/*.css, MODUL() = public/neu/module.css, jsModule() = Inhalt aller public/*.js und public/neu/*.js.
const FERTIG = [] // Präfixe mit geprüfter Abdeckung; Task 10–13 ergänzen sie
const UNGENUTZT = new Set([]) // alte Klassen, die in keiner JS-Datei vorkommen; der Test prüft das
const ALT = { tm: 'termine.css', sh: 'shop.css', bk: 'buchungen.css', zt: 'zeiten.css', an: 'analyse.css', hs: 'analyse.css', nw: 'funktionen.css' }
const FN_NEWS = ['fn-project', 'fn-booking', 'fn-price', 'fn-estimate', 'fn-desc-link', 'fn-consent', 'fn-thanks', 'fn-status', 'fn-facts', 'fn-pill', 'fn-pill-setup']
const ALT_KLASSEN = ['kb-panel', 'kb-btn', 'kb-btn-primary', 'kb-btn-outline', 'kb-empty', 'kb-link', 'kb-checks', 'abo-actions', 'abo-btn', 'hidden', 'muted', 'error']
const PALETTE = new Set(['#fff', '#ffffff', '#1a1f36', '#5e6678', '#8a93a3', '#ebeef1', '#d8dde3', '#f6f8fa', '#f8f9fb', '#111', '#111111', '#414552', '#e0f2fe', '#075985', '#fcedb9', '#a82c00', '#efeafe', '#5b21b6', '#d7f7c2', '#006908', '#ffe7f2', '#b3093c', '#c0123c'])
const ZETTEL = /#fff6d6|#ddeffb|#ffe1d9/i
const RING = new Set(['60,66,87,.16', '60,66,87,0.16', '0,0,0,.06', '0,0,0,0.06'])
const HANDY = '@media (max-width: 880px), (pointer: coarse) and (max-height: 500px)'
test('module.css: jede Regel steht unter .kn und hängt nicht an der alten Seite', () => {
for (const s of selektoren(MODUL())) assert.match(s, /^\.kn(\s|[.:#[>~+]|$)/, s)
assert.doesNotMatch(MODUL(), /#view-|--kb-|\.kb-page/)
})
test('module.css: Verbote (Lila, Teal, Verlauf, Glas, Dunkel, Gewicht 800, Animation)', () => {
assert.doesNotMatch(MODUL(), /#9d3dfe|#d4b4ff|157,\s*61,\s*254|#2dd4bf|#5eead4|gradient|backdrop-filter|color-scheme:\s*dark|prefers-color-scheme|font-weight:\s*(800|900)|@keyframes/)
})
test('module.css: Farben nur aus der Palette der Übersicht; Zettelfarben nur in .nw-paper-*; rgba nur der Ring-Schatten', () => {
for (const { selektor, koerper } of regeln(MODUL())) {
for (const [, hex] of koerper.matchAll(/(#[0-9a-fA-F]{3,8})\b/g)) assert.ok(PALETTE.has(hex.toLowerCase()) || (ZETTEL.test(hex) && /\.nw-paper-/.test(selektor)), `${selektor}: ${hex}`)
for (const [, wert] of koerper.matchAll(/rgba?\(([^)]*)\)/g)) assert.ok(RING.has(wert.replace(/\s/g, '')), `${selektor}: rgba(${wert})`)
}
})
test('module.css: Fokus schwarz, Handy-Regeln wörtlich mit 44px, reduzierte Bewegung', () => {
const css = MODUL()
assert.match(css, /:focus-visible[^{]*\{[^}]*outline:\s*2px solid (#111|var\(--kn-[a-z-]+\))/)
assert.ok(css.includes(HANDY) && /min-height:\s*44px/.test(css.slice(css.indexOf(HANDY))))
assert.match(css, /prefers-reduced-motion:\s*reduce/)
})
test('module.css: jedes var(--kn-…) ist in public/neu/*.css definiert', () => {
const definiert = new Set([...neuCss().matchAll(/(--kn-[a-z0-9-]+)\s*:/g)].map((m) => m[1]))
for (const [, name] of MODUL().matchAll(/var\((--kn-[a-z0-9-]+)/g)) assert.ok(definiert.has(name), name)
})
test('Alt-Klassen der Module sind unter .kn gestaltet (in einer Datei von public/neu)', () => {
const sel = selektoren(neuCss()).filter((s) => s.startsWith('.kn'))
for (const k of ALT_KLASSEN) assert.ok(sel.some((s) => new RegExp(`\\.${k}(?![a-z0-9-])`).test(s)), k)
})
test('UNGENUTZT enthält nur Klassen, die kein JS nennt', () => {
for (const k of UNGENUTZT) assert.ok(!jsModule().includes(k), k)
})
for (const praefix of FERTIG) test(`Abdeckung ${praefix}: jede Klasse der alten CSS-Datei ist unter .kn gestaltet`, () => {
const alt = praefix === 'fn' ? FN_NEWS : [...klassen(lies(ALT[praefix]), praefix)]
const neu = klassen(neuCss(), praefix)
assert.deepEqual(alt.filter((k) => !neu.has(k) && !UNGENUTZT.has(k)).sort(), [])
})
Für das Präfix fn gilt nur die Liste FN_NEWS; die Kachel-Klassen (fn-tile*, fn-tiles, fn-count, fn-ico) gehören P3. In ALT steht kein Eintrag für fn.
- Step 2: Run test to verify it fails
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulCss.test.js
Expected: FAIL (Fokus-Regel, Handy-Block und Alt-Klassen fehlen im Stub von P1).
- Step 3: Abschnitt 0 in
public/neu/module.cssschreiben
Kopfkommentar mit der Abschnittsliste (0 gemeinsame Bausteine, 1 Terminplaner, 2 Webshop und Buchungen, 3 Öffnungszeiten, 4 News, 5 Analyse). Abschnitt 0: die Alt-Klassen aus ALT_KLASSEN nach den Gestaltungsregeln, soweit noch nicht in einer anderen Datei von public/neu/ unter .kn definiert (command grep -a -n "kb-panel" public/neu/*.css je Klasse; keine Doppeldefinition, VF3); Fokus-Regel für button, input, select, textarea, summary, a mit outline: 2px solid #111; outline-offset: 2px; der Block HANDY mit min-height: 44px für Knöpfe und Felder; @media (prefers-reduced-motion: reduce) setzt transition: none. Token-Namen (var(--kn-…)) nur aus tokens.css.
- Step 4: Run test to verify it passes
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulCss.test.js
Expected: PASS (Abdeckungstests laufen noch nicht, FERTIG ist leer).
- Step 5: Commit
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git add public/neu/module.css test/kundenansichtNeuModulCss.test.js && git commit -m "module.css: Gerüst, Prüftest und gemeinsame Bausteine unter .kn" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 10: module.css Terminplaner (tm-)
Files:
- Modify:
public/neu/module.css(Abschnitt 1),test/kundenansichtNeuModulCss.test.js(FERTIG)
Interfaces:
-
Consumes: Gestaltungsregeln aus Task 9; alte Klassenquelle
public/termine.css(auch dietm--Bausteine von Shop, Buchungen und Öffnungszeiten). -
Produces: Abdeckung
tm. -
Step 1: Präfix in den Test aufnehmen
In test/kundenansichtNeuModulCss.test.js FERTIG = ['tm'].
- Step 2: Run test to verify it fails
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulCss.test.js
Expected: FAIL; „Abdeckung tm“ listet die fehlenden Klassen.
- Step 3: Abschnitt 1 schreiben
Alle tm--Klassen der Liste. Entscheidungen, die die Regeltabelle nicht abdeckt:
-
Fortschrittsleiste (
tm-schritte,tm-schritt*): Zahlenkreis 28px, erledigt#111mit#fff, aktuell Ring#111; am Handy zeigttm-schritt-standden Zähler („Schritt n von 8“) undtm-schritt-titelist nur noch für Screenreader da (visuell versteckt). -
Bestellseite (
tm-bestellen*) undtm-einschalten: Abschnitte ohne Kasten;tm-rechtstextbleibt ein scrollbarer Bereich (max-height,overflow-y: auto,white-space: pre-wrap, Fläche#f8f9fb);tm-einschalten-platzohne eigenes Aussehen (P3 gestaltet den Ablauf). -
Statuskarte (
tm-statuskarte): Abschnitt, Pille oben, Fakten als zweispaltige Liste (dt#5e6678). -
Terminliste und Editoren (
tm-eintrag,tm-editor-box,tm-formular-box): Zeilen mit Haarlinie, Editor klappt darunter ohne Rahmen auf. -
tm-srbleibt als Screenreader-Klasse unverändert. -
Step 4: Run test to verify it passes
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulCss.test.js
Expected: PASS.
- Step 5: Commit
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git add public/neu/module.css test/kundenansichtNeuModulCss.test.js && git commit -m "module.css: Terminplaner und gemeinsame Bestellbausteine (tm-)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 11: module.css Webshop und Buchungskalender (sh-, bk-)
Files:
- Modify:
public/neu/module.css(Abschnitt 2),test/kundenansichtNeuModulCss.test.js
Interfaces:
-
Consumes: Gestaltungsregeln; Klassenquellen
public/shop.css,public/buchungen.css. -
Produces: Abdeckung
shundbk. -
Step 1:
FERTIG = ['tm', 'sh', 'bk'] -
Step 2: Run test to verify it fails
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulCss.test.js
Expected: FAIL; „Abdeckung sh“ und „Abdeckung bk“ listen die fehlenden Klassen.
-
Step 3: Abschnitt 2 schreiben
-
Shop:
sh-reiterleisteundsh-reiterwiean-tabs(der Buchungskalender nutzt sie mit); Anfrage (sh-anfrage,sh-form,sh-erklaerung,sh-gruppe): Formular einspaltig, Stichpunkte der Erklärung#5e6678;sh-hinweis[data-satz](Satz über dem Knopf) 14px#1a1f36mit Abstand 12px zum Knopf; Zahlentabular-nums. -
Buchungen:
bk-zeileals Listenzeile mit Haarlinie;bk-posten-*am Handy zwei Spalten (wie die alte Regel); Kalender nach der Zeile „belegt/frei/angefragt/gesperrt“ der Tabelle. -
Artikelbilder und Vorschauen behalten ihre Seitenverhältnisse, der Hintergrund ist
#f6f8fa. -
Step 4: Run test to verify it passes
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulCss.test.js
Expected: PASS.
- Step 5: Commit
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git add public/neu/module.css test/kundenansichtNeuModulCss.test.js && git commit -m "module.css: Webshop und Buchungskalender (sh-, bk-)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 12: module.css Öffnungszeiten und News (zt-, fn-, nw-)
Files:
- Modify:
public/neu/module.css(Abschnitte 3 und 4),test/kundenansichtNeuModulCss.test.js
Interfaces:
-
Consumes: Gestaltungsregeln; Klassenquellen
public/zeiten.css,public/funktionen.css(nurFN_NEWSundnw-). -
Produces: Abdeckung
zt,fn(ListeFN_NEWS),nw. -
Step 1:
FERTIG = ['tm', 'sh', 'bk', 'zt', 'fn', 'nw'] -
Step 2: Run test to verify it fails
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulCss.test.js
Expected: FAIL; „Abdeckung zt“, „fn“, „nw“ listen die fehlenden Klassen.
-
Step 3: Abschnitte 3 und 4 schreiben
-
Öffnungszeiten (
zt-*): Editor als Tageszeilen mit Haarlinie,zt-hinweisundzt-statusals Abschnitt; Zeitfelder 44px am Handy. -
News:
fn-booking,fn-status,fn-factsals Abschnitt;fn-thanksgrüne Pille;fn-pill-setuporange Pille;nw--Formular einspaltig mit Haarlinien. Vorschau-Zettel (nw-paper,nw-polaroid) behalten Drehung und Papierfarben (.nw-paper-sun|sky|coral|whitesetzen--paperauf#fff6d6,#ddeffb,#ffe1d9,#fff); der Schatten wird der Ring-Schatten; Auswahl der Zettelfarbe (nw-sw[aria-pressed='true']) mit Außenlinie#111. -
Step 4: Run test to verify it passes
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulCss.test.js
Expected: PASS.
- Step 5: Commit
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git add public/neu/module.css test/kundenansichtNeuModulCss.test.js && git commit -m "module.css: Öffnungszeiten und News (zt-, fn-, nw-)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 13: module.css Analyse (an-, hs-)
Files:
- Modify:
public/neu/module.css(Abschnitt 5),test/kundenansichtNeuModulCss.test.js
Interfaces:
-
Consumes: Gestaltungsregeln; Klassenquelle
public/analyse.css;analyse-cards.js(Klassenvertrag im Kopf der Datei). -
Produces: Abdeckung
anundhs. Die Analyse bleibt über.anmit eigenen Variablen gebaut; sie werden hell belegt. -
Step 1:
FERTIG = ['tm', 'sh', 'bk', 'zt', 'fn', 'nw', 'an', 'hs'] -
Step 2: Run test to verify it fails
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulCss.test.js
Expected: FAIL; „Abdeckung an“ und „hs“ listen die fehlenden Klassen.
- Step 3: Abschnitt 5 schreiben
.kn .an belegt die Variablen neu: --an-fg: #1a1f36, --an-fg2 und --an-muted: #5e6678, --an-faint: #8a93a3 (nur für Nebensächliches), --an-line: #ebeef1, --an-line2: #d8dde3, --an-card: #fff, --an-acc und --an-acc-strong: #111, --an-acc-soft: #f6f8fa, --an-on-acc: #fff, --an-up-bg: #d7f7c2 (Schrift #006908), --an-down: #b3093c, --an-down-bg: #ffe7f2, --an-prev: #8a93a3, --an-warn: #a82c00, --an-bar-fill: #ebeef1. Diagramme: Linie und Punkte #111, Fläche #f6f8fa, Vergleichslinie #8a93a3 gestrichelt, Balken #111 auf #ebeef1. Die Banner-Vorschau (an-bn*) bildet die Kundenseite ab: Fläche #fff mit Ring-Schatten; ihre Knopffarbe kommt inline aus dem Kunden-Akzent. Heatmap-Rahmen (hs-*) bekommen Fläche #f6f8fa; geraete.css bleibt unberührt. Keine color-scheme: dark, kein Verlauf in --an-bar-fill.
- Step 4: Run test to verify it passes
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulCss.test.js
Expected: PASS (alle Abdeckungen).
- Step 5: Commit
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git add public/neu/module.css test/kundenansichtNeuModulCss.test.js && git commit -m "module.css: Analyse hell (an-, hs-)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 14: Sichtprüfung mit dem Werkzeug aus P0 und Bild der Analyse
Files:
- Modify (Werkzeug-Git,
KW=/root/reports/kundenansicht-neu-2026-10-15/werkzeug):views.mjs,gates.mjs,fixtures/faelle.mjsundfixtures/index.mjs(nur Einträge für die neuen Fälle),fixtures/endpunkte.mjs(ein Import),vertrag-endpunkte.mjs(Einträge der zwei Analyse-Endpunkte); Create:fixtures/module.mjs(17 Fälle),fixtures/endpunkte-p4.mjs. Nicht angefasst werdentest/backend.test.mjs(Pin „Fälle in fester Reihenfolge“) undvorschau/boot.js(VF5). - Modify (Portal-Worktree, bei Befund):
public/neu/module.css; neu erzeugt:public/neu/bilder/analyse.webp
Interfaces:
- Consumes (P0, P1, P3): Ansichtsfelder
{ id, fall, adresse, geraete, warte, schritte?(seite), bewegung?, ignorieren? };GATES.p<N> = { checks, ansichten }mitALWAYS_ON,SEITEN_PRUEFUNGEN = ['dark-surface', 'low-contrast', 'overflow-x', 'lila']; Gerätehandy390×844,tablet820×1180,desktop1440×900; LäuferKN_STRANG_ENV=<KW>/tmp/strang-ui.env <KW>/shots.sh <Ausgabe> --gate p4; die Fällebetrieb-wartungundprivat-fristund die Modul-Endpunktetermine,shop,unterkunft,zeiten,news(Formen laut Kartewoms-vorschau.mdB4); Bild-Skriptbash <KW>/run-browser.sh <KW>/bilder/erzeuge.mjs --ziel <Ordner> [key …](P3 Task 8), das füranalysedie Ansichtkn-modul-analysebraucht. Einen Fallbildergibt es in keinem Plan: P3 arbeitet mitbetrieb-wartung, die Analyse-Fälle legt dieser Task an. Auch die Endpunkte der aktiven Analyse (analyse.stats,analyse.heatmap-link) fehlen in P0 und P3 und kommen hier dazu. - Produces:
GATES.p4 = { checks: [...ALWAYS_ON, ...SEITEN_PRUEFUNGEN], ansichten: ['kn-modul-*'] }grün; 17 neue Fällem-*hinter den vierzehn von P0 inFALL_NAMEN; 20 Ansichten, je auf Handy, Tablet und Desktop:
id (kn-modul-…) |
Fall, Adresse | Zeigt und prüft |
|---|---|---|
termine-einschalten |
m-termine-fragebogen, #termine?projekt=p1 |
letzter Schritt „Prüfen und einschalten“; keine Leiste im Inhalt; Etage ohne Reiter, „Zurück“ füllt die Zeile |
termine-aktiv |
m-termine-aktiv, #termine?projekt=p1 |
Etage mit vier Reitern, Handy „Team“, Desktop „Team & Plätze“; die Zeile scrollt bei 320 px, kein seitlicher Überlauf der Seite |
termine-setup, termine-aus |
m-termine-setup, m-termine-aus |
Statuskarte „Eingeschaltet am“; Hinweis zum Abo-Ende |
shop-anfrage, shop-privat, shop-angenommen, shop-aktiv |
m-shop-anfrage, privat-frist, m-shop-angenommen, m-shop-aktiv |
Satz über „Anfrage senden“; Privat-Hinweis mit Chat; Zusammenfassung und Ablauf; fünf Reiter, Handy „Start“ und „Recht“ |
buchungen-anfrage, buchungen-angenommen, buchungen-aktiv |
m-buchungen-anfrage, m-buchungen-angenommen, m-buchungen-aktiv |
kein Preis; fünf Reiter, Handy „Preise“ |
zeiten-einschalten, zeiten-aktiv, zeiten-abo-ende, zeiten-abgeschaltet |
m-zeiten-aus, m-zeiten-aktiv, m-zeiten-abo-ende, m-zeiten-abgeschaltet |
Ablauf ohne Haken; Z2 ohne Behalten-Knopf; Z3 |
news-einschalten, news-editor |
m-news-aus, betrieb-wartung |
Platz des Ablaufs; Editor mit Zettelvorschau in den drei Zettelfarben |
analyse-aktivieren, analyse, analyse-heatmap |
m-analyse-aus, m-analyse-aktiv, m-analyse-aktiv |
Linkzeile und ein Häkchen; Übersicht (Quelle des Bildes für P3) mit Diagramm schwarz auf Weiß und Etage mit drei Reitern (Handy und Desktop „Übersicht“, „Heatmap“, „Einstellungen“), keine Leiste im Inhalt; Heatmap (schritte: Tipp auf den Etagen-Reiter „Heatmap“): Reiter aktiv, Geräterahmen |
Jede Ansicht prüft zusätzlich: getComputedStyle(document.body).backgroundColor ist rgb(255, 255, 255), im Modul-Inhalt kein Element mit role="tablist" (auch nicht in der Analyse), Tippflächen der Knöpfe am Handy mindestens 44 px hoch.
- Step 1: Fälle, Ansichten und Gate anlegen
fixtures/module.mjs liefert die 17 Fälle m-* (aus betrieb-wartung abgeleitet, nur der Modul-Endpunkt und GET /api/funktionen mit modell: 'abo' ändern sich): die 16 der Tabelle und m-analyse-aktiv (Analyse active im Projekt, mit Besucher- und Heatmap-Daten: Statistik über 30 Tage mit Reihe, Seiten, Herkunft, Ländern, Geräten und Kontakten in der Form von stats() in test/analyseCards.test.js:17-40). Die Namen werden an FALL_NAMEN angehängt (17 hinter den vierzehn von P0), weil views.test aus P0 FALL_NAMEN.includes(fall) verlangt; fixtures/faelle.mjs und fixtures/index.mjs bekommen dafür je einen Import. fixtures/endpunkte-p4.mjs legt analyse.stats (GET /api/funktionen/analyse/stats, Antwort aus daten des Falls) und analyse.heatmap-link (POST /api/funktionen/analyse/heatmap-link, server/routes/funktionen.js:519 und :540) an; heatmap-link antwortet mit { url: '/vorschau/demo-baeckerei.html?__wk_heat=x&__wk_embed=1' } (same-origin, sonst meldet das Gate external-request). In vertrag-endpunkte.mjs stehen beide als NICHT_PRUEFBAR mit dem Grund „Analytics-Dienst läuft nicht über die Testnachbauten“, wie bei P3. views.mjs bekommt die 20 Ansichten, gates.mjs das Gate p4. Ausgangslage: printf 'export WT=/home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui\n' > /root/reports/kundenansicht-neu-2026-10-15/werkzeug/tmp/strang-ui.env, falls P1 sie nicht schon angelegt hat. Keine eigene Browser-Instanz.
Danach die Werkzeug-Tests, ohne dass Pin oder boot.js angefasst wurden: cd /root/reports/kundenansicht-neu-2026-10-15/werkzeug && timeout 900 nice -n 10 node --max-old-space-size=1024 --test --test-concurrency=1 test/backend.test.mjs test/vertrag.test.mjs test/gates.test.mjs test/vorschau-bau.test.mjs Expected: PASS. Schlägt der Pin „Fälle in fester Reihenfolge“ oder der boot.js-Test („alle vierzehn Fälle genannt“) nur wegen der angehängten Namen fehl: nicht selbst ändern, stoppen und VF5 dem Controller melden.
- Step 2: Erster Lauf
Run: free -m && cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && KN_STRANG_ENV=/root/reports/kundenansicht-neu-2026-10-15/werkzeug/tmp/strang-ui.env /root/reports/kundenansicht-neu-2026-10-15/werkzeug/shots.sh /root/reports/kundenansicht-neu-2026-10-15/werkzeug/tmp/shots-p4 --gate p4
Expected: „available“ über 2000 MB; der Lauf listet Befunde (dark-surface, low-contrast, overflow-x, lila, unknown-api, step-failed), solange module.css nicht fertig geprüft ist. Danach die Aufnahmen für Handy und Desktop mit dem Read-Werkzeug ansehen: kein Lila oder Türkis, Haarlinien statt Kästen, Reiterzeile in der Etage.
- Step 3: Befunde beheben
CSS-Befunde in public/neu/module.css (Abschnitt der Klasse), Fixture-Befunde im Werkzeug. Danach cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulCss.test.js Expected: PASS.
- Step 4: Gate erneut laufen lassen
Run: wie Step 2.
Expected: 0 Befunde, Exit 0, für alle 20 Ansichten und alle Geräte.
- Step 5: Bild der Analyse neu erzeugen (P3 VF2)
erzeuge.mjs analyse nimmt die Ansicht kn-modul-analyse mit dem Fall m-analyse-aktiv (Step 1).
Run: bash /root/reports/kundenansicht-neu-2026-10-15/werkzeug/run-browser.sh /root/reports/kundenansicht-neu-2026-10-15/werkzeug/bilder/erzeuge.mjs --ziel /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui/public/neu/bilder analyse && ls -l /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui/public/neu/bilder/analyse.webp
Expected: Die Datei liegt da, kleiner als 122880 Byte (960×600 prüft P3s test/knFunktionenStatik.test.js: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/knFunktionenStatik.test.js PASS). Weicht die Form des Skripts im P3-Plan ab, gilt die Form dort.
- Step 6: Commit
git -C /root/reports/kundenansicht-neu-2026-10-15/werkzeug add views.mjs gates.mjs fixtures/module.mjs fixtures/endpunkte-p4.mjs fixtures/endpunkte.mjs fixtures/faelle.mjs fixtures/index.mjs vertrag-endpunkte.mjs && git -C /root/reports/kundenansicht-neu-2026-10-15/werkzeug commit -m "Ansichten, Fälle und Gate p4 der Funktionsmodule"
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git add public/neu/module.css public/neu/bilder/analyse.webp && git commit -m "module.css: Korrekturen aus der Sichtprüfung, Bild der Analyse in neuer Optik" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Gab es keine CSS-Befunde, steht im zweiten Commit nur das Bild.
Task 15: Selbstprüfung
Files: keine Änderungen.
Interfaces: Consumes alle Ergebnisse aus Task 1–14. Produces den Tag kn-p4.
- Step 1: Speicher prüfen
Run: free -m
Expected: „available“ mindestens 2000 MB, sonst warten und dem Controller melden.
- Step 2: Alle neuen Testdateien
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 1800 nice -n 10 node --max-old-space-size=1024 --test --test-concurrency=1 test/kundenansichtNeuModulReiter.test.js test/kundenansichtNeuModulEinschalten.test.js test/kundenansichtNeuModulTermine.test.js test/kundenansichtNeuModulNews.test.js test/kundenansichtNeuModulShop.test.js test/kundenansichtNeuModulBuchungen.test.js test/kundenansichtNeuModulZeiten.test.js test/kundenansichtNeuModulAnalyse.test.js test/kundenansichtNeuModulCss.test.js
Expected: alle Tests PASS, 0 fail.
- Step 3: Bestehende Modul-Tests
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 2400 nice -n 10 node --max-old-space-size=1024 --test --test-concurrency=1 test/termineView.test.js test/termineFragebogenView.test.js test/funktionBestellenKey.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/zeitenEditor.test.js test/funktionenTiles.test.js test/funktionenZeitenTile.test.js test/funktionenRecht.test.js test/analyseView.test.js test/avvPage.test.js
Expected: alle PASS, 0 fail. Die Pins shopWiring.test.js:31 und unterkunftWiring.test.js:130 sind die einzigen geänderten Zeilen bestehender Tests.
-
Step 4: Verträge abhaken
-
V2:
initNewsundinitAnalyselesenroot.dataset.wunschProjekt(Task 4, 8); die Module behalten ihre Klassen (Task 9–13, Abdeckungstest). -
V5: Shop-Anfrage ohne
unternehmer, mitkeineAusgeschlossenen: true(Task 5); Terminplaner ohne Häkchengesundheit, Einschalten statt Checkout (Task 3); Analyse überanalyse/activate(Task 8). -
P1 Vertragsfrage 5: Reiter von Terminplaner, Webshop, Buchungskalender als
kn:reitermitkurz, leere Liste ohne Reiter (Task 1, 3, 5); Reiter der Analyse ebenso, ohne eigene Leiste (Task 8, Gatep4ohne Ausnahme). -
Global Constraints: keine Preise (
KEIN_PREISin Task 2 und 4–8), Du-Form (Pins der Bestandstests), Medienabfrage und 44px (Task 9), Palette (Task 9). -
Vertragsfragen VF1–VF6 stehen dem Controller zur Entscheidung; die Annahmen sind in den Tasks umgesetzt.
-
Step 5: Alte Ansicht und Dateiliste prüfen
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git diff --stat kn-p3 HEAD -- public/dashboard.html public/app.js public/login.html public/termine.css public/shop.css public/buchungen.css public/zeiten.css public/funktionen.css public/analyse.css public/geraete.css public/kundenbereich.css
Expected: leere Ausgabe.
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git diff --name-only kn-p3 HEAD | sort
Expected genau diese 26 Zeilen: public/analyse.js, public/buchungen-anfrage.js, public/buchungen.js, public/fn-reiter.js, public/funktion-bestellen.js, public/funktionen.js, public/neu/bilder/analyse.webp, public/neu/module.css, public/shop-anfrage.js, public/shop.js, public/termine-fragebogen.js, public/termine.js, public/zeiten-bestellen.js, public/zeiten.js, test/kundenansichtNeuModulAnalyse.test.js, test/kundenansichtNeuModulBuchungen.test.js, test/kundenansichtNeuModulCss.test.js, test/kundenansichtNeuModulEinschalten.test.js, test/kundenansichtNeuModulNews.test.js, test/kundenansichtNeuModulReiter.test.js, test/kundenansichtNeuModulShop.test.js, test/kundenansichtNeuModulTermine.test.js, test/kundenansichtNeuModulZeiten.test.js, test/shopWiring.test.js, test/support/modulAbo.js, test/unterkunftWiring.test.js.
- Step 6: Einbindung, Sauberkeit, Tag
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && command grep -a -n "module.css" public/neu/dashboard.html; command grep -a -n "einschalten" public/neu/huelle.js; git status --short; ls -l /dev/null
Expected: eine Zeile mit <link rel="stylesheet" href="/neu/module.css">; mindestens eine Zeile in huelle.js, die einschalten an die Module übergibt (fehlt sie: nicht selbst ändern, VF1 melden); git status --short leer; /dev/null zeigt crw-rw-rw-.
Run: cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git tag kn-p4 && git -C /root/reports/kundenansicht-neu-2026-10-15/werkzeug tag kn-p4
Expected: kein Fehler; git tag --list 'kn-p*' nennt kn-p4.
Vertragsfragen an den Controller
VF1 Einschalt-Ablauf (P1, P3, P4). P3 liefert zeigeEinschalten(platz, { key, projectId, onFertig, daten?, viewAs?, … }) (P3 Task 4) und erwartet die Übergabe als Init-Option einschalten an initTermine, initNews, initShop, initBuchungen, initZeiten; P4 nutzt genau diese Form (einschalten(platz, { key, projectId, viewAs, onFertig })). P1 Task 11 ruft die Module bisher nur als init*(wurzel) auf. Vorschlag wie in P3 VF1: P1 ergänzt in huelle.js init*(wurzel, { einschalten: (platz, o) => import('/neu/funktion-einschalten.js').then((m) => m.zeigeEinschalten(platz, o)) }). Ohne das zeigen die Module im Abo-Modell die Fehlerkarte E4 (Task 15 prüft es). Grund für die Übergabe statt import() in den Modulen: Pins verbieten dort neue Importe und import( (Datei:Zeile in den Global Constraints), und die gemeinsamen Module sollen keine /neu/-Datei kennen.
VF2 Reiter (P1 und P4). P4 setzt P1s Vertragsfrage 5 um: Im Abo-Modell zeichnen Terminplaner, Webshop, Buchungskalender und Analyse keine eigene Leiste, sondern melden kn:reiter (leere Liste ohne Reiter). Spec 5.4 verlangt die gemeinsame Mechanik „statt der vier eigenen Kopien“, Spec 5.2 und C2 verlangen Unterseiten-Reiter immer unten in Etage 2; eine Analyse mit Reitern im Inhalt und nur „Zurück“ in der Etage würde das Muster „unten Ebene, oben Ansicht“ brechen. Die Analyse meldet deshalb aus der Closure mountCockpit (bei: pickTab), und der Test läuft mit den Attrappen von htmlWurzel, die Statistik-Anfrage antwortet 503 (Task 8). Die frühere Ausnahme „die Analyse behält ihre Leiste“ ist gestrichen, ebenso „außer analyse“ im Gate p4. kn:titel sendet P4 nicht (der Titel kommt aus der Routentabelle von P1). Die Kurzlabels (Team, Start, Recht, Preise) sind Vorschläge; die Analyse braucht keine (Übersicht, Heatmap, Einstellungen); die Vorschau prüft sie (C2).
VF3 Alt-Klassen. kb-panel, kb-btn, kb-btn-primary, kb-btn-outline, kb-empty, kb-link, kb-checks, abo-actions, abo-btn, hidden, muted, error stehen heute in kundenbereich.css und login.css, die die neue Seite nicht lädt; in den Plänen von P0 bis P3 definiert sie niemand. Vorschlag: module.css Abschnitt 0 definiert sie, außer eine Datei von public/neu/ hat es schon getan (Task 9 prüft per grep; der Prüftest akzeptiert jede Datei dort). P2 prüft für seine 1:1-Seiten, ob es dieselben Klassen nutzt, und stützt sich dann auf Abschnitt 0.
VF4 Analyse-Aktivierung im Abo-Modell (P5). P4 sendet im Abo-Modell zusätzlich audienceConfirmed: true (P5 Vertragsfrage 8 plant dafür den Fehlercode zielgruppe_fehlt); der heutige Server ignoriert das Feld. Der Nachweis in featureConsents (Zeilen A1 und A2, Knopftext Analyse einschalten, Kundenart) bleibt Sache von P5.
VF5 Werkzeug (P0, P3, P4). Task 14 nimmt die Form aus P0, P1 und P3 an (views.mjs, gates.mjs mit GATES.p4, shots.sh … --gate p4, erzeuge.mjs) und ergänzt 17 Fälle m-* (16 Modul-Fälle und m-analyse-aktiv) in einer neuen Datei fixtures/module.mjs, eingetragen in fixtures/faelle.mjs und fixtures/index.mjs (Dateien von P0, im Werkzeug-Git), dazu die Analyse-Endpunkte in fixtures/endpunkte-p4.mjs (Konvention aus P0 Vertragsfrage 5). Außerdem erzeugt P4 public/neu/bilder/analyse.webp neu (P3 VF2; sonst Besitz von P3).
Regel: Pin und boot.js bleiben unberührt. P4 ändert weder den Pin „Fälle in fester Reihenfolge“ in KW/test/backend.test.mjs noch KW/vorschau/boot.js. Das setzt voraus, dass P0 (und P7, das ebenfalls Fälle anhängt) den Pin auf die vierzehn Basisnamen begrenzt und boot.js die Fallliste nicht fest einträgt; sonst schlagen backend.test.mjs und vorschau-bau.test.mjs nach dem Anhängen der 17 Namen fehl, und Task 14 Step 1 stoppt und meldet es. Bitte alles bestätigen und die Regel in P0 und P7 gleich ziehen.
VF6 Hinweis für P10. Der Abdeckungstest kundenansichtNeuModulCss.test.js liest die alten CSS-Dateien als Klassenquelle. Löscht P10 sie, ersetzt es die Quelle durch den Stand der Klassenliste (Ausgabe von klassen(...) je Präfix, einmal als test/support/modulKlassen.json abgelegt) und passt FN_NEWS an. Die Alt-Zweige der Module (Bestellseiten, „Behalten“, lange Reiterleisten) entfernt P10 erst nach der Umschaltung.