40 KiB
Kundenansicht neu: Umsetzungsplan, Übersicht und Verträge
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. Diese Übersicht ist der Vertrag zwischen den Paketen. Namen, Signaturen, Datenformen, Endpunkte und Dateizuständigkeiten stehen nur hier und gelten für alle Paket-Pläne. Weicht ein Paket-Plan davon ab, gilt diese Übersicht.
Goal: Die Kundenansicht des Portals wird weiß und schlank wie das Ticketsystem, bekommt die Handy-Leiste „Fahrstuhl-Zeile“ und die Funktionen mit „Meine/Entdecken“. Die Funktionen stecken im Abo und werden eingeschaltet statt gekauft. Bestätigungen schrumpfen auf das rechtlich Nötige. Alles liegt hinter einem Pilot-Schalter, bis der Inhaber umschaltet.
Architecture:
- Die neue Oberfläche ist eine eigene Seite
public/neu/dashboard.htmlmit eigenen ES-Modulen und CSS unterpublic/neu/. Der Server liefert sie unter derselben Adresse/dashboard.htmlaus, wenn der Pilot-Schalter für die Sitzung gilt. - Die Funktionsmodule (
public/termine.js,shop.js…) bleiben gemeinsam. Ihr Aussehen kommt aus dem CSS der jeweils geladenen Seite, ihr Verhalten unterscheidet sich nur über das Feldmodellder Server-Antwort. - Neue Server-Logik ist nur für Sitzungen mit neuer Ansicht aktiv (
neueAnsicht(req)) oder rein additiv. Die alte Ansicht verhält sich bis zum Umschalten exakt wie heute.
Tech Stack:
- Portal: Node 20 (Prod-Container
node:20-alpine, Host 24), Express 4, Appwrite-REST (server/services/appwriteAdmin.js), Stripe SDK 19, Vanilla JS ohne Build. - Tests Portal:
node --testmit den Nachbauten austest/support/(appwriteMock,fakeStripe,testApp,fixtures,fakeDom,mailSink) und Playwright-Skriptetest/ui/*.ui.mjs. - WOMS: React 18, Vite, Tests
node --testübertests/unit/load.mjs.
Spec: docs/superpowers/specs/2026-10-15-kundenansicht-neu-design.md (Commit d3d2849, Abschnitte 1–8 mit E1–E21 und C1–C15). Jeder Implementierer liest die Spec, diese Übersicht und seinen Paket-Plan.
Code-Karten (Stand 16.10., nur gelesen): /root/reports/kundenansicht-neu-2026-10-15/karten/
huelle-tests.md: dashboard, app.js, Router, Tests, Pinsfunktionen-server.md: Funktionen, Zustände, Bestellen, Aktivieren, Abo-Abgleichabo-server.md: Stundenkonto, Abo, Wechsel, Widerruf, Fassungen, Kundenart, Mailswoms-vorschau.md: WOMS-Stellen, Vorschau
Dazu die Navigations-Demo /root/reports/kundenansicht-neu-2026-10-15/handy-leiste/final-2-frei.html.
Ausführungsmodell
Phase A P0 Werkzeug, Pilot-Schalter, Vorschau-Gerüst (1 Agent, Hauptzweig)
Phase B parallel in eigenen Worktrees:
Strang UI: P1 Fundament+Hülle → P2 Website/Chat/Verträge
Strang Server: P5 Abo-Modell Kern → P6 Abo-Modell Betrieb
Strang Recht: P8 Rechtsrunde Fassung 6 (Entwürfe + Fassungs-Technik)
Strang WOMS: P9 WOMS (gegen die Verträge V4/V9; Portal-Teile von P9 erst nach Merge von P5)
Phase C Merge Server → Hauptzweig, dann Strang UI weiter: P3 Funktionen-Rahmen → P4 Funktionsmodule
Phase D Merge Recht + UI → P7 Bestellseiten schlank (Hauptzweig)
Phase E Integration, ganze Suite, Vorschau aktualisieren, Abschlussprüfung (mehrere Blickwinkel), Übergabe
→ der Inhaber pusht (Schalter off = für Kunden unsichtbar), Pilot fürs Team, Rechtsrunde mit dem Inhaber
Phase F P10 Umschalten und Aufräumen (nur mit ausdrücklichem OK des Inhabers)
- Ein Implementierer je Strang, Modell
sonnet. Die Tasks eines Strangs laufen der Reihe nach mit TDD. - Prüfungen: keine Reviewer je Task. Jeder Paket-Plan endet mit einer Selbstprüfung: eigene Tests grün, Verträge abgehakt,
git statussauber. Eine gemeinsame Abschlussprüfung gibt es in Phase E. Kleine Nachprüfungen mithaiku. Keinopus, keinfable(Memory „feedback-billige-modelle“). - Merge-Reihenfolge in den Hauptzweig: P0 → Server (P5, P6) → Recht (P8) → UI (P1–P4) → P7. Konflikte entstehen nur in den Dateien, die die Zuständigkeitstabelle als „geteilt“ markiert.
- Tags: nach jedem Paket
kn-p<N>im jeweiligen Zweig. Nach dem Merge auf den Hauptzweigkn-p<N>-haupt.
Pakete, Plan-Dateien, Worktrees
| Paket | Plan-Datei (docs/superpowers/plans/) |
Worktree | Branch | Start |
|---|---|---|---|---|
| P0 Werkzeug, Schalter, Vorschau | 2026-10-16-kundenansicht-01-werkzeug-vorschau.md |
/home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht |
feature/kundenansicht-neu |
sofort |
| P1 Fundament und Hülle | …-02-fundament-huelle.md |
…/dev-kn-ui |
kn/ui |
nach P0 |
| P2 Website, Chat, Verträge | …-03-website-chat-vertraege.md |
…/dev-kn-ui |
kn/ui |
nach P1 |
| P3 Funktionen-Rahmen | …-04-funktionen-rahmen.md |
…/dev-kn-ui |
kn/ui (rebased auf Hauptzweig nach Server-Merge) |
nach P2 und Server-Merge |
| P4 Funktionsmodule | …-05-funktionsmodule.md |
…/dev-kn-ui |
kn/ui |
nach P3 |
| P5 Abo-Modell Kern | …-06-abo-modell-kern.md |
…/dev-kn-server |
kn/server |
nach P0 |
| P6 Abo-Modell Betrieb | …-07-abo-modell-betrieb.md |
…/dev-kn-server |
kn/server |
nach P5 |
| P7 Bestellseiten schlank | …-08-bestellseiten.md |
…/dev-kundenansicht |
feature/kundenansicht-neu |
nach Merge von UI und Recht |
| P8 Rechtsrunde Fassung 6 | …-09-rechtsrunde.md |
…/dev-kn-recht |
kn/recht |
nach P0 |
| P9 WOMS | …-10-woms.md |
/home/webklar/apps/ticket.webklar.com/dev-kundenart |
feature/kundenart (WOMS, von origin/test) |
nach P0 |
| P10 Umschalten und Aufräumen | …-11-umschalten-aufraeumen.md |
…/dev-kundenansicht |
feature/kundenansicht-neu |
nach Phase E und OK des Inhabers |
Worktrees der Stränge entstehen aus dem Stand nach P0, zum Beispiel:
git -C /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht worktree add ../dev-kn-ui -b kn/ui kn-p0
- Danach
ln -s ../build/node_modules node_modulesim neuen Worktree. Der Symlink ist unversioniert, deshalb niegit add -A, Dateien immer einzeln hinzufügen. - Der WOMS-Worktree entsteht von
origin/test:git -C /home/webklar/apps/ticket.webklar.com/build worktree add ../dev-kundenart -b feature/kundenart origin/test. Danachnpm cimit Speicherbremse, siehe Global Constraints. - Der WOMS-Zweig
fix/akquise-sichtbar(5f13553, Worktreedev-akquise-sichtbar) ist nicht gepusht und ändertCreateLeadFormundcreateLead. P9 baut auforigin/test. Beim Merge intestgilt die Reihenfolge, die der Inhaber festlegt. Istfix/akquise-sichtbarbis dahin intest, rebased P9 darauf.
Dateizuständigkeit (Konfliktschutz)
Jede Datei hat genau einen Besitzer. Wer etwas aus einer fremden Datei braucht, nutzt die Verträge V1–V12.
| Datei / Bereich | Besitzer |
|---|---|
server/services/kundenansicht/schalter.js (neu), Route /dashboard.html in server/index.js, Konfig config.kundenansichtNeu, Feld ansicht in /api/auth/me; ein Platzhalter public/neu/dashboard.html für den Routentest (ab P1 gehört die Datei P1) |
P0 |
Werkzeug /root/reports/kundenansicht-neu-2026-10-15/werkzeug/ (eigenes Git), test/ui/kundenansicht-neu*.ui.mjs |
P0 legt an, P1–P4 und P7 ergänzen nur eigene Ansichten und Fixtures |
public/neu/dashboard.html, public/neu/tokens.css, basis.css, huelle.css, public/neu/dom.js, api.js, router.js, routen.js, huelle.js, leiste.js, konto.js, fristen-streifen.js, zaehler.js, etage.js, reiter.js |
P1 |
public/neu/ansicht-website.js, ansicht-chat.js, ansicht-vertraege.js, ansicht-stunden.js, ansicht-speicher.js, ansicht-gutschrift.js, ansicht-passwort.js, public/neu/ansichten.css; public/neu/ansicht-bestellen.js und ansicht-wechsel.js als 1:1-Übernahme der heutigen Bestell- und Wechselseiten im neuen Aussehen (alte Texte, Fassung 5, C14); öffentliche Seiten im neuen Stil unter public/neu/oeffentlich/ (login.html, kuendigen.html, widerrufen.html, offline.html), die erst P10 ausliefert |
P2 |
public/neu/ansicht-funktionen.js, funktion-einschalten.js, public/neu/funktionen.css, public/neu/bilder/* |
P3 |
public/neu/module.css (neues Aussehen aller Funktionsmodule). In public/termine*.js, shop*.js, buchungen*.js, zeiten*.js, funktionen.js (nur News), analyse*.js und funktion-bestellen.js nur Verzweigungen nach modell === 'abo' und die Kurzlabels der Reiter (C2) |
P4 |
Umbau von public/neu/ansicht-bestellen.js und ansicht-wechsel.js auf das schlanke Muster (Fassung 6). In server/routes/abo.js nur die Pfade für Fassung 6. server/services/planSwitch.js:351 mit Umgebung. server/services/vertragsNachweis.js (neu) |
P7 |
server/services/kundenart.js (neu), server/services/aboFunktionen.js (neu), server/services/funktionEinschalten.js (neu), server/services/fristen.js (neu), server/routes/funktionen.js (Katalog, Einschalten), server/routes/fristen.js (neu), server/routes/auth.js (nur sanitizeCustomer + Kundenart), server/services/customerAdmin.js (Kundenart), server/services/featureRegistry.js (Katalogfelder), server/services/featureConsents.js (Attribut kundenart), scripts/kundenansicht-setup.mjs (neu), Shop-/Unterkunft-Anfrage (Unternehmer aus Kundenart), Termine-Zustimmung ohne gesundheit im Abo-Modell |
P5 |
server/services/hourLedger.js (Vorschuss), server/routes/team/customers.js (Stundenkonto-Endpunkt), server/services/siteFeatures.js (adminActivate mit Abo-Prüfung und Abrechnung), server/routes/admin/funktionenAdmin.js, server/services/aboFunktionenAbgleich.js (neu), Wechsel mit gemerkter Funktion in planSwitch.js (nur details.funktion, completeSwitch, markSwitchFailed) und GET /api/abo (wechselFunktion), Ticket-Texte zum Stundenkonto, neue Mailvorlagen K38–K41 und T24–T25, Anpassung K02/K03/K22/K29/K30/T16 nur für das Abo-Modell |
P6 |
docs/rechtliches/*-v6*.md und neue Entwürfe, public/vertragsunterlagen/v5/* (Archiv), public/neu/rechtliches/* (neue Fassungen), server/services/products.js (Fassungstabelle), config.abo.termsVersionNeu, scripts/build-legal-*, scripts/rechtliches-hochladen.mjs (neu), docs/rechtliches/kundenansicht-entscheidungen.md (neu) |
P8 |
| WOMS-Repo komplett | P9 |
scripts/kundenart-bestand.mjs (neu), Umschalten, Aufräumen alter Dateien |
P10 |
geteilt: server/config.js (P0: Abschnitt kundenansichtNeu; P5: Abschnitt aboFunktionen; P8: abo.termsVersionNeu; jeder nur sein eigener Abschnitt), test/support/schema.js (nur neue Collection-/Attribut-Quellen ergänzen), scripts/setup-abo-collections.mjs (nicht ändern; neue Attribute nur in scripts/kundenansicht-setup.mjs) |
wie angegeben |
Bestehende Testdateien ändert ein Paket nur dort, wo sein Plan sie ausdrücklich nennt, etwa für Pins auf Texte, die das Paket ändert.
Global Constraints
- Alte Ansicht unverändert. Bis P10 ist jede Verhaltensänderung für Kunden ohne neue Ansicht verboten. Neue Server-Logik prüft
neueAnsicht(req), ist rein additiv (neue Felder, neue Endpunkte) oder betrifft nur Datensätze, die erst über die neue Ansicht entstehen (unlockedBy: 'abo' | 'projekt'). - Schalter:
KUNDENANSICHT_NEUistoff | pilot | on, Vorgabeoff.KUNDENANSICHT_NEU_PILOT_EMAILSist eine Komma-Liste,*heißt alle. Der Modusoffschlägt jede Liste. - Aussehen (Spec 5.1, Werte wörtlich aus dem Ticketsystem
src/styles/global.css:7-51):- Farben:
#fffFläche, Text#1a1f36, Zweittext#5e6678, Tertiär#8a93a3nur für Nebensächliches, Linien#ebeef1und#d8dde3, Hover#f6f8fa, Fläche#f8f9fb, Akzent#111. - Status-Pillen (Fläche/Schrift): grau
#ebeef1/#414552, blau#e0f2fe/#075985, orange#fcedb9/#a82c00, violett#efeafe/#5b21b6, grün#d7f7c2/#006908, rot#ffe7f2/#b3093c. Fehler#c0123c. - Radien 6 und 12 px. Ring-Schatten
0 0 0 1px rgba(60,66,87,.16), 0 1px 1px rgba(0,0,0,.06). - Systemschrift
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif. Kein Gewicht 800, Zahlentabular-nums. - Verboten: Karten, Verläufe, Glow, Punktraster, Glas als Deko, Dunkelmodus, Lila.
- Ladezustände als Text („Wird geladen …“). Dialoge schließen nur per × und Esc.
- Farben:
- Anrede „du“ in allen neuen Texten der Oberfläche. Ausnahmen: Rechtsdokumente und bestehende Mails (Spec 5.7); die Bestellseiten bekommen „du“ erst mit Fassung 6 (C14, P7).
- Keine Preise für Funktionen in der neuen Oberfläche, nirgends.
- Hashes: Alle alten Hashes bleiben gültig (
VIEW_HASHES, Aliase#wartung,#abo,#fortschritt). Die Query nach?gelangt unverändert ins Modul. Neue Hash-Namen nur aus[a-z]wegen der Login-Whitelist/^\/dashboard\.html(#[a-z]+)?$/(public/app.js:466,public/login.html). - Rechtlich fest (Spec 4):
- Kerninfo und „Zahlungspflichtig bestellen“ stehen auf demselben Bildschirm.
- Der Knopftext bleibt wörtlich.
- „Verträge hier kündigen“ ist immer erreichbar. „Vertrag widerrufen“ steht immer im Konto.
kuendigen.htmlundwiderrufen.htmlbleiben schlicht.
- Kundenart: Der Server ist die einzige Quelle. Er nimmt nie die Kundenart vom Client. Eine leere oder unbekannte Kundenart gilt als
privat(E4), überall überkundenartWirksam()(V4). - Nachweise: Wortlaut, Fassung, Zeit, IP, Browser und Konto werden weiter gespeichert, ab jetzt mit der Kundenart zum Klickzeitpunkt.
- Tests Portal (aus dem jeweiligen Worktree):
- Einzeldatei:
timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/<datei>.test.js. - Mehrere Dateien in einem Aufruf nur mit
--test-concurrency=1. - Die ganze Suite nur in Phase E und P10:
timeout 2400 nice -n 10 systemd-run --scope -q -p MemoryMax=2500M -p MemorySwapMax=0 env NODE_OPTIONS=--max-old-space-size=1024 node --test --test-concurrency=2 test/*.test.js. - Vor jedem schweren Lauf
free -m. Bei weniger als 2000 MB verfügbar warten und den Controller melden.
- Einzeldatei:
- Tests WOMS:
cd /home/webklar/apps/ticket.webklar.com/dev-kundenart && timeout 900 nice -n 10 systemd-run --scope -q -p MemoryMax=1500M -p MemorySwapMax=0 env NODE_OPTIONS=--max-old-space-size=1024 npm test. - Playwright nur mit dem Werkzeug aus P0 (
run-browser.sh, eine Instanz, Speicherbremse).chromium-1234,playwright-core1.62.1. - Commits: Dateien einzeln hinzufügen. Nachrichten deutsch, mit Zeile
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>.- Kein Push, kein
--force, kein Merge aufmainodertest. Pushen tut nur der Inhaber. - Keine Änderungen in
build/, weder im Portal noch im WOMS.
- Kein Push, kein
- Sicherheitsblock in jedem Agent-Auftrag (Memory „feedback-destruktive-skripte“):
- Kein
rm -rfaußerhalb des eigenen Scratch-Ordners, nie/dev,/etcoder/anfassen. - Kein
docker run, keine globalen Installationen. - Das Vorschau-Skript nie mit Ziel aufrufen, nur
--pruefen. Nie mit Pfaden wie..testen. TMPDIRauf den eigenen Scratch-Ordner setzen.- Nach großen Läufen prüfen, dass
ls -l /dev/nullcrw-rw-rw-zeigt.
- Kein
- Prod-Daten: Kein Paket schreibt in die Live-Datenbank. Ausnahmen sind die Setup-Skripte (
scripts/kundenansicht-setup.mjs) undscripts/kundenart-bestand.mjs. Beide laufen nur auf ausdrückliche Anweisung des Controllers nach OK des Inhabers, vorher immer mit--pruefen.
Review Focus
- Kundenart leer, unbekannt oder vom Client gefälscht: Ohne gesetzte Kundenart sieht der Kunde das eine Häkchen, und der Server verlangt es auch. Ein Body-Feld
kundenartvom Client wird ignoriert. Tests: P5 Task „kundenartWirksam“ und „Einschalten privat in Frist“, P7 Task „Bestellung Fassung 6“. - Pilot-Schalter an den Rändern: Groß-/Kleinschreibung und Leerraum der E-Mail,
*, leere Liste,offmit*, Mitarbeiter-Kundenansicht (viewAs+viewId) im Pilot. Ohne neue Ansicht liefern die Abo-Modell-Endpunkte 404, und/dashboard.htmlist die alte Seite. Tests: P0 Tasks „Schalter“ und „Dashboard-Weiche“, P5 Task „Einschalten nur in neuer Ansicht“. - Alte Links nach dem Umbau:
#termine?bestellt=1&projekt=x,#webshop?stripe=zurueck&projekt=x,#rechnungen,#stunden,#gutschrift,#wartung,#fortschritt,?abo=success, Login mitnext=/dashboard.html#chat. Jeder Link landet am richtigen neuen Ort, und die Query bleibt erhalten. Test: P1 Task „Routen“. - Doppeltes oder widersprüchliches Einschalten: Zwei Taps, schon aktiv, zweite Website, Hosting-Kunde bei einer Wartungs-Funktion (Wechsel mit gemerkter Funktion), gescheiterter Wechsel, Privatkunde in der Frist ohne Häkchen, Funktion per Schalter aus. Ergebnis: höchstens ein Ticket, klare 409-Codes, kein Zustand ohne Nachweis. Tests: P5 Task „Einschalten“, P6 Task „Wechsel mit Funktion“.
- Vorschuss statt Mehraufwand: Nicht gedeckte Einrichtungsminuten erscheinen nie als Mehraufwand. Normale Arbeit über dem Guthaben bleibt Mehraufwand. Eine spätere Gutschrift gleicht den Vorschuss zuerst aus. Bei Vertragsende bleibt ein Restminus stehen, wird aber nicht berechnet. Gilt auch für Git-Push-Arbeitsblätter auf Einrichtungs-Tickets. Tests: P6 Task „Vorschuss im Stundenkonto“, P9 Task „Abo-Vorgabe bei Einrichtungs-Tickets“.
Verträge
V1 Pilot-Schalter (P0)
server/config.js:
kundenansichtNeu: { mode: 'off'|'pilot'|'on', pilotEmails: string[] } // aus KUNDENANSICHT_NEU, KUNDENANSICHT_NEU_PILOT_EMAILS (commaList)
server/services/kundenansicht/schalter.js:
export function neueAnsichtModus() // 'off' | 'pilot' | 'on'
export function neueAnsichtFuerSession(session) // boolean
export function neueAnsicht(req) // = neueAnsichtFuerSession(req.session ?? parsePortalSession(req))
Regeln für neueAnsichtFuerSession:
off→ false.on→ true für jede gültige Kunden- oder Kundenansicht-Sitzung.pilot→ true, wenn eine dieser Bedingungen gilt:session.viewAs === true && session.viewId(Mitarbeiter-Kundenansicht),- die Liste enthält
'*', - die Liste enthält die getrimmte, klein geschriebene
session.email.
- Ohne Sitzung → false.
Auslieferung und Clients:
app.get('/dashboard.html')steht vorexpress.static(server/index.js:154). Gilt die neue Ansicht, liefert die Routeres.sendFile(public/neu/dashboard.html)mitCache-Control: no-cache. Sonstnext(), und die alte Seite kommt wie heute.GET /api/auth/mebekommt zusätzlichansicht: 'neu' | 'alt'.- Die alte
app.jslädt ohne Weiche. Die neue Seite lädt nur/neu/*-Dateien und die gemeinsamen Funktionsmodule.
V2 Dateien und Module der neuen Oberfläche (P1, P2, P3)
Grundregeln
- Alle neuen Dateien liegen unter
public/neu/. ES-Module (<script type="module" src="/neu/huelle.js">), kein Build. - Wurzel
<body class="kn">. Klassen und Tokens tragen das Präfixkn-bzw.--kn-. - Die gemeinsamen Funktionsmodule behalten ihre Klassen (
tm-*,sh-*,bk-*,zt-*,an-*,fn-*,kb-*). Ihr neues Aussehen steht inpublic/neu/module.css(P4) unter.kn.
public/neu/dom.js
export function h(tag, attrs = {}, ...kinder) // wie termine-dom.js/shop-dom.js; attrs.on = { click: fn }
export function leeren(el)
export function text(el, wert)
public/neu/api.js
export async function api(pfad, { method = 'GET', body, roh = false } = {})
// Wirft ApiFehler { status, code, message, field, body }. Bei code 'session_revoked' Redirect auf /login.html.
// Netzwerkfehler: status 0.
export class ApiFehler extends Error {}
public/neu/routen.js (rein, ohne DOM, testbar)
export const BEREICHE = ['website', 'funktionen', 'chat', 'vertraege']
export const ROUTEN // hash → { bereich, reiter?: string, etage?: string }
export function routeAusHash(hash) // '#termine?bestellt=1' → { bereich:'funktionen', etage:'termine', query:'bestellt=1' }
export function hashFuerRoute(route) // Umkehrung
Pflichtinhalt von ROUTEN:
''/'#'→ websiteprojekt→ website, etageprojektfortschritt→ website (scrollt zur Aktivität)funktionen→ funktionen, reitermeine;entdecken→ funktionen, reiterentdeckennews,termine,webshop,buchungen,zeiten,analyse→ funktionen, etage = gleicher Namechat→ chatvertraege,abo,wartung→ vertraege, reiteraborechnungen→ vertraege, reiterrechnungenstunden→ vertraege, reiterstundenspeicher(neu) → vertraege, reiteremailgutschrift→ vertraege, etagegutschriftwechsel→ vertraege, etagewechselbestellen-hosting,bestellen-wartung→ vertraege, etagebestellen, plus Produktpasswort→ website, etagepasswort(Rückweg = Herkunft)
public/neu/router.js
export function starteRouter({ zeige }) // liest location.hash, hört auf popstate, ruft zeige(route, { nav: 'tab'|'push'|'back'|'same' })
export function geheZu(hash, { ersetzen = false } = {}) // Reiterwechsel: replaceState; Etage 2, Sheet, Seiten: pushState
Hülle
huelle.js(Einstieg): lädt/api/auth/meund leitet ab wie heute.leiste.js: Fahrstuhl-Zeile Handy, Menü links am Desktop.konto.js: Konto-Sheet bzw. Popover.fristen-streifen.js,zaehler.js.etage.js: Etage 2 mit Zurück und Reitern.reiter.js: gemeinsame Unterstrich- und Balken-Reiter.
Ansichten. Jede exportiert:
export async function zeigeX(wurzel, { route, me, nav }) // rendert in wurzel, darf Daten selbst laden
Funktionsmodule werden von der neuen Hülle wie heute aufgerufen: initTermine(document.getElementById('termine-body')) usw. Die Projektwahl übergibt die Hülle einheitlich als wurzel.dataset.wunschProjekt (C1). News und Analyse lesen dasselbe Attribut zusätzlich zu ihren bisherigen Modulvariablen (P4).
V3 Fahrstuhl-Zeile und Hülle (P1)
- Referenz ist
/root/reports/kundenansicht-neu-2026-10-15/handy-leiste/final-2-frei.html: Balken mit Overlay-Kopie und Gegenbewegung, Rückfallfb, Etage 2 alsreelmittranslateY(-56px), Blattt2-dp. - Handy-Layout gilt mit derselben Medienabfrage wie heute:
(max-width: 880px), (pointer: coarse) and (max-height: 500px). - Leiste:
- Wörter:
Website,Funktionen,Chat,Verträge. Schrift 15 px/600, bei Platzmangel 14 px. - Balken: Radius 6, Höhe 36 px.
- Tap-Ziele mindestens 44 px. Unten
env(safe-area-inset-bottom). - Bei offener Tastatur wird die Leiste ausgeblendet. Die
visualViewport-Logik von heute wird übernommen.
- Wörter:
- Etage 2: „Zurück“ und die Reiter der Unterseite. Hat die Unterseite keine Reiter, füllt „Zurück“ die ganze Zeile. Bei Platzmangel scrollt die Zeile waagerecht, und der aktive Reiter wird in den sichtbaren Bereich gerückt (C2).
- Desktop: Menü links mit 232 px und Haarlinie rechts, aktiver Eintrag schwarz. Unterseiten zeigen eine Brotkrume „Funktionen / Terminplaner“. Oben rechts das Profilbild, das Konto öffnet als Popover.
- Konto (Reihenfolge laut Spec 5.2):
- Name und E-Mail
- Passwort ändern
- Datenschutz
- Impressum
- App installieren (nur wenn sinnvoll)
- Verträge kündigen (
/kuendigen.html) - Vertrag widerrufen (
/widerrufen.html, immer da; hervorgehoben mit der Pille „Frist läuft“, wennGET /api/fristenanzeigenliefert) - Abmelden
- Widerrufsstreifen: erscheint, wenn
anzeigen === true, mit dem TextWiderrufsfrist bis TT.MM. · Widerrufen(früheste Frist, V6). Nicht wegtippbar, bei offener Tastatur ausgeblendet. - Zähler:
- Chat:
GET /api/chat/unread-count, Poll alle 45 s wie heute. - Verträge: Zahl der Rechnungen mit
balance > 0ausGET /api/invoicesplus Rückfragen ausGET /api/abo/takeover-credits(status === 'rueckfrage'). Beides wird nach einer Zahlung, bei der Rückkehr in den Tab und beim Reiterwechsel neu geladen. - „!“ steht bei
subscription.paymentProblem. Nicht beiswitchPending.
- Chat:
- Bewegung reduziert: keine Federn, nur 150 ms Überblenden.
V4 Kundenart (P5)
server/services/kundenart.js:
export const KUNDENARTEN = ['unternehmen', 'privat'] // Re-Export aus auftraege/konstanten.js
export function kundenartGespeichert(customerDoc) // 'unternehmen' | 'privat' | ''
export function kundenartWirksam(customerDoc) // 'unternehmen' | 'privat' (leer/unbekannt → 'privat')
Wo die Kundenart erscheint
GET /api/auth/meliefert incustomerzusätzlichkundenart: kundenartGespeichert(doc)undkundenartWirksam: kundenartWirksam(doc). Das gilt für alle drei Zweige (Login, Kundenansicht, Kunde).
Anlegen und Ändern (customerAdmin.js)
createCustomerWithPortalAccess(payload)nimmtkundenart. Pflichtwert ist'unternehmen' | 'privat', sonst 400 mitcode: 'kundenart_fehlt'.updateCustomerWithPortalAccessnimmtkundenartoptional, mit gleicher Prüfung.sanitizeCustomergibtkundenartzurück.
Nachweis
featureConsentsbekommt das Attributkundenart(string 12). Es wird inscripts/kundenansicht-setup.mjsangelegt und vontest/support/schema.jsgelesen.- Jede neue Zustimmung speichert
kundenartWirksamzum Klickzeitpunkt.
V5 Katalog und Einschalten (P5)
Abo-Zuordnung. server/services/aboFunktionen.js:
export const ENTHALTEN_IN = Object.freeze({ analyse: ['hosting','wartung'], news: ['wartung'], termine: ['wartung'], shop: ['wartung'], unterkunft: ['wartung'], zeiten: ['wartung'] })
export async function aboProdukt(customerId) // { produkt: 'hosting'|'wartung'|null, laeuft: boolean, gekuendigt: boolean, widerrufsfristBis: string|null /*ISO, nur Abschluss/Wechsel*/ }
export async function aboDeckt(customerId, key) // { ok: true } | { ok: false, grund: 'kein_abo'|'falsches_abo'|'gekuendigt'|'nicht_aktiv' }
laeuftgilt für die Statusactive | trialing | past_due, wieACTIVE_STATUSES.gekuendigtbedeutet „Kündigung vorgemerkt“.aboDecktliefert danngekuendigt, eingeschaltet wird nicht.
GET /api/funktionen. Ohne neue Ansicht bleibt die Antwort unverändert. Mit neuer Ansicht kommen hinzu:
modell: 'abo',
kundenart: 'unternehmen'|'privat',
abo: { produkt, laeuft, gekuendigt, widerrufsfristBis },
katalog: [{
key, // Reihenfolge: news, termine, shop, unterkunft, zeiten, analyse
name, beschreibung, // beschreibung: höchstens 2 Sätze, du-Form
bild: '/neu/bilder/<key>.webp',
enthaltenIn: 'hosting'|'wartung', // kleinstes Abo, das die Funktion enthält
verfuegbar: boolean, // Schalter der Funktion für diesen Kunden (off → false, Pilot → Liste)
ablauf: 'direkt'|'fragebogen'|'anfrage', // termine: fragebogen; shop, unterkunft: anfrage; sonst direkt
einrichtung: string, // Dauer, z. B. 'meist in 1–2 Wochen'
unterlagen: { leistungsbeschreibung: {version,url}|null, avv: {version,url}|null },
nurBetrieb: boolean // shop: true (C15)
}]
- Je Projekt und Funktion zusätzlich
anfrageStatus: ''|'offen'|'angenommen'|'abgelehnt'|'zurueckgezogen'(nur Shop und Unterkunft). - Funktionen mit
verfuegbar: falsestehen trotzdem im Katalog. Die Oberfläche blendet sie aus (C9). - Die Analyse erscheint im Katalog nur, wenn
ANALYSE_ENABLED === 'on'.
POST /api/funktionen/:key/einschalten. Ohne neue Ansicht → 404. In der Kundenansicht → 403. Für analyse → 404, die Analyse nutzt weiter POST /api/funktionen/analyse/activate.
Body:
{ projectId, zustimmung: { version, haken: [] | ['beginn'] } }
Antwort 200:
{ feature: featureView(doc, key), ticketWoid }
Fehler 409 mit code, Prüfreihenfolge:
nicht_verfuegbar(Schalter)abo_fehltmitbody.grundundbody.wechsel: true, wenn Hosting läuft und die Funktion Wartung brauchtnur_betrieb(Shop bei privat)bestellung_offenmitbody.offen, wie heute: Fragebogen beim Terminplaner, Anfrage nicht angenommen bei Shop und Unterkunftbeginn_noetig, wennkundenartWirksam === 'privat', die Widerrufsfrist des Abos läuft undhakennicht'beginn'enthält;body.abnennt das ISO-Datum nach Fristendebereits_eingeschaltet(Zustandsetupoderactive)fassung_veraltet
Erfolg:
einschalten({ customerId, projectId, key, by: 'kunde', abrechnung: 'abo', nachweis, now })(V7).- Der Nachweis kommt in
featureConsentsmit Texten. Pflicht sindeinschalten-zeile(Linkzeile) undknopf(Einschalten). Das Häkchenbeginnkommt nur dazu, wenn es nötig war.
Zustimmungs-Register für das Abo-Modell. Jede Funktion im Register bekommt consentAbo:
{ version, knopf: 'Einschalten', items: [{ id: 'einschalten-zeile', text, haken: false }, { id: 'beginn', text, haken: true, nurPrivatInFrist: true }] }
- Den Wortlaut von
beginnlegt P8 fest. Bis dahin gilt der Entwurf aus Spec 5.6, Punkt 3, für die Einrichtung. - Das Item
gesundheitentfällt im Abo-Modell. Der Fragebogen sperrt den Gesundheitsbereich weiter.
Anfragen im Abo-Modell
- Shop:
unternehmerwird nicht mehr vom Client verlangt, sondern auskundenartWirksam === 'unternehmen'gesetzt. Privat führt zu 409nur_betrieb. keineAusgeschlossenenbleibt Pflicht als Satz über dem Knopf. Der Client sendettruebeim Klick auf „Anfrage senden“, der Nachweis kommt in die Anfrage.
V6 Fristen (P5)
server/services/fristen.js:
export async function sammleFristen(customerId, { now = new Date() } = {})
// → { fristen: [{ quelle: 'abschluss'|'wechsel'|'speicher'|'funktion'|'projekt', bezeichnung, bis /*ISO*/, featureKey?, projektName? }],
// fruehestesEnde: string|null, anzeigen: boolean }
anzeigenist wahr, wennkundenartWirksam(customer) === 'privat'gilt undfristen.length > 0.- Quellen:
openWithdrawals(Abo),paidFeatureOrders+withdrawalDeadline(gekaufte Funktionen) und die Aufträge (nur beiAUFTRAEGE_ENABLEDund privat).
Route GET /api/fristen (server/routes/fristen.js, Mount /api/fristen, requireSession). Antwort wie oben. In der Kundenansicht ist der Aufruf erlaubt, weil er lesend ist.
V7 Generisches Einschalten und Tickets (P5, P6, P9)
server/services/funktionEinschalten.js:
export async function einschalten({ customerId, projectId, key, by, abrechnung, nachweis = null, staff = null, now = new Date() })
// by: 'kunde'|'team'; abrechnung: 'abo'|'projekt'
// → { doc, ticketWoid }
- Setzt
state: 'setup',unlockedBy: abrechnung === 'projekt' ? 'projekt' : 'abo'undunlockedByPurchase: false. - Legt das Ticket über
createFeatureTicketan, unter der Sperrefeature-order:${customerId}:${projectId}. - Für
zeitennutzt die Funktion den bestehenden Weg (einschaltenZeiten, mitunlockedByje nachabrechnung).
Ticket-Attribute (workorders, angelegt in scripts/kundenansicht-setup.mjs):
einrichtungFuer(string 16): FunktionsschlüsseleinrichtungAbrechnung(string 8):'abo' | 'projekt' | ''
Gesetzt werden sie von createFeatureTicket, sobald der Aufrufer abrechnung übergibt (neuer optionaler Parameter). Der Ticket-Text sagt bei abo: „Zeit im Arbeitsblatt erfassen und aufs Abo-Stundenkonto anrechnen (Einrichtung aus dem Abo, Vorschuss möglich).“ Bei projekt: „Zeit im Arbeitsblatt erfassen, nicht aufs Abo anrechnen (im Projekt enthalten).“
unlockedBy hat die Werte 'kauf'|'abo'|'team'|'projekt'. unlockedByVon und featureView liefern unlockedBy künftig für alle Funktionen, nicht nur für aboInklusive.
V8 Stundenkonto mit Vorschuss (P6)
computeLedger(credits, usage, opts):
- Ein Eintrag mit
einrichtung === trueist gesetzt, wenn das Arbeitsblatt zu einem Ticket miteinrichtungAbrechnung === 'abo'gehört. Den Join machtlistPlanUsage, auch für Git-Blätter. - Der ungedeckte Rest eines solchen Eintrags zählt als
vorschussMinutes, nicht alsoverageMinutes. - Gedeckt wird wie heute: FIFO, rückwirkend,
expiresAtnach der Arbeitszeit. Neu: Gutschriften, die erst nach der Arbeitszeit gebucht wurden, decken Einrichtungseinträge vor jedem anderen Eintrag. So gleicht eine spätere Gutschrift zuerst den Vorschuss aus. - Ergebnis zusätzlich:
{ vorschussMinutes, usage: [{ ..., einrichtung: boolean, vorschussMinutes }] }
- Bei
active === falsebleibtvorschussMinutesstehen. Die Oberfläche zeigt dann „verfällt, wird nicht berechnet“. - Team-Endpunkt
GET /api/team/customers/:id/stundenkonto(requireStaffJwt):
{ produkt: 'hosting'|'wartung'|null, ledger: { balanceMinutes, vorschussMinutes, overageMinutes, nextExpiry } | null }
V9 Kundenart und Abo-Vorgabe im WOMS (P9 gegen V4, V7, V8)
- Pflichtfeld Kundenart, ohne Vorgabe. Die Auswahl erfolgt per Segment-Schalter
.segmit den Labels „Unternehmen“ und „Privatperson“, Werte ausKUNDENART_OPTIONENohne den Leer-Eintrag. Es steht an diesen Stellen:CreateLeadForm:createLeadschreibtkundenart.CreateCustomerForm: Body anPOST /api/admin/customersmitkundenart.- „Zum festen Kunden machen“: Upgrade nur mit gesetzter Kundenart.
- Arbeitsblatt-Fluss „Zugesagt“ (
worksheetFlow.js): Ohne Kundenart wird nicht hochgestuft, das Team bekommt einen Hinweis.
- Kundenliste: Schalter „Kundenart fehlt“ in der Toolbar, kein neuer Reiter.
- Arbeitsblätter: Bei Tickets mit
einrichtungAbrechnung === 'abo'stehtcountsToPlanstandardmäßig auf an, im Modal und beim Nachtragen der Zeit eines Git-Blatts. Bei'projekt'steht es auf aus. - Ticket-Seite eines Einrichtungs-Tickets: zeigt Kontostand und Vorschuss aus V8.
- Funktionen-Reiter:
- Kein Preis mehr (
funktionenView.js:22). - „Aktivieren“ fragt „aus dem Abo“ oder „im Projekt enthalten“ und sendet
abrechnung. - Die Abo-Prüfung macht der Server (V10). Antwortet er mit
abo_fehlt, zeigt WOMS den Grund.
- Kein Preis mehr (
V10 Team-Aktivierung (P6)
POST /api/team/funktionen/:customerId/:key mit action: 'activate' nimmt zusätzlich abrechnung: 'abo'|'projekt' (Vorgabe 'abo').
- Ohne
aboDeckt(customerId, key).ok→ 409 mitcode: 'abo_fehlt'undgrund(E21). Ausnahme: Dokumente mitunlockedBy === 'kauf'(Bestand). - Die Aktivierung läuft über
einschalten({ by: 'team', abrechnung, staff })mit Ticket. Wie bisher wird nicht direktactivegesetzt. Ausnahme:action: 'activate'auf einen ZustanddisabledmitdisabledReason === 'team'setzt wie heute direktactive. - Die Textform-Regeln bleiben, wie sie heute sind (
textformFuerAktivierung).
V11 Wechsel mit gemerkter Funktion (P6)
POST /api/abo/switchnimmt optionalfunktion: { key, projectId }, aber nur mit neuer Ansicht. Ohne neue Ansicht wird das Feld ignoriert. Es wird indetails.funktiondes Ereignisseswechselgespeichert.GET /api/aboliefert insubscriptionzusätzlichwechselFunktion: { key, projectId } | null, nur mit neuer Ansicht:- gesetzt, wenn das jüngste
wechsel-Ereigniserledigtist,details.funktionhat und diese Funktion für das Projekt nochoffist, längstens 14 Tage nachdoneAt; - bei
gescheitertoderwiderrufen→null.
- gesetzt, wenn das jüngste
- Ein Widerruf des Wechsels beendet zusätzlich alle Funktionen mit
unlockedBy === 'abo', deren Schlüssel nur inwartungsteckt (disable(..., { reason: 'abo_ende' })). - Client (P3 → P2/P7): Antwortet „Einschalten“ mit
abo_fehltundwechsel: true, schreibt die OberflächesessionStorage['kn-wechsel-funktion'] = JSON.stringify({ key, projectId })in einem try/catch und geht zu#wechsel.ansicht-wechsel.jssendet den Wert alsfunktionmit und löscht ihn nach Erfolg. Steht inGET /api/aboeinsubscription.wechselFunktion, öffnetansicht-funktionen.jsbeim nächsten Aufruf den Einschalt-Ablauf dieser Funktion.
V12 Abo-Fassung 6 und Bestellseiten (P8, P7)
server/config.js (P8):
abo.termsVersionNeu = '<JJJJ-MM-TT>-v6' // das Datum setzt P8; config.abo.termsVersion bleibt v5
server/services/products.js (P8): Statt endsWith steht dort eine Fassungstabelle.
export const FASSUNGEN // [{ muster: /-v6$/, url:'/abo-bedingungen-v6.html'?, pdfFile, endParagraph }, { muster: /-v5$/, ... v5-Archiv }, { muster: /-v4$/, ... }, { sonst: v3 }]
export function aktuelleAboFassung(req) // neueAnsicht(req) ? config.abo.termsVersionNeu : config.abo.termsVersion
export function isTermsV4OrNewer(t) // v4, v5, v6
termsDocForundcontractAttachmentForlesen aus der Tabelle.- Die Seite der Fassung 6 entsteht unter einem eigenen Pfad, solange der Schalter nicht
onist. Erst P10 macht sie zu/abo-bedingungen.html, nach Archivierung von v5 unterpublic/vertragsunterlagen/v5/. Den Pfad legt P8 fest und trägt ihn in diese Übersicht nach.
Server-Regeln für Fassung 6 (P7). Sie gelten nur, wenn der Body termsVersion === config.abo.termsVersionNeu trägt und neueAnsicht(req) gilt:
POST /api/abo/checkout
acceptTerms === true(der Client sendet es beim Klick auf „Zahlungspflichtig bestellen“), dazuearlyStart: boolean.unternehmen→startChoice: 'sofort',earlyStartConsentAt: '', Grundkundenart: 'unternehmen'im Ereignis.privatundearlyStart→sofortmitearlyStartConsentAt.privatohneearlyStart→nach_frist.
POST /api/abo/switch
earlyStartist nur beiprivatPflicht.- Bei
unternehmengilt der Wechsel ohneearlyStartConsentAt, der Grund steht im Ereignis.planSwitch.js:351bekommt dafür den Parameterkundenart.
POST /api/abo/storage: unverändert. Das Plus sendet acceptTerms: true beim Klick.
Nachweis. server/services/vertragsNachweis.js:
export async function speichereVertragsNachweis({ customerId, art: 'abo'|'wechsel'|'speicher'|'gutschrift', bezug, texte, fassungen, kundenart, req, at })
- Speichert in
featureConsentsmitfeatureKey = artundprojectId = ''. stripeSessionIdist die Sitzungs-ID, wenn vorhanden.texte= die angezeigten Zeilen, Häkchen und Knopf.
V13 Mails (P6)
- Neue Kennungen K38–K41 und T24–T25 (frei laut
test/mailTemplates.test.js). Belegt werden:- K38 „Deine Funktion wird eingerichtet“ (Einschalten im Abo-Modell)
- K39 „Deine Funktion wurde beendet“ (Abo-Ende für News, Shop, Unterkunft im Abo-Modell; Platzhalter
funktion,projekt,loeschtag) - T24 „Funktion eingeschaltet (Abo)“ ans Team, mit Block
vorschuss, wenn das Guthaben nicht reicht
- K40, K41 und T25 bleiben frei für P6, falls nötig. P6 trägt die Belegung nach.
- Alle neuen Vorlagen sind in „du“-Form und im Layout von K16.
Ablaufhinweise für den Controller
- P0 zuerst. P0 endet mit dem Tag
kn-p0und dem Arbeitsauftrag an den Inhaber: In WOMS „Neues Projekt“ (Website, interner Kunde) mit Subdomainkundenansicht-neuanlegen, damithttps://kundenansicht-neu.project.webklar.comexistiert. Vorher wird die Vorschau nicht ausgeliefert. - Phase B: vier Stränge parallel. P9 baut gegen V4, V7, V8 und V10, mit dem Testbackend der WOMS-Vorschau (Whitelist wie
customerAdmin.js, siehe Karte woms-vorschau Fund 2). - Merges in den Hauptzweig nach der Reihenfolge oben. Danach jeweils die betroffenen Testdateien mit den Befehlen aus den Global Constraints.
- Phase E:
- ganze Suite Portal und WOMS
- Werkzeug-Gates aller Ansichten auf Handy, Tablet und Desktop
- Vorschau aktualisieren (nur
cp) - Abschlussprüfung mit vier Blickwinkeln: Recht und Nachweise, alte Ansicht unverändert, Handy und Barrierefreiheit, Abo-Modell und Stundenkonto
- Korrekturen
- Übergabe-Notiz unter
/root/reports/kundenansicht-neu-2026-10-15/uebergabe/mit den Push-Befehlen für den Inhaber
- Rechtsrunde: P8 legt
docs/rechtliches/kundenansicht-entscheidungen.mdan. Der Controller geht die Liste mit dem Inhaber durch (E20). Hochgeladen wird erst nach seiner Freigabe, mitscripts/rechtliches-hochladen.mjs, zuerst--probe. - P10 startet erst, wenn der Inhaber ausdrücklich „umschalten“ sagt.