# Kundenansicht neu P2: Website, Chat, Verträge 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 Bereiche Website, Chat und Verträge sowie die Etagen Projektdetails, Gutschrift, Passwort, Bestellen und Wechsel laufen mit den heutigen APIs in der neuen weißen Oberfläche. Die öffentlichen Seiten (Login, Kündigen, Widerrufen, Auftragsbestätigung, Offline) gibt es im neuen Stil unter `public/neu/oeffentlich/`, noch nicht ausgeliefert.
**Architecture:** Je Ansicht ein ES-Modul `public/neu/ansicht-*.js` mit `zeigeX(wurzel, { route, me, nav })`, gebaut mit `h()` und `textContent` auf den heutigen Endpunkten (Karte woms-vorschau B4). Die acht Stub-Dateien und `ansichten.css`, die P1 anlegt, werden gefüllt. Reine Logik (Formate, Reiterwahl, Status, Zeitleiste) steht in kleinen exportierten Funktionen und wird mit `node --test` geprüft, DOM-Teile laufen gegen `test/support/fakeDom.js`. Die Wortlaut-Seiten (Bestellen, Wechsel, E-Mail-Speicher, Gutschrift, öffentliche Seiten) übernehmen den Text der Fassung 5 unverändert. Ein Wortlaut-Orakel lädt `public/app.js` in eine VM und vergleicht den sichtbaren Text Zeichen für Zeichen mit der neuen Seite.
**Tech Stack:** ES-Module ohne Build (Browser, Node 20/24), `node --test`, `test/support/fakeDom.js`, `window.ChatComposer` (klassisches Skript `public/chat-composer.js`), `public/auftrag-portal.js`, Sichtprüfung nur über das Werkzeug aus P0.
**Spec:** `docs/superpowers/specs/2026-10-15-kundenansicht-neu-design.md` (5.1 Aussehen, 5.3 Gruppierung, 5.7 Sprache, C14 Wortlaut der Bestellseiten)
**Ü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` (Strang UI, nach P1, Stand Tag `kn-p1`). Alle Pfade dieses Plans sind relativ zu diesem Worktree, soweit sie nicht mit `/` beginnen. Tag am Ende: `kn-p2`.
## Global Constraints
Alle Global Constraints der Übersicht gelten (Schalter, Aussehen mit allen Verboten, Anrede, keine Funktionspreise, Hashes, rechtlich Fest, Testbefehle mit Speicherbremse, Commits mit Zeile `Co-Authored-By: Claude Opus 5.5 `, Sicherheitsblock, kein Push). Paketeigene Zusätze:
- **Importe relativ** in `public/neu/*.js` (`./dom.js`, `../auftrag-portal.js`), damit `node --test` die Module direkt lädt. `ChatComposer` kommt über `globalThis`.
- **Kein HTML-Einsetzen:** kein `innerHTML`, `insertAdjacentHTML`, `outerHTML`. Kundentext nur über `h()` und `textContent`. URLs aus Daten (`previewUrl`, `portalUrl`, `paymentUrl`) nur über `safeUrl` (http/https) in `href`/`src`.
- **Anrede:** `du` in allen P2-Texten. Ausnahme nur die Wortlaut-Dateien `ansicht-bestellen.js`, `ansicht-wechsel.js`, `ansicht-speicher.js`, `ansicht-gutschrift.js` und die Seiten unter `oeffentlich/`: Dort bleibt der Text der Fassung 5 wörtlich (C14, Sie-Form inklusive). Untertitel unter Überschriften entfallen überall (Spec 5.7).
- **Keine eigene `h1`:** Bereichstitel zeichnet die Hülle (P1 Task 12), Etagentitel kommen aus `titelFuer(route)`. Weicht der Titel ab (Projektname, Produktname), meldet die Ansicht ihn mit `meldeTitel(wurzel, text)` (Ereignis `kn:titel`, P1 Task 6).
- **Links zwischen Ansichten** tragen `href` und `data-view` (Brücke der Hülle, `hashFuerAnsicht`), erzeugt mit `zuAnsicht(name, projektId?)`. Programmatisch navigiert nur `navigiere(hash, { ersetzen })`.
- **Wiederholbar:** Jeder `zeigeX`-Aufruf leert `wurzel`, zeigt sofort den Text `Wird geladen …`, verwirft späte Antworten (Zähler je Aufruf) und räumt Timer ab. Fällt eine Quelle aus, zeigt nur ihr Abschnitt einen Fehlertext.
- **Ansichtsmodus** (`me.viewAs === true`): Jeder schreibende Knopf ist gesperrt und es geht kein schreibender Aufruf raus. Neuer Sperrtext `Im Ansichtsmodus nicht möglich.`, Chat, Passwort und Gutschrift behalten ihre alten Sperrtexte.
- **Schreibende Aufrufe** über `api()` (V2) mit Body als Objekt. Alt-Module, die einen JSON-String senden (`auftrag-portal.js`), nur über `legacyRequest(api)`.
- **Verlaufsstempel (P1 VF7, VF9):** Wer die Adresse aufräumt, ruft `history.replaceState(history.state, …)`, nie mit `null`.
- **Schreibverbot für Altes:** `public/app.js`, `dashboard.html`, `login.html`, `kuendigen.html`, `widerrufen.html`, `offline.html`, `auftrag.html`, `auftrag.css`, `legal-form.*`, `auftrag*.js`, `chat-composer.*` (alle direkt unter `public/`) bleiben unverändert (Prüfung in Task 20). Was die neue Oberfläche anders braucht, liegt als neue Datei unter `public/neu/`.
- **Kein Lila in `public/neu/`:** Die Prüfung `lila` (P0) liest den Text jedes geladenen Stylesheets, auch ungenutzter Regeln. Die neue Oberfläche lädt deshalb weder `/chat-composer.css` noch `/auftrag.css`, `/login.css` oder `/legal-form.css`, sondern bereinigte Dateien unter `public/neu/` (`chat-composer.css` in Task 7, `oeffentlich/oeffentlich.css` in Task 17). Ein Quelltext-Test (Task 1) sucht in jeder Textdatei unter `public/neu/` nach `#9d3dfe` und `rgba(157, 61, 254`.
- **Pin:** `test/adminPanelGone.test.js:55-66` liest `public/` rekursiv. Kein neuer Quelltext enthält `admin.html`, `kalkulation.html` oder `/api/portal-admin`.
- **Testcode in diesem Plan** zeigt Namen und Assertions. Importe (`node:test`, `node:assert/strict`, `fakeDom`, `knDaten`, das Modul) und Hilfsfunktionen schreibt der Implementierer dazu.
## Review Focus
1. **Alt-Modul sendet doppelt kodierten Body:** `auftrag-portal.js` schickt `JSON.stringify(...)`, das neue `api()` kodiert Objekte selbst. Ohne Adapter scheitert „Auftrag bestätigen“ still mit 400. Tests: Task 1 (`legacyRequest`), Task 8 (POST-Body ist ein Objekt).
2. **Reiter-Sprung überschreibt ausdrückliche Wahl:** Mit offener Rechnung springt nur der Bereichs-Einstieg (Route ohne `reiter`) auf Rechnungen. `#abo`, `#wartung`, `#stunden`, `#speicher` und ein Klick auf einen Reiter nie. Allein `route.reiter` entscheidet, die Adressleiste ist keine zweite Wahrheit. Hosting-Kunden dürfen auf `#stunden` nicht ins Leere laufen. Tests: Task 16.
3. **Ansichtsmodus schreibt als Kunde:** Mitarbeiter markieren Chat gelesen, bestellen, ändern Speicher oder Passwort. Tests: Task 7 (Chat), 8 (Aufträge), 9 (Zahlungsart), 11 (Speicher), 12 (Bestellen), 13 (Wechsel), 14 (Gutschrift), 15 (Passwort).
4. **Wortlaut der Fassung 5 driftet:** Ein geänderter Satz auf Bestell-, Wechsel-, Speicher-, Gutschrift- oder öffentlichen Seiten wäre eine unbemerkte Rechtsänderung. Tests: Orakel in Task 11 bis 14 und 17, Hash-Pin der Gutschrift-Bedingungen in Task 14.
5. **Teilausfall und fremde Inhalte:** Fällt Aktivität, Rechnungen, Aufträge, Stundenkonto, Verträge oder Vorschau aus, oder steht `` bzw. `javascript:` in Projektname, Rechnungsprojekt, Chat, Rückfrage oder Vorschau-Adresse, bleibt der Rest der Seite stehen und nichts wird als HTML oder Link ausgeführt. Tests: Task 5, 6, 8, 9, 10, 14, 16.
## Festlegungen dieses Plans
Diese Punkte legen weder Spec noch Übersicht fest.
- **Projektdetails** sind immer Etage 2 (`#projekt?projekt=`), auch bei einer einzigen Website. P1 ruft dafür `zeigeWebsite` mit `route.etage === 'projekt'` (Wurzel `#etage-body`). Die ID liest die Ansicht aus `projekt=`, hilfsweise aus `id=`.
- **Reiter nach Abo:** Ohne laufendes Abo gibt es nur Rechnungen und Abo (dort die Abo-Auswahl). Hosting zeigt zusätzlich E-Mail, Wartung zusätzlich Stunden und E-Mail.
- **„Verträge hier kündigen“** steht unter jedem Reiter, auch ohne Abo. „Vertrag widerrufen · bis TT.MM.“ steht im Reiter Abo nur bei `withdrawalOpen`.
- **„Postfach anfragen“** wandert in den Reiter E-Mail. Der Chat öffnet mit der Vorgabe `Bitte lege ein neues Postfach an: `.
- **Offene Rechnung** heißt wie in V3 `Number(balance) > 0`. Eine offene: `Rechnung offen · RE-… · 50,00 € · Zahlen`. Mehrere: `2 Rechnungen offen · 150,00 € · Zahlen`.
- **Reiterwahl in Verträge:** Allein `route.reiter` entscheidet. Die Ansicht liest `location.hash` nicht. Fehlt `route.reiter` (Bereichs-Einstieg über `#vertraege` oder die Leiste), gilt Rechnungen bei offener Rechnung, sonst Abo. Das setzt voraus, dass P1 `routeAusHash('#vertraege')` ohne `reiter` liefert (VF8). Reiterklicks navigieren mit den ausdrücklichen Hashes `#rechnungen`, `#abo`, `#stunden`, `#speicher`.
- **Wechsel mit gemerkter Funktion (E14, C5):** Hat `ansicht-wechsel.js` eine Funktion gesendet und liefert der Server danach `subscription.wechselFunktion`, geht die Ansicht mit `ersetzen` zu `#funktionen`, nicht zu `#vertraege`. Dort öffnet P3 (Task 6) den Einschalt-Ablauf. Dasselbe gilt für die Rückkehr mit `?abo=success` in Task 16. Ohne gemerkte Funktion bleibt alles wie in Fassung 5.
- **Stunden** erscheinen als `2,5 Std.` (Komma, höchstens zwei Stellen).
- Der Erklärtext unter dem Chat-Composer („Enter = Senden …“) und alle Untertitel unter Überschriften entfallen.
## Dateiübersicht
Quelle `Tabelle` = Zuständigkeitstabelle der Übersicht, `Stub` = Datei, die P1 als Stub anlegt und die P2 füllt (P1 Task 12), `VF1` = Zusatzdatei, die P2 braucht (Vertragsfrage VF1).
| Datei | Quelle | Aufgabe | Task |
|---|---|---|---|
| `public/neu/ansicht-hilfen.js` | VF1 | Formate, Bausteinklassen `K`, `legacyRequest`, `navigiere`, `zuAnsicht`, Ereignisse, Hinweis- und Chat-Vorgabe-Speicher | 1 |
| `test/support/knDaten.js`, `test/support/knWortlautOrakel.js` | VF1 | Testdaten und `mit()`-Rahmen, VM-Orakel für den alten Text | 1, 2 |
| `public/neu/rechnung-offen.js` | VF1, VF3 | Zeile „Rechnung offen“ für Website und Funktionen (P3) | 3 |
| `public/neu/install-hinweis.js` | VF1 | Installationshinweis | 4 |
| `public/neu/website-details.js` | VF1 | Auftrag, Änderungen, Arbeiten, Dateien, Aktivität | 5 |
| `public/neu/ansicht-website.js` | Tabelle, Stub | Projektliste, Etage `projekt`, Aktivität | 6 |
| `public/neu/ansicht-chat.js` | Tabelle, Stub | Chat | 7 |
| `public/neu/dashboard.html` | P1-Datei | zwei Zeilen für den Composer (P1 Task 12 erlaubt sie): Skript und die bereinigte CSS-Kopie | 7 |
| `public/neu/chat-composer.css` | VF1 | Kopie von `public/chat-composer.css` ohne Lila, Gewicht 800 und Glas (Prüfung `lila` liest den Text jedes Stylesheets) | 7 |
| `test/knHuelleSeite.test.js` | P1-Test | eine Zeile: erlaubt `/neu/chat-composer.css` statt `/chat-composer.css` | 7 |
| `public/neu/vertraege-rechnungen.js` | VF1 | Reiter Rechnungen mit Aufträgen | 8 |
| `public/neu/vertraege-abo.js` | VF1 | Reiter Abo, Abo-Auswahl, Einstiege | 9 |
| `public/neu/ansicht-stunden.js` | Tabelle | Stundenkonto mit Vorschuss | 10 |
| `public/neu/ansicht-speicher.js` | Tabelle | E-Mail-Speicher 1:1, Postfach anfragen | 11 |
| `public/neu/ansicht-bestellen.js`, `ansicht-wechsel.js` | Tabelle, Stub | Bestellen und Wechsel 1:1 (Fassung 5); der Wechsel mit gemerkter Funktion springt zu `#funktionen` | 12, 13 |
| `public/neu/ansicht-gutschrift.js`, `ansicht-passwort.js` | Tabelle, Stub | Gutschrift 1:1, Passwort | 14, 15 |
| `public/neu/ansicht-vertraege.js` | Tabelle, Stub | Rahmen mit Reitern | 16 |
| `public/neu/oeffentlich/{login,kuendigen,widerrufen,offline}.html` | Tabelle | Öffentliche Seiten im neuen Stil | 17 |
| `public/neu/oeffentlich/auftrag.html` | Spec 5.1, VF1 | Auftragsbestätigung im neuen Stil (Link aus Mail K31) | 17 |
| `public/neu/oeffentlich/oeffentlich.css` | VF1 | Aussehen dieser fünf Seiten, auch der `ab-*`-Klassen der Auftragsseite | 17 |
| `public/neu/ansichten.css` | Tabelle, Stub | Aussehen aller P2-Ansichten und des Aufträge-Abschnitts | 18 |
| `/root/reports/kundenansicht-neu-2026-10-15/werkzeug/` (eigenes Git): `views.mjs`, `gates.mjs`, `fixtures/endpunkte-p2.mjs`, `fixtures/endpunkte.mjs`, `vertrag-endpunkte.mjs`, `vorschau/seiten.mjs` (ein Eintrag) | Tabelle (P0 legt an, P2 ergänzt) | Ansichten, Gate `p2`, drei Endpunkte, Seite `auftrag.html` | 19 |
| `test/knAnsicht*.test.js` (19 Dateien) | VF1 | Tests dieses Pakets | 1 bis 18 |
**Bestehende Testdateien und Pins:** Genau eine wird geändert, `test/knHuelleSeite.test.js` (P1-eigene Datei im selben Strang, eine Zeile in Task 7). Geprüft: Kein Test liest `public/` rekursiv außer `adminPanelGone.test.js:32-66` (siehe Constraint); die `readdirSync`-Stellen in `termineView.test.js:324,397,418`, `termineFragebogenView.test.js:1023`, `shopWiring.test.js:49`, `zeitenView.test.js:391` lesen `public/` nicht rekursiv. `test/knHuelleSeite.test.js` (P1) erlaubt `/chat-composer.js` und `/chat-composer.css` in `dashboard.html`; Task 7 stellt die Stil-Zeile auf `/neu/chat-composer.css` um (VF10). Nur lesend benutzt: `legalTexts.test.js:18` (Hash `2b7a745c…473e62` des Blocks `TAKEOVER_CONDITIONS`, Task 14) und `auftraegeKundenportalDaten.js` (Testdaten `eintrag`, Task 8).
---
### Task 1: Hilfen, Testdaten und Quelltext-Regeln
**Files:**
- Create: `public/neu/ansicht-hilfen.js`, `test/support/knDaten.js`
- Test: `test/knAnsichtHilfen.test.js`, `test/knAnsichtQuelltext.test.js`
**Interfaces:**
- Consumes: `api(pfad, { method, body })` und `ApiFehler` aus `./api.js`, `geheZu(hash, { ersetzen })` aus `./router.js`, `hashFuerAnsicht(name)` aus `./routen.js` (alle P1), `safeUrl` aus `../fn-util.js`.
- Produces `ansicht-hilfen.js`:
```js
export const K // Bausteinklassen aus P1 basis.css: knopf, primaer, zweit, textknopf, feld, liste, zeile, abschnitt, meldung, fehler, leer, versteckt:'hidden', pille:{grau,blau,orange,violett,gruen,rot}
export const fmtEuro // (euro) → '50,00\u00a0€' (geschütztes Leerzeichen vor dem Euro-Zeichen)
export const fmtDatum // (wert) → 'TT.MM.JJJJ' (Europe/Berlin), 'TT.MM.JJJJ' bleibt, leer → '–'
export const fmtDatumZeit // (wert) → 'TT.MM.JJ, HH:MM' (Europe/Berlin)
export const fmtTagMonat // (wert) → 'TT.MM.' (Europe/Berlin), ungültig → ''
export const fmtStd // (minuten) → '2,5 Std.' | '2 Std.' | '0,75 Std.'
export const fmtMinuten // (minuten) → '' | '45 min' | '2 h' | '2 h 30 min'
export const fmtBytes // (bytes) → '12 B' | '1,5 KB' | '5,0 MB'
export const ABO_NICHT_GELADEN = 'Deine Verträge konnten gerade nicht geladen werden. Bitte versuche es später noch einmal.'
export function legacyRequest(api) // → (pfad, opts) => api(pfad, { ...opts, body: Objekt }); String-Body wird per JSON.parse zum Objekt
export function navigiere(hash, { ersetzen = false } = {}) // geheZu(); bei Ausnahme window.location.hash = hash
export function zuAnsicht(name, projektId = '') // → { href, 'data-view': name, 'data-project'?: projektId }; href = hashFuerAnsicht(name) (+ '?projekt=')
export function meldeTitel(el, text) // el.dispatchEvent(new CustomEvent('kn:titel', { bubbles: true, detail: { text } }))
export function meldeZaehlerNeu(was) // 'chat'|'vertraege'|'alle': document.dispatchEvent(new CustomEvent('kn:zaehler-neu', { bubbles: true, detail: { was } })), no-op ohne document/CustomEvent
export function merkeHinweis(text); export function nimmHinweis() // einmaliger Hinweis für die nächste Verträge-Ansicht, danach ''
export function setzeChatVorgabe(text); export function nimmChatVorgabe() // Nachrichtenvorgabe für den nächsten Chat
export async function ladeAbo() // GET /api/abo → Objekt | null (Fehler → null)
export function wechselFunktionAus(sub) // sub = subscription | undefined | null → { key, projectId } | null (V11: beide Felder nichtleere Strings)
export { safeUrl }
```
- Produces `test/support/knDaten.js` (Formen laut `server/routes/abo.js:88-214`, `routes/customerData.js:18-100` mit `services/customerInvoices.js:61-87`, `routes/projects.js:25-90`, `routes/chat.js:26-35`, `services/takeoverCredits.js:366-384`):
```js
export const mit = (tabelle, fn) => async () => … // installFakeDom mit 80 Antwortfunktionen; Schlüssel 'GET /api/abo' (oder nur Pfad); Wert Objekt (200) oder (aufruf) => ({ status, body }); unbekannt → 404 { error: 'unbekannt' }. Setzt globalThis.CustomEvent auf eine Attrappe mit beschreibbaren Feldern (fakeDom schreibt target und ergebnisse) und stellt sie danach zurück
export const mitDom = (fn) => … // wie mit() ohne Antworten, führt fn(fx) sofort aus (synchrone Tests)
export const neueWurzel = (fx) => Element // div, an document.body gehängt
export const VORSCHAU = { prorationDate: 1760000000, amountDueCents: 3000, creditCents: 1500, monthlyCents: 5000, nextChargeAt: '2026-11-16T00:00:00.000Z', minTermEnd: '2027-10-16T00:00:00.000Z' } // POST /api/abo/switch/preview
export const VORSCHAU_SPEICHER = { direction: 'down', units: 1, totalGb: 10, monthlyCents: 100, deltaMonthlyCents: -100, prorationCents: 0, nextChargeAt: '2026-11-16T00:00:00.000Z', effectiveAt: '2026-11-01T00:00:00.000Z' } // POST /api/abo/storage/preview
export const LIMITS = { maxFiles: 3, maxFileBytes: 8388608, maxTotalBytes: 10485760 } // takeover-credits
export const me = (over = {}), projekt = (over = {}), rechnung = (over = {}), nachricht = (over = {}), angebot = (over = {})
export const abo = ({ produkt = 'wartung', ...over } = {}) // subscription: active, productKey, priceAmount, storage, canChangeStorage, canSwitch, withdrawals …
export const ledger = (over = {}), aboAntwort = (over = {}), anfrageGutschrift = (over = {})
```
- [ ] **Step 1: Voraussetzungen prüfen**
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git tag --list 'kn-p*' && ls public/neu`
Expected: `kn-p0` und `kn-p1` stehen in der Liste. `ls` zeigt `dom.js api.js routen.js router.js reiter.js etage.js tokens.css basis.css huelle.css dashboard.html` und die Stubs `ansicht-website.js ansicht-chat.js ansicht-vertraege.js ansicht-passwort.js ansicht-gutschrift.js ansicht-wechsel.js ansicht-bestellen.js ansichten.css`. Fehlt etwas, anhalten und dem Controller melden.
- [ ] **Step 2: Tests schreiben**
`test/knAnsichtHilfen.test.js`:
```js
test('Formate: Euro mit geschütztem Leerzeichen, Daten in Berliner Zeit', () => {
assert.equal(fmtEuro(50), '50,00\u00a0€')
assert.equal(fmtDatum('2026-10-17T10:00:00Z'), '17.10.2026')
assert.equal(fmtDatum('17.10.2026'), '17.10.2026')
assert.equal(fmtDatum(''), '–')
assert.equal(fmtDatumZeit('2026-10-17T10:00:00Z'), '17.10.26, 12:00')
assert.equal(fmtTagMonat('2026-10-17T21:59:59Z'), '17.10.')
assert.equal(fmtTagMonat('2026-10-17T22:00:00Z'), '18.10.')
assert.equal(fmtTagMonat('kaputt'), '')
})
test('Formate: Stunden, Minuten, Bytes', () => {
assert.deepEqual([150, 120, 45, 0].map(fmtStd), ['2,5 Std.', '2 Std.', '0,75 Std.', '0 Std.'])
assert.deepEqual([0, 45, 120, 150].map(fmtMinuten), ['', '45 min', '2 h', '2 h 30 min'])
assert.deepEqual([12, 1536, 5 * 1024 * 1024].map(fmtBytes), ['12 B', '1,5 KB', '5,0 MB'])
})
test('legacyRequest: String-Body wird zum Objekt, Objekt und fehlender Body bleiben', async () => {
const aufrufe = []
const alt = legacyRequest(async (pfad, opts) => { aufrufe.push([pfad, opts]); return {} })
await alt('/a', { method: 'POST', body: '{"x":1}' }); await alt('/b', { method: 'POST', body: { y: 2 } }); await alt('/c')
assert.deepEqual(aufrufe, [['/a', { method: 'POST', body: { x: 1 } }], ['/b', { method: 'POST', body: { y: 2 } }], ['/c', {}]])
})
test('meldeZaehlerNeu ohne Dokument tut nichts, Hinweis und Chat-Vorgabe gelten einmal', () => {
assert.doesNotThrow(() => meldeZaehlerNeu('chat'))
merkeHinweis('Gespeichert.'); assert.equal(nimmHinweis(), 'Gespeichert.'); assert.equal(nimmHinweis(), '')
setzeChatVorgabe('Hallo'); assert.equal(nimmChatVorgabe(), 'Hallo'); assert.equal(nimmChatVorgabe(), '')
})
test('wechselFunktionAus: nur vollständige Werte zählen (V11)', () => {
assert.deepEqual(wechselFunktionAus({ wechselFunktion: { key: 'termine', projectId: 'p1', extra: 1 } }), { key: 'termine', projectId: 'p1' })
for (const sub of [undefined, null, {}, { wechselFunktion: null }, { wechselFunktion: { key: 'termine' } }, { wechselFunktion: { key: '', projectId: 'p1' } }, { wechselFunktion: { key: 1, projectId: 'p1' } }]) assert.equal(wechselFunktionAus(sub), null)
})
test('zuAnsicht ist die Brücke der Hülle, meldeTitel steigt zu den Eltern auf', () => mitDom((fx) => {
assert.deepEqual(zuAnsicht('takeover'), { href: '#gutschrift', 'data-view': 'takeover' })
assert.deepEqual(zuAnsicht('projekt', 'p1'), { href: '#projekt?projekt=p1', 'data-view': 'projekt', 'data-project': 'p1' })
assert.equal(zuAnsicht('invoices').href, '#rechnungen')
const aussen = fx.document.createElement('div'); const innen = fx.document.createElement('p'); aussen.append(innen); const titel = []
aussen.addEventListener('kn:titel', (e) => titel.push(e.detail.text)); meldeTitel(innen, 'Zwei'); assert.deepEqual(titel, ['Zwei'])
}))
```
`test/knAnsichtQuelltext.test.js` prüft die vorhandenen Module aus der Liste `ansicht-hilfen, rechnung-offen, install-hinweis, website-details, ansicht-website, ansicht-chat, ansicht-vertraege, vertraege-rechnungen, vertraege-abo, ansicht-stunden, ansicht-speicher, ansicht-bestellen, ansicht-wechsel, ansicht-gutschrift, ansicht-passwort` (Kommentare vorher entfernt, fehlende Dateien übersprungen, `WORTLAUT = ['ansicht-speicher','ansicht-bestellen','ansicht-wechsel','ansicht-gutschrift']`). Die Stubs von P1 bestehen die Regeln schon:
```js
test('P2-Module: kein HTML-Einsetzen, nur relative Importe, keine Admin-Verweise', () => {
for (const n of vorhanden) {
const q = lies(n)
assert.doesNotMatch(q, /innerHTML|insertAdjacentHTML|outerHTML|document\.write|console\.log/, n)
assert.doesNotMatch(q, /from\s+'\/|import\(\s*'\//, `${n}: absoluter Import`)
assert.doesNotMatch(q, /admin\.html|kalkulation\.html|\/api\/portal-admin/, n)
}
})
test('P2-Module: Du-Form, nur die vier Wortlaut-Dateien behalten die Sie-Form (C14)', () => {
for (const n of vorhanden.filter((x) => !WORTLAUT.includes(x))) assert.doesNotMatch(lies(n), /\b(Sie|Ihr(e[nmrs]?)?|Ihnen)\b/, n)
})
test('public/neu/: kein Lila in irgendeiner Textdatei (die Prüfung lila liest jedes geladene Stylesheet samt ungenutzter Regeln)', () => {
for (const pfad of dateienUnter('public/neu', /\.(css|html|js|mjs|svg|webmanifest)$/)) assert.doesNotMatch(readFileSync(pfad, 'utf8'), /#9d3dfe|rgba?\(\s*157\s*,\s*61\s*,\s*254/i, pfad)
})
```
Der Lila-Test liest den Rohtext einschließlich Kommentaren (so wie die Prüfung den Stylesheet-Text liest); `dateienUnter(ordner, muster)` durchsucht rekursiv und gehört zu den Hilfen des Tests. Er läuft ab Task 1 und deckt damit auch alle späteren Dateien dieses Pakets (Task 7, 17, 18) und die von P3 und P4 ab.
- [ ] **Step 3: Test laufen lassen, muss scheitern**
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/knAnsichtHilfen.test.js test/knAnsichtQuelltext.test.js`
Expected: `knAnsichtHilfen` FAIL mit `ERR_MODULE_NOT_FOUND` für `public/neu/ansicht-hilfen.js`, `knAnsichtQuelltext` grün.
- [ ] **Step 4: Implementieren**
`ansicht-hilfen.js` nach der Signatur oben. Datums- und Zeitformate über `Intl.DateTimeFormat('de-DE', { timeZone: 'Europe/Berlin', … }).formatToParts` zusammensetzen, nicht auf die Ausgabe von `toLocaleString` verlassen. `K` mit den Klassen, die P1 `basis.css` führt (`kn-knopf`, `kn-knopf kn-knopf-primaer`, `kn-pille kn-pille-` usw.). `knDaten.js` mit den Bauern; Standardwerte aus den Quellzeilen oben: Abo mit `startedAt`, `currentPeriodEnd`, `priceAmount` in Cent, `storage:{units,pendingSetup,pendingReduction}`; Produkte `hosting` 2000 und `wartung` 5000 Cent mit je einer Feature-Zeile `['Titel','Detail']`; `termsVersion: '2026-09-30-v5'`, `lateStartAt: '2026-10-30T00:00:00.000Z'`, `mailStorage: { includedGb: 5, gbPerUnit: 5, unitPriceCents: 100, maxUnits: 10 }`, `available: true`; `me()` mit Kunde `anna@example.com`.
- [ ] **Step 5: Lauf grün**
Run: derselbe Befehl wie Step 3. Expected: PASS, 9 Tests.
- [ ] **Step 6: Commit**
```bash
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git add public/neu/ansicht-hilfen.js test/support/knDaten.js test/knAnsichtHilfen.test.js test/knAnsichtQuelltext.test.js && git commit -m "Kundenansicht neu P2: Hilfen, Testdaten und Quelltext-Regeln" -m "Co-Authored-By: Claude Opus 5.5 "
```
---
### Task 2: Wortlaut-Orakel
**Files:**
- Create: `test/support/knWortlautOrakel.js`
- Test: `test/knAnsichtOrakel.test.js`
**Interfaces:**
- Consumes: `public/app.js` (klassisches Skript, nur gelesen), `test/support/knDaten.js`.
- Produces:
```js
export function sichtbarerText(quelle) // HTML-String oder Element → Text ohne Tags, Kommentare,
'), 'HalloWelt&Co')
assert.equal(sichtbarerText('50,00\u00a0€ '), '50,00€')
})
test('ohneKopf streicht nur den Kopf, nicht die Absätze danach', () => {
assert.equal(sichtbarerText(ohneKopf('