Plaene fuer das weisse Design (Uebersicht, P0-P10)

Umsetzungsplaene je Paket mit gemeinsamen Regeln (env.sh/check.sh,
Gate-Schema, CSS-Reihenfolge, TP2-Pruefung vor jedem Paket) und den
Schnittstellen aus P1. Gegen Spec und untereinander geprueft (zwei
Pruefer, Korrekturrunde, Nachpruefung).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-06 06:02:54 +00:00
1 parent 9d604aa550
commit 4af78f0bad
12 files changed
+7381

No files matched your search

@@ -0,0 +1,269 @@
# WOMS „Weißes Design“ – Übersicht der Umsetzungspläne
> **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:** Das ganze Ticketsystem WOMS bekommt das freigegebene weiße Design (Akzent Schwarz, kaum Rahmen, kaum Icons). Ein Ticket öffnet künftig als eigene Seite. Die Umsetzung kommt in elf einzeln auslieferbaren Paketen.
**Architecture:**
- P1 legt Tokens, Gerüst, Bausteine und eine Übergangsschicht (KOMPAT) in `global.css` an. Danach ist die App sofort weiß.
- P2 bis P9 bauen je einen Bereich auf die neuen Bausteine um.
- P10 räumt Altlasten und Icon-Bibliotheken ab.
- Jedes Paket geht nur über ein Git-Tag live.
**Tech Stack:**
- React 18
- react-router-dom 6
- Vite 5
- Appwrite Web SDK 13 (Legacy-Query-Syntax)
- reines CSS ohne Framework
- Tests: `node --test` mit esbuild-Lader (`tests/unit/load.mjs`)
- Screenshots: playwright-core 1.62.1 und chromium-headless-shell 1234
**Spec:**
- `docs/superpowers/specs/2026-10-04-weisses-design-design.md`, verbindlich
- Anhang `docs/superpowers/specs/2026-10-04-weisses-design-umbaukarte.md`
- Prototyp: `/root/reports/ticketsystem-design-2026-10-04/quelle/` (`styles.css`, `app.js`)
## Pläne
| Paket | Datei | Kurz |
|---|---|---|
| P0 | `2026-10-05-weisses-design-p0-vorbereitung.md` | Worktree lauffähig, Totcode, Screenshot-Werkzeug |
| P1 | `2026-10-05-weisses-design-p1-fundament.md` | Tokens, KOMPAT, Gerüst, Menü, Bausteine, Dialog, Login, Pflicht-Fixes |
| P2 | `2026-10-05-weisses-design-p2-tickets.md` | Ticketliste, Ticket-Seite, Arbeitsblätter, Ticket-Dialoge |
| P3 | `2026-10-05-weisses-design-p3-planboard.md` | Planboard |
| P4 | `2026-10-05-weisses-design-p4-kunden.md` | Kundenliste, Kundenseite |
| P5 | `2026-10-05-weisses-design-p5-projekte.md` | Projekte, Zuordnen, Services, Projekt-Bausteine |
| P6 | `2026-10-05-weisses-design-p6-kunden-unterseiten.md` | Chat, Gutschriften, Abo-Angebote, E-Mail-Speicher |
| P7 | `2026-10-05-weisses-design-p7-finanzen.md` | Finanzen |
| P8 | `2026-10-05-weisses-design-p8-rechtliches-admin.md` | Rechtliches, Admin |
| P9 | `2026-10-05-weisses-design-p9-serverpuls.md` | Serverpuls |
| P10 | `2026-10-05-weisses-design-p10-aufraeumen.md` | KOMPAT, Aliase, Icon-Pakete entfernen |
## Reihenfolge
- Die Pakete werden strikt in der Reihenfolge P0 → P10 umgesetzt.
- P0 geht nicht allein live, sondern zusammen mit P1. Das Tag `design-p1` enthält die P0-Commits.
- An Paket N+1 wird erst gearbeitet, wenn `design-p<N>` gesetzt ist.
## Global Constraints
Diese Regeln gelten für jede Aufgabe aller Pakete.
**Befehle und Prüfungen**
- Jede Bash-Ausführung beginnt mit `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh`. Das Skript (aus P0) setzt:
- `WT=/home/webklar/apps/ticket.webklar.com/dev-design`
- `W=/root/reports/ticketsystem-design-2026-10-04/werkzeug`
- `TMPDIR` auf den Scratchpad-Ordner der Sitzung (sonst `$W/tmp`)
- `set -o pipefail`
- `cd "$WT"`
Temporäre Dateien liegen nie direkt in `/tmp`. Auch die ```bash-Commit-Blöcke der Pläne laufen in einem Shell-Aufruf, der zuerst `env.sh` lädt.
- Tests und Build werden nur über `"$W/check.sh"` als Bedingung genutzt. Varianten:
- `"$W/check.sh"` für Tests und Build
- `"$W/check.sh" --nur-tests`
- `"$W/check.sh" --nur-build`
Das Skript gibt die Zeilen `# pass`/`# fail` und die Bundle-Größen aus und endet mit dem Exit-Code von `npm test` bzw. `npm run build`. Ein Tag wird nur mit `"$W/check.sh" && git tag design-p<N>` gesetzt. Ketten wie `npm test | grep … && …` sind verboten, weil sie den Exit-Code von `grep` werten.
- Browser-Skripte (Playwright, Pixelvergleich) laufen nie direkt mit `node`, sondern über die Hüllen `$W/shots.sh` oder `$W/run-browser.sh <skript> [args]`. Beide starten unter `systemd-run --scope -q -p MemoryMax=1500M -p MemorySwapMax=0`.
- `rg` auf diesem Host ist Version 13.0.0 ohne PCRE2. Deshalb kein `rg -P`, keine Lookarounds.
**CSS-Reihenfolge**
- `src/main.jsx` importiert `App` vor `./styles/global.css`. Deshalb stehen alle von Komponenten importierten Stylesheets im Bundle **vor** `global.css`: `chat.css`, `serverpuls.css`, `admin-areas.css`, `pages/AdminPage.css` und `uplot.min.css`.
- Bei gleicher Spezifität gewinnt `global.css`. Bereichs-CSS, das eine globale Regel überstimmen muss, braucht eine höhere Spezifität (z. B. `.page.admin-page`) und nie `!important`.
**Parallele Arbeit an WOMS**
- Auf `feature/terminplaner-tp2` arbeitet eine andere Sitzung weiter. Stand 2026-10-05: 5 Commits vor `test`, sie berühren u. a. `CustomerFunktionen.jsx`, `lib/funktionenView.js` und Rechtliches.
- Vor dem Start jedes Pakets wird `git log --oneline origin/test..origin/feature/terminplaner-tp2 -- <Dateien des Pakets>` geprüft. Gibt es Treffer, stoppt der Agent und fragt den Eigentümer. Er empfiehlt dabei, TP2 zuerst nach `test` zu bringen.
**Paketstart und Abnahme-Reihenfolge**
- Paketstart:
- `git fetch origin`
- `git merge-base --is-ancestor design-p<N-1> HEAD`, sonst stoppen und melden
- `git rebase origin/test`
- `"$W/check.sh"`
- Abnahme: erst `git fetch origin && git rebase origin/test`, dann Gate-Lauf, Screenshots und Review. So zeigen die Bilder den Stand, der getaggt wird.
- Jede Übergabe nennt neben der Push-Zeile auch die Rücknahme: `git revert --no-commit design-p<N-1>..design-p<N>` als Commit auf `test`. Das gilt nur für das jüngste Paket.
**Bausteine benutzen**
- Jeder Knopf in einem `<Dialog as="form">` außer dem Absenden-Knopf trägt `type="button"`. `Button` setzt keinen eigenen Standard-Typ, weil Bestandsformulare darauf bauen.
- Abstände zwischen aufeinanderfolgenden Abschnitten regelt P1 global mit `.page section + section { margin-top: 36px }`. Pakete bauen dafür keine eigenen Regeln. Eigene Abstände sind nur innerhalb von Bereichs-Containern erlaubt, die keine `section` sind.
- Klickbare Tabellenzeilen prüfen Klicks immer mit `isRowActivation(event)` aus `src/lib/rowClick.js` (entsteht in P2, Task 2). Kein Paket baut diese Regel neu.
**Commit-Trailer**
- In den Commit-Beispielen steht `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`. Maßgeblich ist die Attribution, die die ausführende Sitzung vorgibt.
**Arbeitsort und Git**
- Gearbeitet wird nur in `/home/webklar/apps/ticket.webklar.com/dev-design` auf dem Branch `feature/design-weiss`. In `/home/webklar/apps/ticket.webklar.com/build/` (Live-Checkout) wird nie etwas geändert.
- `git add` nur mit ausdrücklichen Pfaden, nie `-A` oder `.`.
- Commit-Nachrichten auf Deutsch, letzte Zeile `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`.
- Niemals `git push`. Live bringt nur der Eigentümer mit `git -C /home/webklar/apps/ticket.webklar.com/dev-design push origin 'design-p<N>^{commit}:refs/heads/test'`.
- Vor dem Paketstart und vor dem Setzen des Tags: `git fetch origin && git rebase origin/test`. Das Tag `design-p<N>` wird danach gesetzt und nie bewegt. Korrekturen nach dem Push: `design-p<N>-fix<k>`.
- Nicht anfassen:
- `.env*` (Latin-1)
- das Portal-Repo
- `src/lib/loadChatComposer.js` samt Test
- die Container-Klassen in `src/components/chat/ChatThread.jsx` (`woms-chat`, `chat-messages`)
- Appwrite-Schema und gespeicherte Werte
**Speicher und Prozesse**
- Speicherintensives (`npm ci`, `npm run build`, Playwright) läuft unter `systemd-run --scope -q -p MemoryMax=1500M -p MemorySwapMax=0`, ein Build zusätzlich mit `NODE_OPTIONS=--max-old-space-size=1200`.
- Prozesse nur per PID beenden, nie mit `pkill -f` (beendet sonst die eigene Shell).
**Tests und Screenshots**
- Tests laufen ohne Produktionszugriff. Das Screenshot-Werkzeug blockiert jede Anfrage an einen echten Host.
- Lauf: `npm test`, alle Tests grün.
- Neue Lib-Funktionen bekommen Tests unter `tests/unit/<name>.test.mjs` im Stil der bestehenden Tests (`node:test`, `assert/strict`, `loadModule`).
**Gestaltung**
- Akzent `#111111`. Pro Seitenkopf höchstens ein `.btn-primary`.
- Breakpoints 1100 px (`.hide-md`) und 860 px (`.hide-sm`, Schublade, Bottom-Sheet).
- Die Token-Werte stehen exakt in der Spec, Abschnitt „Design-Tokens“, und werden nicht neu erfunden.
- Keine Icon-Komponenten in umgebauten Dateien. Handelnde Zeichen werden Text: „ד, „←“, „→“, „↑“, „↓“.
- Keine neuen `style={{…}}` außer für Werte, die erst zur Laufzeit feststehen.
- Kein Farbliteral in JSX. Farben nur über Tokens. Einzige Ausnahme: Canvas-Farben in `src/components/serverpuls/chartColors.js`.
**Texte und Verhalten**
- Texte bleiben wörtlich. Geändert wird nur, was die Spec unter „Texte“ und in den Bausteinen nennt.
- Rechtstexte, Mail- und Chat-Vorlagen, Fehlermeldungen und der Wortlaut in `lib/analyseView.js` bleiben unverändert.
- Verhalten bleibt, außer „Bewusste Änderungen am Verhalten“ der Spec. Alle `window.confirm`-Rückfragen, `data-testid`, Doppelklick-Sperren (`busyRef`, `lockRef`) und URL-Parameter bleiben.
**Abschluss jedes Pakets**
- Abnahme nach Spec „Abnahme“, Punkte 1 bis 7.
- Code-Review mit dem Modell `sonnet`. Wichtige Befunde werden behoben, Kleinigkeiten notiert.
## Gemeinsame Schnittstellen aus P1
Diese Bausteine und Klassen sind ab P1 vorhanden. Spätere Pakete benutzen sie und erfinden keine eigenen.
**Komponenten**
- **`src/components/ui/Button.jsx`**
- Signatur: `Button({ variant = 'default', size, as = 'button', className = '', ...props })`.
- Varianten: `'default' | 'ghost'` → `btn`, `'primary'` → `btn btn-primary`, `'quiet'` → `btn btn-quiet`, `'danger'` → `link-danger`.
- `size` wird ignoriert.
- **`src/components/ui/Badge.jsx`**
- Signatur: `Badge({ tone = 'muted', className = '', style, children })`.
- Tonzuordnung: `muted|grey` → `''`, `info|blue` → `blue`, `ok|green` → `green`, `warn|orange` → `orange`, `danger|red` → `red`, dazu `violet` und `plain`.
- Die Prop `dot` wird ignoriert.
- **`src/components/ui/Card.jsx`**
- Signatur: `Card({ title, actions, className, children })`.
- Rendert `<section className={className}>`, mit Titel `<h2 className="section"><span>{title}</span>{actions && <span className="actions">{actions}</span>}</h2>`.
- Ohne `title` und ohne `actions` entfällt das `h2`, die Kinder stehen direkt in `<section>`.
- `pad`, `bodyStyle` und `style` werden ignoriert.
- **`src/components/ui/PageHeader.jsx`**
- Signatur: `PageHeader({ title, subtitle, actions, eyebrow, crumb, children })`.
- `crumb` ist ein Array `[{ label: string, to?: string }]`, gerendert als `.crumb` mit „/“ in `--text-3` vor `.page-head`.
- **`src/components/ui/Tabs.jsx`**
- Signatur: `Tabs({ tabs: [{ id, label, count? }], active, onChange })`.
- Rendert `.tabs`, `role="tablist"`, Buttons `.tab` mit `role="tab"`, `aria-selected`, Zähler `<span class="count">` bei `count != null`.
- **`src/components/ui/EmptyState.jsx`**: `EmptyState({ title, hint, action })` → `.empty`.
- **`src/components/ui/Field.jsx`**: `Field({ label, htmlFor, hint, children })` → `.field`, `.hint`.
- **`src/components/ui/Stat.jsx`**
- Signatur: `Stat({ label, value, sub, tone = '' })`, wobei `tone` `'' | 'warn' | 'neg'` ist.
- Rendert `.stat` mit `.k` (Label), `.v {tone}` (Wert) und `.sub`.
- Der Container `.stats` kommt vom Aufrufer.
- **`src/components/ui/Kpi.jsx`**
- Signatur: `Kpi({ label, value, sub, icon, accent })`.
- Gibt `<Stat … tone={accent === 'var(--danger)' ? 'neg' : ''}>` zurück und ignoriert `icon`.
- Wird in P7 gelöscht.
- **`src/components/ui/StatusPill.jsx`**
- `StatusPill({ status })` → `<span className={'badge ' + statusTone(status)}>{statusLabel(status)}</span>`.
- `UrgentPill({ priority })` → `badge red` „Dringend“, nur wenn `isUrgent(priority)`.
- **`src/components/ui/Dialog.jsx`**
- Signatur: `Dialog({ title, onClose, busy = false, wide = false, as = 'div', onSubmit, footer, children })`.
- Aufbau: Overlay `.overlay`, Box `.dialog` (`.dialog-wide`), Kopf `.dialog-head` mit `<h3 id>` und `<button type="button" className="dialog-close" aria-label="Schließen" disabled={busy}>×</button>`, darunter `.dialog-body` und `.dialog-foot`.
- Mit `as="form"` ist die Box ein `<form onSubmit>`, die native Pflichtfeld-Prüfung bleibt aktiv.
- Schließen: Esc nur bei `!e.defaultPrevented && !busy`. Ein Klick auf den Hintergrund schließt nie.
- Fokus über `useDialogFocus`.
- **`src/components/Logo.jsx`**: `Logo({ size = 24 })` mit dem SVG aus dem Prototyp (`app.js`, Konstante `LOGO`).
- **`src/lib/ticketStatus.js`**: `statusTone(status) → '' | 'blue' | 'orange' | 'violet' | 'green' | 'plain'`.
- **`src/components/serverpuls/chartColors.js`**: `export const CHART = { line: '#111111', fill: 'rgba(17,17,17,.06)', peak: '#8a93a3', axis: '#5e6678', grid: '#ebeef1', tick: '#d8dde3' }`.
**CSS-Klassen in `global.css`**
Prototyp-Namen, Werte aus `quelle/styles.css`:
| Bereich | Klassen |
|---|---|
| Gerüst | `.shell(.menu-open)`, `.sidebar`, `.brand`, `.nav`, `.nav-item(.sub .active .paused)`, `.nav-count(.hot)`, `.nav-dot(.critical)`, `.sidebar-foot`, `.main`, `.mobile-bar`, `.scrim`, `.page` |
| Seitenkopf | `.crumb`, `.page-head`, `.eyebrow`, `.sub`, `.actions`, `h2.section` |
| Knöpfe | `.btn`, `.btn-primary`, `.btn-quiet`, `.btn-block` |
| Links | `.link`, `.link-danger` |
| Badges | `.badge(.blue .orange .violet .green .red .plain)` |
| Reiter | `.tabs`, `.tab(.active)`, `.count` |
| Formular | `.toolbar`, `.grow`, `.input`, `.select`, `.textarea`, `.search`, `.select-quiet`, `.check`, `.field`, `.hint`, `.field-row`, `.field-row-3`, `.code-input` |
| Tabelle | `.table`, `tr.row`, `.right`, `.cell-title`, `.cell-sub`, `.id`, `.table-foot`, `.hide-md`, `.hide-sm` |
| Zustände | `.empty`, `.loading`, `.msg(.err .ok .warn)`, `.toast` |
| Werte | `.facts`, `.fact`, `.k`, `.v`, `.stats`, `.stat`, `.v.warn`, `.v.neg`, `.kv`, `.two-col`, `.prose` |
| Bedienelemente | `.urgent-toggle(.on)`, `.sw`, `.list-row`, `.seg(button.on)` |
| Dialog | `.overlay`, `.dialog`, `.dialog-wide`, `.dialog-head`, `.dialog-close`, `.dialog-body`, `.dialog-foot` |
| Login | `.login`, `.login-box`, `.login-links`, `.login-foot` |
| Hilfsklassen | `.num`, `.muted`, `.faint`, `.small`, `.xs`, `.strong`, `.nowrap` |
| Zustände | `:disabled` (Deckkraft .55) und `[readonly]` (`--bg-subtle`) |
`.ws*` kommt mit P2, `.week`/`.day`/`.appt` mit P3.
**Screenshot-Werkzeug aus P0**
Es liegt unter `$W`.
| Befehl | Wirkung |
|---|---|
| `env.sh` | Umgebung, siehe oben |
| `check.sh [--nur-tests\|--nur-build]` | Tests und/oder Build mit echtem Exit-Code |
| `build.sh $WT <name>` | Vite-Build mit Test-Endpunkten nach `$W/dist/<name>` |
| `shots.sh <name> <ausgabe> [--only id,id] [--gate p<N>]` | Screenshots plus Prüfbericht `<ausgabe>/report.json` |
| `run-browser.sh <skript.mjs> [args]` | jedes andere Browser-Skript unter Speichergrenze |
Zu `shots.sh`:
- Eine unbekannte ID in `--only` bricht mit Exit 2 ab.
- `--gate p<N>` endet bei Verstößen mit Exit 1.
**`$W/views.mjs`**
- Format: `VIEWS: Array<{ id, path, fixtures = 'standard', scenario = 'angemeldet', viewports?: Array<[w, h]>, steps?: async (page, ctx) => void }>`.
- `ctx.backend` gibt Zugriff auf `calls` (alle beantworteten Requests) und `violations`.
- Ein Fehler in `steps` erzeugt den Verstoß `step-failed`.
- Standard-Viewports sind `[[1440, 900], [390, 844]]`.
**`$W/gates.mjs`**
- Format: `GATES: { p<N>: { checks: string[], views: '*' | string[], scoped?: { [check]: { views: string[], ignore?: string[] } } } }`.
- `checks` gilt für `views`. Einträge in `views` dürfen mit `*` enden und gelten dann als Präfix (z. B. `'tickets-*'`).
- `scoped` schaltet eine weitere Prüfung nur für die genannten Ansichten ein. Beispiel: `icon-svg` für die Ansichten des Pakets, `ignore` mit Selektoren, die bis zu einem späteren Paket ausgenommen bleiben.
- `step-failed`, `external-request` und `console-error` sind in jedem Gate aktiv.
- Paket N legt `p<N>` als `{ ...GATES['p<N-1>'], scoped: { ...vorherige, 'icon-svg': { views: [… Ansichten von N und allen früheren Paketen] } } }` an.
**Ansichten-IDs der Kundenseite**
- Die Reiter-IDs kommen aus `src/lib/customerTabs.js`: `kunde-overview`, `kunde-tickets`, `kunde-invoices`, `kunde-projects`, `kunde-chat`, `kunde-abo`, `kunde-gutschriften`, `kunde-funktionen`, `kunde-protokoll`.
- Pakete verwenden diese IDs und legen keine zweite Ansicht für dieselbe URL an.
## Review Focus
Diese Eingaben und Zustände werden von keinem Lib-Test abgedeckt, treffen aber echte Nutzer. Jede Zeile hat eine Prüfung im Paket, das den Code besitzt.
1. **Altdaten in Tickets:**
- Freitext-Datum `2.5.2026`, `31.02.2026` oder ISO-Datum
- Ticket ohne `woid`
- unbekannter oder leerer Status
- fehlender `customerName`
Erwartet wird, dass die Seite rendert und „–“ oder den Rohtext zeigt. Die Prüfung liegt in P0 (Testdaten) und P2 (Ansicht `ticket-altdaten`).
2. **Lange Inhalte am Handy:**
- Betreff mit 120 Zeichen
- Firmenname mit 60 Zeichen
- viele Badges
Erwartet wird kein waagerechtes Scrollen bei 390 px. Die Prüfung `overflow-x` liegt in P0 und ist ab P1 für alle Ansichten scharf.
3. **Teilausfall der Abfragen** (Zähler, Spalte „Zeit“, Mitarbeiterliste antworten mit 500): Die Liste bleibt bedienbar, statt Zahlen steht „–“. Die Prüfung ist das Szenario `fehler` in P0, angewendet in P2.
4. **Nur Tastatur:**
- Dialog per Esc schließen
- Tab bleibt im Dialog
- „ד ist bei `busy` gesperrt
- Tabellenzeilen per Tab erreichbar
Die Prüfung liegt in P1 (Dialog) und P2 (Ticketzeilen), jeweils als Tastaturschritte in `views.mjs`.
5. **Viele Einträge:**
- Ticket mit 300 Arbeitsblättern
- Suche mit 250 Treffern
Erwartet werden: Spalte „Zeit“ nur für die ersten 200 Zeilen mit Hinweis im Fuß, keine falschen Summen, Seite bleibt bedienbar. Die Prüfung ist das Testdaten-Set `viel` in P0, angewendet in P2.
@@ -0,0 +1,389 @@
# WOMS „Weißes Design“ P0: Vorbereitung – Umsetzungsplan
> **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:** Nach P0 baut und testet der Worktree sauber, und der tote Code ist weg. Ein Screenshot- und Prüfwerkzeug erzeugt für jede Ansicht Bilder und meldet Design-Verstöße. Dabei wird kein Produktionssystem berührt.
**Architecture:**
- `node_modules` wird aus Git genommen, danach läuft im Worktree ein echtes `npm ci`.
- Das Werkzeug liegt außerhalb des Repos unter `$W`.
- Es baut die App mit **Vite** gegen erfundene Test-Hosts (`https://ticket.test`, `https://portal.test`). Vite nutzt intern esbuild und liefert dasselbe CSS-Bündel und dieselbe Reihenfolge wie der Produktions-Build. Ein eigenes esbuild-Bündel müsste Vite-Env und JSX-Einstellungen nachbauen. Die Spec nennt „esbuild-Bündel“, gleichwertig ist dieser Weg.
- Playwright beantwortet alle Anfragen aus Testdaten und blockiert jeden anderen Host.
**Tech Stack:**
- Vite 5
- playwright-core 1.62.1 aus `/root/.npm/_npx/db89d7302a373f10/node_modules/playwright-core`, Laden per `createRequire`
- Browser `/root/.cache/ms-playwright/chromium_headless_shell-1234/chrome-headless-shell-linux64/chrome-headless-shell`
- Node 24
**Spec:** `docs/superpowers/specs/2026-10-04-weisses-design-design.md`, Paket P0 und „Abnahme“. Globale Regeln stehen in `2026-10-05-weisses-design-00-uebersicht.md`. Die Befehlsregeln dort (`env.sh`, `check.sh`, `run-browser.sh`) entstehen in diesem Paket.
## Global Constraints
Es gelten die Regeln der Übersicht. Für P0 kommt hinzu:
- Das Werkzeug ändert weder `package.json` noch die Lock-Datei noch `tests/` im Repo.
- Kein Request darf einen echten Host erreichen. Jeder nicht nachgebildete Request wird abgebrochen und als Verstoß `external-request` gemeldet.
- Für die Bilder blendet das Werkzeug die Schrift Inter ein, damit die Breiten realistisch sind. Quelle: `/home/webklar/apps/webklar.com/dev-relaunch/public/fonts/inter-latin.woff2` und `inter-latin-ext.woff2`, gesetzt per `addInitScript` als `--font`. Am Repo ändert sich dadurch nichts.
## Review Focus
P0 liefert die Testdaten und Prüfungen für die Übersichtspunkte 1 bis 5:
- Altdaten (`altdaten`)
- lange Inhalte (`altdaten`, Prüfung `overflow-x`)
- Teilausfall (Szenario `fehler`)
- Tastatur (Schritte über `steps`)
- viele Einträge (`viel`)
---
### Task 1: Worktree lauffähig machen
**Files:**
- Modify (Git-Index): `node_modules/**` wird aus Git genommen, nicht gelöscht
- Read: `.gitignore` (muss `node_modules` enthalten)
**Interfaces:**
- Produces: Im Worktree laufen `npm test` und `npm run build` ohne Symlink.
- [ ] **Step 1: Startprüfung**
Run: `cd /home/webklar/apps/ticket.webklar.com/dev-design && git fetch origin && git log --oneline origin/test..origin/feature/terminplaner-tp2 -- src/pages/LeadsPage.jsx src/hooks/useLeads.js && git rebase origin/test && git status --short | grep -v '^ D node_modules' ; grep -n '^node_modules' .gitignore`
Expected:
- Die `git log`-Zeile für TP2 gibt nichts aus. Gibt sie etwas aus: stoppen und den Eigentümer fragen.
- Der Rebase läuft ohne Konflikt.
- Vor dem `grep` auf `.gitignore` erscheint keine Zeile.
- Danach erscheint eine Zeile `node_modules` oder `node_modules/`.
- [ ] **Step 2: `node_modules` aus dem Git-Index nehmen**
Run: `git rm -r -q --cached node_modules && git commit -q -m "node_modules nicht mehr im Repo (Deploy installiert ohnehin neu)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>" && git show --stat HEAD | tail -1`
Expected: rund 8890 Löschungen.
- [ ] **Step 3: Sauber installieren**
Run: `rm -rf node_modules && systemd-run --scope -q -p MemoryMax=1500M -p MemorySwapMax=0 npm ci --no-audit --no-fund && git status --short`
Expected: Exit 0 und keine Zeile aus `git status`.
- [ ] **Step 4: Ausgangswerte festhalten**
Run: `cd /home/webklar/apps/ticket.webklar.com/dev-design && T=/root/reports/ticketsystem-design-2026-10-04/werkzeug/tmp && mkdir -p "$T" && npm test > "$T/p0-test.log" 2>&1; echo "exit $?"; grep -E "^# (pass|fail)" "$T/p0-test.log"; systemd-run --scope -q -p MemoryMax=1500M -p MemorySwapMax=0 env NODE_OPTIONS=--max-old-space-size=1200 npm run build 2>&1 | tail -6`
Task 1 und 2 laufen vor `env.sh` (kommt in Task 3), deshalb der feste Ordner `$T`.
Expected: `exit 0`, `# pass 312`, `# fail 0` und ein Build ohne Fehler. Testzahl und Bundle-Größen (JS, CSS) kommen in die Commit-Nachricht von Task 2.
### Task 2: Toten Code löschen
**Files:**
- Delete: `src/pages/LeadsPage.jsx`, `src/hooks/useLeads.js`
- Keep: `src/lib/leads.js`, die Route `/leads` mit Umleitung in `src/App.jsx`, den pausierten Menüpunkt in `src/lib/navLinks.js`
- [ ] **Step 1: Unbenutzt beweisen**
Run: `cd /home/webklar/apps/ticket.webklar.com/dev-design && rg -n "LeadsPage|useLeads" src tests`
Expected: Treffer nur in den beiden Dateien selbst.
- [ ] **Step 2: Löschen und prüfen**
Run: `cd /home/webklar/apps/ticket.webklar.com/dev-design && T=/root/reports/ticketsystem-design-2026-10-04/werkzeug/tmp && git rm -q src/pages/LeadsPage.jsx src/hooks/useLeads.js && npm test > "$T/p0-test.log" 2>&1 && systemd-run --scope -q -p MemoryMax=1500M -p MemorySwapMax=0 env NODE_OPTIONS=--max-old-space-size=1200 npm run build > "$T/p0-build.log" 2>&1 && echo OK`
Expected: `OK`.
- [ ] **Step 3: Commit**
```bash
git commit -q -m "LeadsPage und useLeads entfernt (seit Lead-Pause unbenutzt)" -m "Ausgangswerte: <Testzahl>, JS <x> B, CSS <y> B" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 3: Werkzeug – Umgebung, Build, Test-Backend
**Files** (alle unter `$W=/root/reports/ticketsystem-design-2026-10-04/werkzeug/`):
- Create: `env.sh`, `check.sh`, `run-browser.sh`, `build.sh`
- Create: `backend.mjs`, `appwrite-query.mjs`, `endpunkte.md`, `endpunkte.mjs`
- Create: `fixtures/standard.mjs`, `fixtures/altdaten.mjs`, `fixtures/viel.mjs`
- Create: `README.md` (Bedienung in zehn Zeilen)
- Test: `selftest.mjs`
**Interfaces:**
- Produces:
- **`env.sh`** setzt `WT`, `W`, `TMPDIR` (Scratchpad, wenn als `$CLAUDE_SCRATCHPAD` bekannt, sonst `$W/tmp`), `set -o pipefail`, `cd "$WT"`.
- **`check.sh [--nur-tests|--nur-build]`**
- `npm test` schreibt nach `$TMPDIR/test.log`. Ausgegeben werden die Zeilen `# pass`/`# fail`.
- Der Build läuft mit `NODE_OPTIONS=--max-old-space-size=1200` unter `systemd-run` (1500M) und schreibt nach `$TMPDIR/build.log`. Ausgegeben werden die Größen der `.js`- und `.css`-Dateien.
- Exit ist 0 nur, wenn beide Teile Exit 0 hatten.
- **`run-browser.sh <skript.mjs> [args]`** startet `node` unter `systemd-run --scope -q -p MemoryMax=1500M -p MemorySwapMax=0`.
- **`build.sh <worktree> <name>`**
- Baut nach `$W/dist/<name>/` mit `VITE_APPWRITE_ENDPOINT=https://ticket.test/v1`, `VITE_APPWRITE_PROJECT_ID=test` und `VITE_PROJECT_ADMIN_URL=https://portal.test`.
- Prozess-Variablen haben in Vite Vorrang vor `.env.production`.
- Dazu `--outDir` und dieselbe Speichergrenze wie `check.sh`.
- **`createBackend({ fixtures, scenario })`** aus `backend.mjs`.
- `scenario` ist `'angemeldet' | 'abgemeldet' | 'mfa' | 'fehler'`.
- Rückgabe: `{ handle(route, request), calls: [{ method, url, body }], violations: [] }`.
- **`applyQueries(documents, queries) → { total, documents }`** aus `appwrite-query.mjs`.
- [ ] **Step 0: Umgebungs-Skripte anlegen**
`env.sh`, `check.sh`, `run-browser.sh` und `build.sh` werden laut Interfaces geschrieben. `chmod +x` gilt für alle außer `env.sh`.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && echo "$WT $W $TMPDIR" && "$W/check.sh" --nur-tests; echo "exit $?"`
Expected: drei Pfade, die Zeilen `# pass`/`# fail` und `exit 0`.
- [ ] **Step 1: Endpunkt-Inventar anlegen**
`endpunkte.mjs` sucht in `$WT/src`:
- `fetch(` mit Basis-URL
- `VITE_PROJECT_ADMIN_URL`-Basen mit Pfad
- `/api/…`-Literale
- `databases.*`-Aufrufe mit Collection-Konstanten
- `account.*`- und `client.call`-Aufrufe
Es gibt eine Liste `{ datei, methode, muster }` aus. Daraus entsteht von Hand `endpunkte.md`, eine Tabelle mit diesen Spalten:
- Quelldatei
- Endpunkt-Muster
- Fixture-Schlüssel
- Antwortform (Felder, die der Code liest)
- Verhalten im Szenario `fehler`
`fehler` ist so festgelegt:
| Anfrage | Antwort im Szenario `fehler` |
|---|---|
| `GET …/collections/worksheets/documents` | 500 |
| `GET …/collections/employees/documents` | 500 |
| `GET https://portal.test/api/admin/employees` | 500 |
| `GET https://portal.test/api/team/*` | 500 |
| `GET …/collections/workorders/documents` mit `limit(1)` (Zähler) | 500 |
| alle anderen | normal |
- [ ] **Step 2: Failing Selbsttest schreiben**
`selftest.mjs` prüft mit `node:assert`:
```js
const { total, documents } = applyQueries(docs, ['equal("status", ["Open","Occupied"])', 'notEqual("type", ["Akquise"])', 'orderDesc("$createdAt")', 'limit(2)'])
// total = Treffer vor limit, documents.length === 2, neueste zuerst
applyQueries(docs, ['equal("woid", ["100131"])']) // findet woid als Zahl oder Text
applyQueries(docs, ['offset(2)', 'limit(1)']) // drittes Dokument
applyQueries(docs, ['cursorAfter("id2")', 'limit(1)'])
applyQueries(docs, ['select(["woid","totalTime"])']) // nur diese Felder plus $id
```
Weitere Fälle:
- Ein Request an `https://example.com/x` wird abgebrochen und steht als `{ kind: 'external-request', url }` in `violations`.
- `GET https://ticket.test/v1/account` liefert 200 im Szenario `angemeldet`, 401 in `abgemeldet` und in `mfa` (nach dem Login) 401 mit `type: 'user_more_factors_required'`.
- Im Szenario `fehler` liefert `GET …/collections/worksheets/documents` 500.
- Jedes Muster aus `endpunkte.md` liefert im Szenario `angemeldet` eine Antwort, nie eine `external-request`-Verletzung. Das Muster wird aus der Tabelle gelesen.
- [ ] **Step 3: Selbsttest laufen lassen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && cd "$W" && node selftest.mjs`
Expected: FAIL (Module fehlen).
- [ ] **Step 4: `applyQueries` implementieren**
Unterstützt werden:
- Filter: `equal`, `notEqual`, `greaterThan`, `lessThan`, `contains`, `search` (Teilstring, ohne Groß-/Kleinschreibung), `isNull`, `isNotNull`
- Sortierung: `orderAsc`, `orderDesc`
- Seiten: `limit` (Standard 25), `offset`, `cursorAfter`
- Felder: `select`
Ein unbekannter Query-Typ ergibt den Verstoß `unknown-query` und wird ignoriert.
- [ ] **Step 5: Backend implementieren**
| Anfrage | Antwort |
|---|---|
| `https://ticket.test/v1/...` | Appwrite-Nachbau |
| `https://ticket.test/api/...` | gleichseitige APIs (Serverpuls u. a.) laut Inventar |
| sonstige `https://ticket.test/...` | Datei aus `$W/dist/<name>/`, unbekannte Pfade bekommen `index.html` |
| `https://portal.test/...` | Portal- und Admin-API laut Inventar. Der Chat-Composer (`loadChatComposer.js`) bekommt eine leere JS- und CSS-Datei, bis P6 den echten Baustein einbindet. |
| andere URLs | `route.abort()` und Verstoß `external-request` |
Schreibende Requests ändern die Testdaten im Speicher. Jeder Request landet in `calls`.
- [ ] **Step 6: Testdaten anlegen**
**`fixtures/standard.mjs`**
- Angemeldet ist `kenso-admin` mit Labels `['admin']`, Name „Kenso Grimm“.
- 4 Mitarbeiter: KNSO, ANDJ, JUSN, NIRE.
- 9 erfundene Kunden aus `quelle/data.js`:
- Stufen fest, lead und archiv
- einer mit `customerStatus: 'lost'` (Abgesagt)
- einer mit `isImportant: true`
- 12 Tickets mit allen Werten aus `TICKET_STATUSES` plus `In Kontakt`, `Zugesagt`, `Abgesagt` und `Assigned`. Arten Webpage, Migration, Project und Akquise.
- **Ticket 100131 ist fest:**
- Kunde „Physiotherapie am Markt“
- `serviceType: 'On Site'`
- Arbeitsblätter:
- Git-Push ohne Zeit (`serviceType: 'GIT'`, `employeeName: 'knso'`, `totalTime: 0`)
- Git-Push mit 45 min (`employeeName: 'ANDJ'`)
- Kommentar (`isComment: true`)
- Arbeitszeit von `andj` über 60 min mit `supporters: '[{"userId":"kenso-admin","task":"DNS geprüft"}]'`
- Arbeitszeit von `kenso-admin` über 90 min mit `countsToPlan: true`
- Projekt `WEBklar/physio-am-markt`
- 10 `websiteProjects` aller Typen, darunter eine Vorlage und ein archiviertes.
- Rechtliches: Dokumente, Versionen und Anbieter.
- Antworten für Finanzen, Serverpuls (mit Zeitreihen für die Diagramme), Gutschriften, Abo-Angebote, E-Mail-Speicher und Chat-Liste, jeweils laut Inventar.
**`fixtures/altdaten.mjs`**
- Ticket `startDate: '2.5.2026'`
- Ticket `deadline: '31.02.2026'`
- Ticket `startDate: '2026-10-07T09:00:00.000Z'`
- Ticket ohne `woid`
- Ticket mit `status: ''`, Ticket mit `status: 'Completed'`
- Ticket ohne `customerName`
- Betreff mit 120 Zeichen
- Kundenname mit 60 Zeichen
**`fixtures/viel.mjs`**
- Ticket 100131 mit 300 Arbeitsblättern
- 250 Tickets, deren Betreff „Test“ enthält
- [ ] **Step 7: Selbsttest grün**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && cd "$W" && node selftest.mjs`
Expected: `selftest ok`.
- [ ] **Step 8: Probebau**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" vorher && ls "$W/dist/vorher/index.html" && rg -n "https://(ticket|project)\.webklar\.com" "$W"/dist/vorher/assets/*.js | cut -c1-160`
Expected: `index.html` ist vorhanden. Treffer der echten Hosts sind nur Texte oder Links für Menschen, keine API-Basen. Jeden Treffer prüfst du einzeln.
- [ ] **Step 9: `check.sh` vollständig prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh"; echo "exit $?"`
Expected: Die Zeilen `# pass 312` und `# fail 0`, die Bundle-Größen und `exit 0`.
### Task 4: Werkzeug – Ansichten, Prüfungen, Vorher-Bilder
**Files** (unter `$W`):
- Create: `views.mjs`, `checks.mjs`, `gates.mjs`, `shots.mjs`, `shots.sh`
**Interfaces:**
- Consumes: Backend, Testdaten und `build.sh` aus Task 3.
- Produces: Formate von `VIEWS` und `GATES` wie in der Übersicht. Zusätzlich:
- `runChecks(page) → Promise<Array<{ check, selector, detail }>>` aus `checks.mjs`.
- `submitReachable(page)` aus `views.mjs` sucht den Absende-Knopf im offenen Dialog (`[type=submit]`, sonst den letzten Primärknopf), ruft `scrollIntoView` auf und wirft, wenn er danach nicht vollständig im Viewport liegt.
- `shots.sh <name> <ausgabe> [--only ids] [--gate p<N>] [--static <ordner>]`
- Eine unbekannte ID in `--only` endet mit Exit 2.
- Einträge in `views` eines Gates dürfen mit `*` enden (Präfix).
- `--static` lädt ein HTML-Verzeichnis ohne Backend. So prüft man das Werkzeug am Prototyp.
- [ ] **Step 1: Ansichten anlegen**
| ID | Pfad / Schritte |
|---|---|
| `login` | `/login`, Szenario `abgemeldet` |
| `login-code` | Szenario `mfa`: Formular absenden, warten auf `#mfa-code` |
| `login-code-senden` | Szenario `mfa`: Formular absenden, Seite neu laden, warten auf den Knopf „Code senden“ |
| `tickets` | `/tickets` |
| `tickets-suche` | Testdaten `viel`, `/tickets`, Suche „Test“ (heute über „Anwenden“) |
| `ticket` | `/tickets?woid=100131` (heute aufgeklappte Karte) |
| `ticket-arbeitsblaetter` | wie `ticket`, dann Reiter „Arbeitsblätter“ |
| `ticket-altdaten` | Testdaten `altdaten`, `/tickets` |
| `fehler-tickets` | Szenario `fehler`, `/tickets` |
| `dlg-ticket-neu` | Dialog „Neues Ticket“ |
| `dlg-arbeitsblatt` | Dialog „Arbeitsblatt hinzufügen“ |
| `dlg-zeiten` | Dialog „Zeit nachtragen“ |
| `dlg-unterstuetzung` | Dialog „Ich habe unterstützt“ |
| `planboard` | `/planboard` |
| `kunden` | `/customers` |
| `kunde-<reiter>` | `/customers/<id>?tab=<reiter>` mit den Reiter-IDs aus der Übersicht |
| `chat` | `/customers/chat` |
| `gutschriften` | `/customers/gutschriften` |
| `angebote` | `/customers/angebote` |
| `speicher` | `/customers/email-speicher` |
| `projekte` | `/projects` |
| `dlg-projekt-neu` | Dialog „Neues Projekt“ |
| `dlg-projekt-bearbeiten` | Dialog „Projekt bearbeiten“ |
| `dlg-projekt-art` | Art-Frage in `TicketProjects` |
| `zuordnen` | `/projects/zuordnen/<id>` |
| `services` | `/services` |
| `analyse-zustimmung` | Zustimmungsablauf in `AnalyseRow` |
| `finanzen` | `/finance` |
| `dlg-rechnung` | Rechnungsdialog |
| `dlg-zuordnen-zahlung` | `AssignDialog` |
| `finanzen-einstellungen` | `/finance/einstellungen` |
| `rechtliches` | `/rechtliches` |
| `rechtliches-anbieter` | `/rechtliches`, zweiter Reiter |
| `dlg-vertrag` | Vertrags-Formular |
| `dlg-version` | Versions-Formular |
| `admin` | `/admin` |
| `admin-neu` | Mitarbeiter-Formular offen |
| `serverpuls` | `/serverpuls`, warten bis `canvas` gezeichnet ist |
Alle `dlg-*`-Ansichten:
- haben `viewports: [[1440, 900], [390, 844], [1440, 700], [390, 700]]`
- rufen am Ende `submitReachable(page)` auf
Jede Ansicht wartet auf `document.fonts.ready` und 300 ms. Dazu kommt `reducedMotion: 'reduce'`.
- [ ] **Step 2: Prüfungen schreiben**
`checks.mjs` läuft im Browser über alle sichtbaren Elemente:
| Prüfung | Regel |
|---|---|
| `dark-surface` | Verstoß bei eigenem, deckendem Hintergrund mit relativer Helligkeit < 0,5. Für `background-image` mit `linear-gradient` zählt der Mittelwert der Farbstopps. Ausnahmen: `.btn-primary`, `.badge`, `.toast`, `.nav-dot`, `.day.today .dn`, `.sw`, `.code-input`, `[class*="lightbox"]`, `[class*="camera"]` und `canvas`. |
| `low-contrast` | Verstoß bei Textkontrast < 3:1 gegen den nächsten deckenden Hintergrund. Gradienten zählen wie oben. Ausnahmen: `disabled`, `aria-disabled` und Platzhalter. |
| `icon-svg` | Verstoß bei einem sichtbaren `svg` außerhalb von `.brand`, `.login-box > svg` und `.woms-chat .chat-composer *` |
| `overflow-x` | Verstoß bei `scrollWidth > innerWidth + 1` |
| `console-error` | Verstoß bei `pageerror` oder `console.error`. Im Szenario `fehler` sind die erwarteten 500-Meldungen ausgenommen. |
| `external-request`, `unknown-query` | Verstöße aus dem Backend |
| `step-failed` | eine Ausnahme in `steps` |
- [ ] **Step 3: Gates festlegen**
```js
export const GATES = {
p0: { checks: ['external-request', 'console-error', 'step-failed'], views: '*' },
p1: { checks: ['external-request', 'console-error', 'step-failed', 'dark-surface', 'low-contrast', 'overflow-x'], views: '*' },
}
```
Spätere Pakete ergänzen nach dem Schema der Übersicht.
- [ ] **Step 4: Läufer schreiben**
`shots.mjs`:
- Je Ansicht und Viewport ein Kontext. Unter 1000 px Breite gilt `isMobile`, `hasTouch` und `deviceScaleFactor: 2`.
- Dazu `locale: 'de-DE'` und `timezoneId: 'Europe/Berlin'`.
- Ausgabe: `<ausgabe>/<w>x<h>/<id>.png` und `<ausgabe>/report.json` mit `{ view, viewport, issues[] }`.
- Der Exit-Code richtet sich nach `--gate`.
`shots.sh` startet `shots.mjs` über `run-browser.sh`.
- [ ] **Step 5: Vorher-Bilder**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/shots.sh" vorher "$W/shots/vorher" --gate p0`
Expected:
- Exit 0.
- Für jede Ansicht ein Bild je Viewport.
- Kein `external-request`.
- `dark-surface` und `low-contrast` dürfen massenhaft auftreten. Sie sind nicht gate-relevant.
- [ ] **Step 6: Gegenprobe am Prototyp**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/shots.sh" prototyp "$W/shots/prototyp" --static /root/reports/ticketsystem-design-2026-10-04/quelle --only tickets`
Expected:
- 0 Verstöße für `dark-surface` und `low-contrast`.
- Zusätzlich hat `$W/shots/vorher/report.json` für `tickets` mindestens einen `dark-surface`-Treffer. Das beweist, dass die Prüfung anschlägt.
### Task 5: Abschluss P0
- [ ] **Step 1: Tag**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git rebase origin/test && "$W/check.sh" && git tag design-p0`
Expected: Exit 0 und das Tag ist gesetzt. P0 wird nicht gepusht, sondern geht mit P1 live.
- [ ] **Step 2: Code-Review (sonnet)**
Geprüft werden `603d08c..design-p0` und das Werkzeug (`$W`), mit Blick auf zwei Fragen:
- Kann ein Request einen echten Host erreichen?
- Melden die Prüfungen falsch-negativ?
Wichtige Befunde werden behoben. Ein späterer Commit bekommt das Tag `design-p0-fix1`.
- [ ] **Step 3: Bericht an den Eigentümer**
Der Bericht nennt die Ausgangswerte, den Ort der Vorher-Bilder und den Hinweis, dass P0 unsichtbar ist und mit P1 live geht.
@@ -0,0 +1,749 @@
# WOMS „Weißes Design“ P1: Fundament – Umsetzungsplan
> **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:** Nach P1 ist das ganze Ticketsystem weiß und lesbar:
- Das Menü ist eine Textliste.
- Das Login ist neu.
- Alle Dialoge sind weiß.
- Die neuen Bausteine stehen für P2 bis P9 bereit.
Die Innenlayouts der Seiten bleiben vorerst alt.
**Architecture:** `src/styles/global.css` bekommt fünf Blöcke:
1. Tokens und Aliase
2. Basis (Reset, Hilfsklassen, Links)
3. Bausteine (Prototyp-Klassen)
4. KOMPAT (Altklassen im neuen Look, bis P10)
5. Bereichs-CSS (Finanzen, Rechtliches), bis auf Pflicht-Fixes unverändert
Gleichnamige Altdefinitionen werden vor dem Einfügen der Bausteine gelöscht. Die Komponenten unter `src/components/ui/` behalten ihre Schnittstellen. Neu kommen `Dialog`, `Stat`, `Logo` und `statusTone` hinzu.
**Tech Stack:** React 18, react-router-dom 6, Vite 5, reines CSS. Tests mit `node --test`, Screenshots mit dem Werkzeug aus P0.
**Spec:** `docs/superpowers/specs/2026-10-04-weisses-design-design.md`, Abschnitte „Gestaltungsregeln“, „Design-Tokens“, „Bausteine“, „Übergangsschicht KOMPAT“ und Paket P1. Die Prototyp-Werte stehen in `/root/reports/ticketsystem-design-2026-10-04/quelle/styles.css`. Globale Regeln, Befehlsregeln (`env.sh`, `check.sh`) und gemeinsame Schnittstellen stehen in `2026-10-05-weisses-design-00-uebersicht.md`.
## Global Constraints
Es gelten die Regeln der Übersicht. Für P1 kommt hinzu:
- Jeder Run-Block beginnt mit `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh`. Danach gelten `$WT`, `$W` und `$TMPDIR`.
- Werte für Tokens, Abstände, Größen und Schatten werden aus der Spec bzw. aus `quelle/styles.css` übernommen und nicht neu erfunden.
- Nicht übernommen werden `html[data-accent]`, `.proto*` und `.nav-gap`.
- Verhalten, Routen und Auth bleiben unverändert. Ausnahmen:
- Der Footer entfällt.
- Das Einklappen entfällt.
- Die Schublade unter 860 px kommt neu dazu.
- Seiten in `src/pages/*` werden nur dort angefasst, wo dieser Plan es nennt:
- `LoginPage`
- `ProjectsPage`: Wurzelklasse
- `AdminPage`: Wurzelelement
- `ServerpulsPage`: Diagrammfarben
- Die Spec erlaubt keine neue `robots`-Meta-Zeile. `index.html` bekommt nur das Favicon.
## Review Focus
- **Übersichtspunkt 2 (lange Inhalte am Handy):** `overflow-x` ist im Gate `p1` für alle Ansichten scharf.
- **Übersichtspunkt 4 (Tastatur):** Die Esc- und Sperrlogik des Dialogs prüft ein Lib-Test in Task 7.
- **Altseiten:** Jede Altseite bleibt mit KOMPAT lesbar. Das prüft das Gate `p1` mit `dark-surface` und `low-contrast`, auch für Verläufe.
- **Alt-Dialoge:** Die zehn Alt-Dialoge müssen bei 390×700 und 1440×700 bis zum Speichern-Knopf erreichbar sein. Das prüft `submitReachable` in jeder `dlg-*`-Ansicht.
---
### Task 0: Paketstart
- [ ] **Step 1: Stand prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git merge-base --is-ancestor design-p0 HEAD && git log --oneline origin/test..origin/feature/terminplaner-tp2 -- src/styles src/components/ui src/App.jsx src/components/Navbar.jsx src/pages/LoginPage.jsx src/lib/navLinks.js src/lib/ticketStatus.js && git rebase origin/test && "$W/check.sh"`
Expected:
- Exit 0.
- Die TP2-Zeile gibt nichts aus. Gibt sie etwas aus: stoppen und den Eigentümer fragen. Empfehlung: TP2 zuerst nach `test` bringen.
- [ ] **Step 2: Gate `p1` um die Menü-Prüfung ergänzen**
In `$W/gates.mjs` bekommt `p1` den Eintrag `scoped: { 'icon-svg': { views: '*', ignore: ['#inhalt *'] } }`. Damit prüft `icon-svg` ab P1 Menü, Mobil-Leiste und Login auf allen Ansichten, den Seiteninhalt aber erst ab dem jeweiligen Paket.
### Task 1: `statusTone`
**Files:**
- Modify: `src/lib/ticketStatus.js` (neuer Export nach `statusGroup`)
- Test: `tests/unit/ticketStatus.test.mjs` (neu)
**Interfaces:**
- Consumes: `statusGroup(status)` aus derselben Datei. Es liefert die Gruppen-ID oder `null`.
- Produces: `statusTone(status) → '' | 'blue' | 'orange' | 'violet' | 'green' | 'plain'`.
- [ ] **Step 1: Failing Test**
```js
const { statusTone } = await loadModule('src/lib/ticketStatus.js')
test('statusTone: Ton je gespeichertem Wert', () => {
for (const s of ['Open', '', undefined, null, 'Completed']) assert.equal(statusTone(s), '')
for (const s of ['Occupied', 'Assigned', 'Added Info', 'In Kontakt']) assert.equal(statusTone(s), 'blue')
for (const s of ['Awaiting', 'Halted']) assert.equal(statusTone(s), 'orange')
assert.equal(statusTone('In Test'), 'violet')
for (const s of ['Closed', 'Zugesagt']) assert.equal(statusTone(s), 'green')
for (const s of ['Cancelled', 'Aborted', 'Abgesagt']) assert.equal(statusTone(s), 'plain')
})
```
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/ticketStatus.test.mjs`
Expected: FAIL, `statusTone is not a function`.
- [ ] **Step 2: Implementieren**
Zuordnung über `statusGroup`:
| Gruppe | Ton |
|---|---|
| `open` | `''` |
| `working` | `'blue'` |
| `waiting` | `'orange'` |
| `review` | `'violet'` |
| `done` | `'green'` |
| `cancelled` | `'plain'` |
| `null` | `''` |
`statusClass` bleibt bis P2.
- [ ] **Step 3: Grün**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/ticketStatus.test.mjs && "$W/check.sh" --nur-tests`
Expected: PASS, Exit 0.
- [ ] **Step 4: Commit**
```bash
git add src/lib/ticketStatus.js tests/unit/ticketStatus.test.mjs
git commit -m "Ticketstatus: statusTone fuer die neuen Badge-Toene" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 2: Tokens, Aliase, Basis, Schrift
**Files:**
- Modify: `src/styles/global.css`
- Zeilen 1-10 (`@font-face`) löschen
- `:root`, alte Zeilen 18-71, ersetzen
- `body`/Scrollbar/`::selection`, alte Zeilen 73-106, ersetzen
- alte Hilfsregeln `.muted`/`.faint` (um Zeile 1051) ersetzen
- Modify: `src/pages/AdminPage.css` (Zeilen 2-3, `--admin-line*`, löschen)
- Delete: `src/assets/fonts/lato-regular.ttf`, `src/assets/fonts/OFL-Lato.txt`
- Modify (Werkzeug): `$W/views.mjs` bekommt die Ansicht `basis-weiss`
**Interfaces:**
- Produces: alle Tokens und Aliase der Spec, dazu die Basis- und Hilfsklassen aus der Übersicht.
- [ ] **Step 1: Prüf-Ansicht (failing)**
`basis-weiss` öffnet `/tickets`. Der Schritt prüft, dass `getComputedStyle(document.body).backgroundColor === 'rgb(255, 255, 255)'` und dass `font-family` mit `-apple-system` beginnt.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p1 && "$W/shots.sh" p1 "$W/shots/p1-t2" --only basis-weiss --gate p0`
Expected: Exit 1 mit `step-failed`.
- [ ] **Step 2: `:root` ersetzen**
- Den Token-Block der Spec wörtlich übernehmen.
- Darunter folgt die Alias-Tabelle der Spec. Sie enthält `--admin-line` und `--admin-line-strong`.
- Die beiden Zeilen in `AdminPage.css` entfallen.
- [ ] **Step 3: Basis setzen**
Aus `quelle/styles.css` Zeilen 63-91 übernehmen. Zusätzlich gilt:
| Regel | Wert |
|---|---|
| `html` | `color-scheme: light` |
| `body` | `font-family: var(--font); font-size: 14px; line-height: 20px; color: var(--text); background: var(--bg); -webkit-font-smoothing: antialiased`, ohne `overflow: hidden` |
| Formularelemente | `button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer }` und `input, select, textarea { font: inherit; color: inherit }` |
| `a` | `color: inherit; text-decoration: none` |
| `a:not([class])` | Unterstrich in `--line-strong`, `text-underline-offset: 3px`, beim Überfahren `text-decoration-color: currentColor` |
| Hilfsklassen | `.num` (`tabular-nums`), `.muted` (`--text-2`), `.faint` (`--text-3`), `.small` 13px, `.xs` 12/16, `.strong` 600, `.nowrap` |
| `.link`, `.link:hover`, `.link-danger`, `.link-danger:hover` | Werte aus dem Prototyp |
| `:focus-visible` | `outline: 2px solid #111111; outline-offset: 2px` |
| `:disabled` | `opacity: .55; cursor: not-allowed` |
| `[readonly]` | `background: var(--bg-subtle)` |
- [ ] **Step 4: Schrift entfernen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git rm -q src/assets/fonts/lato-regular.ttf src/assets/fonts/OFL-Lato.txt && rg -n "lato|Lato" src`
Expected: keine Treffer.
- [ ] **Step 5: Grün**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p1 && "$W/shots.sh" p1 "$W/shots/p1-t2" --only basis-weiss --gate p0`
Expected: Exit 0.
- [ ] **Step 6: Commit**
```bash
git add src/styles/global.css src/pages/AdminPage.css
git commit -m "Design: weisse Tokens, Aliase, Basis und Hilfsklassen, Systemschrift" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 3: Bausteine-CSS
**Files:**
- Modify: `src/styles/global.css`: Altdefinitionen gleichnamiger Klassen löschen, Block „Bausteine“ hinter der Basis anlegen
**Interfaces:**
- Produces: alle Klassen aus der Übersicht („CSS-Klassen in `global.css`“).
- [ ] **Step 1: Gleichnamige Altdefinitionen löschen**
Gelöscht werden die Regeln dieser Selektoren samt ihrer Varianten (Hover, Kinder, Media-Queries). Die Zeilenangaben beziehen sich auf den Stand 603d08c, gesucht wird nach dem Selektor:
| Selektor | Zeilen (603d08c) |
|---|---|
| `.btn` und `.btn:hover`, ohne `.btn-dark`/`.btn-teal`/`.btn-green` (die gehen nach KOMPAT bzw. sterben in Task 8) | ab 188 |
| `.table`, `.table th`, `.table td`, `.table td[rowspan]` | 236-275 |
| `.overlay` und `.overlay-close` (Neufassung in KOMPAT, Task 4) | ab 563 |
| `.page` | 867 |
| `.badge` (Basis) | 909-915 |
| `.urgent-toggle` | 971-976 |
| `.list-row` | 997-1005 |
| `.toolbar` | 1018 |
| `.seg` | 1019-1024 |
| `.empty` | 1028 |
| `.field` | 1032 |
Der übrige alte Block „REDESIGN“ (796-1049) bleibt bis Task 4. Dazu gehören `.ui-card*`, `.ui-btn*`, `.badge-*`, `.kpi*`, `.tcard*`, `.ui-table`, `.list`, `.ticket-tabs`/`.ticket-tab*` und die Hilfsregeln `.mt-*`, `.flex*`, `.gap-*`, `.grow`, `.wrap` und `.text-ellipsis`.
- [ ] **Step 2: Bausteine übernehmen**
Aus `quelle/styles.css` werden diese Abschnitte übernommen:
- „Grundgerüst“ ohne `.nav-gap`
- „Seitenkopf“
- „Knöpfe“
- „Status-Marken“
- „Reiter“
- „Filterzeile“
- „Tabellen“
- „Kennzahlen / Schlüsselwerte“
- „Ticket-Kopf“
- „Listen“
- „Dialog“
- „Anmeldung“
Dazu kommen die Media-Queries 1100 und 860. **Ausgeschlossen** sind dort `.proto*`, `.ws*`, `.week`, `.day*` und `.appt` (sie kommen mit P2 und P3) sowie `html[data-accent]`.
Zusätzlich:
| Regel | Wert |
|---|---|
| `.page section + section` | `margin-top: 36px` |
| `.nav-dot` | `background: var(--nav-dot)` |
| `.nav-dot.critical` | `background: var(--danger)` |
| `.dialog-wide` | `max-width: 760px` |
| `.msg` | 13px |
| `.msg.err` | Farbe `--red-fg` |
| `.msg.ok` | Farbe `--green-fg` |
| `.msg.warn` | Farbe `--orange-fg` |
| `.loading` | `padding: 56px 0; text-align: center; color: var(--text-2)` |
| `.v.neg` | Farbe `--red-fg` |
| `.stat .sub` | `font-size: 12px; line-height: 16px; color: var(--text-2)` |
| `.badge.plain` | Farbe `--text-2` |
| `.tab.active`, `.tab[aria-selected="true"]` | aktiver Reiter |
| `.seg button.on`, `.seg button[aria-pressed="true"]` | aktiver Teil |
| `.autocomplete-list` | `background: var(--bg); box-shadow: var(--shadow-pop); border-radius: 8px` |
| `.autocomplete-item.active`, `.autocomplete-item:hover` | `background: var(--bg-hover)` |
| `.datetime-pair` | zwei Felder nebeneinander, Abstand 8px, kein `color-scheme: dark` |
| `[aria-invalid="true"]` und `.is-invalid` an `.input`, `.select`, `.textarea`, `.form-control` | `box-shadow: 0 0 0 1px var(--danger), 0 0 0 4px var(--danger-ring)` |
- [ ] **Step 3: Jede Basisregel genau einmal**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && for s in btn table overlay page badge urgent-toggle list-row toolbar seg empty field; do printf '%s ' "$s"; rg -c "^\.$s( |\{|,)" src/styles/global.css; done`
Expected: Hinter jedem Namen steht `1`.
- [ ] **Step 4: Bauen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" --nur-build`
Expected: Exit 0.
- [ ] **Step 5: Commit**
```bash
git add src/styles/global.css
git commit -m "Design: Bausteine aus dem Prototyp, gleichnamige Altregeln ersetzt" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 4: KOMPAT und tote Klassen
**Files:**
- Create: `$W/css-klassen.mjs`
- Create: `docs/superpowers/plans/2026-10-05-weisses-design-p1-tote-klassen.md`
- Modify: `src/styles/global.css` (Block „KOMPAT“, Legacy löschen)
**Interfaces:**
- Produces:
- Der KOMPAT-Block nach der Spec-Tabelle „Übergangsschicht KOMPAT“. P10 entfernt ihn.
- `css-klassen.mjs <worktree> [--ohne-ausnahmen]` gibt JSON aus: `{ defined, used, dead, undefinedUsed }`.
- [ ] **Step 1: Skript schreiben**
„Benutzt“ ist eine Klasse, wenn eine dieser Bedingungen zutrifft:
- Sie steht als Wort in einem `className`-Literal oder Template-String in `src/**/*.jsx`.
- Sie steht in einer Zeichenkette in `src/lib/*.js` oder `src/hooks/*.js`.
- Ein Template-Präfix deckt sie ab: `xxx-${…}` gilt für `xxx-*`.
- Sie steht in der Ausnahmeliste: `fin-neg`, `fin-fixed-off`, `legal-tone-*`, `legal-version-*`, `badge-*`, `status-*`, `ticket-tab-active`, `ui-btn-*`, `side-dot-*`, `woms-chat`, `chat-*`. Mit `--ohne-ausnahmen` entfällt diese Liste.
„Definiert“ ist eine Klasse, wenn sie in einem Selektor in `global.css`, `chat.css`, `serverpuls.css`, `admin-areas.css` oder `pages/AdminPage.css` steht.
- [ ] **Step 2: Liste erzeugen und gegenprüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node "$W/css-klassen.mjs" "$WT" > "$TMPDIR/klassen.json"`
Danach für jeden Kandidaten in `dead`: `rg -w <name> src --glob '!*.css'`. Ein Kandidat gilt nur bei 0 Treffern als tot. Die bestätigte Liste kommt mit Klassenname und definierender Datei:Zeile in `2026-10-05-weisses-design-p1-tote-klassen.md`.
Expected: etwa 66 Namen. Liegt die Zahl unter 40 oder über 90, wird die Abweichung dort begründet.
- [ ] **Step 3: Legacy löschen, KOMPAT anlegen**
Gelöscht werden:
- die Regeln der bestätigten toten Klassen
- alle verbliebenen `!important`
- `.side*` außer `.side-count` und `.side-logo` (die gehen nach KOMPAT)
- `.footer`, `.navbar*`, `.ticket-row*`, `.worksheet-expansion*`, `.slider`, `.drop-zone`, `.modern-card`
- die Legacy-Responsive-Regeln (alt 789-794)
- die Keyframes `slideDown`, `fadeIn` und `slideIn`
- `.kpi*`, weil `Kpi` ab Task 6 `Stat` rendert
Danach wird KOMPAT laut Spec-Tabelle angelegt, markiert mit `/* ===== KOMPAT (bis P10) ===== */`. Der Block enthält auch `.page .page` und `.page .main-content`, die Abstand und Breite neutralisieren.
- [ ] **Step 4: Keine benutzte Klasse ohne Regel**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node "$W/css-klassen.mjs" "$WT" | node -e "const j=JSON.parse(require('fs').readFileSync(0));console.log(j.undefinedUsed.join('\n'))"`
Expected: Nur diese Namen tauchen auf:
- `p-4`, `btn-sm`, `status-filter`, `supporters-editor`, `timeline`
- die Bootstrap-Namen aus `WorksheetList.jsx` (heute schon wirkungslos)
- [ ] **Step 5: Altseiten ansehen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p1 && "$W/shots.sh" p1 "$W/shots/p1-t4" --gate p1; echo "exit $?"`
Expected: `step-failed` (`submitReachable`) ist auf keiner `dlg-*`-Ansicht mehr gemeldet. Erlaubt sind nur noch Treffer `dark-surface` und `low-contrast` in Dateien aus Task 9.
- [ ] **Step 6: Commit**
```bash
git add src/styles/global.css docs/superpowers/plans/2026-10-05-weisses-design-p1-tote-klassen.md
git commit -m "Design: KOMPAT-Schicht fuer Altklassen, tote Regeln und !important entfernt" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 5: Gerüst, Menü, Logo, Favicon
**Files:**
- Create: `src/components/Logo.jsx`, `public/favicon.svg` (Kopie von `quelle/favicon.svg`)
- Modify:
- `src/App.jsx`
- `src/components/Navbar.jsx`
- `src/lib/navLinks.js` (`hot: true` am Chat-Unterpunkt)
- `index.html` (nur `<link rel="icon" href="/favicon.svg" type="image/svg+xml">`)
- `src/styles/serverpuls.css` (Zeilen 70-74 `.side-dot*` löschen)
- `src/pages/ProjectsPage.jsx` (Wurzel `main-content` → `page`)
- `src/pages/AdminPage.jsx` (`<main className="page admin-page">` → `<div className="page admin-page">`, zwei Stellen)
- Delete: `src/components/Footer.jsx`, `src/lib/sidebar.js`, `tests/unit/sidebar.test.mjs`
- Test: `tests/unit/navLinks.test.mjs`
- Modify (Werkzeug): `$W/views.mjs` bekommt die Ansicht `menue-mobil`
**Interfaces:**
- Consumes: Klassen aus Task 3.
- Produces:
- `Logo({ size = 24 })` mit `aria-hidden="true"`. Die Pfade kommen aus `quelle/app.js`, Konstante `LOGO`.
- `Sidebar({ onNavigate })`.
- [ ] **Step 1: Test für `hot` (failing)**
In `tests/unit/navLinks.test.mjs` die Zeile `assert.equal(kunden.children.find((c) => c.key === 'chat').hot, true)` ergänzen. Die bestehenden `deepEqual` bleiben.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/navLinks.test.mjs`
Expected: FAIL.
- [ ] **Step 2: `navLinks.js` ergänzen, Test grün**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/navLinks.test.mjs`
Expected: PASS.
- [ ] **Step 3: Logo und Favicon anlegen**
- [ ] **Step 4: Gerüst in `App.jsx`**
`AppContent` hält `menuOpen` und baut dieses Gerüst:
```jsx
<div className={`shell${menuOpen ? ' menu-open' : ''}`}>
<Sidebar onNavigate={close} />
<div className="scrim" onClick={close} />
<div className="main">
<div className="mobile-bar">
<Link className="brand" to="/tickets"><Logo />WEBklar</Link>
<button type="button" className="btn btn-quiet" onClick={open} aria-expanded={menuOpen} aria-controls="menue">Menü</button>
</div>
<main id="inhalt"><AppRoutes /></main>
</div>
</div>
```
- Ein Effekt auf `location.pathname` (nur darauf) schließt die Schublade und ruft `window.scrollTo(0, 0)` auf.
- Ein Esc-Listener ist nur bei `menuOpen` aktiv.
- Der Ladezustand von `ProtectedRoute` ist `<div className="loading">Wird geladen …</div>`.
- [ ] **Step 5: `Navbar.jsx` als Textliste**
Aufbau: `<aside className="sidebar" id="menue">`, darin `.brand` (Logo und „WEBklar“), `<nav className="nav">` mit `.nav-item` und `.sub` und am Ende `.sidebar-foot`.
| Element | Markup |
|---|---|
| Pausiert | `<span className="nav-item paused" aria-disabled="true">Leads<span className="nav-count">pausiert</span></span>` |
| Zähler | `<span className={`nav-count${child.hot ? ' hot' : ''}`}>` |
| Serverpuls | `<span className={`nav-dot${dot === 'critical' ? ' critical' : ''}`} data-testid="sp-nav-dot" title=…>`, nur wenn `dot` nicht `'ok'` ist. Titeltexte wie heute. |
| Fuß | `<span className="who text-ellipsis">{user.name \|\| user.email}</span>` und `<button type="button" className="link" onClick={handleLogout}>Abmelden</button>` |
Klicks auf Einträge rufen `onNavigate()` auf.
Entfernt werden:
- `@tabler`-Imports und `ICONS`
- Einklappen und `startsCollapsed`
Bleiben:
- `isActive` und `isLinkActive`
- das Serverpuls-Polling
- `useSidebarCounts`
- [ ] **Step 6: Altdateien löschen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git rm -q src/components/Footer.jsx src/lib/sidebar.js tests/unit/sidebar.test.mjs && rg -n "Footer|startsCollapsed|lib/sidebar" src tests`
Expected: keine Treffer.
- [ ] **Step 7: Ansicht `menue-mobil`**
Bei 390×844 läuft:
1. Klick auf „Menü“: `.shell.menu-open` ist da.
2. Screenshot.
3. Esc: `.menu-open` ist weg.
4. Menü erneut öffnen, Klick auf „Projekte“: Der Pfad ist `/projects`, und das Menü ist zu.
- [ ] **Step 8: Prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" && "$W/build.sh" "$WT" p1 && "$W/shots.sh" p1 "$W/shots/p1-t5" --only tickets,kunden,serverpuls,menue-mobil --gate p1`
Expected:
- Exit 0.
- Im Menü ist kein `svg` außer dem Logo.
- [ ] **Step 9: Commit**
```bash
git add src/App.jsx src/components/Navbar.jsx src/components/Logo.jsx src/lib/navLinks.js tests/unit/navLinks.test.mjs index.html public/favicon.svg src/styles/serverpuls.css src/pages/ProjectsPage.jsx src/pages/AdminPage.jsx
git commit -m "Design: Geruest mit Textmenue, Schublade fuers Handy, Logo; Footer und Einklappen entfallen" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 6: UI-Bausteine
**Files:**
- Create: `src/components/ui/tones.js`, `src/components/ui/Stat.jsx`
- Modify:
- `Button.jsx`, `Badge.jsx`, `Card.jsx`, `PageHeader.jsx`, `Tabs.jsx`, `EmptyState.jsx`, `Field.jsx`, `Kpi.jsx`, `StatusPill.jsx`, `index.js`
- nur Klasse `form-control` → `input` bzw. `select`: `AutocompleteInput.jsx`, `DateTimeInput.jsx`, `DurationSelect.jsx`
- Test: `tests/unit/uiTones.test.mjs`
**Interfaces:**
- Produces:
- die Signaturen aus der Übersicht
- `badgeToneClass(tone) → string`
- `BUTTON_CLASSES`
Beide liegen in `tones.js`. `Button` setzt keinen eigenen Standard-`type`.
- [ ] **Step 1: Failing Test**
```js
const { badgeToneClass, BUTTON_CLASSES } = await loadModule('src/components/ui/tones.js')
test('badgeToneClass', () => {
for (const t of ['muted', 'grey', undefined, 'unbekannt']) assert.equal(badgeToneClass(t), 'badge')
assert.equal(badgeToneClass('info'), 'badge blue')
assert.equal(badgeToneClass('blue'), 'badge blue')
assert.equal(badgeToneClass('ok'), 'badge green')
assert.equal(badgeToneClass('warn'), 'badge orange')
assert.equal(badgeToneClass('danger'), 'badge red')
assert.equal(badgeToneClass('violet'), 'badge violet')
assert.equal(badgeToneClass('plain'), 'badge plain')
})
test('Button-Varianten', () => {
assert.deepEqual(BUTTON_CLASSES, { default: 'btn', ghost: 'btn', primary: 'btn btn-primary', quiet: 'btn btn-quiet', danger: 'link-danger' })
})
```
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/uiTones.test.mjs`
Expected: FAIL.
- [ ] **Step 2: `tones.js` anlegen, Test grün**
- [ ] **Step 3: Komponenten umschreiben**
| Komponente | Änderung |
|---|---|
| `Card` | ohne Titel und ohne Aktionen nur `<section>` |
| `PageHeader` | Brotkrumen als `Link`, wenn `to` gesetzt ist. Das letzte Element ist Text, das „/“ steht in `<span className="faint">`. |
| `Tabs` | `active` und `aria-selected` |
| `EmptyState` | ohne Inline-Styles |
| `Field` | Hinweis als `.hint` |
| `Kpi` | gibt `Stat` zurück |
| `StatusPill` | ohne `pillBase` |
- [ ] **Step 4: Prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" && "$W/build.sh" "$WT" p1 && "$W/shots.sh" p1 "$W/shots/p1-t6" --only tickets,ticket,kunde-overview,finanzen --gate p1`
Expected: Exit 0. Die Status-Badges in `tickets` zeigen die neuen Töne.
- [ ] **Step 5: Commit**
```bash
git add src/components/ui tests/unit/uiTones.test.mjs
git commit -m "Design: UI-Bausteine rendern die neuen Klassen (Button, Badge, Card, Kopf, Reiter, Stat)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 7: `Dialog`
**Files:**
- Create: `src/components/ui/Dialog.jsx`, `src/lib/dialogKeys.js`
- Modify: `src/components/ui/index.js`, `$W/views.mjs` (Ansicht `dlg-probe`)
- Test: `tests/unit/dialogKeys.test.mjs`
**Interfaces:**
- Consumes: `useDialogFocus(dialogRef)` aus `src/hooks/useDialogFocus.js`, unverändert.
- Produces:
- `Dialog` laut Übersicht
- `shouldCloseOnKey({ key, defaultPrevented }, busy) → boolean`
- [ ] **Step 1: Failing Test**
```js
const { shouldCloseOnKey } = await loadModule('src/lib/dialogKeys.js')
test('Esc schliesst nur ohne Sperre und ohne vorherige Behandlung', () => {
assert.equal(shouldCloseOnKey({ key: 'Escape', defaultPrevented: false }, false), true)
assert.equal(shouldCloseOnKey({ key: 'Escape', defaultPrevented: true }, false), false)
assert.equal(shouldCloseOnKey({ key: 'Escape', defaultPrevented: false }, true), false)
assert.equal(shouldCloseOnKey({ key: 'Enter', defaultPrevented: false }, false), false)
})
```
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/dialogKeys.test.mjs`
Expected: FAIL.
- [ ] **Step 2: Implementieren, Test grün**
- [ ] **Step 3: `Dialog.jsx`**
| Teil | Umsetzung |
|---|---|
| Esc | `keydown`-Listener auf `document`, prüft mit `shouldCloseOnKey(e, busy)` und ruft dann `onClose()` auf |
| Hintergrund | Klick ohne Wirkung |
| Box | `role="dialog"`, `aria-modal="true"`, `aria-labelledby={titleId}` (`useId()`), `ref`, `tabIndex={-1}` |
| Overlay | `onKeyDown` aus `useDialogFocus` |
| `×` | `<button type="button" className="dialog-close" aria-label="Schließen" disabled={busy} onClick={onClose}>×</button>` |
| `as="form"` | die Box ist `<form onSubmit>` ohne `noValidate` |
- [ ] **Step 4: `dlg-probe`**
Die Ansicht öffnet `/tickets` und fügt per `page.evaluate` einen statischen Klon des Dialog-Markups ein: ein Titel, drei Felder und ein Fuß mit Absende-Knopf. Geprüft wird nur das CSS:
- Bei 390 px liegt die Box unten.
- Das Overlay hat Fenstergröße.
- `submitReachable` gelingt bei 390×700.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/shots.sh" p1 "$W/shots/p1-t7" --only dlg-probe --gate p1`
Expected: Exit 0.
- [ ] **Step 5: Commit**
```bash
git add src/components/ui/Dialog.jsx src/components/ui/index.js src/lib/dialogKeys.js tests/unit/dialogKeys.test.mjs
git commit -m "Design: Dialog-Baustein (Esc, Sperre waehrend Speichern, Fokus, Bottom-Sheet)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 8: Login
**Files:**
- Modify: `src/pages/LoginPage.jsx` (nur Markup)
- Delete: `src/components/LetterGlitch.jsx`
**Interfaces:**
- Consumes:
- `useAuth()` mit `login`, `mfaPending`, `sendCode`, `verifyCode` und `cancelMfa`, unverändert
- `Logo`
- die Klassen `.login*`, `.field`, `.input`, `.code-input`, `.btn-primary.btn-block` und `.msg`
- [ ] **Step 1: Markup ersetzen**
Grundaufbau: `.login > .login-box > <Logo size={40} />`.
**Schritt E-Mail und Passwort**
| Element | Inhalt |
|---|---|
| Überschrift | h1 „Anmelden“ |
| Unterzeile | `.sub` „Ticketsystem für das WEBklar-Team“ |
| Feld E-Mail | Label „E-Mail“, `type="email"`, `autoComplete="username"`, Platzhalter `deine@email.com` |
| Feld Passwort | Label „Passwort“, `type="password"`, `autoComplete="current-password"`, `minLength={8}` |
| Knopf | `.btn.btn-primary.btn-block` „Weiter“, beim Laden „Login läuft...“ |
| Fehler | `.msg.err` mit `role="alert"` |
**Schritt Code**
| Element | Inhalt |
|---|---|
| Überschrift | h1 „Code eingeben“ |
| Text | die beiden Bestandssätze wörtlich |
| Hinweis | `notice` als `.msg.ok` |
| Feld | `#mfa-code` mit `className="input code-input"`, Label „Code aus der E-Mail“ |
| Knopf | „Anmelden“ bzw. „Prüfe…“ |
| Links | `.login-links` mit zwei `button.link` (`type="button"`): „Code erneut senden“ und „Abbrechen“ |
Ohne `challengeId` erscheint stattdessen „Code senden“ bzw. „Sende…“ und darunter „Abbrechen“.
**Fuß:** `.login-foot` „Webklar Ticket-Plattform · © {Jahr}“.
**Bleibt unverändert:** alle Handler und Zustände.
- [ ] **Step 2: `LetterGlitch` löschen und prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git rm -q src/components/LetterGlitch.jsx && ! rg -n "LetterGlitch" src && "$W/build.sh" "$WT" p1 && "$W/shots.sh" p1 "$W/shots/p1-t8" --only login,login-code,login-code-senden --gate p1`
Expected: Exit 0. Im Bild `login-code-senden` steht der Knopf „Code senden“.
- [ ] **Step 3: Commit**
```bash
git add src/pages/LoginPage.jsx
git commit -m "Design: neues Login (weiss, Logo, Code-Schritte), Glitch-Hintergrund entfernt" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 9: Pflicht-Fixes gegen Unlesbares
**Files:**
- Create: `src/components/serverpuls/chartColors.js`, `$W/farben.mjs`
- Modify:
- CSS: `src/styles/chat.css`, `src/pages/AdminPage.css`, `src/styles/admin-areas.css`, `src/styles/serverpuls.css`, `src/styles/global.css` (Bereich Finanzen und Rechtliches)
- Projekt-Bausteine: `src/components/AssignedProjectCard.jsx` (Zeilen 27, 82, 102), `src/components/PreviewBlock.jsx`, `src/components/PreviewLinkButton.jsx`, `src/components/GiteaLinkButton.jsx`, `src/components/AnalyseRow.jsx` (`COLORS`)
- Arbeitsblätter: `src/components/CreateWorksheetModal.jsx` (Zeilen 194, 262), `src/components/WorksheetTimesModal.jsx` (Zeile 143), `src/components/WorksheetList.jsx` (nur Farben, siehe Step 4)
- Serverpuls: `src/components/serverpuls/Chart.jsx`, `src/pages/ServerpulsPage.jsx` (Serienfarben)
- Doku: `docs/superpowers/plans/2026-10-05-weisses-design-p1-tote-klassen.md` (Abschnitt „Bewusst offen“)
**Interfaces:**
- Produces:
- `CHART` laut Übersicht.
- `farben.mjs <worktree>` listet je Treffer Datei:Zeile, Literal und Grund:
- `kontrast<3` (Text auf Weiß)
- `dunkel-auf-akzent`
- `weiss` (`#fff`, `#ffffff`, `white`, `rgba(255,255,255,x)` als Text- oder Randfarbe außerhalb von `.btn-primary` und Elementen auf `--accent`)
- `verlauf` (`linear-gradient` in JSX oder CSS)
- [ ] **Step 1: Scan (failing)**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node "$W/farben.mjs" "$WT"`
Expected: eine Liste mit Treffern aller vier Gründe, unter anderem aus `WorksheetList.jsx`.
- [ ] **Step 2: Chat-Variablen**
Unter `.woms-chat` in `chat.css`:
- `--chat-accent: var(--accent)`
- `--chat-accent-hover: var(--accent-hover)`
- `--chat-accent-soft: var(--accent-soft)`
- `--chat-accent-line: rgba(17,17,17,.14)`
- `--chat-accent-text: var(--accent)`
- `--chat-on-accent: #ffffff`
- `--chat-surface: var(--bg)`
- `--chat-line: var(--line-strong)`
- `--chat-muted: var(--text-2)`
Weitere Regeln:
- Rand der eigenen Blase: `var(--line)`.
- `.woms-chat .chat-messages { background: var(--bg) }`.
- `.woms-chat .chat-composer-note`, `.woms-chat .chat-tray-item.is-error .chat-file-size` und `.woms-chat .chat-audio.is-error .chat-audio-time` bekommen `color: var(--red-fg)`.
- `.woms-chat .chat-composer-note.is-info` bekommt `color: var(--text-2)`.
- [ ] **Step 3: Literale umstellen nach fester Zuordnung**
| Literal | Token |
|---|---|
| `#a0aec0`, `#cbd5e0`, `#9aa7b4`, `#e2e8f0` (als Text) | `var(--text-2)` |
| `#34d399`, `#6ee7b7`, `#10b981` (als Text) | `var(--green-fg)` |
| `#f87171`, `#fca5a5`, `#fecaca`, `#ef4444` (als Text) | `var(--red-fg)` |
| `#fbbf24`, `#f59e0b` (als Text) | `var(--orange-fg)` |
| `#818cf8`, `#9d3dfe` | `var(--violet-fg)` |
| `#3b82f6`, `#60a5fa` | `var(--blue-fg)` |
| `rgba(255,255,255,x)` als Rand oder Linie | `var(--line)` |
| `rgba(255,255,255,x)` als Fläche | entfällt bzw. wird `var(--bg-subtle)` |
| `#05251c`, `#04130d` auf Akzent | `#ffffff` |
| `rgba(30,41,59,x)` und ähnliche dunkle Flächen | `var(--bg-subtle)` |
`.admin-primary-button` bekommt `color: #fff` und beim Überfahren `background: var(--accent-hover)`.
- [ ] **Step 4: `WorksheetList.jsx` minimal**
Nur die Farben werden geändert:
- Verläufe und Weiß-Alphas gehen auf Tokens nach der Tabelle oben.
- Weiße Schrift auf farbigen Köpfen wird `var(--text)` auf `var(--bg-subtle)`.
Struktur und Klassen bleiben. Den Umbau macht P2.
- [ ] **Step 5: Diagrammfarben**
`chartColors.js` wird angelegt. `Chart.jsx` nutzt `CHART.axis`, `CHART.grid` (nur horizontal), `CHART.tick` und als Standard-Serienfarbe `CHART.line`. `ServerpulsPage` setzt:
- alle Serien `CHART.line`
- Spitze `CHART.peak` mit `dash: [4, 4]`
- Füllung `CHART.fill`
- [ ] **Step 6: Scan erneut**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node "$W/farben.mjs" "$WT"`
Expected:
- Kein Treffer `kontrast<3`, `dunkel-auf-akzent` oder `weiss`.
- `verlauf`-Treffer nur dort, wo `.btn-primary` oder ein Canvas sie braucht.
- Jeder Rest steht unter „Bewusst offen“ im Dokument mit Zielpaket und gemessenem Kontrast von mindestens 3:1.
- [ ] **Step 7: Commit**
```bash
git add src/components/serverpuls/chartColors.js src/components/serverpuls/Chart.jsx src/pages/ServerpulsPage.jsx src/styles/chat.css src/pages/AdminPage.css src/styles/admin-areas.css src/styles/serverpuls.css src/styles/global.css src/components/AssignedProjectCard.jsx src/components/PreviewBlock.jsx src/components/PreviewLinkButton.jsx src/components/GiteaLinkButton.jsx src/components/AnalyseRow.jsx src/components/CreateWorksheetModal.jsx src/components/WorksheetTimesModal.jsx src/components/WorksheetList.jsx docs/superpowers/plans/2026-10-05-weisses-design-p1-tote-klassen.md
git commit -m "Design: Pflicht-Fixes gegen helle Schrift und dunkle Flaechen, Diagrammfarben, Chat-Variablen" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 10: Gate P1, Review, Tag
- [ ] **Step 1: Rebase**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git rebase origin/test && "$W/check.sh"`
Expected: Exit 0. Die Testzahl ist 312 − 3 (`sidebar`) + 1 (`statusTone`) + 2 (`uiTones`) + 1 (`dialogKeys`) = 313. Liegen neue Commits auf `test`, kann sie höher sein; die Zahl wird notiert.
- [ ] **Step 2: Vollständiger Gate-Lauf**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p1 && "$W/shots.sh" p1 "$W/shots/p1" --gate p1`
Expected: Exit 0 für alle Ansichten aus P0 plus `basis-weiss`, `menue-mobil` und `dlg-probe`. Alle `dlg-*` bestehen `submitReachable` bei 700 px Höhe.
- [ ] **Step 3: Bundle-Größe**
Die Werte aus der `check.sh`-Ausgabe werden mit P0 verglichen. Das CSS sollte kleiner sein, und die Schrift fehlt.
- [ ] **Step 4: Code-Review (sonnet)**
Der Diff `603d08c..HEAD` (inklusive P0) wird gegen die Spec geprüft, Abschnitte:
- „Gestaltungsregeln“
- „Bausteine“
- „KOMPAT“
- Pakete P0 und P1
Wichtige Befunde werden behoben. Danach kommt der Rebase aus Step 1 noch einmal und Step 2 als Kurzlauf mit `--only tickets,login`.
- [ ] **Step 5: Tag und Übergabe**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" && git tag design-p1 && git log --oneline origin/test..design-p1 && git rev-parse design-p1`
Übergabe an den Eigentümer:
- Screenshots aus `$W/shots/p1` (Login, Tickets, Kunde, Projekte, Finanzen, Serverpuls, ein Alt-Dialog, Menü mobil)
- Hinweis zu den neuen Statusfarben für das Team
- Push-Zeile `git -C /home/webklar/apps/ticket.webklar.com/dev-design push origin 'design-p1^{commit}:refs/heads/test'`
- Rücknahme: `git revert --no-commit 603d08c..design-p1`, Commit und Push durch den Eigentümer
- [ ] **Step 6: Nach dem Push (erst, wenn der Eigentümer gepusht hat)**
- Im Log von `webklar-ticket-build` steht „Deployment completed successfully“.
- `curl -s https://ticket.webklar.com/ | grep -o 'assets/index-[^"]*\.js'` stimmt mit `dist/index.html` des Tags überein.
- Ein lesender Sichttest im Chrome des Eigentümers: Login-Seite und Ticketliste.
@@ -0,0 +1,679 @@
# WOMS „Weißes Design“ P10: Aufräumen – Umsetzungsplan
> **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:** Nach P10 gibt es keine Übergangsschicht mehr. `react-icons` und `@tabler/icons-react` sind deinstalliert, die KOMPAT-Regeln, die alten Token-Aliase und alle toten Klassen sind gelöscht, `previewEnv.dotColor` ist entfernt. Sichtbar ändert sich nichts, das Bundle wird kleiner. Die Umleitung `/tickets?woid=N` auf `/tickets/N` bleibt.
**Architecture:**
- Es wird nur gelöscht, umbenannt oder auf vorhandene Bausteine umgestellt. Die Bilder von `design-p9` sind die Referenz, ein Pixelvergleich (`vergleich.mjs`, gestartet über `run-browser.sh`) beweist nach jedem Löschschritt, dass nichts kippt.
- Reihenfolge nach Risiko: Pakete, `dotColor`, Token-Aliase (reine Umbenennung), KOMPAT, tote Klassen, Inline-Styles und Regelprüfung, Gate und Tag.
- Der Wächtertest `tests/unit/hygiene.test.mjs` hält die Ziele fest (drei Tests, je einer in Task 2, 4 und 5).
- Die Ergebnisdatei `docs/superpowers/plans/2026-10-05-weisses-design-p10-ergebnisse.md` sammelt die Belege.
**Tech Stack:** wie P1. Node 24 (`readdirSync` mit `recursive`), npm 11, ripgrep 13 ohne PCRE2 (keine PCRE2-Option, keine Lookarounds).
**Spec:** `docs/superpowers/specs/2026-10-04-weisses-design-design.md`, Abschnitte „Gestaltungsregeln“, „Design-Tokens“ (Alias-Tabelle), „Übergangsschicht KOMPAT“, „Ticket als eigene Seite / Route und Links“, „Was gleich bleibt“, „Abnahme“ und Zeile P10. Dateidetails im Anhang `…-umbaukarte.md`, Abschnitt 5 (P10), 2.2, 2.3, 4 (D) und 7. Globale Regeln, Befehlsregeln (`env.sh`, `check.sh`, `run-browser.sh`) und Schnittstellen: `2026-10-05-weisses-design-00-uebersicht.md`.
## Global Constraints
Jeder Run-Block beginnt mit `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&`. Danach gelten `$WT` (Worktree `/home/webklar/apps/ticket.webklar.com/dev-design`, Arbeitsverzeichnis), `$W` (Werkzeug) und `$TMPDIR` (Scratchpad). Tests und Build laufen nur über `"$W/check.sh"`, Browser-Skripte nur über `"$W/shots.sh"` oder `"$W/run-browser.sh"`. Suche nur mit `rg` oder `git grep`, nie mit dem Shell-`grep` auf Dateien.
Es gelten die Regeln der Übersicht. Für P10 kommt hinzu:
- Voraussetzung sind die Tags `design-p0` bis `design-p9`, alle Diff-Prüfungen vergleichen gegen `design-p9`. Erlaubt ist nur Löschen, Umbenennen und Umstellen auf vorhandene Bausteine, keine neuen Funktionen, Klassen, Texte oder Tokens. Ausnahmen: Wächtertest, Ergebnisdatei, das Werkzeug außerhalb des Repos und, nur falls sie fehlt, die Deklaration `font-variant-numeric: tabular-nums` an `.num`, `.stat .v` und `.fin-amount` (Task 7 Step 4). Neue Ansichten braucht P10 nicht, das Gate läuft über alle bestehenden.
- Nicht angefasst werden zusätzlich: die `?woid=`-Umleitung (`woidRedirectTarget` in `lib/ticketListUrl.js`, `<Navigate replace>` in `TicketsPage.jsx`), der Token-Block der Spec und die Klassen-Hooks aus „Was gleich bleibt“ (`data-testid`, `data-label`, `data-storage-done`, `data-hidden-hits`).
- Klassensuche token-genau mit `(^|[^A-Za-z0-9_-])NAME([^A-Za-z0-9_-]|$)`. `rg -w` reicht nicht, weil `-` als Wortgrenze gilt und `card` sonst `fin-card` trifft.
- `git add` mit ausdrücklichen Pfaden. Wo ein Schritt viele Dateien ändert, läuft vorher `git diff --name-only`, und `git add` bekommt genau diese geprüfte Liste.
- **Entscheidung:** Pixelinvariante. Das Aussehen der Bilder von `design-p9` gilt, jede Abweichung ist ein Fehler und wird behoben. Ausgenommen sind nur Ansichten mit Rauschen (relative Zeiten wie „vor 5 Minuten“), das ein zweiter Lauf von `design-p9` selbst zeigt.
- **Entscheidung:** Stoppregel für Icons. Liefert `rg "react-icons|@tabler" src` einen Treffer, stammt er aus einem früheren Paket oder aus Commits, die der Rebase von `origin/test` mitbrachte. P10 behebt ihn nach der Ersetzungsregel der Spec (Icon-Knopf wird Textknopf oder Textlink, Zeichen „ד „←“ „→“) in einem eigenen Commit vor Task 2 und nennt ihn in der Ergebnisdatei.
- **Entscheidung:** Die Tokens des Spec-Blocks bleiben, auch ein ungenutztes. Nur die Aliase der Spec-Tabelle fallen, `--sp-1…6` bleiben. `Button` behält die Prop `size` in der Signatur (Spec „Knöpfe“), Aufrufe mit `size="sm"` verschwinden, weil sie nichts bewirken.
- **Entscheidung:** Farbliterale in JSX sind nur in `src/components/Logo.jsx` (Markenzeichen, `#111` und `#fff` wie `public/favicon.svg`) und `src/components/serverpuls/chartColors.js` erlaubt. Inline-`<svg>` bleibt nur in `Logo.jsx` und `Sparkline.jsx`. Der Check `icon-svg` nimmt `svg.sp-spark` (P9) und `.woms-chat .chat-composer *` (P0) in `checks.mjs` aus, dazu das Gate alles unter `.woms-chat` (`ignore`, P6). P10 fügt keine Ausnahme hinzu.
- **Entscheidung:** Beim Umstellen gelten die Regeln „Bausteine benutzen“ der Übersicht und die CSS-Reihenfolge: In jedem `<Dialog as="form">` trägt jeder Knopf außer dem Absenden-Knopf `type="button"`, ein `Card` ohne Titel und Aktionen rendert kein `h2`, und Bereichs-CSS (`chat.css`, `serverpuls.css`, `pages/AdminPage.css`) lädt vor `global.css`. Überstimmt eine globale Regel es nach dem Löschen nicht mehr, braucht der Bereichs-Selektor eine höhere Spezifität, nie `!important`.
- **Entscheidung:** Die Ergebnisdatei hat sieben Abschnitte: 1 Token-Aliase, 2 KOMPAT-Inventur, 3 Tote Klassen, 4 Inline-Styles, 5 Regelprüfung und Invarianten, 6 Bundle, 7 Offene Punkte. Task 4 legt sie mit allen Überschriften an, jeder Task nimmt seinen Abschnitt in seinen Commit auf.
## Review Focus
Aus der Übersicht gehört P10 kein Punkt allein. Alle fünf laufen aber als Ansichten im Gate `p10` mit (`ticket-altdaten*`, `*-lang*`, `fehler-*`, `tasten-*`, `ticket-viel`, `tickets-suche`) und müssen unverändert bleiben. P10 verantwortet sechs eigene Prüfungen:
1. **Nichts Sichtbares ändert sich.** Pixelvergleich `design-p9` gegen P10 über alle Ansichten in allen Viewports (Standard 1440×900 und 390×844, bei `dlg-*` auch 1440×700 und 390×700), keine abweichende Ansicht außerhalb des Rauschens (Task 4, 5, 6 und 8).
2. **Keine noch gebrauchte Regel geht verloren.** Jede KOMPAT-Klasse hat eine Inventurzeile mit Nutzerzahl, jeder Restnutzer wird vor dem Löschen umgestellt (Task 5). Kein `var(--…)` zeigt ins Leere, `undefinedUsed` ist leer (Task 4).
3. **Die Icon-Pakete fehlen nirgends.** `rg` meldet 0 Treffer, der Build ist grün, kein `console-error` in irgendeiner Ansicht (ein fehlender Import wäre zur Laufzeit ein `pageerror`), das Lock passt zu `package.json` (Task 2).
4. **Die `?woid=`-Umleitung bleibt.** Ansicht `alt-link` im Gate, Test `woidRedirectTarget` aus P2 grün, `TicketsPage.jsx` und `ticketListUrl.js` unverändert (Task 8).
5. **Die Spec-Invarianten halten:** alle `data-testid`, die Zahl der `window.confirm`, Hooks und Lib-Logik nur wie in den Paketplänen geändert (Task 7 Step 5, Task 8 Review).
6. **Die übergreifenden Gestaltungsregeln gelten:** höchstens ein `.btn-primary` je `.page-head` (Spec-Regel 4, Check `max-primary` im Gate, Task 1 Step 6 und Task 8) und `tabular-nums` an Zahlen und Beträgen (Spec-Regel 6, `rg` in Task 7 Step 4).
---
### Task 1: Ausgangslage, Referenzbilder, Werkzeug
**Files** (alle unter `$W/`, kein Commit im Repo):
- Create: `vergleich.mjs`, `token-check.mjs`
- Modify: `css-klassen.mjs` (Flag `--ohne-ausnahmen`), `checks.mjs` (Check `max-primary`), `gates.mjs` (Gate `p10`)
**Interfaces:**
- Consumes: Tag `design-p9`, `build.sh`, `shots.sh`, `run-browser.sh`, `check.sh`, `css-klassen.mjs`, `farben.mjs`, `GATES.p9` (P0 bis P9).
- Produces:
- `"$W/run-browser.sh" "$W/vergleich.mjs" <A> <B> [--rauschen <A2>] [--out <Ordner>]` → JSON `{ geprueft, anders: [{ view, viewport, diffPixels, bbox }], rauschen: [view], fehlt: [view] }`. Exit 1, wenn `anders` oder `fehlt` nicht leer ist. A, B und A2 sind Ausgabeordner von `shots.sh` (`<w>x<h>/<id>.png`, z. B. `1440x900/tickets.png` und `390x844/tickets.png`).
- `node "$W/token-check.mjs" <worktree> [--fix]` → JSON `{ legacy: [{ name, ersatz, file, line, art }], undefinedUsed: [{ name, file, line }], mitFallback: [{ name, file, line }], definedUnused: [name] }`. Exit 1, wenn `legacy` oder `undefinedUsed` nicht leer ist.
- `css-klassen.mjs --ohne-ausnahmen`: ignoriert die Ausnahmeliste und gibt zusätzlich `nurAusnahme: [name]` aus, die Klassen, die nur durch die Liste als benutzt gelten.
- Check `max-primary` in `checks.mjs`: Verstoß `{ check: 'max-primary', selector: '.page-head', detail }` für jedes sichtbare `.page-head` mit mehr als einem sichtbaren `.btn-primary`.
- `GATES.p10` nach dem Schema der Übersicht (Step 6).
- [ ] **Step 1: Paketstart und Stand prüfen**
Run:
```bash
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
git fetch origin && git merge-base --is-ancestor design-p9 HEAD && git rebase origin/test && "$W/check.sh"
git log --oneline origin/test..origin/feature/terminplaner-tp2 -- src package.json package-lock.json tests
git tag --list 'design-p*'; git log --oneline design-p9..origin/test; git status --short
```
Expected:
- Erste Zeile: `design-p9` ist Vorfahre (sonst Exit 1: stoppen und melden, der Rebase schrieb die Pakete um), der Rebase läuft ohne Konflikt, `check.sh` endet mit Exit 0, `# fail 0`, `# pass` ist `N9` (notieren).
- Zweite Zeile (Abgleich mit `feature/terminplaner-tp2`, P10 berührt fast alle Dateien unter `src`): leer. Gibt sie etwas aus, stoppt der Agent und fragt den Eigentümer. Er empfiehlt dabei, TP2 zuerst nach `test` zu bringen.
- Tags `design-p0` bis `design-p9`, `git log` und `git status` leer. Kommen fremde Commits mit, werden ihre Dateien in den folgenden Tasks wie alle anderen geprüft.
- [ ] **Step 2: Voraussetzungen im Werkzeug**
Run:
```bash
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
rg -n "GATES\.p9|p1:" "$W/gates.mjs"; rg -n "ignore" "$W/gates.mjs"; rg -n "sp-spark" "$W/checks.mjs"
ls "$W/check.sh" "$W/run-browser.sh" "$W/css-klassen.mjs" "$W/farben.mjs" "$W/build.sh" "$W/shots.sh"
```
Expected: Gates `p1` und `p9`, im `ignore` nur `.woms-chat` (P6, `.ticket-projects` strich P5), Ausnahme `svg.sp-spark` und alle sechs Dateien sind da. Fehlt etwas, wird gestoppt und gemeldet.
- [ ] **Step 3: Ausgangswerte des Bundles festhalten**
Run:
```bash
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
git log design-p0 --format=%B | rg "Ausgangswerte" &&
"$W/check.sh" --nur-build &&
{ find dist -type f \( -name '*.js' -o -name '*.css' -o -name '*.ttf' \) -printf '%s %p\n'; for f in dist/assets/*.js dist/assets/*.css; do echo "$f gzip=$(gzip -9 -c "$f" | wc -c)"; done; wc -l src/styles/*.css src/pages/*.css; } | tee "$W/p10-baseline-p9.txt"
```
Expected: die P0-Zeile `Ausgangswerte: <Testzahl>, JS <x> B, CSS <y> B` (Gegenprobe der Umbaukarte: JS 680.818 B, CSS 64.433 B, Lato 96.184 B, 2.079 CSS-Zeilen), Build grün, keine `.ttf` in `dist`, alle Zahlen in `$W/p10-baseline-p9.txt`.
- [ ] **Step 4: Referenzbilder und Rauschmaß**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p9-ref && "$W/shots.sh" p9-ref "$W/shots/p9-ref" --gate p9 && "$W/shots.sh" p9-ref "$W/shots/p9-ref2" --gate p9 && ls "$W/shots/p9-ref/1440x900" | wc -l`
Expected: beide Läufe Exit 0, je Ansicht ein Bild je Viewport (`1440x900` und `390x844`, bei `dlg-*` zusätzlich `1440x700` und `390x700`). Die Zahl der Ansichten ist die Sollzahl aller späteren Läufe (notieren).
- [ ] **Step 5: `vergleich.mjs` schreiben und prüfen**
Umsetzung:
- Das Skript startet Chromium selbst (Ladeweise wie `shots.mjs`) und läuft deshalb nur über `run-browser.sh`.
- Die PNGs werden in Playwright-Chromium (`about:blank`) per `createImageBitmap` und Canvas dekodiert und pixelweise verglichen. Eine andere Bildgröße ist `anders` und nennt beide Größen. `bbox` ist der kleinste Rahmen um alle abweichenden Pixel.
- `--out` bekommt je abweichender Ansicht ein Bild unter `<w>x<h>/<id>.png`, abweichende Pixel rot.
- Eine Ansicht mit Abweichung zwischen A und A2 zählt als `rauschen`, außer die Abweichung zwischen A und B reicht über deren `bbox` hinaus. Dann ist sie `anders`.
Run:
```bash
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
cp -r "$W/shots/p9-ref" "$TMPDIR/probe" && cp "$TMPDIR/probe/1440x900/tickets.png" "$TMPDIR/probe/1440x900/kunden.png"
"$W/run-browser.sh" "$W/vergleich.mjs" "$W/shots/p9-ref" "$TMPDIR/probe"; echo "exit $?"
"$W/run-browser.sh" "$W/vergleich.mjs" "$W/shots/p9-ref" "$W/shots/p9-ref2"; echo "exit $?"
```
Expected: Der erste Aufruf nennt genau `kunden` bei 1440×900 als `anders`, Exit 1. Der zweite endet mit Exit 0 oder nennt wenige Rausch-Ansichten. Jede wird einmal angesehen und in `$W/p10-rauschen.txt` notiert.
- [ ] **Step 6: `token-check.mjs`, Flag, Check `max-primary` und Gate**
Umsetzung von `token-check.mjs`:
- Es liest `src/**/*.{css,jsx,js}` und schreibt nur dort. Definiert ist ein Name durch `--name:` in CSS oder den Schlüssel `'--name'` in JSX-Objekten, benutzt durch `var(--name`.
- `legacy` enthält jede Nutzung und jede Definition eines Alias-Namens der Tabelle in Task 4 (`art` ist `nutzung` oder `definition`, `ersatz` der neue Name oder der Wert `4px` bzw. `none`).
- `undefinedUsed` sind benutzte, nirgends definierte Namen ohne Rückfallwert, mit Rückfallwert landen sie in `mitFallback`.
- `--fix` schreibt nur die `nutzung`-Einträge um (`var(--alt)` und `var(--alt, …)`), `--sidebar-w-collapsed` nie.
Dazu kommen:
- das Flag in `css-klassen.mjs`,
- der Check `max-primary` in `checks.mjs` (Spec, Gestaltungsregel 4: höchstens ein `.btn-primary` je Seitenkopf), mit `detail` `<n> .btn-primary`,
- `GATES.p10` in `gates.mjs`. P10 hat keine eigenen Ansichten, `icon-svg` gilt deshalb für alle (`'*'` ist der Präfix-Platzhalter für alle Ansichten, wie in P2), mit dem `ignore` aus `p9` unverändert. `step-failed`, `external-request` und `console-error` sind wie in jedem Gate aktiv.
```js
const iconSvg = GATES.p9.scoped['icon-svg']
GATES.p10 = {
...GATES.p9,
checks: [...GATES.p9.checks, 'max-primary'],
scoped: { ...GATES.p9.scoped, 'icon-svg': { ...iconSvg, views: ['*'] } },
}
```
Run:
```bash
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
mkdir -p "$TMPDIR/tc/src" && printf 'a{color:var(--nope);border:1px solid var(--border)}' > "$TMPDIR/tc/src/x.css"
node "$W/token-check.mjs" "$TMPDIR/tc"; echo "exit $?"
node "$W/token-check.mjs" . | node -e "const j=JSON.parse(require('fs').readFileSync(0));console.log(j.legacy.length, j.undefinedUsed.length)"
node "$W/css-klassen.mjs" . --ohne-ausnahmen | node -e "const j=JSON.parse(require('fs').readFileSync(0));console.log(j.dead.length, j.nurAusnahme.length)"
mkdir -p "$TMPDIR/probe-primary" && printf '<!doctype html><meta charset="utf-8"><title>Probe</title><div class="page-head"><h1>Test</h1><button class="btn btn-primary">A</button><button class="btn btn-primary">B</button></div>' > "$TMPDIR/probe-primary/index.html"
"$W/shots.sh" probe "$TMPDIR/probe-primary-out" --static "$TMPDIR/probe-primary" --only tickets --gate p10; echo "exit $?"
rg -o "max-primary" "$TMPDIR/probe-primary-out/report.json" | wc -l
```
Expected:
- Der erste Aufruf endet mit Exit 1, `undefinedUsed` nennt `--nope`, `legacy` nennt `--border` mit `ersatz` `--line`.
- Der zweite zeigt eine Zahl über 0 für `legacy` (die Alias-Definitionen stehen noch) und 0 für `undefinedUsed`.
- Der dritte nennt zwei Zahlen, sie werden notiert.
- Die Probe mit zwei Primärknöpfen in einem `.page-head` endet mit Exit 1, die letzte Zeile nennt eine Zahl über 0 (der Check `max-primary` schlägt an).
- [ ] **Step 7: Gate `p10` gegen `design-p9`**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/shots.sh" p9-ref "$W/shots/p9-ref-p10" --gate p10; echo "exit $?"`
Expected: Exit 0 (die Bilder dieses Laufs werden nicht gebraucht). Ein Treffer `icon-svg` folgt der Stoppregel für Icons. Ein Treffer `max-primary` ist ein Fehler eines früheren Pakets: Der Agent stoppt und meldet ihn dem Eigentümer, weil der überzählige Knopf das Aussehen ändern würde.
### Task 2: Icon-Pakete entfernen
**Files:**
- Create: `tests/unit/hygiene.test.mjs`
- Modify: `package.json` (Zeilen 6 und 11), `package-lock.json`
**Interfaces:**
- Consumes: nichts. Voraussetzung ist `rg "react-icons|@tabler" src` = 0.
- Produces: ein Lock ohne `@tabler/icons-react`, `@tabler/icons` und `react-icons`, dazu `hygiene.test.mjs` mit den Helfern `files`, `read`, `offenders(re, only)`, auf denen Task 4 und 5 aufbauen.
- [ ] **Step 1: Failing Test schreiben**
```js
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync, readdirSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..')
const files = readdirSync(path.join(root, 'src'), { recursive: true })
.filter((f) => /\.(jsx?|css)$/.test(f))
.map((f) => path.join('src', f))
const read = (rel) => readFileSync(path.join(root, rel), 'utf8')
const offenders = (re, only = /./) => files.filter((f) => only.test(f) && re.test(read(f)))
test('Icon-Pakete sind weder Abhängigkeit noch Import', () => {
const pkg = JSON.parse(read('package.json'))
for (const n of ['react-icons', '@tabler/icons-react']) assert.equal(pkg.dependencies[n], undefined, n)
assert.deepEqual(offenders(/react-icons|@tabler/), [])
})
```
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/hygiene.test.mjs`
Expected: FAIL (`package.json` führt beide Pakete).
- [ ] **Step 2: Beweisen, dass nichts mehr importiert**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|@tabler" src`
Expected: keine Ausgabe. Bei einem Treffer gilt die Stoppregel der Global Constraints.
- [ ] **Step 3: Deinstallieren**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && systemd-run --scope -q -p MemoryMax=1500M -p MemorySwapMax=0 npm uninstall react-icons @tabler/icons-react --no-audit --no-fund`
Expected: Exit 0.
- [ ] **Step 4: Lock und Index prüfen**
Run:
```bash
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
git status --short; git diff package.json | rg "^[-+] "; rg -c "tabler|react-icons" package.json package-lock.json; git diff --numstat package-lock.json
npm ls react-icons @tabler/icons-react 2>&1 | rg "empty"; npm ci --dry-run --no-audit --no-fund; echo "exit $?"
```
Expected:
- `git status` zeigt genau `M package-lock.json`, `M package.json` und `?? tests/unit/hygiene.test.mjs`, keine `D`-Zeile (`node_modules` ist nicht getrackt).
- Der Diff von `package.json` hat genau zwei `-`-Zeilen, `rg -c` gibt nichts aus.
- `numstat` zeigt 0 hinzugefügte Zeilen. Stehen dort wenige, sind es höchstens geänderte Flags (`"peer"`, `"dev"`), und jede `+`-Zeile wird angesehen.
- `npm ls` meldet `(empty)`, `npm ci --dry-run` endet mit Exit 0.
- [ ] **Step 5: Tests und Build**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh"`
Expected: Exit 0, `# fail 0`, `# pass` = `N9 + 1`, Build grün.
- [ ] **Step 6: Commit**
```bash
git add package.json package-lock.json tests/unit/hygiene.test.mjs
git commit -m "Aufraeumen: react-icons und @tabler/icons-react deinstalliert (src ohne Icon-Importe), Waechtertest" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 3: `dotColor` und Icon-Kommentare
**Files:**
- Modify: `src/lib/previewEnv.js` (Zeile 1 und Zeilen 15–18 laut Ausgangsstand), `tests/unit/previewEnv.test.mjs` (Zeile 5 und der Test `dotColor`, Zeilen 21–29)
- Modify: `src/lib/navLinks.js` (Zeile 1), `tests/unit/navLinks.test.mjs` (Zeile 5)
**Interfaces:**
- Consumes: `isValidEnvName`, `buildEnvPayload`, `isDevPreview`, `envValueTooLong`, `MAX_ENV_VARS` aus `previewEnv.js`, unverändert.
- Produces: `previewEnv.js` ohne `dotColor` und `DOT_COLORS`.
- [ ] **Step 1: Beweisen, dass `dotColor` unbenutzt ist**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "dotColor|DOT_COLORS" src tests`
Expected: Treffer nur in `src/lib/previewEnv.js` und `tests/unit/previewEnv.test.mjs` (P5 hat `PreviewBlock` und `ServicesPage` umgestellt). Weitere Treffer sind ein Fehler eines früheren Pakets.
- [ ] **Step 2: Test zuerst anpassen**
Der Test `dotColor` und der Name im `loadModule`-Destrukturieren entfallen.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/previewEnv.test.mjs`
Expected: PASS mit 4 Tests (`isValidEnvName`, `buildEnvPayload`, `isDevPreview`, `envValueTooLong`).
- [ ] **Step 3: Funktion löschen**
`DOT_COLORS` und `dotColor` fallen, der Kopfkommentar heißt „(Variablen, Dev-Erkennung)“.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/previewEnv.test.mjs; rg -n "dotColor|DOT_COLORS|#10b981|#f59e0b|#a0aec0" src tests`
Expected: PASS, keine Treffer.
- [ ] **Step 4: Icon-Kommentare**
- `src/lib/navLinks.js` Zeile 1 wird `// Eintraege der Sidebar als reine Daten (unter Node testbar); Navbar.jsx rendert sie als Textliste.` Der zweite Kommentarsatz zu Unterpunkten und `count` bleibt.
- `tests/unit/navLinks.test.mjs` Zeile 5 wird `// Sidebar: Eintraege als reine Daten (Navbar.jsx rendert sie als Textliste)`.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n -i "(^|[^a-z])icons?([^a-z]|$)" src tests --glob '!*.css' --glob '!hygiene.test.mjs'`
Expected: keine Treffer, höchstens Kommentare zum fremden Chat-Baustein in `src/components/chat` und `src/lib/loadChatComposer.js`. Jeder andere Treffer wird umformuliert. „favicon“ und der Wächtertest zählen nicht.
- [ ] **Step 5: Test**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" --nur-tests`
Expected: Exit 0, `# fail 0`, `# pass` = `N9` (hygiene +1, `dotColor` −1).
- [ ] **Step 6: Commit**
```bash
git add src/lib/previewEnv.js tests/unit/previewEnv.test.mjs src/lib/navLinks.js tests/unit/navLinks.test.mjs
git commit -m "Aufraeumen: previewEnv.dotColor samt Test entfernt, Icon-Kommentare in navLinks angepasst" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 4: Token-Aliase abbauen
**Files:**
- Modify: `src/styles/global.css` (Alias-Block in `:root`), alle Dateien unter `src/` mit alten Namen (Liste aus Step 3)
- Create: `docs/superpowers/plans/2026-10-05-weisses-design-p10-ergebnisse.md` (Überschriften 1 bis 7, Abschnitt 1 gefüllt)
- Test: `tests/unit/hygiene.test.mjs` (zweiter Test)
**Interfaces:**
- Consumes: `$W/token-check.mjs` (Task 1) und die Alias-Tabelle der Spec:
| Alter Name | Neu |
|---|---|
| `--surface-1`, `--surface-3`, `--surface`, `--dark-bg`, `--dark-card-bg` | `--bg` |
| `--surface-2`, `--surface-hover` | `--bg-hover` |
| `--border`, `--gray-700`, `--admin-line` | `--line` |
| `--border-strong`, `--admin-line-strong` | `--line-strong` |
| `--border-accent` | `--accent-ring` |
| `--text-muted`, `--text-faint`, `--gray-800` | `--text-2` |
| `--dark-text`, `--gray-900` | `--text` |
| `--accent-2`, `--green-secondary` | `--accent-hover` |
| `--green-primary` | `--accent` |
| `--info`, `--warn`, `--ok` | `--blue-fg`, `--orange-fg`, `--green-fg` |
| `--r-sm`, `--r-md`, `--r-lg`, `--r-pill` | `--radius`, `--radius-lg`, `--radius-lg`, Wert `4px` |
| `--shadow-1`, `--shadow-2`, `--shadow-3` | `none`, `--shadow-btn`, `--shadow-pop` |
| `--sidebar-w-collapsed` | entfällt, kein Nutzer erlaubt |
- Produces: `:root` ohne Aliase, `token-check` meldet `legacy` und `undefinedUsed` leer.
- [ ] **Step 1: Failing Test ergänzen**
```js
const LEGACY = ['surface-1', 'surface-2', 'surface-3', 'surface-hover', 'surface', 'dark-bg', 'dark-card-bg', 'border', 'border-strong', 'border-accent',
'gray-700', 'gray-800', 'gray-900', 'admin-line', 'admin-line-strong', 'text-muted', 'text-faint', 'dark-text', 'accent-2', 'green-secondary',
'green-primary', 'info', 'warn', 'ok', 'r-sm', 'r-md', 'r-lg', 'r-pill', 'shadow-1', 'shadow-2', 'shadow-3', 'sidebar-w-collapsed']
test('keine alten Token-Namen mehr (Aliase sind gelöscht)', () => {
const re = new RegExp(`(^|[^A-Za-z0-9-])--(${LEGACY.join('|')})([^A-Za-z0-9-]|$)`)
assert.deepEqual(offenders(re), [])
})
```
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/hygiene.test.mjs`
Expected: FAIL im neuen Test.
- [ ] **Step 2: Bestandsaufnahme**
Run:
```bash
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
node "$W/token-check.mjs" . > "$TMPDIR/token-vorher.json"; node -e "const j=JSON.parse(require('fs').readFileSync(process.argv[1]));const c={};for(const e of j.legacy)c[e.name+' '+e.art]=(c[e.name+' '+e.art]||0)+1;console.log(c)" "$TMPDIR/token-vorher.json"
```
Expected: Zahl der Nutzungen und Definitionen je Alias. Sie kommt in Abschnitt 1 der Ergebnisdatei (Spalten: alter Name, Nutzungen vorher, ersetzt durch).
- [ ] **Step 3: Nutzungen umbenennen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node "$W/token-check.mjs" . --fix; git diff --name-only`
Expected: Es ändern sich nur `.css`, `.jsx` und `.js` unter `src/`. Das ist eine reine Umbenennung, der Wert jedes Tokens bleibt gleich.
- [ ] **Step 4: Alias-Block löschen**
In `global.css` fallen der Alias-Block hinter dem Token-Block samt Kommentar und jede übrige Definition eines Alias-Namens, auch außerhalb von `:root`.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/hygiene.test.mjs && node "$W/token-check.mjs" . | node -e "const j=JSON.parse(require('fs').readFileSync(0));console.log(j.legacy.length, j.undefinedUsed.length, j.mitFallback.length)"`
Expected: PASS und `0 0` plus eine Zahl für `mitFallback`. Jeder Eintrag dort wird angesehen: Der Rückfallwert fällt, wenn das Token existiert, sonst steht der Eintrag in Abschnitt 7.
- [ ] **Step 5: Bauen, Gate, Pixelvergleich**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p10 && "$W/shots.sh" p10 "$W/shots/p10-t4" --gate p10 && "$W/run-browser.sh" "$W/vergleich.mjs" "$W/shots/p9-ref" "$W/shots/p10-t4" --rauschen "$W/shots/p9-ref2" --out "$W/shots/p10-t4-diff"`
Expected: Gate Exit 0, Vergleich Exit 0. Weicht eine Ansicht ab, hat `--fix` einen Namen falsch ersetzt oder eine Definition lag in einer Datei, die das Skript nicht liest. Das wird behoben.
- [ ] **Step 6: Ergebnisdatei und Commit**
Die Ergebnisdatei wird mit den Überschriften 1 bis 7 angelegt, Abschnitt 1 bekommt die Tabelle aus Step 2.
```bash
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
git diff --name-only
git add docs/superpowers/plans/2026-10-05-weisses-design-p10-ergebnisse.md tests/unit/hygiene.test.mjs <die geprüften Dateien aus der Liste oben>
git commit -m "Aufraeumen: Token-Aliase der Altnamen durch die neuen Tokens ersetzt und geloescht" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 5: KOMPAT-Block entfernen
**Files:**
- Modify: `src/styles/global.css` (Block `/* ===== KOMPAT (bis P10) ===== */` und der Gliederungskommentar am Dateianfang), Dateien mit Restnutzern (Liste aus Step 3)
- Modify: Ergebnisdatei Abschnitt 2
- Test: `tests/unit/hygiene.test.mjs` (dritter Test)
**Interfaces:**
- Consumes: die Bausteine aus P1 (`.btn`, `.btn-primary`, `.btn-quiet`, `.link`, `.link-danger`, `.badge`, `.field`, `.input`, `.select`, `.textarea`, `.msg`, `.muted`, `.dialog`, `.overlay`, `.seg button.on`) und `Dialog`, `Card`, `Badge`, `StatusPill`.
- Produces: `global.css` ohne KOMPAT. Kein Selektor der Spec-Tabelle „Übergangsschicht KOMPAT“ ist übrig, `.page` bleibt als Wurzelklasse der Seiten.
- [ ] **Step 1: Failing Test ergänzen**
```js
test('keine Übergangsschicht und kein !important im CSS', () => {
assert.deepEqual(offenders(/KOMPAT|!important/, /\.css$/), [])
})
```
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/hygiene.test.mjs`
Expected: FAIL im neuen Test.
- [ ] **Step 2: Grenzen und Inventur**
Run:
```bash
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
read A B < <(awk '/KOMPAT \(bis P10\)/{a=NR; next} a && /^\/\* (=====|---)/{print a+1, NR-1; f=1; exit} END{if(a&&!f)print a+1, NR}' src/styles/global.css); echo "KOMPAT $A-$B"
sed -n "${A},${B}p" src/styles/global.css | rg -o "\.[a-z][a-z0-9_-]*" | sort -u | sed 's/^\.//' | while read c; do echo "$(rg -l "(^|[^A-Za-z0-9_-])${c}([^A-Za-z0-9_-]|$)" src --glob '!*.css' | wc -l) $c"; done | sort -n | tee "$TMPDIR/kompat-inventur.txt"
```
Expected:
- Der Block endet an der nächsten Kopfzeile `/* =====` oder `/* ---`, sonst am Dateiende.
- Die Inventur hat 0 Nutzer für jede Klasse der Tabelle in Step 3. Bekannte Ausnahmen: `overlay` (nur `src/components/ui/Dialog.jsx`), `row` (nur an `<tr>`) und Klassen, die auch ein neuer Baustein trägt (`btn`, `badge`, `field`, `link`). Deren Regel wandert nach Step 4 in Block 3.
- Die Liste kommt als Tabelle (Klasse, Nutzer, Entscheidung) in Abschnitt 2 der Ergebnisdatei.
- [ ] **Step 3: Restnutzer finden und umstellen**
| KOMPAT-Zeile der Spec | Gesuchte Namen | Ersatz |
|---|---|---|
| `.ui-card*` | `ui-card` | `Card` (ohne Titel und Aktionen rendert es kein `h2`) oder `<section>` mit `h2.section` |
| `.ui-btn*` | `ui-btn`, `-primary`, `-ghost`, `-danger`, `-sm`, Muster `ui-btn-${…}` | `btn`, `btn btn-primary`, `link-danger`, Zeilenaktion `link` |
| `.form-group`, `.form-label`, `.form-control` | dieselben drei | `field`, Label im `.field`, `input`, `select`, `textarea` |
| `.badge-*`, `.status-*`, `.priority-*` | `badge-` und `status-` mit `muted`, `info`, `ok`, `warn`, `danger`, `open`, `working`, `awaiting`, `assigned`, `closed`; `priority-critical`; Muster `badge-${…}`, `status-${…}` | `Badge`, `StatusPill` |
| `.overlay` (Altform), `.overlay-content`, `.overlay-close` | `overlay-content`, `overlay-close`, `overlay` außerhalb von `Dialog.jsx`, `fin-dialog` (760 px, laut P7 tot) | `Dialog` |
| `.seg button.active` | kein festes Muster: jedes `seg` in JSX ansehen | Knopf mit `on` und `aria-pressed` |
| `.bg-red`, `.text-white`, `.text-grey`, `.text-red` | dieselben vier | `msg err` bzw. `muted` |
| `.side-count`, `.side-logo` | dieselben zwei | schon entfernt: `.side-logo` in P4, `.side-count` in P6. Ein Treffer ist ein Rest und wird entfernt |
| `.btn-dark`, `.btn-teal` | dazu `btn-green` | `btn btn-primary` |
| `.card*`, `.row`, `.col*`, `.dropdown*` | `card`, `card-header`, `card-body`, `col`, `col-6`, `dropdown*`, `row` außerhalb von `<tr>` | entfallen |
| `.spinner` | `spinner` | Text „Wird geladen …“ |
| `.page .page`, `.page .main-content` | `main-content` | entfällt (Step 5) |
| Links in Bestandsklassen | `fin-link`, `credit-cols`, `area-rows`. Die Klasse `legal-files` bleibt (P8), nur die Regel `.legal-files a` fällt. | Klasse `link` am `<a>`, sonst die Regel in den Block des Nutzers |
In jedem `<Dialog as="form">`, das dabei entsteht oder angefasst wird, trägt jeder Knopf außer dem Absenden-Knopf `type="button"`.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n '(^|[^A-Za-z0-9_-])(ui-(card|btn)|form-(group|label|control)|(badge|status)-(muted|info|ok|warn|danger|open|working|awaiting|assigned|closed)|priority-critical|overlay-(content|close)|fin-dialog|bg-red|text-(white|grey|red)|side-(count|logo)|btn-(dark|teal|green)|spinner|dropdown[a-z-]*|card|card-header|card-body|col|col-6|main-content|fin-link|credit-cols|area-rows)([^A-Za-z0-9_-]|$)' src --glob '!*.css'; rg -n '(badge|status|ui-btn)-\$\{' src; rg -n 'className="[^"]*(^|[^A-Za-z0-9_-])row([^A-Za-z0-9_-]|")' src | rg -v '<tr'`
Expected: keine Treffer. Ein Treffer in einem Kommentar wird umformuliert, jeder Code-Treffer nach der Tabelle umgestellt, eine Gruppe je Commit (Meldung „Aufraeumen: Restnutzer von <Gruppe> auf <Baustein> umgestellt“).
- [ ] **Step 4: Regeln, die ein Baustein noch braucht**
Entscheidung: Das Aussehen der Bilder von `design-p9` gilt. Eine KOMPAT-Regel, deren Selektor auch einen neuen Baustein trifft, wird nicht gelöscht. Ihre Deklarationen gehören dann an den Baustein in Block 3, nur die alten Selektoren fallen. Bekannter Fall ist `.overlay`, die Hülle von `Dialog`:
- Ist die KOMPAT-Zeile für die Altform ein eigener Selektor (`overlay-content`, `:has(…)`), fällt sie weg.
- Hat sie die Basisregel `.overlay` selbst verändert (`overflow-y: auto`, Abstand 24/16 px), gelten die Werte, die die Bilder von `design-p9` für `Dialog` zeigen, und bleiben in der Basisregel.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "^\.(overlay|dialog)\b" src/styles/global.css`
Expected: eine Basisregel `.overlay` mit der Mobilregel bei 860 px, ohne Verweis auf `overlay-content`.
- [ ] **Step 5: Block löschen, Verschachtelung statisch beweisen**
Der Block samt Marker fällt, der Gliederungskommentar nennt danach vier Blöcke statt fünf.
Entscheidung: `.page` bleibt als Wurzelklasse jeder Seite. Gelöscht werden nur die Verschachtelungsregel `.page .page, .page .main-content` und die alte `.main-content`-Regel.
Run:
```bash
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
node --test tests/unit/hygiene.test.mjs
rg -n "KOMPAT|main-content" src
rg -n 'className=\{?["`]([^"`]* )?page([ "`}]|$)' src --glob '*.jsx' | rg -v '^src/pages/'
rg -n '<[A-Za-z]+Page\b' src --glob '!App.jsx'
```
Expected: PASS und drei leere Ausgaben. Es gibt also kein `KOMPAT` und kein `main-content` mehr, die Klasse `page` steht nur in `src/pages/*.jsx` (der Treffer-Filter erfasst nur Klassenstellen, keine Props wie `page` in `Pager`), und nur `App.jsx` rendert `*Page`-Komponenten. Eine Seite in einer Seite ist damit ausgeschlossen. Ein Treffer, der nur ein Wort im Kommentar ist, wird umformuliert.
- [ ] **Step 6: Bauen, Gate, Pixelvergleich**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p10 && "$W/shots.sh" p10 "$W/shots/p10-t5" --gate p10 && "$W/run-browser.sh" "$W/vergleich.mjs" "$W/shots/p9-ref" "$W/shots/p10-t5" --rauschen "$W/shots/p9-ref2" --out "$W/shots/p10-t5-diff"`
Expected: Gate Exit 0, Vergleich Exit 0. Auch alle Alt-Dialoge (`dlg-*`), die `.seg`-Ansichten (`finanzen`, `serverpuls`) und Links bleiben unverändert. Weicht eine Ansicht ab, wird das Diff-Bild angesehen. Es gibt zwei Ursachen:
- Die gelöschte Regel gehört an die Stelle ihres Bausteins (Step 4).
- Bereichs-CSS (`chat.css`, `serverpuls.css`, `pages/AdminPage.css`) lädt vor `global.css` und wurde bisher von KOMPAT überstimmt. Der Bereichs-Selektor bekommt dann eine höhere Spezifität, nie `!important`.
- [ ] **Step 7: Commit**
Abschnitt 2 der Ergebnisdatei ist gefüllt.
```bash
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
git diff --name-only
git add docs/superpowers/plans/2026-10-05-weisses-design-p10-ergebnisse.md tests/unit/hygiene.test.mjs src/styles/global.css <weitere geprüfte Dateien>
git commit -m "Aufraeumen: KOMPAT-Block, .page-Verschachtelungsregel und Altklassen entfernt" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 6: Tote Klassen und Keyframes
**Files:**
- Modify: `src/styles/*.css`, `src/pages/*.css`, JSX-Dateien mit Klassen ohne Regel, Ergebnisdatei Abschnitt 3, `$W/css-klassen.mjs` (Ausnahmeliste)
- Delete: CSS-Dateien ohne verbleibende Regel samt Importzeilen
**Interfaces:**
- Consumes: `css-klassen.mjs` mit `--ohne-ausnahmen` (Task 1).
- Produces: `dead` und `undefinedUsed` sind leer, die Ausnahmeliste enthält nur Namen mit belegter Konstruktionsstelle.
- [ ] **Step 1: Liste erzeugen**
Run:
```bash
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
node "$W/css-klassen.mjs" . --ohne-ausnahmen > "$TMPDIR/klassen-p10.json"; node -e "const j=JSON.parse(require('fs').readFileSync(process.argv[1]));for(const k of ['dead','undefinedUsed','nurAusnahme'])console.log(k, j[k].length+':', j[k].join(' '))" "$TMPDIR/klassen-p10.json"
```
Expected: drei Zeilen mit Namen. `dead` enthält zunächst auch die Namen aus `nurAusnahme`.
- [ ] **Step 2: Gegenprobe und Löschen**
Für jeden Namen aus `dead`, der nicht in `nurAusnahme` steht:
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "(^|[^A-Za-z0-9_-])NAME([^A-Za-z0-9_-]|$)" src --glob '!*.css'`
Expected: keine Treffer. Dann fällt jede Regel mit diesem Namen in der definierenden Datei, in einer Selektorliste nur der Name.
Für jeden Namen aus `nurAusnahme` bekommt Abschnitt 3 der Ergebnisdatei eine Zeile mit der Stelle, die ihn baut: ein Template-Präfix wie `fin-fixed-${`, eine Zeichenkette in `lib/*.js` oder der Fremd-Composer bei `chat-*`. Ohne Beleg gilt die Klasse als tot und fällt.
- [ ] **Step 3: Klassen ohne Regel, `size`, Keyframes**
Run:
```bash
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
rg -n 'size="sm"' src; rg -n '(^|[^A-Za-z0-9_-])(btn-sm|p-4|status-filter|supporters-editor|timeline)([^A-Za-z0-9_-]|$)' src --glob '!*.css'
rg -o "@keyframes [A-Za-z0-9_-]+" src --glob '*.css' | sed 's/.*@keyframes //' | sort -u | while read k; do rg -q "animation[^;]*(^|[^A-Za-z0-9_-])${k}([^A-Za-z0-9_-]|;)" src --glob '*.css' || echo "TOT $k"; done
```
Expected: alle drei Ausgaben leer. Treffer der ersten beiden Befehle werden aus dem JSX entfernt (die Klassen haben keine Regel, `size` bewirkt nichts). Ein toter Keyframe fällt samt Regel. Zu erwarten ist `spin`, wenn `.spin` (Notiz aus P8) und `.spinner` keinen Aufrufer mehr haben.
- [ ] **Step 4: Leere Dateien, Ausnahmeliste, Schriftordner**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && wc -l src/styles/*.css src/pages/*.css; rg -n "admin-areas" src index.html; ls src/assets 2>&1 | head -1; rg -n -i "assets/fonts|lato" src index.html`
Expected:
- Eine CSS-Datei ohne Regel wird mit `git rm` gelöscht, ihre `import`-Zeilen verschwinden (`rg -l "<Dateiname>" src`). `admin-areas.css` hat P6 schon gelöscht, ein Treffer von `rg -n "admin-areas" src index.html` ist ein Rest und wird entfernt.
- `src/assets` existiert nicht, das letzte `rg` hat keine Treffer.
- Die Ausnahmeliste in `$W/css-klassen.mjs` enthält nur noch die Namen mit Beleg aus Step 2. `badge-*`, `status-*`, `ticket-tab-active`, `ui-btn-*` und `side-dot-*` fallen.
- [ ] **Step 5: Prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node "$W/css-klassen.mjs" . | node -e "const j=JSON.parse(require('fs').readFileSync(0));console.log(j.dead.length, j.undefinedUsed.length)" && "$W/check.sh" && "$W/build.sh" "$WT" p10 && "$W/shots.sh" p10 "$W/shots/p10-t6" --gate p10 && "$W/run-browser.sh" "$W/vergleich.mjs" "$W/shots/p9-ref" "$W/shots/p10-t6" --rauschen "$W/shots/p9-ref2" --out "$W/shots/p10-t6-diff"`
Expected: `0 0`, `check.sh` Exit 0 mit `# fail 0` und `# pass` = `N9 + 2`, Gate Exit 0, Vergleich Exit 0.
- [ ] **Step 6: Commit**
Abschnitt 3 der Ergebnisdatei hat die Tabelle: Klasse, definierende Datei, Beleg oder „gelöscht“.
```bash
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
git status --short
git add docs/superpowers/plans/2026-10-05-weisses-design-p10-ergebnisse.md <die geprüften Dateien>
git commit -m "Aufraeumen: tote Klassen, Keyframes und leere CSS-Dateien geloescht, Klassen ohne Regel aus dem JSX entfernt" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 7: Inline-Styles, Regelprüfung, Invarianten
**Files:**
- Modify: JSX-Dateien mit Inline-Styles ohne Laufzeitwert (Ersatz sind vorhandene Klassen, neue kommen nicht dazu), Ergebnisdatei Abschnitte 4, 5 und 7
**Interfaces:**
- Consumes: `$W/farben.mjs` (P1) und der Abschnitt „Bewusst offen“ in `docs/superpowers/plans/2026-10-05-weisses-design-p1-tote-klassen.md`.
- Produces: eine begründete Liste aller `style=`-Stellen und die Regelprüfung der Spec als belegte Liste.
- [ ] **Step 1: Inline-Styles nur noch für Laufzeitwerte**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -nU "style=\{\{(\s*[A-Za-z]+:\s*('[^']*'|\"[^\"]*\"|-?[0-9]+(\.[0-9]+)?)\s*,?)+\s*\}\}" src --glob '*.jsx'`
Expected: keine Treffer. Das Muster findet jedes Objekt, dessen Werte alle feste Zahlen oder Zeichenketten sind. Jeder Treffer wird durch eine vorhandene Klasse ersetzt.
- [ ] **Step 2: Rest begründen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "style=\{" src --glob '*.jsx'`
Expected: wenige Zeilen. Abschnitt 4 der Ergebnisdatei bekommt je Zeile Datei:Zeile, Ausdruck und Begründung. Erlaubt sind ein aus Daten oder Messung berechneter Wert (Breite in Prozent, Höhe, Spaltenzahl) und das Durchreichen der `style`-Prop in `Badge.jsx`. Feste Abstände, Farben, Schrift, Flex und Größen sind nicht erlaubt, solche Stellen werden umgestellt, und Step 1 beginnt von vorn.
- [ ] **Step 3: Farben**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n '#[0-9a-fA-F]{3,8}\b|rgba?\(|hsla?\(' src --glob '*.jsx' --glob '*.js'; node "$W/farben.mjs" .`
Expected:
- Der `rg` nennt nur `src/components/Logo.jsx` und `src/components/serverpuls/chartColors.js`.
- `farben.mjs` listet kein Literal unter 3:1 auf Weiß, keine dunkle Schrift auf `--accent` und kein Weiß-Alpha.
- Jede Zeile des Abschnitts „Bewusst offen“ in `p1-tote-klassen.md` ist erledigt. Hat sie das Zielpaket P10, wird sie hier behoben und in Abschnitt 5 genannt.
- [ ] **Step 4: Regelprüfung der Spec**
Run:
```bash
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
rg -n "!important" src # 1
rg -n -e "gradient\(" -e "backdrop-filter" -e "filter: *blur" src --glob '*.css' # 2
rg -n "text-transform" src --glob '*.css' # 3
rg -n "font-weight: *(800|900)" src # 4
rg -n -e "Lato" -e "@font-face" src index.html # 5
rg -n "font-family" src --glob '*.css' # 6
rg -n -e "color-scheme" -e "data-theme" -e "data-accent" src index.html # 7
rg -no -e "@media[^{]*" -e "@container[^{]*" src --glob '*.css' # 8
rg -n "<svg" src --glob '*.jsx' # 9
rg -n -e "Kpi" -e "kpi" src # 10
rg -n -e "<title>" -e 'rel="icon"' index.html # 11
for s in '\.num' '\.stat \.v' '\.fin-amount'; do printf '%s: ' "$s"; rg -l -U "$s([^A-Za-z0-9_{}-][^{}]*)?\{[^}]*tabular-nums" src --glob '*.css' | tr '\n' ' '; echo; done # 12
```
Expected:
- 1, 4, 5 und 10: leer.
- 2: höchstens eine Zeile, der Chevron von `.select`. 3: eine Zeile, die Wochentage `.wd` im Planboard.
- 6: nur `var(--font)` und der Monospace-Stapel (`ui-monospace`). 7: nur `color-scheme: light`.
- 8: nur `max-width: 1100px` und `860px`, bei `@container` 560, 640 und 860 px, dazu `prefers-reduced-motion`. Zugelassen ist außerdem der eine Block `max-width: 640px` in `src/styles/chat.css`: Er setzt im Eingabefeld des fremden Portal-Composers wieder `font-size: 16px` (gegen das iOS-Zoomen, P6) und ist die WOMS-eigene Überschreibung des Composers.
- 9: nur `Logo.jsx` und `Sparkline.jsx`. 11: `Webklar WOMS 2.0` und die Zeile für `/favicon.svg`.
- 12 (Spec-Regel 6, Zahlen und Beträge): drei Zeilen, jede nennt mindestens `src/styles/global.css`. Fehlt bei einem Selektor die Datei, trägt seine Regel kein `font-variant-numeric: tabular-nums`. P10 ergänzt dann genau diese Deklaration an der bestehenden Regel. Das ist die einzige erlaubte Abweichung von der Pixelinvariante. Die Ansichten, die dadurch im Vergleich von Task 8 abweichen, stehen mit Begründung in Abschnitt 5.
- Spec-Regel 4 (höchstens ein schwarzer Knopf je Seitenkopf) prüft kein `rg`, sondern der Check `max-primary` im Gate `p10` (Task 1 Step 6, Lauf in Task 8 Step 2).
Seitenbreakpoints außerhalb von 860 und 1100 px werden auf diese Werte gezogen (Spec, Regel 7), ausgenommen der 640-px-Block in `chat.css`. Jede andere Abweichung steht mit Begründung in Abschnitt 7. Befehl, Ausgabe und Urteil jeder Zeile stehen in Abschnitt 5.
- [ ] **Step 5: Invarianten gegen `design-p0`**
Run:
```bash
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
git grep -ho 'data-testid=[^ >]*' design-p0 -- src | sort -u > "$TMPDIR/ti-p0.txt"
rg -oN --no-filename 'data-testid=[^ >]*' src | sort -u > "$TMPDIR/ti-jetzt.txt"
diff "$TMPDIR/ti-p0.txt" "$TMPDIR/ti-jetzt.txt" | rg '^<'
echo "confirm: $(git grep -h 'window\.confirm' design-p0 -- src | wc -l) -> $(rg --no-filename 'window\.confirm' src | wc -l)"
git diff --name-status design-p0..HEAD -- src/hooks src/lib
```
Expected:
- Das `diff` zeigt keine `<`-Zeile, es ging keine `data-testid` verloren. Neue (`>`) sind erlaubt und werden genannt.
- Die Zahl der `window.confirm` ist gleich.
- In `src/hooks` steht nur `M useWorkorders.js` plus die neuen `useWorkorder.js`, `useTicketCounts.js`, `useTicketMinutes.js`. In `src/lib` stehen nur `D sidebar.js`, neue Dateien (darunter `rowClick.js` aus P2) und `M` für `ticketStatus.js`, `navLinks.js`, `previewEnv.js`, `serverpulsFormat.js`, `creditFormat.js`, `offerFormat.js`, `storageFormat.js`, `projectList.js`, `leads.js`. Jede andere `M`- oder `D`-Zeile ist ein Befund.
Die Ausgaben stehen in Abschnitt 5.
- [ ] **Step 6: Commit**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" --nur-tests && git status --short`
Expected: Exit 0, `# fail 0`, `# pass` = `N9 + 2`, nur die erwarteten Änderungen.
```bash
git add docs/superpowers/plans/2026-10-05-weisses-design-p10-ergebnisse.md <die geprüften Dateien aus Step 1 bis 4>
git commit -m "Aufraeumen: Inline-Styles nur noch fuer Laufzeitwerte, Regelpruefung und Invarianten dokumentiert" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 8: Gate P10, Bundle, Review, Tag
- [ ] **Step 1: Rebase**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git merge-base --is-ancestor design-p9 HEAD && git rebase origin/test && "$W/check.sh"`
Expected: `design-p9` ist Vorfahre, der Rebase läuft ohne Konflikt, Exit 0 mit `# fail 0` und `# pass` = `N9 + 2`. Gate, Bilder und Review beziehen sich damit auf den Stand, der getaggt wird.
- [ ] **Step 2: Vollständiger Lauf und Pixelvergleich**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p10 && "$W/shots.sh" p10 "$W/shots/p10" --gate p10 && "$W/run-browser.sh" "$W/vergleich.mjs" "$W/shots/p9-ref" "$W/shots/p10" --rauschen "$W/shots/p9-ref2" --out "$W/shots/p10-diff"`
Expected:
- Das Gate `p10` endet mit Exit 0. Über alle Ansichten, auch die der Review-Focus-Punkte 1 bis 5 der Übersicht, gibt es keine Verstöße gegen `external-request`, `console-error`, `dark-surface`, `low-contrast`, `overflow-x`, `icon-svg` (außer unter `.woms-chat`), `max-primary` und `step-failed`.
- Der Vergleich endet mit Exit 0, `fehlt` ist leer, die Zahl der Ansichten gleicht der Sollzahl aus Task 1 Step 4. Rausch-Ansichten stehen in `$W/p10-rauschen.txt`.
- Die Ansicht `alt-link` hat keinen Verstoß. Ihr Schritt prüft, dass `/tickets?woid=100131&status=done` auf `/tickets/100131` endet.
- [ ] **Step 3: Bundle-Größe**
Run:
```bash
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
"$W/check.sh" --nur-build &&
{ find dist -type f \( -name '*.js' -o -name '*.css' -o -name '*.ttf' \) -printf '%s %p\n'; for f in dist/assets/*.js dist/assets/*.css; do echo "$f gzip=$(gzip -9 -c "$f" | wc -c)"; done; wc -l src/styles/*.css src/pages/*.css; } | tee "$W/p10-baseline-p10.txt"
```
Expected:
- Abschnitt 6 der Ergebnisdatei bekommt eine Tabelle mit den Zeilen JS in Byte, JS gzip, CSS in Byte, CSS gzip, Schrift in Byte, CSS-Zeilen und Testzahl und den Spalten P0, `design-p9`, P10. Die P0-Werte stammen aus der Commit-Zeile `Ausgangswerte` (Gegenprobe: Umbaukarte), die P9-Werte aus `$W/p10-baseline-p9.txt`.
- CSS und CSS-Zeilen sind kleiner als bei `design-p9`, JS ist nicht größer. Gegenüber P0 fehlen 96.184 B Schrift. Werte, die gegenüber P0 wachsen, stehen in Abschnitt 7.
- [ ] **Step 4: Ergebnisdatei abschließen**
Abschnitt 7 (Offene Punkte) ist gefüllt, auch wenn er nur „keine“ sagt.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git status --short`
Expected: nur `M docs/superpowers/plans/2026-10-05-weisses-design-p10-ergebnisse.md`.
```bash
git add docs/superpowers/plans/2026-10-05-weisses-design-p10-ergebnisse.md
git commit -m "Aufraeumen: Ergebnisdatei mit Bundle-Vergleich und offenen Punkten" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
- [ ] **Step 5: Code-Review (sonnet)**
Geprüft wird der Diff `design-p9..HEAD` gegen die Spec-Abschnitte „Übergangsschicht KOMPAT“, „Gestaltungsregeln“ und „Was gleich bleibt“, dazu liest der Prüfer die Ergebnisdatei. Schwerpunkte:
- Der Diff enthält nur Löschungen, Umbenennungen und Umstellungen auf vorhandene Bausteine, keine neuen Texte, Klassen oder Tokens außer dem Wächtertest.
- Zehn Stichproben aus Abschnitt 2 (KOMPAT) und 3 (tote Klassen): Die Klasse hat per `rg` keinen Nutzer, ihre Regel ist weg.
- `package.json` und Lock haben genau `@tabler/icons-react`, `@tabler/icons` und `react-icons` verloren.
- `git diff design-p9 -- src/pages/TicketsPage.jsx src/lib/ticketListUrl.js` ist leer oder enthält nur Kommentare, `woidRedirectTarget` ist erhalten.
- Jede Begründung in Abschnitt 4 stimmt, die Invarianten aus Abschnitt 5 sind erfüllt.
Wichtige Befunde werden behoben, danach laufen Step 1 bis 3 erneut. Kleinigkeiten kommen in Abschnitt 7.
- [ ] **Step 6: Tag und Übergabe**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" && git tag design-p10 && git log --oneline origin/test..design-p10 && git rev-parse design-p10`
Expected: Exit 0, Tag gesetzt. Brachte der Rebase in Step 1 neue Commits mit, liefen Gate und Vergleich bereits auf dem rebaseten Stand.
Der Eigentümer bekommt:
- den Bericht des Pixelvergleichs („0 abweichende Ansichten von <Zahl>“) und die Screenshots aus `$W/shots/p10/1440x900` und `$W/shots/p10/390x844` (Auswahl: `login`, `tickets`, `ticket`, `kunde-overview`, `projekte`, `finanzen`, `dlg-rechnung-kunde`, `serverpuls`, ein Alt-Dialog)
- die Bundle-Tabelle aus Abschnitt 6 der Ergebnisdatei
- den Hinweis, dass sichtbar nichts Neues erscheint, der Link `/tickets?woid=N` aus Portal-Mails weiter funktioniert und die offenen Punkte in Abschnitt 7 stehen
- den Hinweis, dass damit alle Pakete ausgeliefert sind und Worktree `dev-design` samt Branch `feature/design-weiss` danach entfernt werden können, wenn er das will
- die Push-Zeile `git -C /home/webklar/apps/ticket.webklar.com/dev-design push origin 'design-p10^{commit}:refs/heads/test'`
- die Rücknahme `git revert --no-commit design-p9..design-p10` (Commit und Push auf `test` durch den Eigentümer, gilt nur solange P10 das jüngste Paket ist)
- [ ] **Step 7: Nach dem Push (erst, wenn der Eigentümer gepusht hat)**
- Im Log von `webklar-ticket-build` steht „Deployment completed successfully“. Dort läuft `npm install` mit dem neuen Lock durch.
- `curl -s https://ticket.webklar.com/ | grep -o 'assets/index-[^"]*\.js'` entspricht `dist/index.html` des Tags.
- `curl -s -o /dev/null -w '%{http_code}' 'https://ticket.webklar.com/tickets?woid=1'` liefert 200 (Seiten-Fallback).
- Ein lesender Sichttest im Chrome des Eigentümers, ohne Schreibaktion: Ein alter Link `/tickets?woid=<N>` mit einer echten Nummer landet auf `/tickets/<N>`. Ticketliste, ein Ticket, das Menü mit dem Serverpuls-Punkt und ein Dialog (öffnen, mit Esc schließen) sehen aus wie vor dem Push, und die Konsole zeigt keinen Fehler.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,441 @@
# WOMS „Weißes Design“ P3: Planboard – Umsetzungsplan
> **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:** Nach P3 zeigt das Planboard eine Wochenansicht ohne Karten:
- sieben Tagesspalten mit Haarlinien, Wochenende in `--bg-subtle`, heutiger Tag als schwarze Datumspille
- Termine als Links auf die Ticketseite, Akquise-Termine hell orange, stornierte Termine weiter sichtbar
- Kopf „Kalenderwoche N · Datum bis Datum · n Termine“ mit den Aktionen ← · Heute · →
- unter 860 px eine Spalte
Die Logik in `lib/planboard.js` bleibt unverändert.
**Architecture:**
- `.week`, `.day`, `.appt` und ihre Unterklassen kommen als eigener Block „Planboard (P3)“ in `src/styles/global.css`, vor den KOMPAT-Block. Die Werte stammen aus `quelle/styles.css` (Zeilen 379–397 und 535–537).
- Die reine Anzeigelogik (Kopfzeile, Terminzeile, Klassen, Linkziel) steht in der neuen `src/lib/planboardView.js` mit Tests. `PlanboardPage.jsx` besteht danach nur noch aus Markup.
- Die Seite nutzt `PageHeader` und `Button` aus P1 sowie `ticketPath` und `ticketRef` aus P2. Sie enthält keine Icons und keine Inline-Styles.
**Tech Stack:** React 18, react-router-dom 6, date-fns 2.30 (`de`, `getISOWeek`), reines CSS. Tests mit `node --test`, Screenshot-Werkzeug aus P0 (Playwright 1.62, `page.clock`).
**Spec:** `docs/superpowers/specs/2026-10-04-weisses-design-design.md`, Abschnitte „Gestaltungsregeln“, „Bausteine“ (Seitenkopf, Knöpfe, Links), „Ticket als eigene Seite / Route und Links“, „Texte“ und Paket P3. Detail im Anhang `2026-10-04-weisses-design-umbaukarte.md`, Abschnitt 5 (P3), 3 und 8.1 (Punkt 8). Globale Regeln und gemeinsame Schnittstellen: `2026-10-05-weisses-design-00-uebersicht.md`.
## Global Constraints
Jeder Run-Block beginnt mit `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&`. Danach gelten `$WT` (Worktree, aktuelles Verzeichnis), `$W` (Werkzeug) und `$TMPDIR`. Tests und Build laufen nur über `"$W/check.sh"`, Browser-Läufe nur über `"$W/shots.sh"` (Regeln der Übersicht).
Es gelten die Regeln der Übersicht. Für P3 kommt hinzu:
- Zeilenangaben gelten für den Stand `603d08c`. Nach P1 bis P2 können sie leicht verschoben sein, maßgeblich sind die genannten Namen.
- Voraussetzung ist das Tag `design-p2` mit `src/lib/ticketRoutes.js` (`ticketPath`, `ticketRef`).
- Geändert werden nur `src/pages/PlanboardPage.jsx` und `src/styles/global.css` (ein neuer Block). Neu sind `src/lib/planboardView.js` und zwei Testdateien. `src/lib/planboard.js`, `useWorkorders` und `useEmployees` bleiben unverändert.
- Sichtbare Texte bleiben wörtlich: „Planboard“, „Heute“, „Keine Termine“, „ohne Uhrzeit“, „niemand zugewiesen“, Tooltip „Ticket #N öffnen“. Neu ist nur, was die Spec unter „Planboard-Kopf“ nennt: die Kopfzeile. Die `aria-label`-Texte „Vorherige Woche“ und „Nächste Woche“ sind nicht sichtbar.
- **Erreichbar bleiben** alle heutigen Bedienelemente: Wochenwechsel (zwei Pfeilknöpfe), „Heute“ und jeder Termin als Link auf sein Ticket. Es gibt keine `data-testid` und keine `window.confirm` in der Seite.
- **Entscheidung:** Der Tageskopf behält die heutigen Texte (voller Wochentag, Datum als `dd.MM`). Die Spec ist dort still, der Prototyp kürzt auf „Mo 5.“. Die Großbuchstaben des Wochentags setzt das CSS (Spec). Weil die Spalten bei 861–1100 px unter 120 px breit sind, darf `.day-head` umbrechen. Das volle Datum steht zusätzlich im `aria-label` der Spalte.
- **Entscheidung:** Kein Fußtext und keine Legende „Akquise“. Der Prototyp hat beides, die Spec nennt es nicht.
- **Entscheidung:** Stornierte Termine sind sichtbar und zählen im Kopf mit, ohne eigene Optik (wie heute).
- **Entscheidung:** Das Linkziel ist `ticketPath(ticketRef({ woid: entry.woid, $id: entry.id }))`, also die `woid` und ohne sie die `$id`. Die Zeile eines Termins ohne `woid` zeigt kein leeres „#“ mehr, sondern nur den Mitarbeiter, sein Tooltip lautet „Ticket öffnen“.
- **Entscheidung:** Während die Tickets laden, endet die Kopfzeile auf „Wird geladen …“ statt auf einer Terminzahl.
- **Entscheidung:** Akquise wird wie heute über `String(type).toLowerCase() === 'akquise'` erkannt. Die grünen und grauen Typfarben entfallen, alle anderen Termine bekommen `--bg-hover`.
- **Entscheidung:** Der Inhalt eines Termins behält die heutige Reihenfolge (Zeit, Kunde oder Betreff, Betreff, Nummer · Mitarbeiter), damit keine Information wandert.
- **Entscheidung:** Auf Wochenendspalten bekommen Termine einen weißen Grund, sonst verschwinden sie im `--bg-subtle`.
## Review Focus
Alle fünf Punkte der Übersicht treffen das Planboard. Jede Prüfung hat eine Ansicht in Task 4.
1. **Altdaten:** Ansicht `planboard-altdaten`.
- `2.5.2026` erscheint am Samstag der Woche 18.
- `31.02.2026`, ein ISO-Datum und ein leeres Datum ergeben weder einen Eintrag noch einen Fehler.
- Ein Ticket ohne `woid` verlinkt über die `$id` und zeigt kein leeres „#“.
- Im Text steht weder `undefined` noch `null` noch `NaN`.
2. **Lange Inhalte:** Ansicht `planboard`. Am Samstag stehen ein Kundenname mit 60 und ein Betreff mit 120 Zeichen. Es gibt kein waagerechtes Scrollen (`overflow-x`), und kein `.day` ist breiter als seine Spalte (`scrollWidth <= clientWidth`).
3. **Teilausfall:** Ansicht `planboard-fehler`. Antwortet die Mitarbeiterliste mit 500, erscheinen alle 13 Termine. Der Mitarbeiter steht dann aus `assignedName`.
4. **Nur Tastatur:** Ansicht `planboard-tastatur`. Der erste Termin ist per Tab erreichbar, trägt den schwarzen Fokusring und öffnet mit Enter `/tickets/100201`.
5. **Viele Einträge:** Der Donnerstag hat sechs Termine. Die Spalte wächst nach unten, nichts überlappt, die anderen Spalten bleiben unverändert hoch.
---
### Task 1: Paketstart und Ausgangswerte
**Files:** keine Änderung (nur Lesen und Prüfen).
**Interfaces:**
- Consumes: Tag `design-p2`, `ticketPath(ref, tab)` und `ticketRef(ticket)` aus `src/lib/ticketRoutes.js`.
- Produces: die Ausgangs-Testzahl N0 für die Übergabe.
- [ ] **Step 1: Paketstart**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git merge-base --is-ancestor design-p2 HEAD && git rebase origin/test && "$W/check.sh" && git status --short`
Expected:
- Alle Befehle enden mit Exit 0, `git status` gibt keine Zeile aus. Schlägt `merge-base` fehl, ist `design-p2` nicht im Branch: Stopp und melden, P3 darf erst nach P2 laufen.
- `check.sh` zeigt `# fail 0`. Die Zahl `# pass` ist N0 (Soll nach P3: N0 + 9).
- [ ] **Step 2: Parallele Arbeit an `feature/terminplaner-tp2`**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git log --oneline origin/test..origin/feature/terminplaner-tp2 -- src/pages/PlanboardPage.jsx src/lib/planboard.js src/lib/planboardView.js src/styles/global.css`
Expected: keine Ausgabe. Gibt der Befehl Zeilen aus, stoppt der Agent und fragt den Eigentümer. Er empfiehlt, tp2 zuerst nach `test` zu bringen.
- [ ] **Step 3: `ticketPath` und `ticketRef` sind da**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "export function (ticketPath|ticketRef)" src/lib/ticketRoutes.js`
Expected: zwei Treffer. Sonst Stopp: P3 darf erst nach P2 laufen.
- [ ] **Step 4: Stand der Seite und des CSS**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "ticketPath|react-icons|style=\{\{" src/pages/PlanboardPage.jsx; rg -n "^\s*\.(week|day|appt)\b" src/styles/global.css`
Expected:
- In der Seite: ein Treffer `ticketPath` (P2 hat die Linkzeile umgestellt), dazu die Icon-Importe und die Inline-Styles.
- Im CSS: keine Treffer. Findet P1 dort schon Mobil-Regeln für `.week` oder `.day`, werden sie in Task 5 Step 1 in den P3-Block verschoben.
### Task 2: Charakterisierungstests für `lib/planboard.js`
Die Logik hat bisher keinen Test. P3 benutzt sie unverändert, die Altwerte aus Review Focus 1 werden jetzt festgeschrieben.
**Files:**
- Test: `tests/unit/planboard.test.mjs` (neu)
**Interfaces:**
- Consumes: `parseGermanDate`, `formatTime`, `dayKey`, `groupAppointmentsByDay`, `countInRange` aus `src/lib/planboard.js`, unverändert.
- Produces: vier Tests.
- [ ] **Step 1: Tests schreiben**
```js
const { parseGermanDate, formatTime, dayKey, groupAppointmentsByDay, countInRange } = await loadModule('src/lib/planboard.js')
test('parseGermanDate: ein- und zweistellig, Altwerte ergeben null', () => {
assert.equal(parseGermanDate('2.5.2026').getTime(), new Date(2026, 4, 2).getTime())
assert.equal(parseGermanDate(' 07.10.2026 ').getTime(), new Date(2026, 9, 7).getTime())
for (const v of ['31.02.2026', '0.1.2026', '2026-10-07T09:00:00.000Z', '2026-10-07', '7.10.26', '', undefined, null, 'morgen']) {
assert.equal(parseGermanDate(v), null, String(v))
}
})
test('formatTime: hhmm und hh:mm, ungültige Werte ergeben leer', () => {
for (const [v, out] of [['0900', '09:00'], ['900', '09:00'], ['9:00', '09:00'], ['09:30', '09:30'], ['2359', '23:59']]) {
assert.equal(formatTime(v), out, v)
}
for (const v of ['2400', '1260', '9', 'abc', '', undefined]) assert.equal(formatTime(v), '', String(v))
assert.equal(dayKey(new Date(2026, 0, 5)), '2026-01-05')
})
const employees = [{ userId: 'u1', displayName: 'Kenso Grimm', shortcode: 'KNSO' }]
const workorders = [
{ $id: 'a', woid: '1', startDate: '7.10.2026', startTime: '1400', endTime: '1500', topic: 'Spät', type: 'Webpage', status: 'Open', customerName: 'Alpha', assignedTo: 'u1' },
{ $id: 'b', woid: '2', startDate: '07.10.2026', startTime: '', topic: 'Ohne Uhrzeit', type: 'Akquise', status: 'Open', assignedName: 'Ehemalige' },
{ $id: 'c', woid: '3', startDate: '7.10.2026', startTime: '0830', topic: 'Früh', type: 'Webpage', status: 'Cancelled' },
{ $id: 'd', startDate: '8.10.2026', startTime: '1000', title: 'Nur Titel' },
{ $id: 'h', startDate: '9.10.2026' },
{ $id: 'e', woid: '5', startDate: '31.02.2026', startTime: '1000', topic: 'Ungültig' },
{ $id: 'f', woid: '6', startDate: '2026-10-07T09:00:00.000Z', topic: 'ISO' },
{ $id: 'g', woid: '7', startDate: '', topic: 'Leer' },
]
test('groupAppointmentsByDay: nach Uhrzeit, ohne Uhrzeit am Ende, Stornierte bleiben, Altwerte fallen weg', () => {
const byDay = groupAppointmentsByDay(workorders, employees)
assert.deepEqual(Object.keys(byDay).sort(), ['2026-10-07', '2026-10-08', '2026-10-09'])
assert.deepEqual(byDay['2026-10-07'].map((a) => a.id), ['c', 'a', 'b'])
assert.equal(byDay['2026-10-07'][0].status, 'Cancelled')
assert.equal(byDay['2026-10-07'][1].employeeShort, 'KNSO')
assert.equal(byDay['2026-10-07'][1].employeeName, 'Kenso Grimm')
assert.equal(byDay['2026-10-07'][2].employeeName, 'Ehemalige')
assert.equal(byDay['2026-10-07'][2].employeeShort, '')
assert.equal(byDay['2026-10-08'][0].woid, '')
assert.equal(byDay['2026-10-08'][0].topic, 'Nur Titel')
assert.equal(byDay['2026-10-09'][0].topic, 'Ohne Betreff')
})
test('countInRange: zählt nur die genannten Tage', () => {
const byDay = groupAppointmentsByDay(workorders, employees)
const week = (day) => Array.from({ length: 7 }, (_, i) => new Date(2026, 9, day + i))
assert.equal(countInRange(byDay, week(5)), 5)
assert.equal(countInRange(byDay, week(12)), 0)
assert.equal(countInRange(byDay), 0)
})
```
- [ ] **Step 2: Tests laufen lassen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/planboard.test.mjs`
Expected: PASS, 4 Tests. Die Tests halten den Bestand fest, es ändert sich kein Produktivcode. Schlägt einer fehl, wurde das Verhalten falsch gelesen. Dann wird der Test korrigiert, nicht `planboard.js`.
- [ ] **Step 3: Commit**
```bash
git add tests/unit/planboard.test.mjs
git commit -m "Planboard: Tests halten Datums- und Terminlogik fest (Altwerte, Stornierte, ohne Uhrzeit)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 3: `planboardView.js` (Anzeigelogik)
**Files:**
- Create: `src/lib/planboardView.js`
- Test: `tests/unit/planboardView.test.mjs` (neu)
**Interfaces:**
- Consumes: `date-fns` (`format`, `getISOWeek`, `addDays`) und `de`.
- Produces (Signaturen, die `PlanboardPage` benutzt):
- `planboardSubtitle({ weekStart: Date, count: number, loading?: boolean }) → string`
- `dayLabels(day: Date) → { weekday: string, number: string, full: string }`
- `appointmentClass(type) → 'appt' | 'appt akquise'`
- `appointmentTimeLabel({ time, endTime }) → string`
- `appointmentOwnerLabel({ woid, employeeName, employeeShort }) → string`
- `appointmentTitle({ woid }) → string`
- [ ] **Step 1: Failing Tests schreiben**
```js
const { planboardSubtitle, dayLabels, appointmentClass, appointmentTimeLabel, appointmentOwnerLabel, appointmentTitle } =
await loadModule('src/lib/planboardView.js')
test('planboardSubtitle: Kalenderwoche, Zeitraum, Terminzahl', () => {
const mo = new Date(2026, 9, 5)
assert.equal(planboardSubtitle({ weekStart: mo, count: 13 }), 'Kalenderwoche 41 · 5. Oktober bis 11. Oktober · 13 Termine')
assert.equal(planboardSubtitle({ weekStart: mo, count: 1 }), 'Kalenderwoche 41 · 5. Oktober bis 11. Oktober · 1 Termin')
assert.equal(planboardSubtitle({ weekStart: mo, count: 0 }), 'Kalenderwoche 41 · 5. Oktober bis 11. Oktober · 0 Termine')
assert.equal(planboardSubtitle({ weekStart: mo, count: 0, loading: true }), 'Kalenderwoche 41 · 5. Oktober bis 11. Oktober · Wird geladen …')
assert.equal(planboardSubtitle({ weekStart: new Date(2026, 11, 28), count: 2 }), 'Kalenderwoche 53 · 28. Dezember bis 3. Januar · 2 Termine')
assert.equal(planboardSubtitle({ weekStart: new Date(2027, 0, 4), count: 2 }), 'Kalenderwoche 1 · 4. Januar bis 10. Januar · 2 Termine')
})
test('dayLabels: Wochentag, Datum, volles Datum', () => {
assert.deepEqual(dayLabels(new Date(2026, 9, 5)), { weekday: 'Montag', number: '05.10', full: 'Montag, 5. Oktober' })
assert.deepEqual(dayLabels(new Date(2026, 9, 11)), { weekday: 'Sonntag', number: '11.10', full: 'Sonntag, 11. Oktober' })
})
test('appointmentClass und appointmentTimeLabel', () => {
for (const t of ['Akquise', 'akquise', 'AKQUISE']) assert.equal(appointmentClass(t), 'appt akquise', t)
for (const t of ['Webpage', 'Project', 'Migration', '', undefined, null]) assert.equal(appointmentClass(t), 'appt', String(t))
assert.equal(appointmentTimeLabel({ time: '08:00', endTime: '09:30' }), '08:00–09:30')
assert.equal(appointmentTimeLabel({ time: '08:00', endTime: '' }), '08:00')
assert.equal(appointmentTimeLabel({ time: '', endTime: '09:30' }), 'ohne Uhrzeit')
assert.equal(appointmentTimeLabel({ time: '', endTime: '' }), 'ohne Uhrzeit')
})
test('appointmentOwnerLabel: Nummer und Mitarbeiter', () => {
assert.equal(appointmentOwnerLabel({ woid: '100201', employeeName: 'Kenso Grimm', employeeShort: 'KNSO' }), '#100201 · KNSO')
assert.equal(appointmentOwnerLabel({ woid: '100201', employeeName: 'Ehemalige', employeeShort: '' }), '#100201 · Ehemalige')
assert.equal(appointmentOwnerLabel({ woid: '100201', employeeName: '', employeeShort: '' }), '#100201 · niemand zugewiesen')
assert.equal(appointmentOwnerLabel({ woid: '', employeeName: 'Kenso Grimm', employeeShort: 'KNSO' }), 'KNSO')
assert.equal(appointmentOwnerLabel({ woid: '', employeeName: '', employeeShort: '' }), 'niemand zugewiesen')
})
test('appointmentTitle: Tooltip mit und ohne woid', () => {
assert.equal(appointmentTitle({ woid: '100201' }), 'Ticket #100201 öffnen')
assert.equal(appointmentTitle({ woid: '' }), 'Ticket öffnen')
})
```
- [ ] **Step 2: Test laufen lassen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/planboardView.test.mjs`
Expected: FAIL, `Could not resolve "src/lib/planboardView.js"` bzw. Funktionen fehlen.
- [ ] **Step 3: `planboardView.js` implementieren**
Festgelegte Werte:
- Zeitraum: `format(d, 'd. MMMM', { locale: de })` für `weekStart` und `addDays(weekStart, 6)`, Verbindung „ bis “.
- Kopfzeile: `Kalenderwoche ${getISOWeek(weekStart)} · ${von} bis ${bis} · ${rest}`. `rest` ist `Wird geladen …` bei `loading`, sonst `${count} Termin` (genau 1) oder `${count} Termine`.
- `dayLabels`: `weekday` = `format(day, 'EEEE', { locale: de })`, `number` = `format(day, 'dd.MM')`, `full` = `format(day, 'EEEE, d. MMMM', { locale: de })`.
- Zeitbereich in `appointmentTimeLabel` mit Halbgeviertstrich „–“ (U+2013), wie heute.
- Die Datei importiert `date-fns`. Der Test-Lader bündelt es (geprüft mit `getISOWeek` und `date-fns/locale`), sie darf nur keinen Appwrite-Import bekommen.
- `appointmentOwnerLabel`: wie heute entscheidet `employeeName`, ob ein Name oder „niemand zugewiesen“ erscheint. Mit `woid` steht `#woid · …`, ohne `woid` nur der Teil nach dem Punkt.
- [ ] **Step 4: Test grün**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/planboardView.test.mjs && "$W/check.sh" --nur-tests`
Expected: PASS, Exit 0, `# fail 0`, `# pass` = N0 + 9.
- [ ] **Step 5: Commit**
```bash
git add src/lib/planboardView.js tests/unit/planboardView.test.mjs
git commit -m "Planboard: Anzeigelogik fuer Kopfzeile, Tageskopf und Terminzeile mit Tests" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 4: Testdaten, Ansichten und Gate (Prüfung vor dem Umbau)
Alles liegt im Werkzeug außerhalb des Repos, deshalb gibt es keinen Commit.
**Files** (unter `$W/`):
- Create: `fixtures/planboard.mjs`
- Modify: `views.mjs`, `gates.mjs`, bei Bedarf `backend.mjs`
**Interfaces:**
- Consumes: `createBackend`, `VIEWS`, `GATES` aus P0.
- Produces: die Ansichten `planboard`, `planboard-woche-vor`, `planboard-leer`, `planboard-altdaten`, `planboard-fehler`, `planboard-tastatur` und das Gate `p3`.
- [ ] **Step 1: `fixtures/planboard.mjs` anlegen**
Aufbau wie `altdaten.mjs`: Es importiert `standard.mjs` und ersetzt `workorders` durch genau diese Tickets. Alle zugewiesenen Tickets tragen zusätzlich `assignedName` (voller Name). Ein Kürzel steht für `assignedTo` = `userId` des Mitarbeiters mit diesem Kürzel aus `standard`. Tickets „ohne `woid`“ haben kein Feld `woid` und die feste `$id` aus der Tabelle.
| woid | startDate | Zeit | Art | Status | Kunde | Betreff | Zuständig |
|---|---|---|---|---|---|---|---|
| 100201 | `5.10.2026` | 0900–1030 | Webpage | Occupied | Bäckerei Sonnenschein | Öffnungszeiten für die Feiertage anpassen | KNSO |
| 100202 | `06.10.2026` | 1400, ohne Ende | Akquise | In Kontakt | Gärtnerei Grünblick | Erstgespräch Rechnungsanalyse | ANDJ |
| 100203 | `7.10.2026` | 0830–0915 | Webpage | Occupied | Kanzlei Weber & Partner | Kontaktformular landet im Spam | JUSN |
| 100204 | `7.10.2026` | ohne Zeit | Migration | Open | ohne Kunde | Umzug ohne Uhrzeit und ohne Kunde | niemand, ohne `assignedName` |
| 100205 | `7.10.2026` | 1500–1600 | Webpage | Cancelled | Fahrschule Roth | Storniertes Gespräch | KNSO |
| 100211–100216 | `8.10.2026` | 0800, 0900, 1000, 1300, 1400, 1530 | 100214 ist Akquise, sonst Webpage | gemischt | Testkunde 1–6 | Termin 1–6 | gemischt |
| ohne `woid`, `$id: 'ohne-woid-fr'` | `9.10.2026` | 1100–1115 | Project | Open | Physiotherapie am Markt | Ticket ohne Ticketnummer | NIRE |
| 100206 | `10.10.2026` | 1000–1100 | Webpage | Occupied | `Müller-Lüdenscheidt Metallbau- und Anlagentechnik GmbH & Co.` (60 Zeichen) | Betreff mit 120 Zeichen (Wert aus `altdaten.mjs`) | `assignedTo: 'ehemalig'` (nicht in der Mitarbeiterliste), `assignedName: 'Ehemalige Kollegin'` |
| 100230 | `12.10.2026` | 0900–1000 | Webpage | Open | Café Mühlental | Termin nächste Woche | JUSN |
Dazu die Altwerte, die nie im Planboard erscheinen dürfen oder in einer anderen Woche liegen:
| woid | startDate | Zeit | Betreff | Erwartung |
|---|---|---|---|---|
| 100301 | `2.5.2026` | 1000–1100 | Altwert einstellig (Kunde „Altkunde GmbH“, KNSO) | erscheint am Samstag der Woche 18 |
| ohne `woid`, `$id: 'ohne-woid-alt'` | `1.5.2026` | 0900 | Ticket ohne Nummer im Altbestand (Altkunde GmbH, niemand) | erscheint am Freitag der Woche 18 |
| 100302 | `31.02.2026` | 0900 | Ungültiges Datum | nie sichtbar |
| 100303 | `2026-10-07T09:00:00.000Z` | – | ISO-Datum | nie sichtbar |
| 100304 | leer | – | Ohne Datum | nie sichtbar |
Soll: In der Woche 5.–11. Oktober stehen 13 Termine (1 + 1 + 3 + 6 + 1 + 1), davon 2 Akquise (100202, 100214). In der Woche 12.–18. Oktober steht 1 Termin, in der Woche 27. April–3. Mai stehen 2.
- [ ] **Step 2: Szenario `fehler` deckt beide Mitarbeiter-Wege ab**
`useEmployees` fragt zuerst `GET https://portal.test/api/admin/employees` und fällt bei einem Fehler auf die Appwrite-Sammlung `employees` zurück.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "fehler" "$W/backend.mjs"`
Expected: In `fehler` antworten beide Wege mit 500. Fehlt einer, wird er in `backend.mjs` ergänzt. Sonst lädt die Liste trotz Szenario, und Review Focus 3 prüft nichts.
- [ ] **Step 3: Ansichten in `views.mjs` ergänzen**
Die Schritte haben die Signatur `steps(page, ctx)` aus der Übersicht. Alle Ansichten setzen zuerst die Uhr (`page.clock.setFixedTime`, ohne die Zeit einzufrieren) und klicken dann „Heute“. Das ist nötig, weil die Seite die aktuelle Woche beim ersten Rendern bestimmt. Eine Zusicherung, die nicht stimmt, wirft einen Fehler (Verstoß `step-failed`, in jedem Gate aktiv).
| ID | Daten | Schritte | Zusicherung |
|---|---|---|---|
| `planboard` | `planboard` | Uhr `2026-10-07T10:00:00+02:00`, „Heute“ | Kopf `Kalenderwoche 41 · 5. Oktober bis 11. Oktober · 13 Termine`. 7 `.day`, genau 2 `.day.weekend`, `.day.today .dn` = „07.10“, 13 `a.appt`, 2 `a.appt.akquise`, 1 Mal „Keine Termine“ (Sonntag). Je `.day` gilt `scrollWidth <= clientWidth`. Der Donnerstag hat 6 `a.appt`, jeder beginnt unter dem Ende des vorigen |
| `planboard-woche-vor` | `planboard` | wie oben, dann „Nächste Woche“ | Kopf `Kalenderwoche 42 · 12. Oktober bis 18. Oktober · 1 Termin` |
| `planboard-leer` | `planboard` | wie oben, zweimal „Nächste Woche“ | Kopf `Kalenderwoche 43 · 19. Oktober bis 25. Oktober · 0 Termine`, 7 Mal „Keine Termine“ |
| `planboard-altdaten` | `planboard` | Uhr `2026-05-04T10:00:00+02:00`, „Heute“, „Vorherige Woche“ | Kopf `Kalenderwoche 18 · 27. April bis 3. Mai · 2 Termine`. Der Text enthält weder `undefined` noch `null` noch `NaN`. Der Freitagstermin hat einen `href`, der auf `/tickets/ohne-woid-alt` endet, und sein Text enthält kein „#“ |
| `planboard-fehler` | `planboard`, Szenario `fehler` | wie `planboard` | 13 `a.appt`. Der Montagstermin zeigt „Kenso Grimm“ statt „KNSO“, der Samstagstermin „Ehemalige Kollegin“. `ctx.backend.calls` enthält je einen GET auf `…/api/admin/employees` und auf `…/collections/employees/documents` (beide Wege wurden versucht) |
| `planboard-tastatur` | `planboard` | wie `planboard`. Tab drücken (höchstens 40 Mal), bis `document.activeElement` ein `a.appt` ist | Die Outline des fokussierten Elements ist `solid` mit 2 px. Enter führt zu `/tickets/100201`. Danach „Zurück“ und Warten auf `.week` |
- [ ] **Step 4: Gate `p3` in `gates.mjs` anlegen**
`p3` ist `p2` plus die Ansichten dieses Pakets im bereichsbezogenen Gate `icon-svg` (Schema der Übersicht). `step-failed`, `external-request` und `console-error` sind ohnehin aktiv, `ignore` von `p2` bleibt erhalten:
```js
GATES.p3 = { ...GATES.p2, scoped: { ...GATES.p2.scoped, 'icon-svg': { ...GATES.p2.scoped['icon-svg'], views: [...GATES.p2.scoped['icon-svg'].views, 'planboard', 'planboard-*'] } } }
```
- [ ] **Step 5: Vorher-Lauf (muss scheitern)**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p3-vorher && "$W/shots.sh" p3-vorher "$W/shots/p3-vorher" --only planboard --gate p3`
Expected: Exit 1, `icon-svg` findet die zwei Chevron-Icons (dazu `step-failed`, weil die neue Kopfzeile fehlt). Die Bilder `shots/p3-vorher/1440/planboard.png` und `390/planboard.png` sind die Vorher-Bilder mit den neuen Testdaten. Die anderen fünf Ansichten brechen vor dem Umbau an den fehlenden Knöpfen und werden hier nicht gestartet.
### Task 5: Planboard-CSS und Seite
**Files:**
- Modify: `src/styles/global.css` (neuer Block „Planboard (P3)“ unmittelbar vor `/* ===== KOMPAT (bis P10) ===== */`)
- Modify: `src/pages/PlanboardPage.jsx` (Zeilen 1–119 ersetzen)
**Interfaces:**
- Consumes:
- `PageHeader({ title, subtitle, actions })`, `Button({ variant })` aus P1.
- `ticketPath(ref)` und `ticketRef(ticket)` aus P2.
- `planboardView.js` aus Task 3.
- `groupAppointmentsByDay`, `countInRange`, `dayKey` aus `lib/planboard.js`.
- Tokens `--bg-hover`, `--bg-subtle`, `--appt-hover`, `--appt-akquise`, `--appt-akquise-hover`, `--accent`.
- Produces: die Klassen `.week`, `.day` (`.today`, `.weekend`), `.day-head` (`.wd`, `.dn`), `.appt` (`.akquise`, `.tm`, `.tt`, `.cu`), `.none`. Spätere Pakete benutzen sie nicht.
- [ ] **Step 1: CSS-Block anlegen**
Alle Werte aus `quelle/styles.css`, Zeilen 379–397, mit den Tokens statt der Literale `#eef1f4`, `#fdf4e3` und `#fbeccd`. Zusätzlich gelten diese Ergänzungen:
| Selektor | Wert |
|---|---|
| `.day-head` | zusätzlich `flex-wrap: wrap; row-gap: 0` |
| `.appt` | zusätzlich `overflow-wrap: anywhere` |
| `.day.weekend .appt:not(.akquise)` | `background: var(--bg)` |
| `.day.weekend .appt:not(.akquise):hover` | `background: var(--appt-hover)` |
| `@media (max-width: 860px)` | am Ende des Blocks: `.week { grid-template-columns: 1fr; border-top: 0; min-height: 0 }`, `.day { border-left: 0; padding: 12px 0; border-top: 1px solid var(--line) }`, `.day.weekend { background: none }`, `.day.weekend .appt:not(.akquise) { background: var(--bg-hover) }` (Quelle: Zeilen 535–537) |
Der Block beginnt mit `/* ===== Planboard (P3) ===== */`.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && sed -n '/Planboard (P3)/,/KOMPAT (bis P10)/p' src/styles/global.css | rg -n "!important|#[0-9a-fA-F]{3,8}\b"`
Expected: nur ein Treffer, `#fff` an `.day.today .dn`. Gab es in Task 1 Step 4 Mobil-Regeln aus P1 für `.week` oder `.day`, sind sie jetzt aus dem P1-Block entfernt, sodass sie nur noch hier stehen (`rg -n "^\s*\.(week|day)\b" src/styles/global.css` zeigt nur Zeilen im P3-Block).
- [ ] **Step 2: `PlanboardPage.jsx` neu schreiben**
Der Code der Seite (State, `useWorkorders({ limit: 500 })`, `useEmployees`, `byDay`, `weekCount`, `navigateWeek`, `goToday`) bleibt. Das Markup wird:
| Element | Umsetzung |
|---|---|
| Wurzel | `<div className="page">` |
| Kopf | `<PageHeader title="Planboard" subtitle={planboardSubtitle({ weekStart: currentWeek, count: weekCount, loading })} actions={…} />` |
| Aktionen | drei Knöpfe in dieser Reihenfolge: `<Button variant="quiet" aria-label="Vorherige Woche">←</Button>`, `<Button>Heute</Button>`, `<Button variant="quiet" aria-label="Nächste Woche">→</Button>`. Kein schwarzer Knopf im Kopf |
| Woche | `<div className="week">` mit sieben `<div className="day">`. Klassen `today` bei `isSameDay(day, new Date())` und `weekend` ab dem sechsten Tag. Attribute `role="group"`, `aria-label={labels.full}`, `aria-current="date"` nur bei heute |
| Tageskopf | `<div className="day-head"><span className="wd">{labels.weekday}</span><span className="dn">{labels.number}</span></div>` |
| Leerer Tag | `<div className="none">Keine Termine</div>` |
| Termin | `<Link className={appointmentClass(entry.type)} to={ticketPath(ticketRef({ woid: entry.woid, $id: entry.id }))} title={appointmentTitle(entry)}>` mit `.tm` (`appointmentTimeLabel`), `.tt` (`entry.customerName \|\| entry.topic`), einem `.cu` mit `entry.topic` (nur bei vorhandenem Kundennamen) und einem `.cu` mit `appointmentOwnerLabel(entry)` |
Es entfallen: alle Icon-Importe, `Card`, `useNavigate`, `TYPE_COLORS`, `format` für die Kopfzeile und jedes `style`.
- [ ] **Step 3: Hygiene prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{\{|navigate\(|Card|TYPE_COLORS" src/pages/PlanboardPage.jsx`
Expected: keine Treffer.
- [ ] **Step 4: Bauen und Gate**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p3 && "$W/shots.sh" p3 "$W/shots/p3" --gate p3`
Expected:
- Exit 0, alle Zusicherungen aus Task 4 Step 3 erfüllt, `icon-svg` ist für die `planboard*`-Ansichten sauber.
- In `shots/p3/390x844/planboard.png` stehen die Tage untereinander, der Mittwoch trägt die schwarze Zahl, und es gibt keine Spaltenlinien.
- In `shots/p3/1440x900/planboard.png` sind Samstag und Sonntag hell abgesetzt, die Donnerstagsspalte ist länger, und die Akquise-Termine sind hell orange.
- [ ] **Step 5: Commit**
```bash
git add src/pages/PlanboardPage.jsx src/styles/global.css
git commit -m "Planboard: Wochenansicht ohne Karten (Tagesspalten, heute als Pille, Termine als Links)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 6: Gate P3, Review, Tag
- [ ] **Step 1: Rebase**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git rebase origin/test && "$W/check.sh"`
Expected: Exit 0, `# fail 0`, `# pass` = N0 + 9. Liegen neue Commits auf `test`, kann die Zahl höher sein; sie wird notiert.
- [ ] **Step 2: Vollständiger Gate-Lauf**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p3 && "$W/shots.sh" p3 "$W/shots/p3" --gate p3`
Expected:
- Gate `p3` Exit 0.
- Keine Verstöße gegen `dark-surface`, `low-contrast`, `overflow-x`, `console-error`, `external-request` und `step-failed`, auch auf allen übrigen Ansichten.
- Die Planboard-Ansichten haben kein `icon-svg`.
- [ ] **Step 3: Bundle-Größe**
Die Größen stehen in der Ausgabe von `check.sh` (Step 1). Sie werden mit dem Stand von `design-p2` verglichen. Das JS wird kleiner (zwei Icon-Importe weniger), das CSS wächst um rund 1 KB.
- [ ] **Step 4: Code-Review (sonnet)**
Geprüft wird der Diff `design-p2..HEAD` gegen die Spec-Abschnitte „Gestaltungsregeln“, „Bausteine“ und Paket P3. Schwerpunkte:
- keine Icons, keine `style={{`, kein Farbliteral in der Seite
- `ticketPath(ticketRef(…))` ist das einzige Linkziel
- das `aria-label` der Spalten und der Pfeilknöpfe
- der CSS-Block benutzt nur Tokens, kein `!important`
- `lib/planboard.js` ist unverändert (`git diff design-p2 -- src/lib/planboard.js` ist leer)
Wichtige Befunde werden behoben, Kleinigkeiten notiert. Danach kommt der Rebase aus Step 1 noch einmal und Step 2 als Kurzlauf mit `--only planboard,planboard-fehler`.
- [ ] **Step 5: Tag und Übergabe**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" && git tag design-p3 && git log --oneline origin/test..design-p3 && git rev-parse design-p3`
Der Eigentümer bekommt:
- die Screenshots aus `shots/p3/1440x900` und `shots/p3/390x844` (Auswahl: `planboard`, `planboard-woche-vor`, `planboard-fehler`) und den Vorher-Vergleich aus `shots/p3-vorher`
- den Hinweis, dass der Tageskopf die heutigen Texte behält (der Prototyp kürzt auf „Mo 5.“) und der Prototyp-Fußtext samt Legende fehlt
- die Push-Zeile `git -C /home/webklar/apps/ticket.webklar.com/dev-design push origin 'design-p3^{commit}:refs/heads/test'`
- die Rücknahme: `git revert --no-commit design-p2..design-p3`, Commit und Push durch den Eigentümer (gilt nur, solange P3 das jüngste Paket ist)
- [ ] **Step 6: Nach dem Push (erst, wenn der Eigentümer gepusht hat)**
- Im Log von `webklar-ticket-build` steht „Deployment completed successfully“.
- `curl -s https://ticket.webklar.com/ | grep -o 'assets/index-[^"]*\.js'` entspricht `dist/index.html` des Tags.
- Ein lesender Sichttest im Chrome des Eigentümers: Planboard öffnen, Woche vor und zurück, ein Termin öffnet das Ticket.
@@ -0,0 +1,618 @@
# WOMS „Weißes Design“ P4: Kunden – Umsetzungsplan
> **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:** Nach P4 sieht der Kundenbereich wie im Prototyp aus:
- Kundenliste als Tabelle mit den Reitern Feste Kunden · Leads · Archiviert · Alle samt Zählern, mit Suche und Abzeichen
- „Neuer Kunde“ und „Neuer Lead“ als Dialoge, auch im Zuordnen-Assistenten
- Kundenseite mit Brotkrumen, Eyebrow und allen neun Reitern. Übersicht als zwei Spalten mit Schlüsselwerten, Stammdaten weiter direkt bearbeitbar, Tickets-Reiter mit Links
- Funktionen, Protokoll und Abo-Status ohne Karten und ohne Icons, neuer Grundstil in `admin-areas.css`
**Architecture:**
- Filter, Zähler und Abzeichen sind reine Funktionen in der neuen `src/lib/customerList.js` mit Tests. `lib/leads.js` importiert Appwrite und ist unter Node nicht ladbar. Deshalb ziehen `customerStage` und `isArchived` dorthin um, und `leads.js` re-exportiert sie. Alle bestehenden Importe bleiben gültig.
- Tabellenzeilen haben als erste Zelle einen echten Link, der Zeilenklick läuft über `isRowActivation` aus `src/lib/rowClick.js` (entsteht in P2, P4 baut den Helfer nicht neu).
- Die beiden Anlege-Formulare rendern `<Dialog>` aus P1.
- CSS: ein kleiner Block „Kunden (P4)“ in `global.css`, dazu der neue Grundstil in `src/styles/admin-areas.css`.
**Tech Stack:** React 18, react-router-dom 6, reines CSS. Tests mit `node --test`, Screenshot-Werkzeug aus P0.
**Spec:** `docs/superpowers/specs/2026-10-04-weisses-design-design.md`, Abschnitte „Gestaltungsregeln“, „Bausteine“ (Seitenkopf, Tabelle, Reiter, Badge, Formular, Dialog, Meldungen, Laden), „Ticket als eigene Seite / Route und Links“, „Texte“, „Risiken“ (Zeile zu `feature/terminplaner-tp2`) und Paket P4. Detail im Anhang `2026-10-04-weisses-design-umbaukarte.md`, Abschnitte 5 (P4), 3, 4 und 7.5. Globale Regeln und gemeinsame Schnittstellen: `2026-10-05-weisses-design-00-uebersicht.md`.
## Global Constraints
Jeder Run-Block beginnt mit `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&`. Danach gelten `$WT` (Worktree, aktuelles Verzeichnis), `$W` (Werkzeug) und `$TMPDIR`. Tests und Build laufen nur über `"$W/check.sh"`, Browser-Läufe nur über `"$W/shots.sh"` (Regeln der Übersicht).
Es gelten die Regeln der Übersicht. Für P4 kommt hinzu:
- Zeilenangaben gelten für den Stand `603d08c`. Nach P1 bis P3 können sie leicht verschoben sein, maßgeblich sind die genannten Namen.
- Voraussetzung sind das Tag `design-p3` und aus P2 `ticketPath` und `ticketRef` (`lib/ticketRoutes.js`), `ticketNumber` (`lib/ticketView.js`), `isRowActivation` (`lib/rowClick.js`) und die Klasse `.row-link`.
- Geändert werden nur `CustomersPage.jsx`, `CustomerDetailPage.jsx`, `components/customer/*`, `admin-areas.css`, ein Block in `global.css` (dazu fällt dort die KOMPAT-Regel `.side-logo`), in `lib/leads.js` zwei Funktionen (Umzug) und eine Stelle in `AssignProjectPage.jsx` (Aufruf von `CreateCustomerForm`). Neu sind `lib/customerList.js` und sein Test.
- Unverändert bleiben `lib/customerTabs.js` (der Test pinnt `[id, label]`, Zähler kommen beim Rendern), `lib/funktionenView.js`, `useCustomers`, `customerAdminApi`, `teamApi`, `CopyableCredential` und `AssignedProjectCard` (P5), Chat, Gutschriften, Angebote (P6) und `CustomerInvoices` (P7).
- **tp2 zuerst klären:** `feature/terminplaner-tp2` hat 5 Commits (`6f03712`, `c001554`, `857e001`, `45e5dc3`, `225e2f1`) vor `test`. Sie ändern `CustomerFunktionen.jsx`, `lib/funktionenView.js`, `admin-areas.css` und Rechtliches. P4 baut `CustomerFunktionen.jsx` und `admin-areas.css` neu. Task 1 stoppt deshalb und fragt den Eigentümer. Einzelne Commits werden nicht per Cherry-Pick geholt.
- Texte bleiben wörtlich, auch die ae/ue-Schreibweisen („Strasse + Nr.“, „Fuer einen Lead …“). Neu sind nur Spaltenköpfe der neuen Tabellen, das Abzeichen „Wichtig“, „Wird geladen …“, „Speichert …“, „Kopieren“ und „Kopiert“.
- **Erreichbar bleiben** alle heutigen Bedienelemente: Reiter und Suche der Liste, Zeile öffnet die Kundenseite, „Neuer Kunde“ (Admin) und „Neuer Lead“ mit beiden Formularen, auch im Zuordnen-Assistenten. Auf der Kundenseite „Kunde löschen“ (Admin, mit `window.confirm`), „Archivieren“/„Wiederherstellen“, „Kundenansicht öffnen“, der Weg zurück über die Brotkrume, die neun Reiter, „Speichern“, „Zum festen Kunden machen“, „Passwort ändern“/„Zugang anlegen“, Aktivieren und Abschalten mit Notiz, Site-Key kopieren und „Erneut versuchen“. Es gibt keine `data-testid` in diesen Dateien.
- **Entscheidung (Reiter der Liste):** Reihenfolge Feste Kunden · Leads · Archiviert · Alle, Start bei Feste Kunden, nicht in der URL (wie heute).
- **Entscheidung (Zähler und Suche):** Die Zähler nutzen dieselbe Funktion wie die Liste und beachten deshalb den Suchtext. Die Zahl im aktiven Reiter ist immer die Zeilenzahl. Die Suche durchsucht Name, Code, Standort, E-Mail wie heute und zusätzlich den Ansprechpartner, weil die Tabelle ihn zeigt (der Prototyp tut es auch).
- **Entscheidung (Spalten der Liste):** Unternehmen mit Ansprechpartner als `.cell-sub`, Kundennummer (`hide-sm`), Standort, E-Mail und Telefon (`hide-md`). Keine Spalte „Offene Tickets“ (Spec), keine Fußzeile.
- **Entscheidung (Abzeichen):** Lead grau (`muted`), Abgesagt und Archiviert `plain`, Wichtig orange (`warn`, wie der frühere Stern). „Fester Kunde“ entfällt als Abzeichen, der Reiter sagt es, die Eyebrow der Kundenseite nennt es.
- **Entscheidung (erste Zelle):** `<div className="cell-title">` mit `<Link className="row-link">` um den Namen und den Abzeichen dahinter, wie in der Ticketliste aus P2.
- **Entscheidung (Kundenseite):** Eyebrow „Kundennummer · Stufe“ (Stufe als Klartext), h1 nur der Name (Rückfall E-Mail, dann „–“). „Wichtig“ und „Archiviert“ stehen als Abzeichen in der `.sub`-Zeile vor Standort · E-Mail · Telefon. Kopfknöpfe: „Kunde löschen“ (Admin, roter Textlink), „Archivieren“/„Wiederherstellen“, „Kundenansicht öffnen“, keiner schwarz. „Alle Kunden“ wird die Brotkrume „Kunden“. Die Reiter bekommen keine Zähler (zusätzliche Abfragen, die Spec nennt Zähler nur für die Liste).
- **Entscheidung (Übersicht):** Schwarz ist nur „Speichern“. „Zum festen Kunden machen“ und „Passwort ändern“/„Zugang anlegen“ sind Standardknöpfe. Rückmeldungen laufen über `.msg` (`ok`, `err`, `warn`).
- **Entscheidung (Tickets-Reiter):** Spalten „Ticket“ (Nummer, Betreff, „Dringend“, darunter die Art roh wie heute) und „Status“, keine zusätzlichen Abfragen.
- **Entscheidung (Dialoge):** `CreateLeadForm` im Standarddialog mit einer `.field-row`, `CreateCustomerForm` `wide` mit zwei `.field-row-3`. Fuß wie im Prototyp: `<span className="grow" />`, Abbrechen (Standardknopf mit `type="button"`, sonst löst er in `<Dialog as="form">` das Absenden aus), Absenden (schwarz, `type="submit"`). Abbrechen ist wie × und Esc während `busy` gesperrt. Beide Formulare bleiben in `CustomersPage.jsx`, der Export von `CreateCustomerForm` bleibt.
- **Entscheidung (Funktionen und Protokoll):** Funktionen: eine Tabelle mit einer Projektzeile über den Funktionszeilen, die Bestätigung als eigene Zeile darunter. Dieselbe Zusatzzeile kann später das Panel von tp2 aufnehmen. Protokoll: Tabelle „Zeit“, „Mitarbeiter“, „Aktion“ mit dem Zusatztext als `.cell-sub`.
- **Bekannt und unverändert:** `CustomerDetailPage.load()` ersetzt die Seite beim Nachladen durch den Ladezustand, die Meldung „Gespeichert“ bleibt deshalb nicht stehen. Das ist heute schon so und kein P4-Thema.
## Review Focus
Alle fünf Punkte der Übersicht treffen P4. Die Ansichten stehen in Task 3.
1. **Altdaten** (`kunden-altdaten*`, `kunde-altdaten`): Kunden ohne Kundennummer, Standort, E-Mail, Telefon und Ansprechpartner zeigen „–“. Ein Kunde ohne Namen zeigt die E-Mail, ohne beides „–“. Im Text steht nie `undefined`, `null` oder `NaN`.
2. **Lange Inhalte:** Firmenname mit 60 Zeichen, ein Kunde mit den Abzeichen Wichtig, Lead und Archiviert, Funktionen mit langem Projektnamen, langer URL, Site-Key und Notiz. Kein waagerechtes Scrollen bei 390 px.
3. **Teilausfall** (`fehler-kunde*`): Antworten die Portal-Wege `portal`, `abo`, `protokoll` und `funktionen` mit 500, zeigen Protokoll, Abo und Funktionen `.msg.err`, die Übersicht „nicht abrufbar“. Die Reiter bleiben bedienbar.
4. **Nur Tastatur** (`kunden-tastatur`, `dlg-*`): Ein Zeilenlink ist per Tab erreichbar, Enter öffnet die Kundenseite. Esc schließt die Dialoge, der erste Tab im offenen Dialog bleibt im Dialog. Ein Klick in die Zeile neben dem Link öffnet genau einmal.
5. **Viele Einträge** (`kunden-viel`): 300 zusätzliche Kunden, wie heute ohne Seitenwechsel. Die Zähler stimmen in jedem Reiter mit den Zeilen überein.
---
### Task 1: Paketstart und tp2-Prüfung
P4 baut `CustomerFunktionen.jsx` und `admin-areas.css` neu, beide Dateien ändert auch `feature/terminplaner-tp2`. Vor dem ersten Umbau wird deshalb geklärt, in welcher Reihenfolge beide Arbeiten live gehen.
**Files:** keine Änderung (nur Lesen und Prüfen).
**Interfaces:**
- Consumes: Tag `design-p3` und die P2-Bausteine `ticketPath`, `ticketRef`, `ticketNumber`, `isRowActivation` und `.row-link`.
- Produces: die Ausgangs-Testzahl N3 und die Entscheidung des Eigentümers zu tp2.
- [ ] **Step 1: Paketstart**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git merge-base --is-ancestor design-p3 HEAD && git rebase origin/test && "$W/check.sh"`
Expected:
- Alle Befehle enden mit Exit 0, `# fail 0`. Schlägt `merge-base` fehl, ist `design-p3` nicht im Branch: Stopp und melden.
- Die Zahl `# pass` ist N3. Soll nach P4: N3 + 6.
- [ ] **Step 2: Parallele Arbeit an `feature/terminplaner-tp2`**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git log --oneline origin/test..origin/feature/terminplaner-tp2 -- src/pages/CustomersPage.jsx src/pages/CustomerDetailPage.jsx src/components/customer src/styles/admin-areas.css src/styles/global.css src/lib/leads.js src/lib/funktionenView.js src/pages/AssignProjectPage.jsx`
Expected: keine Ausgabe. Gibt der Befehl Zeilen aus (Stand 2026-10-05: `6f03712`, `c001554`, `857e001`, `45e5dc3`), **stoppt der Agent und fragt den Eigentümer**:
- Er empfiehlt, tp2 zuerst nach `test` zu bringen.
- Er holt keinen Commit einzeln per Cherry-Pick. `6f03712` allein ließe `CustomerFunktionen.jsx` und `lib/funktionenView.js` in einem Zwischenstand, den die übrigen tp2-Commits wieder umbauen.
Weiter geht es erst nach der Antwort:
- **Fall A, tp2 ist in `test`:** Der Rebase aus Step 1 wird wiederholt, dieser Step ist dann leer.
- **Fall B, der Eigentümer legt P4 vor tp2:** Er nennt, wer die Konflikte in `CustomerFunktionen.jsx`, `lib/funktionenView.js` und `admin-areas.css` später auflöst. Die Übergabe (Task 8) hält das fest.
- [ ] **Step 3: P2-Bausteine sind da**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "export function (ticketPath|ticketRef)" src/lib/ticketRoutes.js && rg -n "export function ticketNumber" src/lib/ticketView.js && rg -n "isRowActivation" src/lib/rowClick.js && rg -m 2 -n "\.row-link" src/styles/global.css`
Expected: Treffer in allen vier Dateien. Sonst Stopp: P4 darf erst nach P2 laufen.
- [ ] **Step 4: Stand von `CustomerFunktionen.jsx`**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -c "abgeschaltetText" src/lib/funktionenView.js; rg -c "AnleitungPanel|TextformPanel" src/components/customer/CustomerFunktionen.jsx`
Expected:
- Fall A (tp2 ist in `test`): Treffer in beiden Dateien. Task 6 enthält dann die Zeile „tp2-Zusatz“.
- Fall B (der Eigentümer hat P4 vor tp2 freigegeben): keine Treffer. In Task 6 entfallen die Zeile „tp2-Zusatz“ und `abgeschaltetText`, und in Task 3 entfällt die Zusicherung zum Abschaltgrund in `kunde-funktionen`.
### Task 2: `customerList.js` (Filter, Zähler, Abzeichen)
**Files:**
- Create: `src/lib/customerList.js`
- Modify: `src/lib/leads.js` (Zeilen 8–18: `customerStage` und `isArchived` samt Kommentar ersetzen)
- Test: `tests/unit/customerList.test.mjs` (neu)
**Interfaces:**
- Consumes: nichts (reine Datei ohne Importe).
- Produces, alle aus `src/lib/customerList.js`, die ersten vier auch aus `src/lib/leads.js`:
- `customerStage(c) → 'lead' | 'lost' | 'customer'` und `isArchived(c) → boolean`, unverändert verschoben
- `filterCustomers(customers, { stage = 'customer', search = '' } = {}) → Kunde[]`
- `stageCounts(customers, search = '') → { customer, lead, archived, all }`
- `CUSTOMER_TABS: [{ id, label }]`
- `customerBadges(c, { stage = true } = {}) → [{ key, label, tone }]`
- `stageLabel(stage)`, `customerEyebrow(c)`, `customerTitle(c)` → string
- [ ] **Step 1: Failing Tests schreiben**
```js
const { CUSTOMER_TABS, customerStage, isArchived, filterCustomers, stageCounts, customerBadges, stageLabel, customerEyebrow, customerTitle } =
await loadModule('src/lib/customerList.js')
const A = { $id: 'a', name: 'Alpha GmbH', code: 'K-1', location: 'Kaiserslautern', email: 'info@alpha.de', contactName: 'Anna Alt', customerStatus: 'customer' }
const B = { $id: 'b', name: 'Beta', customerStatus: 'lead' }
const C = { $id: 'c', name: 'Gamma', customerStatus: 'lost' }
const D = { $id: 'd', name: 'Delta' } // Bestandsdaten ohne Status gelten als fester Kunde
const E = { $id: 'e', name: 'Epsilon', customerStatus: 'lead', archived: true, importantCustomer: true }
const F = { $id: 'f', name: 'Zeta', customerStatus: 'customer', archived: true }
const ALL = [A, B, C, D, E, F]
const ids = (list) => list.map((c) => c.$id)
test('CUSTOMER_TABS, customerStage, isArchived', () => {
assert.deepEqual(CUSTOMER_TABS, [
{ id: 'customer', label: 'Feste Kunden' }, { id: 'lead', label: 'Leads' },
{ id: 'archived', label: 'Archiviert' }, { id: 'all', label: 'Alle' },
])
assert.equal(customerStage({ customerStatus: 'LEAD' }), 'lead')
assert.equal(customerStage({ customerStatus: 'Lost' }), 'lost')
for (const c of [{}, { customerStatus: '' }, { customerStatus: 'sonstwas' }, null, undefined]) assert.equal(customerStage(c), 'customer')
assert.equal(isArchived({ archived: true }), true)
for (const c of [{}, { archived: false }, null, undefined]) assert.equal(isArchived(c), false)
})
test('filterCustomers: jeder Reiter', () => {
assert.deepEqual(ids(filterCustomers(ALL, { stage: 'customer' })), ['a', 'd'])
assert.deepEqual(ids(filterCustomers(ALL, { stage: 'lead' })), ['b'])
assert.deepEqual(ids(filterCustomers(ALL, { stage: 'archived' })), ['e', 'f'])
assert.deepEqual(ids(filterCustomers(ALL, { stage: 'all' })), ['a', 'b', 'c', 'd'])
assert.deepEqual(ids(filterCustomers(ALL)), ['a', 'd'])
assert.deepEqual(ids(filterCustomers(ALL, { stage: 'x' })), [])
assert.deepEqual(filterCustomers(undefined, { stage: 'all' }), [])
})
test('filterCustomers: Suche in Name, Code, Standort, E-Mail und Ansprechpartner', () => {
const find = (search, stage = 'all') => ids(filterCustomers(ALL, { stage, search }))
for (const s of ['ALPHA', 'k-1', 'kaisers', 'info@alpha', 'anna']) assert.deepEqual(find(s), ['a'], s)
assert.deepEqual(find('xyz'), [])
assert.deepEqual(find('eta', 'lead'), ['b'])
assert.deepEqual(find(''), ['a', 'b', 'c', 'd'])
assert.deepEqual(ids(filterCustomers([{ $id: 'n' }], { stage: 'all', search: 'undefined' })), [])
})
test('stageCounts: dieselbe Filterfunktion wie die Liste', () => {
assert.deepEqual(stageCounts(ALL), { customer: 2, lead: 1, archived: 2, all: 4 })
assert.deepEqual(stageCounts(ALL, 'eta'), { customer: 0, lead: 1, archived: 1, all: 1 })
assert.deepEqual(stageCounts([]), { customer: 0, lead: 0, archived: 0, all: 0 })
})
test('customerBadges: Reihenfolge, Töne, Option stage', () => {
assert.deepEqual(customerBadges(E), [
{ key: 'important', label: 'Wichtig', tone: 'warn' },
{ key: 'lead', label: 'Lead', tone: 'muted' },
{ key: 'archived', label: 'Archiviert', tone: 'plain' },
])
assert.deepEqual(customerBadges(C), [{ key: 'lost', label: 'Abgesagt', tone: 'plain' }])
for (const c of [A, D, null]) assert.deepEqual(customerBadges(c), [])
assert.deepEqual(customerBadges(E, { stage: false }).map((b) => b.key), ['important', 'archived'])
})
test('stageLabel, customerEyebrow, customerTitle', () => {
assert.deepEqual(['customer', 'lead', 'lost'].map(stageLabel), ['Fester Kunde', 'Lead', 'Abgesagt'])
assert.equal(customerEyebrow(A), 'K-1 · Fester Kunde')
assert.equal(customerEyebrow(B), 'Lead')
assert.equal(customerEyebrow({ code: ' ', customerStatus: 'lost' }), 'Abgesagt')
assert.equal(customerTitle(A), 'Alpha GmbH')
assert.equal(customerTitle({ email: 'x@y.de' }), 'x@y.de')
assert.equal(customerTitle({}), '–')
})
```
- [ ] **Step 2: Test laufen lassen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/customerList.test.mjs`
Expected: FAIL, `src/lib/customerList.js` fehlt.
- [ ] **Step 3: `customerList.js` implementieren**
- `customerStage` und `isArchived` werden Zeile für Zeile aus `leads.js` übernommen.
- `filterCustomers`:
- Ohne Liste `[]`, die Reihenfolge der Eingabe bleibt.
- Der Reiter `archived` liefert nur Archivierte, alle anderen Reiter schließen sie aus.
- Stufe `lost` erscheint nur im Reiter `all`. `customer` und `lead` liefern ihre Stufe, ein unbekannter Reiter `[]`.
- Die Suche ist ein Teilstring-Vergleich in Kleinbuchstaben ohne Trim über `${name} ${code} ${location} ${email} ${contactName}`, jedes Feld mit `|| ''`. Eine leere Suche filtert nicht.
- `stageCounts`: je Eintrag von `CUSTOMER_TABS` die Länge von `filterCustomers(customers, { stage, search })`.
- `customerBadges`: Wichtig (`importantCustomer`, Ton `warn`), dann die Stufe (`lead` Ton `muted`, `lost` „Abgesagt“ Ton `plain`, `customer` nichts, nur mit `stage: true`), dann Archiviert (Ton `plain`).
- `customerEyebrow` = `[String(c.code || '').trim(), stageLabel(customerStage(c))].filter(Boolean).join(' · ')`. `customerTitle` = `c.name || c.email || '–'`.
- [ ] **Step 4: `leads.js` umstellen**
In `src/lib/leads.js` werden die Definitionen von `customerStage` und `isArchived` durch `export { customerStage, isArchived, filterCustomers, stageCounts } from './customerList'` ersetzt. Alles andere bleibt, auch `isInvoiceReady`.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "from '../lib/leads'" src`
Expected: drei Importeure (`CustomersPage`, `CustomerDetailPage`, `AssignProjectPage`), alle ohne Änderung gültig.
- [ ] **Step 5: Test grün und Build**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/customerList.test.mjs && "$W/check.sh" --nur-build`
Expected: PASS mit 6 Tests, Build Exit 0.
- [ ] **Step 6: Commit**
```bash
git add src/lib/customerList.js src/lib/leads.js tests/unit/customerList.test.mjs
git commit -m "Kunden: Filter, Zaehler und Abzeichen als reine Funktionen (lib/customerList.js, Tests)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 3: Testdaten, Ansichten und Gate (Prüfung vor dem Umbau)
Alles liegt im Werkzeug außerhalb des Repos, deshalb gibt es keinen Commit.
**Files** (unter `$W/`):
- Modify: `fixtures/standard.mjs`, `fixtures/altdaten.mjs`, `fixtures/viel.mjs`, `backend.mjs`, `views.mjs`, `gates.mjs`
**Interfaces:**
- Consumes: `createBackend`, `VIEWS`, `GATES` aus P0.
- Produces: die Ansichten aus Step 3 und das Gate `p4`.
- [ ] **Step 1: Kunden-Testdaten prüfen und ergänzen**
- `standard`: Der „Abgesagte“ trägt `customerStatus: 'lost'` (nicht `crmStatus`). Soll-Zähler: Feste Kunden 6 · Leads 2 · Archiviert 1 · Alle 9.
- `altdaten`: zusätzlich zu den Standardkunden fünf Kunden:
1. `Müller-Lüdenscheidt Metallbau- und Anlagentechnik GmbH & Co.` (60 Zeichen), fest, `importantCustomer: true`, ohne Kundennummer, Standort, E-Mail, Telefon, Ansprechpartner
2. Lead `Frisch Gelead UG` mit `importantCustomer: true`
3. archivierter Lead `Archivierter Wichtiger Lead` mit `importantCustomer: true`
4. fester Kunde ohne Namen mit E-Mail `ohne-name@example.test`
5. fester Kunde mit leerem Namen und ohne E-Mail
- `viel`: zusätzlich 300 Kunden `Testkunde 001` bis `Testkunde 300`, 200 fest und 100 Leads. Soll-Zähler: 206 · 102 · 1 · 309.
- [ ] **Step 2: Portal-Antworten ergänzen**
Falls P0 sie nicht liefert (unter `https://portal.test/api/team`):
| Pfad | Antwort |
|---|---|
| `/customers/:id/portal` | `{ hasAccess: true, lastLoginAt: '2026-10-04T08:15:00.000Z' }` |
| `/customers/:id/abo` | ein aktives Abo mit Zusatzspeicher, für einen zweiten Kunden `{ status: '' }` („kein Abo“) |
| `/customers/:id/protokoll` | `{ entries }` mit 5 Einträgen: `geoeffnet`, `news_angelegt` mit Zusatztext, `termin_aktion` mit 120 Zeichen Zusatztext, einer ohne `staffName`, einer mit unbekannter Aktion `xyz` |
| `/funktionen/:id` | `{ projects }` mit den drei Projekten unten |
Projekte der Funktionen:
1. `Bäckerei Sonnenschein Website` mit `liveUrl`. News `active` mit `ticketWoid: '100131'`, Terminplaner `setup` mit `ticketWoid: '100140'` und `adminNote` von 80 Zeichen.
2. Projekt mit 70 Zeichen langem Namen und `liveUrl` von 80 Zeichen ohne Leerzeichen. News `off`, Terminplaner `active` mit `siteKey: 'q74ehq7hvnpiymqy'` und `ticketMissing: true`.
3. `Altes Projekt` ohne `liveUrl`. News `disabled` mit `disabledReason: 'abo_ende'` und `loeschungAm: '2026-11-04'`, Terminplaner `disabled` mit `disabledReason: 'abo_ende_vor_einrichtung'` und `loeschungAm: '2026-11-04'`.
Im Szenario `fehler` antworten `/customers/:id/portal`, `/abo`, `/protokoll` und `/funktionen/:id` mit 500.
- [ ] **Step 3: Ansichten in `views.mjs` ergänzen**
Die Schritte haben die Signatur `steps(page, ctx)` aus der Übersicht. Eine nicht erfüllte Zusicherung wirft einen Fehler (Verstoß `step-failed`, in jedem Gate aktiv). `zaehlerStimmen` klickt jeden der vier Reiter und vergleicht die Zahl im Reiter mit der Zahl der Zeilen `tbody tr.row` (leer: 0). `keinNull` wirft, wenn der Seitentext `undefined`, `null` oder `NaN` enthält.
| ID | Daten | Schritte | Zusicherung |
|---|---|---|---|
| `kunden` | `standard` | wie P0 | Reiter 6 · 2 · 1 · 9, `zaehlerStimmen` |
| `kunden-leads`, `kunden-archiv`, `kunden-alle` | `standard` | je der Reiter | 2 Zeilen mit „Lead“ · 1 Zeile mit „Archiviert“ · 9 Zeilen, davon eine mit „Abgesagt“ |
| `kunden-suche` | `standard` | Suche „Weber“ | 1 Zeile, Reiter 1 · 0 · 0 · 1 |
| `kunden-altdaten`, `kunden-altdaten-leads`, `kunden-altdaten-archiv` | `altdaten` | Reiter Alle, Leads, Archiviert | `keinNull`, der 60-Zeichen-Name steht vollständig im Link, eine Zeile mit „Wichtig“ und „Lead“, eine mit „Wichtig“, „Lead“ und „Archiviert“ |
| `kunden-viel` | `viel` | wie P0 | Reiter 206 · 102 · 1 · 309, `zaehlerStimmen` |
| `kunden-tastatur` | `standard` | Tab, bis ein `tbody tr a.row-link` Fokus hat, dann Enter | Fokusring 2 px `solid`. Pfad beginnt mit `/customers/`. Nach „Zurück“ erhöht ein Klick in die Zeile neben dem Link `history.length` um genau 1 |
| `dlg-lead-neu`, `dlg-kunde-neu` | `standard` | Klick „Neuer Lead“ bzw. „Neuer Kunde“ | `role="dialog"`, Titel „Neuer Lead (potenzieller Kunde)“ bzw. „Neuer Kunde mit Portal-Zugang“. Ein Tab landet im Dialog (`document.activeElement.closest('.dialog')`). Esc schließt, der Fokus kehrt zum Knopf zurück. Danach erneut öffnen |
| `dlg-kunde-neu-fehler` | `standard` | Name und E-Mail füllen, Passwort „kurz“, absenden | `.msg.err` mit „Das Portal-Passwort muss mindestens 8 Zeichen haben.“, Dialog bleibt offen |
| `zuordnen-kunde-neu` | `standard` | `/projects/zuordnen/<id>`, erste Art, „Ja, für einen Kunden“, „Neuer Kunde“ | Dialog „Neuer Kunde mit Portal-Zugang“ über der Seite |
| `kunde-overview`, `kunde-tickets`, `kunde-invoices`, `kunde-projects`, `kunde-chat`, `kunde-abo`, `kunde-gutschriften`, `kunde-funktionen`, `kunde-protokoll` | `standard` | die neun Ansichten aus P0 (Reiter-IDs aus `src/lib/customerTabs.js`, siehe Übersicht). Fehlende werden als `/customers/<id>?tab=<reiter>` ergänzt. Für dieselbe URL entsteht keine zweite Ansicht | keine eigene Zusicherung, außer bei `kunde-funktionen` (nächste Zeile) |
| `kunde-funktionen` (zusätzliche Schritte) | `standard` | Tab „Funktionen“, „Abschalten“ in der News-Zeile des ersten Projekts, „Abbrechen“, „Abschalten“ erneut | Nach dem ersten Klick steht die Zeile „News abschalten? Die Website zeigt dann keine Beiträge mehr.“ mit Feld „Notiz (optional)“, „Ja“ und „Abbrechen“. „Abbrechen“ schließt sie, am Ende ist sie offen, damit das Bild sie zeigt. Der Text enthält „Abgeschaltet (Abo beendet), Löschung am 04.11.2026“ (nur Fall A, siehe Task 1 Step 4) |
| `kunde-lead`, `kunde-archiv`, `kunde-altdaten` | `standard`, `standard`, `altdaten` | Kunde „Gärtnerei Grünblick“, „Blumen Lehmann“, Kunde Nr. 1 aus Step 1 | Eyebrow „Lead“ mit „Zum festen Kunden machen“ · „Archiviert“ mit „Wiederherstellen“ · `keinNull`, langer Name in h1 und Brotkrume |
| `kunde-nicht-gefunden` | `standard` | `/customers/gibt-es-nicht` | „Kunde nicht gefunden“ und Knopf „Zurück“ |
| `fehler-kunde`, `fehler-kunde-funktionen`, `fehler-kunde-protokoll`, `fehler-kunde-abo` | `standard`, `fehler` | Übersicht bzw. `?tab=funktionen`, `?tab=protokoll`, `?tab=abo` | „nicht abrufbar“ beim letzten Login bzw. je ein `.msg.err`. Ein Klick auf „Übersicht“ zeigt die Stammdaten |
- [ ] **Step 4: Gate `p4` in `gates.mjs` anlegen**
`p4` ist `p3` plus die P4-Ansichten im bereichsbezogenen Gate `icon-svg` (Schema der Übersicht). `step-failed`, `external-request` und `console-error` sind ohnehin aktiv. Das Gate deckt auch `gutschriften`, `angebote` und `speicher` ab, die der neue Grundstil in `admin-areas.css` trifft.
```js
const I3 = GATES.p3.scoped['icon-svg']
GATES.p4 = { ...GATES.p3, scoped: { ...GATES.p3.scoped, 'icon-svg': {
views: [...I3.views, 'kunden', 'kunden-*', 'dlg-lead-neu', 'dlg-kunde-neu', 'dlg-kunde-neu-fehler', 'kunde-overview', 'kunde-lead', 'kunde-archiv', 'kunde-altdaten', 'kunde-tickets', 'kunde-funktionen', 'kunde-protokoll', 'kunde-nicht-gefunden', 'fehler-kunde', 'fehler-kunde-funktionen', 'fehler-kunde-protokoll'],
ignore: [...(I3.ignore || []), '.credential'],
} } }
```
`.credential` (`CopyableCredential`) ist bis P5 ausgenommen. Nicht im `icon-svg`-Umfang sind `kunde-abo` und `fehler-kunde-abo` (Angebote, P6), `kunde-projects` (P5), `kunde-chat`, `kunde-invoices`, `kunde-gutschriften` und `zuordnen-kunde-neu` (fremde Bausteine). Deshalb stehen die `fehler-kunde-*`-Ansichten einzeln in der Liste. Ihre P4-Anteile prüft Task 8 über `rg` auf Icon-Importe.
- [ ] **Step 5: Vorher-Lauf (muss scheitern)**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p4-vorher && "$W/shots.sh" p4-vorher "$W/shots/p4-vorher" --only kunden,kunde-overview,kunde-funktionen,kunde-protokoll --gate p4`
Expected: Exit 1 wegen `icon-svg`. Die Bilder in `shots/p4-vorher/1440` und `390` sind die Vorher-Bilder mit den neuen Testdaten.
### Task 4: Kundenliste und Dialoge
**Files:**
- Modify: `src/pages/CustomersPage.jsx` (Zeilen 1–208 ersetzen)
- Modify: `src/pages/AssignProjectPage.jsx` (Zeilen 275–289: Aufruf von `CreateCustomerForm`)
- Modify: `src/styles/global.css` (nur die KOMPAT-Regel `.side-logo` löschen)
**Interfaces:**
- Consumes: `PageHeader`, `Tabs`, `Badge`, `Button`, `EmptyState`, `Field`, `Dialog` (P1). `CUSTOMER_TABS`, `filterCustomers`, `stageCounts`, `customerBadges`, `customerTitle` (Task 2). `isRowActivation` aus `src/lib/rowClick.js` (P2). `createLead`, `useCustomers`, `useAuth` unverändert.
- Produces:
- `export function CreateCustomerForm({ onCancel, onCreate })` rendert einen `wide` Dialog. Die Signatur bleibt, P5 braucht sie.
- `CreateLeadForm({ onCancel, onDone })` (nicht exportiert) rendert einen Dialog.
- [ ] **Step 1: Seite neu aufbauen**
| Element | Umsetzung |
|---|---|
| Kopf | `PageHeader title="Kunden" subtitle="Potenzielle Kunden (Leads) und feste Kunden – eine Pflege"`. Aktionen: für Admins `Button variant="primary"` „Neuer Kunde“, für alle `Button` „Neuer Lead“. Beide setzen `showCreate` direkt, kein Umschalten mehr |
| Reiter | `Tabs` mit `CUSTOMER_TABS` und `count = stageCounts(customers, search)[id]`, bis zum Laden ohne Zähler |
| Suche | `<div className="toolbar">` mit `<input className="input search" type="search" placeholder="Kunde suchen …" aria-label="Kunde suchen">`, filtert sofort wie heute |
| Laden, leer | `<div className="loading">Wird geladen …</div>`. Leer: `EmptyState title="Keine Einträge" hint="Keine Kunden in dieser Ansicht."` |
| Tabelle | `.table`, `<tr className="row">` mit `onClick`, der bei `isRowActivation(e)` zu `/customers/<$id>` navigiert |
| Dialoge | `showCreate === 'lead'` rendert `CreateLeadForm`, `'customer'` und `isAdmin` rendert `CreateCustomerForm`, mit denselben Handlern wie heute. Bei Erfolg schließt die Seite, setzt bei Kunden den Reiter auf Feste Kunden und lädt neu |
Spalten (Klasse gilt für Kopf und Zelle):
| Kopf | Klasse | Inhalt |
|---|---|---|
| Unternehmen | – | `<div className="cell-title">` mit `<Link className="row-link" to=…>{customerTitle(c)}</Link>` und dahinter den `Badge`s aus `customerBadges(c)`. Darunter `.cell-sub` mit `contactName`, nur wenn vorhanden |
| Kundennummer | `hide-sm num` | `code`, sonst `<span className="faint">–</span>` |
| Standort | – | `location`, sonst „–“ |
| E-Mail | `hide-md muted` | `email`, sonst „–“ |
| Telefon | `hide-md muted nowrap num` | `phone`, sonst „–“ |
Formulare (jedes Feld ist ein `Field` mit `htmlFor` und `id`, `Dialog` bekommt `onClose={onCancel}` und `busy`, Fehler wie in P2 als `.msg.err` oben im Körper):
- `CreateLeadForm`: `as="form"`, Titel „Neuer Lead (potenzieller Kunde)“. Im Körper der Hinweis „Fuer einen Lead reichen Unternehmen und E-Mail. Es wird automatisch ein Akquise-Ticket erstellt.“ (`.muted .small`), dann `.field-row` mit „Unternehmen“ (`required`) und „E-Mail“ (`type="email"`). Ein Fehler steht als `.msg.err` mit `role="alert"` als erstes Kind des Körpers. Fuß: `<Button type="button">` „Abbrechen“ und `Button variant="primary" type="submit"` „Lead anlegen + Akquise-Ticket“ (bei `busy` „Speichert …“). „Unternehmen erforderlich“ bleibt.
- `CreateCustomerForm`: `wide`, Titel „Neuer Kunde mit Portal-Zugang“, zwei `.field-row-3`: „Unternehmen / Name“, „E-Mail (Portal-Login)“, „Kundennummer“ und „Telefon“, „Standort“, „Portal-Passwort (mind. 8 Zeichen)“. `name`, `required`, `type` und `autoComplete="new-password"` bleiben, ebenso die Texte „Das Portal-Passwort muss mindestens 8 Zeichen haben.“ und „Kunde konnte nicht angelegt werden.“. Fuß: `<Button type="button">` „Abbrechen“ und `Button variant="primary" type="submit"` „Kunde anlegen“ (bei `busy` „Speichert …“).
- [ ] **Step 2: Aufruf im Zuordnen-Assistenten**
In `AssignProjectPage.jsx` entfällt nur der Umschlag `<div style={{ marginBottom: 12 }}>` um `CreateCustomerForm`, der Dialog liegt über der Seite. Props und Handler bleiben. Der Rest der Datei gehört zu P5.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git diff --stat src/pages/AssignProjectPage.jsx`
Expected: nur wenige Zeilen geändert (der Umschlag).
- [ ] **Step 3: Hygiene prüfen, `.side-logo` löschen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{\{|form-control|ui-card|badge-|\bdot\b" src/pages/CustomersPage.jsx; rg -w side-logo src`
Expected: Aus `CustomersPage.jsx` keine Treffer. `side-logo` steht nur noch als KOMPAT-Regel in `src/styles/global.css`, keine Datei benutzt es. Die Spec lässt die Regel bis P4 stehen, deshalb wird sie jetzt gelöscht (samt Hover- und Kindregeln).
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -w side-logo src`
Expected: keine Treffer.
- [ ] **Step 4: Bauen und prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p4 && "$W/shots.sh" p4 "$W/shots/p4-t4" --only kunden,kunden-leads,kunden-archiv,kunden-alle,kunden-suche,kunden-altdaten,kunden-altdaten-leads,kunden-altdaten-archiv,kunden-viel,kunden-tastatur,dlg-lead-neu,dlg-kunde-neu,dlg-kunde-neu-fehler,zuordnen-kunde-neu --gate p4`
Expected: Exit 0 und alle Zusicherungen aus Task 3. Die Bilder zeigen Haarlinien, schwarzes „Neuer Kunde“ und Standard-„Neuer Lead“. Bei 390 px bleiben Unternehmen und Standort.
- [ ] **Step 5: Commit**
```bash
git add src/pages/CustomersPage.jsx src/pages/AssignProjectPage.jsx src/styles/global.css
git commit -m "Kundenliste als Tabelle mit Reitern und Zaehlern, Neuer Kunde und Neuer Lead als Dialoge" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 5: Kundenseite und Kundenansicht-Knopf
**Files:**
- Modify: `src/pages/CustomerDetailPage.jsx` (Kopf Zeilen 56–124, `OverviewTab` 126–212, `PortalAccessCard` 214–298, `CustomerTickets` 300–344, `CustomerProjects` 346–370)
- Modify: `src/components/customer/KundenansichtButton.jsx` (Zeilen 1–36)
- Modify: `src/styles/global.css` (neuer Block „Kunden (P4)“ vor dem KOMPAT-Block)
**Interfaces:**
- Consumes: `PageHeader` (`crumb`, `eyebrow`, `subtitle`), `Tabs`, `Card`, `Badge`, `Button`, `EmptyState`, `StatusPill`, `UrgentPill` (P1). `customerEyebrow`, `customerBadges`, `customerTitle`, `customerStage`, `isArchived` (Task 2). `ticketPath(ref)`, `ticketRef(ticket)` (P2), `ticketNumber(ticket)` (P2). `isRowActivation` aus `src/lib/rowClick.js` (P2).
- Produces: `KundenansichtButton({ customer, variant = 'default', checkAccess = true })`. Die Voreinstellung wechselt von `primary` auf `default`. Die anderen Aufrufer (`CreditDetail`, P6) übergeben `ghost` und sehen gleich aus. Sonst nichts Neues, die Aufrufe der Reiter-Inhalte bleiben wie heute.
- [ ] **Step 1: Kopf prüfen und umbauen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "subtitle" src/components/ui/PageHeader.jsx`
Expected: `subtitle` steht in `.sub` und nimmt einen Knoten an. Die Abzeichen sind `span` und damit auch in einem `<p>` gültig.
| Element | Umsetzung |
|---|---|
| Laden | `<div className="page"><div className="loading">Wird geladen …</div></div>` |
| Nicht gefunden | `EmptyState title="Kunde nicht gefunden" hint={error}` mit `Button as={Link} to="/customers"` „Zurück“ (ohne Pfeil) |
| Kopf | `crumb={[{ label: 'Kunden', to: '/customers' }, { label: customerTitle(customer) }]}`, `eyebrow={customerEyebrow(customer)}`, `title={customerTitle(customer)}`. `.sub`: Abzeichen aus `customerBadges(customer, { stage: false })`, dann `[location, email, phone].filter(Boolean).join(' · ')`, beides leer: kein `.sub` |
| Aktionen | `Button variant="danger"` „Kunde löschen“ (nur Admin), `Button` „Wiederherstellen“ bzw. „Archivieren“, `KundenansichtButton`. `removeCustomer` (mit `window.confirm`/`window.alert`) und `toggleArchived` bleiben |
`KundenansichtButton`: Icon und Spinner entfallen, der Text „Kundenansicht öffnen“ bleibt, bei `busy` steht „Wird geladen …“. `title`, die `disabled`-Regel, `window.alert` und `openInNewTab` bleiben. `PortalAccessCard` übergibt keine `variant` mehr.
Reiter: `Tabs tabs={customerTabs(isAdmin)}`, `activeTab`, `?tab=` und `setSearchParams(…, { replace: true })` bleiben. Die neun Reiter:
| id | Label | Inhalt |
|---|---|---|
| `overview` | Übersicht | `OverviewTab` (P4) |
| `tickets` | Tickets | `CustomerTickets` (P4) |
| `invoices` | Rechnungen (nur Admins) | `CustomerInvoices` unverändert (P7) |
| `projects` | Projekte | `CustomerProjects` mit `AssignedProjectCard` (P5) |
| `chat` | Chat | `<Card><ChatThread …/></Card>` unverändert (P6) |
| `abo` | Abo & Angebote | `AboStatusCard` (Task 7) und `OffersView` (P6), ohne Umschlag mit Inline-Style |
| `gutschriften` | Gutschriften | `CreditsView` unverändert (P6) |
| `funktionen` | Funktionen | `CustomerFunktionen` (Task 6) |
| `protokoll` | Protokoll | `CustomerViewLog` (Task 7) |
- [ ] **Step 2: Übersicht**
Wurzel `<div className="two-col">` mit zwei `<div>`-Spalten. Links `Card title="Stammdaten"` und `Card title="Rechnungsdaten"`, rechts `PortalAccessCard`. State und Handler (`save`, `upgrade`, `savePassword`) bleiben, ebenso alle Texte und dass der Name `companyName` mitschreibt. Aus den `Field`-Zeilen werden `.kv`-Zeilen (`<dt><label htmlFor>`, `<dd>` mit `className="input"`), jedes Feld bekommt eine `id`.
| Abschnitt | Inhalt in dieser Reihenfolge |
|---|---|
| Stammdaten | Unternehmen / Name, Code, E-Mail (`type="email"`), Telefon, Standort, Ansprechpartner, Notizen (`<textarea className="textarea" rows={3}>`). Darunter `<label className="check">` „Wichtiger Kunde“, dann `.toolbar` mit `Button variant="primary"` „Speichern“ (bei `busy` „Speichert …“), bei Leads `Button` „Zum festen Kunden machen“ und der Meldung |
| Rechnungsdaten | oben `<Badge tone="warn">Adresse unvollständig</Badge>` bei `!ready`, dann Strasse + Nr., PLZ, Stadt, Land, USt-IdNr. |
| Portal-Zugang | Kundenportal (`Badge ok` „freigeschaltet“ bzw. `muted` „kein Zugang“), Letzter Login („…“, „nicht abrufbar“ oder `formatLastLogin`), Login (E-Mail) und Passwort als `CopyableCredential`, das Feld „Neues Passwort setzen“ bzw. „Passwort festlegen (min. 8 Zeichen)“, `.toolbar` mit `Button` „Passwort ändern“ bzw. „Zugang anlegen“ (bei `busy` „Speichert …“), `KundenansichtButton`, Meldung, zuletzt der Portal-Hinweis als `<p className="muted small">` |
Meldungen: `msg` wird `{ tone, text }` mit den bisherigen Texten. Fehler und Pflichthinweise sind `err` bzw. `warn` (mit `role="alert"`), Erfolge `ok`.
- [ ] **Step 3: Tickets- und Projekte-Reiter**
- `CustomerTickets`: Abfrage und Zustände bleiben. Laden `.loading`, leer `EmptyState title="Keine Tickets" hint="Fuer diesen Kunden gibt es noch keine Tickets."`.
- Tabelle „Ticket“ und „Status“:
- Erste Zelle `<div className="cell-title">` mit `<Link className="row-link" to={ticketPath(ticketRef(t))}>`, darin `<span className="id">{ticketNumber(t)}</span>` und der Betreff (`topic || title || '-'`), daneben `UrgentPill`. Darunter `.cell-sub` mit `t.type`. Die Kurznummer ist nie Linkziel.
- Zweite Zelle `StatusPill`. `<tr className="row">` mit `isRowActivation` wie in Task 4.
- `CustomerProjects`: Laden `.loading`, leer wie heute, sonst `Card title={`Projekte (${n})`}` mit `AssignedProjectCard`.
- [ ] **Step 4: CSS-Block „Kunden (P4)“**
Der Block steht hinter den Bausteinen und vor dem KOMPAT-Block und beginnt mit `/* ===== Kunden (P4) ===== */`:
- `.kv .textarea { width: 100%; max-width: 360px }` (die Prototypregel gilt nur für `.input`)
Den Abstand zwischen aufeinanderfolgenden Abschnitten in den Spalten (`h2.section`) liefert P1 mit `.page section + section { margin-top: 36px }`. P4 baut dafür keine eigene Regel.
- [ ] **Step 5: Hygiene und Prüfung**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{\{|form-control|ui-card|badge badge-|\bdot\b" src/pages/CustomerDetailPage.jsx src/components/customer/KundenansichtButton.jsx`
Expected: keine Treffer.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p4 && "$W/shots.sh" p4 "$W/shots/p4-t5" --only kunde-overview,kunde-tickets,kunde-projects,kunde-lead,kunde-archiv,kunde-altdaten,kunde-nicht-gefunden,kunde-chat,kunde-invoices,kunde-gutschriften --gate p4`
Expected: Exit 0. `kunde-overview` zeigt zwei Spalten mit `.kv`, der Abstand zwischen Stammdaten und Rechnungsdaten beträgt mindestens 24 px (36 px aus P1), der Kopf hat keinen schwarzen Knopf. Bei 390 px stehen die Spalten untereinander, und die Reiter scrollen waagerecht.
- [ ] **Step 6: Commit**
```bash
git add src/pages/CustomerDetailPage.jsx src/components/customer/KundenansichtButton.jsx src/styles/global.css
git commit -m "Kundenseite: Brotkrumen, Eyebrow, Uebersicht in zwei Spalten, Tickets-Reiter mit Links" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 6: Funktionen als Tabelle
**Files:**
- Modify: `src/components/customer/CustomerFunktionen.jsx` (Zeilen 1–130, die Logik bleibt)
- Modify: `src/styles/global.css` (Block „Kunden (P4)“ aus Task 5: Funktionen-Regeln)
- Modify: `src/styles/admin-areas.css` (Funktionen-Regeln löschen: Zeilen 24–32, 36 und 60)
**Interfaces:**
- Consumes: `FUNKTIONEN`, `ZUSTAND`, `funktionOf`, `aktionen`, `mergeFunktion`, `siteKeyOf` und, nur in Fall A (Task 1 Step 4), `abgeschaltetText` aus `lib/funktionenView` (Stand von `test`, nicht geändert). `copyText` aus `CopyableCredential`. `getFunktionen`, `setFeature`. `ticketPath` (P2).
- Produces: `CustomerFunktionen({ customerId })`, gleiche Signatur.
- [ ] **Step 1: Markup**
State, `load`, `save`, `ask` und `short` bleiben. Jede Bedienung von heute bleibt erreichbar, alle Texte bleiben. Der Import von `admin-areas.css` entfällt, die Funktionen-Regeln wandern nach `global.css`, damit P6 die Datei ohne P4-Nutzer löschen kann.
| Element | Umsetzung |
|---|---|
| Zustände | Fehler `<div className="msg err" role="alert">`. Laden `.loading` „Wird geladen …“. Leer `EmptyState title="Keine Projekte für diesen Kunden."` |
| Rahmen | `Card title="Funktionen"` mit `<table className="table funktionen-table">`, Kopf „Funktion“, „Status“, „Ticket“, „Aktionen“ |
| Projektzeile | je Projekt ein `<tbody>`, dessen erste Zeile `<tr className="funktionen-group"><th colSpan={4} scope="rowgroup">` den Projektnamen trägt. Dahinter `liveUrl` ohne Protokoll als `<a className="link" target="_blank" rel="noreferrer">` |
| Funktionszeile | je Funktion: `f.label`, Status, Ticket, Aktionen |
| Status | `Badge tone={tone}` mit `label`. Darunter (Fall A) `.cell-sub` mit `abgeschaltetText(feature)` (wenn nicht leer), `.cell-sub` mit `short(feature.adminNote)` und `title={feature.adminNote}` und bei `siteKeyOf(...)` der `SiteKey` |
| `SiteKey` | `.funktionen-sitekey` mit „Site-Key“ (`.muted`), `<code>` und `<button type="button" className="link">` „Kopieren“ bzw. „Kopiert“ (1,8 s). `title` ist „Site-Key kopieren“ bzw. „Kopiert“, `aria-label` „Site-Key kopieren“ bzw. „Site-Key kopiert“, beides wie heute. Kein Icon |
| Ticket | `ticketMissing` → `<Badge tone="danger">Ticket fehlt</Badge>`. Mit `ticketWoid` → `<Link className="link" to={ticketPath(feature.ticketWoid)}>#{feature.ticketWoid}</Link>`. Sonst `<span className="faint">–</span>` |
| Aktionen | je `aktionen(feature.state)` ein `<button type="button" className="link">` „Aktivieren (ohne Zahlung)“ bzw. „Abschalten“ in `.funktionen-actions`. Solange die Bestätigung dieser Zeile offen ist, bleibt die Zelle leer |
| Bestätigung | Direkt unter der Funktionszeile `<tr className="funktionen-confirm"><td colSpan={4}>` mit einem `.flex.items-center.gap-2.wrap`: Text `f.bestaetigen[confirm.action]`, `<input className="input funktionen-note" placeholder="Notiz (optional)" maxLength={120} aria-label="Notiz (optional)">`, `Button variant="primary"` „Ja“ (bei `busy` „Speichert …“, `disabled`) und `Button` „Abbrechen“ (`disabled` bei `busy`) |
| Rückmeldung | unter der Tabelle `<div className={`msg ${status.ok ? 'ok' : 'err'}`}>`, bei Fehler mit `role="alert"` |
| tp2-Zusatz (nur Fall A) | Die Datei enthält zusätzlich `KopierKnopf`, `AnleitungPanel`, `TextformPanel`, den Knopf „Anleitung“ (`aria-expanded`) und `hindernisText` beim Status. Logik, Zustände und Texte bleiben. Umbau wie oben: Knöpfe ohne `size` und Icons (`Button type="button"` bzw. `button.link`), der Spinner wird „Wird geladen …“, Kopierknöpfe zeigen „Kopiert“ bzw. ihren Namen, ein Icon-Knopf zeigt seinen `aria-label`-Text, `form-control` wird `input`, Inline-Styles werden Klassen im Block „Kunden (P4)“, `.area-*` wird `.msg err`, `.msg warn` bzw. `.muted`. Die Panels stehen als `<tr className="funktionen-extra"><td colSpan={4}>` direkt unter der Funktionszeile, `<details>` bleibt |
- [ ] **Step 2: CSS verschieben**
Aus `admin-areas.css` entfallen alle `.funktionen-*`-Regeln samt `.funktionen-leer` in den beiden Responsive-Blöcken. In Fall A ziehen auch die `.funktionen-panel*`-Regeln von tp2 um, mit Tokens statt `--surface-*` und `--border` (`--bg-subtle`, `--line`). Im Block „Kunden (P4)“ von `global.css` stehen:
| Selektor | Wert |
|---|---|
| `.funktionen-group th` | `text-align: left; font-size: 14px; font-weight: 600; color: var(--text); padding-top: 20px; overflow-wrap: anywhere` |
| `.funktionen-group .link` | `font-size: 13px; font-weight: 400; margin-left: 8px` |
| `.funktionen-sitekey` | `display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; margin-top: 4px` |
| `.funktionen-sitekey code` | `font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; overflow-wrap: anywhere; user-select: all` |
| `.funktionen-actions` | `display: flex; flex-wrap: wrap; gap: 4px 14px` |
| `.funktionen-confirm td` | `padding-top: 0` |
| `.funktionen-note` | `width: 100%; max-width: 260px` |
- [ ] **Step 3: Hygiene und Prüfung**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{\{|form-control|area-|admin-areas|ui-btn|size=" src/components/customer/CustomerFunktionen.jsx; rg -n "funktionen" src/styles/admin-areas.css`
Expected: keine Treffer.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p4 && "$W/shots.sh" p4 "$W/shots/p4-t6" --only kunde-funktionen,fehler-kunde-funktionen --gate p4`
Expected: Exit 0. Das Bild von `kunde-funktionen` zeigt die geöffnete Bestätigungszeile. Bei 390 px läuft nichts über (Site-Key und lange URL brechen um), in Fall A ist „Abgeschaltet (Abo beendet), Löschung am 04.11.2026“ lesbar, und jeder Textlink zeigt den Fokusring.
- [ ] **Step 4: Commit**
```bash
git add src/components/customer/CustomerFunktionen.jsx src/styles/global.css src/styles/admin-areas.css
git commit -m "Kunden-Reiter Funktionen als Tabelle mit Textaktionen, Bestaetigung als eigene Zeile" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 7: Abo-Status, Protokoll und Grundstil von `admin-areas.css`
**Files:**
- Modify: `src/components/customer/AboStatusCard.jsx` (Zeilen 1–44)
- Modify: `src/components/customer/CustomerViewLog.jsx` (Zeilen 1–56)
- Modify: `src/styles/admin-areas.css` (Zeilen 1–61)
**Interfaces:**
- Consumes: `Card`, `Badge`, `Button`, `EmptyState`. `getCustomerAboStatus`, `getCustomerViewLog`. `formatViewLogTime`, `viewLogLabel`.
- Produces: den Grundstil der `.area-*`-Klassen für die P6-Seiten. Die P4-Dateien benutzen keine `.area-*`-Klasse und keinen Import von `admin-areas.css` mehr.
- [ ] **Step 1: `AboStatusCard`**
`Card title="Abo"`. Der Import von `admin-areas.css` entfällt. Laden `.loading`, Fehler `<div className="msg err" role="alert">`, ohne Abo `<p className="muted">kein Abo</p>`. Die Zeilen werden `<dl className="kv">`: „Abo“ (Produktname plus `Badge`, im `<dd className="flex items-center gap-2 wrap">`), „Leistungsbeginn“, „Nächste Abbuchung“ (nur bei `abo.active`), „Gekündigt zum“, „Zusatzspeicher“. Texte und Bedingungen wie heute.
- [ ] **Step 2: `CustomerViewLog`**
- Fehler: `.msg.err` mit der Meldung und daneben `Button` „Erneut versuchen“ (ohne Icon, `onClick={load}`), beides in einem `.flex.items-center.gap-2.wrap`. Laden `.loading`. Leer wie heute (`EmptyState title="Noch keine Einträge"`).
- Tabelle „Zeit“ (`formatViewLogTime(e.at)`, `nowrap num`), „Mitarbeiter“ (`e.staffName || '–'`), „Aktion“ (`viewLogLabel(e.action)`, darunter `.cell-sub` mit `e.detail`). Keine Zeilenaktion, keine Links.
- [ ] **Step 3: Grundstil von `admin-areas.css`**
| Regel | Neu |
|---|---|
| `.area-row` | `padding: 12px 0` statt 12px 16px, Haarlinie `var(--line)` |
| `.area-row.head` | `color: var(--text-2); font-size: 12px; font-weight: 500`, ohne `text-transform` und `letter-spacing` |
| `.area-row.active` | `background: var(--bg-subtle)` |
| `.area-muted`, `.area-danger`, `.area-warn` | 13 px in `var(--text-2)`, `var(--red-fg)`, `var(--orange-fg)` |
| `.area-summary` | ohne Hintergrund, Radius und Innenabstand, `margin-bottom: 12px`, `.warn` färbt die Schrift `var(--orange-fg)` |
| `.area-message`, `.area-error` | wie `.msg.ok` und `.msg.err`: 13 px, `var(--green-fg)` bzw. `var(--red-fg)`, ohne Kasten, Abstand `8px 0` |
| `.area-kv dt`, `.credit-calc label`, `.credit-field` | `color: var(--text-2)` |
| `.area-rows a`, `.area-rows a:hover`, `.credit-cols a` | entfallen. Links ohne Klasse bekommen Farbe und feinen Unterstrich von `a:not([class])` (P1). `admin-areas.css` lädt vor `global.css`, eine eigene Regel mit gleicher Spezifität würde dort verlieren |
| Responsive | Beide Blöcke (`@media (max-width: 720px)` wird `860px`, und `@container (max-width: 960px)`) stehen am Dateiende hinter allen Spaltenregeln, `!important` entfällt in beiden. Bei gleicher Spezifität gewinnt die spätere Regel. Hat ein Spaltenselektor mehr als eine Klasse, bekommt der Responsive-Selektor dieselben Klassen |
| Bleiben bis P6 | `.storage-row`, `.credit-row`, `.credit-cols`, `.credit-*`, `.offer-row`, `.area-filter`, `.area-inline`, `.area-kv` |
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "!important|#[0-9a-fA-F]{3,6}\b|rgba\(|--text-muted|--text-faint|--surface|--border|--r-sm|--ok\b|--accent" src/styles/admin-areas.css`
Expected: keine Treffer.
- [ ] **Step 4: Hygiene und Prüfung**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{\{|form-control|ui-card|area-|admin-areas" src/components/customer`
Expected: keine Treffer.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p4 && "$W/shots.sh" p4 "$W/shots/p4-t7" --only kunde-abo,kunde-protokoll,fehler-kunde-protokoll,fehler-kunde-abo,kunde-gutschriften,gutschriften,angebote,speicher --gate p4`
Expected: Exit 0. Auf `gutschriften`, `angebote` und `speicher` stehen die Zeilen mit Haarlinien ohne Kästen und die Kopfzeilen ohne Großbuchstaben. Sie bleiben lesbar, P6 ersetzt sie.
- [ ] **Step 5: Commit**
```bash
git add src/components/customer/AboStatusCard.jsx src/components/customer/CustomerViewLog.jsx src/styles/admin-areas.css
git commit -m "Abo-Status und Protokoll ohne Karten und Icons, neuer Grundstil fuer admin-areas.css" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 8: Gate P4, Review, Tag
- [ ] **Step 1: Rebase**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git rebase origin/test && "$W/check.sh"`
Expected: Exit 0, `# fail 0`, `# pass` = N3 + 6. Liegen neue Commits auf `test`, kann die Zahl höher sein; sie wird notiert.
- [ ] **Step 2: Vollständiger Gate-Lauf**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p4 && "$W/shots.sh" p4 "$W/shots/p4" --gate p4`
Expected:
- Gate `p4` Exit 0.
- Keine Verstöße gegen `dark-surface`, `low-contrast`, `overflow-x`, `console-error`, `external-request` und `step-failed` auf allen Ansichten, auch auf `gutschriften`, `angebote` und `speicher`.
- Die Kundenansichten aus dem `icon-svg`-Umfang haben kein `icon-svg` außerhalb `.credential`.
- [ ] **Step 3: Code-Hygiene im umgebauten Bereich**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|@tabler|style=\{\{|/tickets\?woid" src/pages/CustomersPage.jsx src/pages/CustomerDetailPage.jsx src/components/customer; rg -w side-logo src`
Expected: keine Treffer. `AssignProjectPage.jsx` behält seine Icons bis P5.
- [ ] **Step 4: Bundle-Größe**
Die Größen stehen in der Ausgabe von `check.sh` (Step 1). Sie werden mit `design-p3` verglichen. Das JS wird kleiner (Icon-Importe in sieben Dateien entfallen).
- [ ] **Step 5: Code-Review (sonnet)**
Geprüft wird der Diff `design-p3..HEAD` gegen die Spec-Abschnitte „Gestaltungsregeln“, „Bausteine“ und Paket P4. Schwerpunkte:
- Texte unverändert (Diff-Stichprobe der Formulare und Meldungen)
- Zeilenklick-Logik und erste Zelle als echter Link
- Dialog-Verdrahtung (`busy`, Esc, `onClose`)
- `ticketPath` als einziges Linkziel auf Tickets
- `admin-areas.css` ohne `!important`
- `lib/leads.js` exportiert weiter alle bisherigen Namen (`git diff design-p3 -- src/lib/leads.js`)
- jede Bedienung aus Task 4 bis 7 ist erreichbar (Kopfknöpfe, neun Reiter, beide Formulare, Aktivieren, Abschalten, Bestätigung mit Notiz, Site-Key kopieren, „Erneut versuchen“)
- „Abbrechen“ in beiden Anlege-Dialogen hat `type="button"`
Wichtige Befunde werden behoben, Kleinigkeiten notiert. Danach kommt der Rebase aus Step 1 noch einmal und Step 2 als Kurzlauf mit `--only kunden,kunde-overview,dlg-kunde-neu`.
- [ ] **Step 6: Tag und Übergabe**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" && git tag design-p4 && git log --oneline origin/test..design-p4 && git rev-parse design-p4`
Der Eigentümer bekommt:
- die Screenshots aus `shots/p4/1440x900` und `shots/p4/390x844` (Auswahl: `kunden`, `kunden-altdaten-archiv`, `dlg-kunde-neu`, `kunde-overview`, `kunde-tickets`, `kunde-funktionen`, `kunde-protokoll`) und den Vorher-Vergleich aus `shots/p4-vorher`
- den Hinweis: „Fester Kunde“ gibt es in der Liste nicht mehr als Abzeichen, „Wichtig“ ersetzt den Stern, die Suche findet jetzt auch den Ansprechpartner
- den tp2-Stand aus Task 1: Fall A (tp2 war vor P4 in `test`, das Anleitungs- und Zustimmungs-Panel ist in die Zusatzzeilen der Tabelle übernommen) oder Fall B (P4 ging vor tp2, die Konflikte in `CustomerFunktionen.jsx`, `lib/funktionenView.js` und `admin-areas.css` löst die vom Eigentümer genannte Stelle beim Rebase von tp2 auf, die Panels müssen dann in die Zusatzzeile `funktionen-extra` der Tabelle)
- Notizen für spätere Pakete:
- P5: `CopyableCredential` und `AssignedProjectCard` erscheinen im Portal-Zugang und im Projekte-Reiter noch in alter Form, die Icons von `AssignProjectPage.jsx` sind unberührt.
- P6: `KundenansichtButton` ist jetzt ein Standardknopf, in `CreditDetail` bleibt „Genehmigen“ der einzige schwarze Knopf. Die Raster `.storage-row`, `.credit-row`, `.offer-row` und `.area-kv` stehen noch in `admin-areas.css`.
- die Push-Zeile `git -C /home/webklar/apps/ticket.webklar.com/dev-design push origin 'design-p4^{commit}:refs/heads/test'`
- die Rücknahme: `git revert --no-commit design-p3..design-p4`, Commit und Push durch den Eigentümer (gilt nur, solange P4 das jüngste Paket ist)
- [ ] **Step 7: Nach dem Push (erst, wenn der Eigentümer gepusht hat)**
- Im Log von `webklar-ticket-build` steht „Deployment completed successfully“.
- `curl -s https://ticket.webklar.com/ | grep -o 'assets/index-[^"]*\.js'` entspricht `dist/index.html` des Tags.
- Ein lesender Sichttest im Chrome des Eigentümers: Kundenliste mit Reitern, eine Kundenseite mit Tickets-Reiter und Funktionen.
@@ -0,0 +1,899 @@
# WOMS „Weißes Design“ P5: Projekte – Umsetzungsplan
> **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:** Nach P5 steht der Projektbereich im neuen Design. Die Projektliste ist eine Tabelle mit Reitern, beide Projekt-Dialoge laufen über `Dialog`, Zuordnen-Assistent und Services bestehen aus ruhigen Abschnitten. Alle Projekt-Bausteine (am Ticket und beim Kunden) kommen ohne Karten, Icons und Farbliterale aus. Jede heutige Funktion bleibt erreichbar.
**Architecture:**
- Reine Logik, die heute in `ProjectsPage.jsx` steckt (Projektart, Hosting-Zustand, Aktionen), wandert mit Tests nach `src/lib/projectRow.js`. `src/lib/projectList.js` bekommt den neuen Export `projectTabs`.
- Die Töne der Status-Vokabulare (Preview, Analyse, Projektstatus) werden in `src/components/projectTones.js` auf Badge-Töne abgebildet. `previewEnv.js`, `previewView.js` und `analyseView.js` bleiben unverändert.
- `global.css` bekommt am Ende den Block „Projekte (P5)“. Die Komponenten benutzen nur Bausteine aus P1.
**Tech Stack:** React 18, react-router-dom 6, date-fns 2 (`formatDistance`, Locale `de`, wie schon in `TicketCard`), reines CSS. Tests mit `node --test`, Screenshot-Werkzeug aus P0.
**Spec:** `docs/superpowers/specs/2026-10-04-weisses-design-design.md`, Abschnitte „Bausteine“ (Tabelle, Liste, Dialog, Badge, Reiter, Meldungen, Laden), „Ticket als eigene Seite / Route und Links“ (Ticket-Spalte), „Texte“, „Was gleich bleibt“ und Paket P5. Dazu der Anhang `docs/superpowers/specs/2026-10-04-weisses-design-umbaukarte.md` (Abschnitte 3, 4, 5.P5, 7). Globale Regeln und gemeinsame Schnittstellen: `2026-10-05-weisses-design-00-uebersicht.md`.
## Global Constraints
Jeder Run-Block beginnt mit `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh`. Danach gelten `$WT` (Worktree, Arbeitsverzeichnis), `$W` (Screenshot-Werkzeug) und `$TMPDIR`. Zeilenangaben beziehen sich auf den Stand `603d08c` und sind nach P1 bis P4 verschoben. Maßgeblich ist der Inhalt.
Es gelten die Regeln der Übersicht. Für P5 kommt hinzu:
- **Vorbedingung:** Die Tags `design-p1` bis `design-p4` existieren. Task 0 prüft das und rebased auf `origin/test`.
- **Dateien dieses Pakets:** `ProjectsPage`, `AssignProjectPage`, `ServicesPage`, `TicketProjects`, `AssignedProjectCard`, `AnalyseRow`, `PreviewBlock`, `PreviewLinkButton`, `GiteaLinkButton`, `CopyableCredential`, dazu `global.css` und `AdminPage.css` (nur `.credential*`).
- **Unverändert bleiben:** alle Hooks, `lib/previewEnv.js`, `lib/previewView.js`, `lib/analyseView.js`, `lib/projectAssign.js`, `lib/assignFlow.js`, `lib/servicesView.js`, `lib/projectAdminApi.js` und die Signaturen von `AssignedProjectCard`, `TicketProjects`, `AnalyseRow`, `PreviewBlock`, `CopyableCredential`, `copyText`, `getCustomerPortalPassword`. `lib/projectList.js` bekommt nur einen neuen Export.
- **Texte:** wörtlich wie heute (auch die ae/ue-Schreibung wie „Oeffentliche“ oder „angestossen“). Neu sind nur die Texte der Spec-Liste „Texte“ und die mit „Entscheidung:“ markierten Zeilen dieses Plans.
- **Status-Vokabulare** (`previewStatus().color`, `analyseStatus().color`, `previewState`, `status`, `provisioningStatus`) werden nicht geändert. Die Tonabbildung liegt in `projectTones.js`.
- **Jeder `Button` und jeder Textlink-Knopf (`button.link`) bekommt ein explizites `type`** (`button` oder `submit`), weil P1 keinen Standard setzt. In einem `<Dialog as="form">` trägt jeder Knopf außer dem Absenden-Knopf `type="button"`.
- **Zeilenklick:** Klickbare Tabellenzeilen prüfen Klicks mit `isRowActivation(event)` aus `src/lib/rowClick.js` (P2, Task 2). P5 baut keinen eigenen Helfer.
- **Abschnittsabstände:** Zwischen aufeinanderfolgenden Abschnitten (`Card`) sorgt `.page section + section` (P1). P5 hat dafür keine eigene Regel.
- **Keine `style={{…}}`:** P5 hat keine Laufzeitwerte. Kein Farbliteral, kein `btn-sm`, keine Icon-Importe in den Dateien dieses Pakets.
- **`window.confirm` und `window.alert`** bleiben an ihren Stellen.
- **Ticketlinks** über `ticketPath` aus `src/lib/ticketRoutes.js` (P2). Die drei Stellen in `AssignProjectPage` (2×) und `ServicesPage` sind seit P2 umgestellt und bleiben es.
- **Testzahl:** Stand `design-p4` plus 13 neue Tests (7 in `projectRow`, 2 in `projectList`, 4 in `projectTones`).
## Review Focus
Aus der Übersicht gehören vier Punkte zu P5. Die Prüfungen laufen als Ansichten im Gate `p5` (Task 4).
- **Punkt 2, lange Inhalte am Handy:** Ansicht `projekte-lang` (Projektname 120 Zeichen, Repo-Pfad 90 Zeichen, Kundenname 60 Zeichen). Dazu `projekte-861` und `projekte-1101` (Menü fest, aber wenig Platz). Erwartet: kein `overflow-x`, die erste Spalte bleibt (gerundet) mindestens 140 px breit.
- **Punkt 3, Teilausfall:** Ansicht `fehler-projekte` (Mitarbeiterliste antwortet mit 500). Erwartet: Tabelle und Reiter bedienbar, Mitarbeiter-Auswahl zeigt nur „Alle Mitarbeiter“.
- **Punkt 4, nur Tastatur:**
- Das `<button class="link">` der ersten Zelle ist per Tab erreichbar, Enter öffnet „Projekt bearbeiten“, Esc schließt.
- Während Speichern ist „ד gesperrt und Esc wirkungslos.
- Enter im Namensfeld speichert, Enter in den Feldern des `PreviewBlock` speichert nie.
- Klicks auf Link, Auswahl und Knopf in einer Zeile öffnen den Dialog nicht.
- **Punkt 5, viele Einträge:** Ansicht `projekte-viel` (300 Projekte, 40 Kunden) rendert ohne Verstoß, der Bearbeiten-Dialog öffnet.
- **Altdaten:** Projekt ohne Namen („(ohne Name)“), Ticket-`$id` ohne Treffer in der Ticketliste (Rohwert als Link), `$updatedAt` unlesbar oder leer („-“), Status leer („-“). Sie stehen in `projekte-lang`.
- **Kein Flackern:** Nach Hosten, Aktualisieren, Kundenwechsel und Speichern bleibt die Tabelle stehen. Der Text „Wird geladen …“ erscheint nur beim ersten Laden.
- **Jede heutige Steuerung ist erreichbar:** Die Zuordnungen stehen in den Tabellen der Tasks 7 bis 13. Task 14 prüft sie anhand der Ansichten.
---
### Task 0: Paketstart
- [ ] **Step 1: Stand und Rebase**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git merge-base --is-ancestor design-p4 HEAD && git rebase origin/test && "$W/check.sh"`
Expected: Exit 0 und `# fail 0`. Die Zahl `# pass` wird notiert (Basis für Task 14). Ist `design-p4` kein Vorfahre von `HEAD`, stoppt der Agent und meldet es dem Eigentümer.
- [ ] **Step 2: Parallele Arbeit an TP2**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git log --oneline origin/test..origin/feature/terminplaner-tp2 -- src/pages/ProjectsPage.jsx src/pages/AssignProjectPage.jsx src/pages/ServicesPage.jsx src/components/TicketProjects.jsx src/components/AssignedProjectCard.jsx src/components/AnalyseRow.jsx src/components/PreviewBlock.jsx src/components/PreviewLinkButton.jsx src/components/GiteaLinkButton.jsx src/components/CopyableCredential.jsx src/pages/AdminPage.css src/styles/global.css src/lib/projectList.js`
Expected: keine Ausgabe. Gibt es Treffer, stoppt der Agent und fragt den Eigentümer. Er empfiehlt dabei, TP2 zuerst nach `test` zu bringen.
- [ ] **Step 3: Bausteine aus P2**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && ls src/lib/rowClick.js src/lib/ticketRoutes.js && rg -n "isRowActivation" src/lib/rowClick.js`
Expected: Beide Dateien sind da, `isRowActivation` steht in `rowClick.js` (P2, Task 2). Fehlt etwas, stoppt der Agent.
### Task 1: Projekt-Helfer `projectRow.js`
**Files:**
- Create: `src/lib/projectRow.js`
- Test: `tests/unit/projectRow.test.mjs` (neu)
**Interfaces:**
- Consumes: `fmtDateTime(value)` aus `src/lib/format.js` (liefert `05.10.26, 09:00`), `formatDistance` aus `date-fns`, `de` aus `date-fns/locale`.
- Produces (1:1 aus `ProjectsPage.jsx` L35-85 herausgezogen, Töne statt `badge-*`-Klassen):
- `PROJECT_TYPES`: `{ preview, website, template, project }`, je `{ label, short, tone, hint, adminOnly? }`. Die Reihenfolge der Schlüssel bestimmt die Optionen im Dialog.
- `HOSTED_TYPES`: `Set(['preview','website','template'])`.
- `typeOf(project) → 'template' | 'website' | 'preview' | 'project'`.
- `isProjectReady(project) → boolean`.
- `projectActions(project) → { type, hosted, ready, host, preview, refresh, gitea, hostedBadge }`.
- `projectTicketRef(project, ticketMap) → string`.
- `updatedLabel(iso, now = new Date()) → { text, title }`.
- Den Zeilenklick-Helfer `isRowActivation(event)` liefert `src/lib/rowClick.js` (P2, Task 2). `projectRow.js` baut keinen eigenen.
- [ ] **Step 1: Failing Test schreiben**
```js
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
const { PROJECT_TYPES, typeOf, isProjectReady, projectActions, projectTicketRef, updatedLabel } = await loadModule('src/lib/projectRow.js')
test('typeOf: Vorlage vor Website vor Preview vor Projekt', () => {
assert.equal(typeOf({ isTemplate: true, isPublic: true, subdomain: 'x' }), 'template')
assert.equal(typeOf({ isPublic: true, subdomain: 'x' }), 'website')
assert.equal(typeOf({ subdomain: 'x' }), 'preview')
assert.equal(typeOf({}), 'project')
})
test('isProjectReady: nur ready/deployed, provisioningStatus hat Vorrang', () => {
assert.equal(isProjectReady({ status: 'ready' }), true)
assert.equal(isProjectReady({ status: 'DEPLOYED' }), true)
assert.equal(isProjectReady({ provisioningStatus: 'pending', status: 'ready' }), false)
assert.equal(isProjectReady({ status: 'building' }), false)
assert.equal(isProjectReady({}), false)
})
test('PROJECT_TYPES: Reihenfolge, Texte und Töne unverändert', () => {
assert.deepEqual(Object.keys(PROJECT_TYPES), ['preview', 'website', 'template', 'project'])
assert.deepEqual(Object.values(PROJECT_TYPES).map((t) => t.label), ['Preview (mit Login)', 'Oeffentliche Website', 'Vorlage', 'Internes Projekt (kein Hosting)'])
assert.deepEqual(Object.values(PROJECT_TYPES).map((t) => t.short), ['Preview', 'Website', 'Vorlage', 'Projekt'])
assert.deepEqual(Object.values(PROJECT_TYPES).map((t) => t.tone), ['info', 'ok', 'warn', 'muted'])
assert.equal(PROJECT_TYPES.template.adminOnly, true)
assert.equal(PROJECT_TYPES.project.hint, 'Nur Gitea-Repository, keine Website/Subdomain')
})
test('projectActions: gehostet und nicht deployt', () => {
const ready = projectActions({ subdomain: 'a', status: 'ready', previewUrl: 'https://a.project.webklar.com', giteaRepoUrl: 'https://git/x' })
assert.deepEqual(ready, { type: 'preview', hosted: true, ready: true, host: false, preview: true, refresh: true, gitea: true, hostedBadge: { text: 'gehostet', tone: 'ok' } })
const open = projectActions({ subdomain: 'a', status: 'provisioning', previewUrl: 'https://a.project.webklar.com' })
assert.deepEqual(open, { type: 'preview', hosted: true, ready: false, host: true, preview: false, refresh: false, gitea: false, hostedBadge: { text: 'nicht deployt', tone: 'warn' } })
})
test('projectActions: archiviert, ohne Website, Vorlage', () => {
const arch = projectActions({ subdomain: 'a', status: 'ready', archived: true, previewUrl: 'u' })
assert.deepEqual([arch.host, arch.preview, arch.refresh, arch.hostedBadge], [false, true, false, null])
const archOpen = projectActions({ subdomain: 'a', status: 'error', archived: true })
assert.deepEqual([archOpen.host, archOpen.refresh, archOpen.hostedBadge], [false, false, null])
const intern = projectActions({ giteaRepoUrl: 'https://git/x' })
assert.deepEqual([intern.type, intern.host, intern.preview, intern.refresh, intern.hostedBadge, intern.gitea], ['project', false, false, false, null, true])
const tpl = projectActions({ isTemplate: true, subdomain: 't', status: 'deployed' })
assert.deepEqual([tpl.type, tpl.refresh, tpl.host], ['template', true, false])
})
test('projectTicketRef: woid aus der Liste, sonst die $id, ohne Ticket leer', () => {
assert.equal(projectTicketRef({ ticketId: 'abc' }, { abc: '100131' }), '100131')
assert.equal(projectTicketRef({ ticketId: 'abc' }, { abc: 100131 }), '100131')
assert.equal(projectTicketRef({ ticketId: 'zzz' }, {}), 'zzz')
assert.equal(projectTicketRef({ ticketId: '' }, { abc: '1' }), '')
assert.equal(projectTicketRef({}, {}), '')
})
test('updatedLabel: deutsch relativ, Titel in Berliner Zeit, ungültig = "-"', () => {
const now = new Date('2026-10-05T10:00:00.000Z')
assert.deepEqual(updatedLabel('2026-10-05T07:00:00.000Z', now), { text: 'vor etwa 3 Stunden', title: '05.10.26, 09:00' })
assert.equal(updatedLabel('2026-10-04T10:00:00.000Z', now).text, 'vor 1 Tag')
for (const bad of ['', 'kaputt', undefined]) assert.deepEqual(updatedLabel(bad, now), { text: '-', title: '' })
})
```
- [ ] **Step 2: Test laufen lassen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/projectRow.test.mjs`
Expected: FAIL, Modul `src/lib/projectRow.js` fehlt.
- [ ] **Step 3: `projectRow.js` implementieren**
- `PROJECT_TYPES`, `typeOf`, `isProjectReady` stehen wörtlich in `ProjectsPage.jsx` L38-85. Übernommen werden `label`, `short`, `hint`, `adminOnly`. `icon` entfällt. `badge: 'badge-info'` wird `tone: 'info'` (`badge-ok` → `ok`, `badge-warn` → `warn`, `badge-muted` → `muted`).
- `projectActions`:
- `host = hosted && !ready && !archived`
- `preview = !host && Boolean(project.previewUrl)`
- `refresh = hosted && ready && !archived`
- `gitea = Boolean(project.giteaRepoUrl)`
- `hostedBadge = null`, wenn `!hosted || archived`, sonst `{ text: 'gehostet', tone: 'ok' }` bzw. `{ text: 'nicht deployt', tone: 'warn' }`
- `projectTicketRef`: `String(ticketMap?.[project.ticketId] ?? project.ticketId)`, bei leerer `ticketId` `''`.
- `updatedLabel`: Text mit `formatDistance(new Date(iso), now, { addSuffix: true, locale: de })`, Titel mit `fmtDateTime(iso)`. Bei fehlendem oder unlesbarem Wert `{ text: '-', title: '' }`.
- [ ] **Step 4: Test grün**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/projectRow.test.mjs`
Expected: PASS, 7 Tests.
- [ ] **Step 5: Commit**
```bash
git add src/lib/projectRow.js tests/unit/projectRow.test.mjs
git commit -m "Projekte: Typ-, Hosting- und Aktionslogik als getestete Lib-Funktionen" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 2: `projectTabs`
**Files:**
- Modify: `src/lib/projectList.js` (neuer Export nach `VIEW_LABELS`)
- Test: `tests/unit/projectList.test.mjs` (Importzeile um `projectTabs` ergänzen, zwei Tests anhängen)
**Interfaces:**
- Consumes: `projectCounts(projects, { isAdmin })` und `VIEW_LABELS` aus derselben Datei.
- Produces: `projectTabs(counts, { isAdmin = false }) → [{ id, label, count }]` für `Tabs` aus P1. Die IDs sind die Filterwerte der Seite (`all`, `unassigned`, `assigned`, `templates`, `archived`, `everything`).
- [ ] **Step 1: Failing Tests schreiben**
```js
const counts = { all: 5, unassigned: 2, assigned: 3, archived: 4, templates: 1, everything: 10 }
test('projectTabs: sechs Reiter für Admins, Wortlaut und Zähler', () => {
assert.deepEqual(projectTabs(counts, { isAdmin: true }), [
{ id: 'all', label: 'Aktive Projekte', count: 5 },
{ id: 'unassigned', label: 'Unzugeordnet', count: 2 },
{ id: 'assigned', label: 'Zugeordnet', count: 3 },
{ id: 'templates', label: 'Vorlagen', count: 1 },
{ id: 'archived', label: 'Archiviert', count: 4 },
{ id: 'everything', label: 'Alle inkl. Archiv', count: 10 },
])
})
test('projectTabs: ohne Admin keine Vorlagen, Zähler passen zur Ansicht', () => {
assert.deepEqual(projectTabs(counts).map((t) => t.id), ['all', 'unassigned', 'assigned', 'archived', 'everything'])
for (const t of projectTabs(projectCounts(P, { isAdmin: true }), { isAdmin: true })) {
assert.equal(t.count, projectsInView(P, t.id, { isAdmin: true }).length)
}
})
```
(`P` ist die Testliste am Dateianfang.)
- [ ] **Step 2: Test laufen lassen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/projectList.test.mjs`
Expected: FAIL, `projectTabs is not a function`.
- [ ] **Step 3: Implementieren**
Reihenfolge wie `FILTER_OPTIONS` in `ProjectsPage.jsx` L358-366. Labels aus `VIEW_LABELS` bzw. fest „Unzugeordnet“ und „Zugeordnet“. `templates` nur bei `isAdmin`. `VIEW_LABELS` bleibt unverändert.
- [ ] **Step 4: Tests grün**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/projectList.test.mjs`
Expected: PASS, alle bisherigen plus 2 neue.
- [ ] **Step 5: Commit**
```bash
git add src/lib/projectList.js tests/unit/projectList.test.mjs
git commit -m "Projekte: Reiter-Liste der sechs Ansichten als getestete Funktion" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 3: Tonabbildung `projectTones.js`
**Files:**
- Create: `src/components/projectTones.js`
- Test: `tests/unit/projectTones.test.mjs` (neu)
**Interfaces:**
- Consumes: nichts. Die Töne sind die Namen der Logik aus `Badge.jsx` (P1): `ok`, `warn`, `info`, `danger`, `muted`, `plain`.
- Produces:
- `previewColorTone(color)` für `previewStatus().color`.
- `previewStateTone(state)` für `previewState` der Services-Antwort.
- `analyseColorTone(color)` für `analyseStatus().color`.
- `statusValueTone(value)` für `status` und `provisioningStatus` eines Projekts.
- [ ] **Step 1: Failing Test schreiben**
```js
const { previewColorTone, previewStateTone, analyseColorTone, statusValueTone } = await loadModule('src/components/projectTones.js')
test('previewColorTone', () => {
assert.deepEqual(['green', 'yellow', 'red', 'grey', undefined].map(previewColorTone), ['ok', 'warn', 'danger', 'plain', 'plain'])
})
test('previewStateTone', () => {
assert.deepEqual(['online', 'building', 'starting', 'sleeping', 'failed', 'off', undefined, 'constructor'].map(previewStateTone), ['ok', 'warn', 'warn', 'warn', 'danger', 'plain', 'plain', 'plain'])
})
test('analyseColorTone', () => {
assert.deepEqual(['green', 'red', 'grey', undefined].map(analyseColorTone), ['ok', 'danger', 'muted', 'muted'])
})
test('statusValueTone: wie die Farben in AssignedProjectCard heute', () => {
assert.deepEqual(['ready', 'LIVE', 'online', 'provisioning', 'pending', 'building', 'error', 'failed'].map(statusValueTone), ['ok', 'ok', 'ok', 'warn', 'warn', 'info', 'danger', 'danger'])
assert.deepEqual(['deployed', '', undefined, 'irgendwas'].map(statusValueTone), ['muted', 'muted', 'muted', 'muted'])
})
```
- [ ] **Step 2: Test laufen lassen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/projectTones.test.mjs`
Expected: FAIL, Modul fehlt.
- [ ] **Step 3: Implementieren**
Je Funktion eine `Map` (kein Nachschlagen im Objekt-Prototyp, sonst liefert `'constructor'` eine Funktion). Die Zuordnung steht im Test. `deployed` bleibt grau, wie die Farbtabelle `STATUS_COLORS` in `AssignedProjectCard` heute (Standardfarbe `#a0aec0`).
- [ ] **Step 4: Test grün**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/projectTones.test.mjs && "$W/check.sh" --nur-tests`
Expected: PASS und `# fail 0` in der Ausgabe von `check.sh`.
- [ ] **Step 5: Commit**
```bash
git add src/components/projectTones.js tests/unit/projectTones.test.mjs
git commit -m "Projekte: Tonabbildung fuer Preview-, Analyse- und Projektstatus" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 4: Werkzeug – Ansichten, Testdaten, Gate `p5`
**Files** (alle unter `$W/`, kein Commit, das Werkzeug liegt außerhalb des Repos):
- Create: `fixtures/projekte-lang.mjs`, `fixtures/projekte-viel.mjs`
- Modify: `views.mjs`, `gates.mjs`, bei Bedarf `fixtures/standard.mjs`
**Interfaces:**
- Consumes: Aufbau von `fixtures/altdaten.mjs` (Standarddaten plus überschriebene Listen), `VIEWS` und `GATES` aus P0, Schritt-Funktionen `steps: async (page, ctx) => void` mit `node:assert/strict`. `ctx.backend.calls` listet alle beantworteten Requests.
- Produces: Ansichten und Gate `p5` für die Tasks 6 bis 14.
- [ ] **Step 1: Testdaten anlegen**
- `projekte-lang`: Standarddaten plus
- Projekt A: `projectName` mit 120 Zeichen, `repoFullName` `WEBklar/` plus 82 Zeichen, Kunde mit 60 Zeichen im Namen, `ticketId` eines Tickets der Liste
- Projekt B: ohne `projectName` und `giteaRepoName`, `ticketId` ohne Treffer in der Ticketliste, `$updatedAt: 'kaputt'`, `status: ''`, `subdomain` und `previewUrl` gesetzt, nicht deployt
- Projekt C: archiviert, `subdomain`, `status: 'ready'`
- `projekte-viel`: Standarddaten plus 300 Projekte und 40 Kunden.
- `fixtures/standard.mjs` braucht ein Projekt an Ticket 100131 mit `subdomain`, `status: 'ready'`, `previewUrl` und `giteaRepoUrl`, mindestens ein Projekt je Typ (Preview, Website, Vorlage, intern) und eines ohne `kind`. Der Kunde, den `kunde-projects` öffnet, hat dieses Projekt zugeordnet. Fehlt etwas, wird es ergänzt.
- [ ] **Step 2: Ansichten in `views.mjs` anlegen oder anpassen**
`projekte`, `dlg-projekt-neu`, `dlg-projekt-bearbeiten`, `dlg-projekt-art`, `zuordnen`, `services` und `analyse-zustimmung` gibt es seit P0, `kunde-projects` und `kunde-overview` aus den Reiter-IDs der Übersicht seit P4. Dort werden nur Schritte angepasst. Alle `dlg-*` behalten `viewports` und `submitReachable` aus P0.
| ID | Pfad / Schritt | Testdaten |
|---|---|---|
| `projekte` | `/projects` | `standard` |
| `projekte-archiv` | `/projects`, Klick auf Reiter „Archiviert“ | `standard` |
| `projekte-treffer` | `/projects`, Suche „vorlage“ (erzeugt die Zeile „Weitere Treffer“) | `standard` |
| `projekte-lang` | `/projects` | `projekte-lang` |
| `projekte-viel` | `/projects` | `projekte-viel` |
| `projekte-861`, `projekte-1101` | `/projects`, Schritt `page.setViewportSize({ width: 861 \| 1101, height: 900 })`, dann Prüfung, dass die erste Zelle der ersten Zeile (gerundet) mindestens 140 px breit ist | `projekte-lang` |
| `projekte-aktion` | `/projects`. Ein `MutationObserver` merkt sich, ob je `.loading` im DOM erscheint. Klick auf „Aktualisieren“, 500 ms warten. Erwartung: `window.__sawLoading === false`, Tabelle sichtbar. | `standard` |
| `projekte-tastatur` | Fokus auf das `button.link` der ersten Zeile, Enter, Dialog sichtbar, Esc, Dialog weg. Erneut öffnen, `page.route` verzögert die Speicher-Anfrage um 1200 ms (`route.fallback()`), Klick auf „Speichern & synchronisieren“. Erwartung: „ד ist `disabled`, Esc lässt den Dialog offen. Danach schließt er. | `standard` |
| `projekte-zeilenklick` | Klick auf die Kunde-Auswahl, auf „Vorschau öffnen“ und auf ein Badge. Erwartung: nur das Badge (Zellfläche) öffnet den Dialog. | `standard` |
| `fehler-projekte` | `/projects`, Szenario `fehler` | `standard` |
| `dlg-projekt-neu` | Klick auf „Neues Projekt“ | `standard` |
| `dlg-projekt-bearbeiten` | Klick auf das `button.link` der ersten Zeile | `standard` |
| `dlg-projekt-archiv` | Reiter „Archiviert“, Klick auf das erste `button.link` | `standard` |
| `dlg-projekt-art` | wie in P0 (Art-Frage in `TicketProjects`) | `standard` |
| `zuordnen`, `zuordnen-kunde`, `zuordnen-ticket`, `zuordnen-fertig` | `/projects/zuordnen/<id>` eines Projekts ohne `kind`: Schritt 1 (Art), nach Art-Wahl und „Weiter“ Kunde ( „Ja, für einen Kunden“ ), nach Kundenwahl Ticket, am Ende Zusammenfassung | `standard` |
| `services` | `/services` | `standard` |
| `analyse-zustimmung` | wie in P0 (Ticket 100131, „Einschalten“ in der Analyse-Zeile) | `standard` |
| `ticket-projekte` | `/tickets/100131`, Klick auf „Preview ▾“ | `standard` |
| `kunde-projects` | `/customers/<id>?tab=projects` (Ansicht aus P4, unverändert, kommt nur ins Gate) | `standard` |
Der Zugang (`CopyableCredential`, Abschnitt „Portal-Zugang“) steht auf der Übersicht. Dafür entsteht keine zweite Ansicht für dieselbe URL. Die `steps` von `kunde-overview` (P4) werden am Ende ergänzt: Klick auf „Anzeigen“ beim Passwort in `.credential`. Erwartung: „Verbergen“ und „Kopieren“ sind sichtbar, kein `svg` in `.credential`. Danach Klick auf „Verbergen“, damit das Bild unverändert bleibt.
- [ ] **Step 3: Gate eintragen**
In `gates.mjs`, nach dem Schema der Übersicht:
```js
GATES.p5 = {
...GATES.p4,
scoped: {
...GATES.p4.scoped,
'icon-svg': {
views: [...GATES.p4.scoped['icon-svg'].views, 'projekte', 'projekte-*', 'fehler-projekte', 'dlg-projekt-*', 'zuordnen', 'zuordnen-*', 'services', 'analyse-zustimmung', 'ticket-projekte', 'kunde-projects'],
},
},
}
```
`checks` und `views: '*'` kommen von `p4`. `step-failed`, `external-request` und `console-error` sind in jedem Gate aktiv. Der neue `icon-svg`-Eintrag hat kein `ignore`: Die Ausnahmen `.ticket-projects` (P2) und `.credential` (P4) entfallen, weil `TicketProjects` und `CopyableCredential` in diesem Paket umgebaut werden.
- [ ] **Step 4: Ausgangslauf**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p5 && "$W/shots.sh" p5 "$W/shots/p5-t4" --gate p5; echo "exit $?"`
Expected: `exit 1`, `icon-svg` auf allen Ansichten mit Projektbausteinen. Einige Schritte finden ihre Knöpfe („Neues Projekt“, „Aktualisieren“, „Anzeigen“) noch nicht. Das ist der Ausgangszustand.
### Task 5: CSS-Block „Projekte (P5)“
**Files:**
- Modify: `src/styles/global.css` (neuer Block „Projekte (P5)“ am Ende, vor dem Bereichs-CSS der Finanzen, falls dieses dort steht)
- Modify: `src/styles/global.css` (alte Regeln `.ticket-projects`, `.ticket-projects-assign`, `.ticket-projects-assign .form-control` löschen, L596-599)
**Interfaces:**
- Consumes: Tokens und Klassen aus P1 (`.table`, `.link`, `.badge`, `.list-row`, `.seg`, `.check`, `.cell-title`, `.cell-sub`, `.hide-md`, `.hide-sm`).
- Produces: die Klassen der Tabellen unten. Die Komponenten-Tasks benutzen sie wörtlich.
- [ ] **Step 1: Vorhandenes prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "button\.link|^\.link" src/styles/global.css`
Expected: Das Zurücksetzen für `button.link` fehlt oder steht schon. Es wird nur ergänzt, was fehlt.
- [ ] **Step 2: Block anlegen**
Die Werte der Projekttabelle sind in einer statischen Probe bei 390, 861, 900, 1101, 1180, 1280 und 1440 px gemessen. Es gibt dabei keinen waagerechten Überlauf. Der Block steht am Ende von `global.css`. Farben kommen nur über Tokens, Abstände zwischen Abschnitten regelt `.page section + section` (P1).
Gemessene und von Ansichten geprüfte Regeln:
| Selektor | Regel |
|---|---|
| `button.link` (falls fehlend) | `background: none; border: 0; padding: 0; font: inherit; text-align: left; cursor: pointer` |
| `.projects-table td:first-child` | `min-width: 140px` (geprüft von `projekte-861` und `projekte-1101`) |
| `.projects-table th, .projects-table td` | `padding-right: 8px`, in der letzten Zelle `0` |
| `.projects-table .cell-title .link, .projects-table .cell-sub` | `overflow-wrap: anywhere` |
| `.projects-table .select` | `width: clamp(104px, 10vw, 180px); max-width: 100%` |
| `.projects-table td.cell-address code` | `display: block; max-width: 84px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px` |
| `.projects-table td.cell-updated` | `min-width: 70px` |
| `@media (max-width: 860px)` für `.projects-table` | Die Zeile wird ein Grid mit zwei Spalten (`1fr 1fr`, Abstand `8px 12px`, `padding: 14px 0`, Trennlinie `1px solid var(--line)`). `thead` ist nur für Screenreader sichtbar. `table`, `tbody` und `td` sind `block`, `td` ohne Innenabstand und Rahmen. Erste Zelle und `td.cell-actions` gehen über beide Spalten. `td[data-label]::before` zeigt `attr(data-label)` (12px, `--text-2`). `.select` ist 100 % breit, `.row-actions` linksbündig, kein Zeilen-Hover. `.projects-table td.hide-sm, .projects-table td.hide-md { display: none }` (0,2,1) schlägt `.hide-sm` (0,1,0) und `.projects-table td { display: block }` (0,1,1) über die Spezifität, ohne `!important`. |
| `.badge.wrap` | `white-space: normal; height: auto; min-height: 20px; padding-top: 2px; padding-bottom: 2px; overflow-wrap: anywhere` |
| `.pv-log` | helle Fläche (`background: var(--bg-subtle)`), `max-height: 260px; overflow: auto; white-space: pre-wrap; word-break: break-all`, Monospace 11px/16px |
| `.pv-var code`, `.pv-var .input`, `.pv-add .input` | `flex: 1 1 140px; min-width: 0`, bei `code` zusätzlich `overflow-wrap: anywhere` |
Übrige Klassen (nur Aufbau, kein Messwert):
| Klassen | Aufbau |
|---|---|
| `.projects-table tr.is-archived td` | `opacity: .72` (wie die Karten heute) |
| `.row-actions` | Flex-Reihe mit Umbruch (Abstand `4px 14px`, 13px), in der Tabelle rechtsbündig, `.link` ohne Zeilenumbruch |
| `.dialog-form` | Flex-Spalte, Abstand 16px |
| `.pv-block` | 13px, Flex-Spalte, Abstand 10px |
| `.pv-row`, `.pv-add`, `.pv-var` | Flex-Reihe mit Umbruch, Abstand 8px, mittig ausgerichtet. `.pv-var` hat unten `1px solid var(--line)`. |
| `.pv-mode`, `.pv-title` | `width: 200px; max-width: 100%` bzw. 13px, `font-weight: 600` |
| `.proj-item`, `.proj-head`, `.proj-main`, `.proj-meta` | Zeile mit `padding: 16px 0` und Trennlinie. Kopf als Flex mit Umbruch und `justify-content: space-between`, `.proj-main { min-width: 0; overflow-wrap: anywhere }`. Meta-Zeile 12px, `--text-2`, mit Umbruch. |
| `.ticket-projects-assign` | Flex-Reihe mit Umbruch, Abstand 8px, `margin-top: 12px`. `.select` mit `flex: 1 1 220px`, `.input` mit `flex: 1 1 160px`, beide `min-width: 0`. |
| `button.list-row` | Knopf-Reset (`width: 100%; text-align: left; background: none; border: 0; border-bottom: 1px solid var(--line); font: inherit; color: inherit; cursor: pointer`). `:hover` und `[aria-pressed="true"]` mit `background: var(--bg-hover)`, `.cell-sub` als `block`. |
| `.pick-list`, `.pick-empty` | `max-height: 360px; overflow-y: auto` bzw. `padding: 12px 0` |
| `.seg.wrap`, `.row-buttons`, `.check.top` | `flex-wrap: wrap`, Flex mit Umbruch und Abstand 8px, `align-items: flex-start` |
| `.summary` | `list-style: none`, Flex-Spalte, Abstand 6px |
- [ ] **Step 3: Bauen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" --nur-build`
Expected: Exit 0.
- [ ] **Step 4: Commit**
```bash
git add src/styles/global.css
git commit -m "Projekte: CSS-Block fuer Tabelle, Dialog-Inhalte, Preview-Block und Zuordnen" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 6: Link-Bausteine (`PreviewLinkButton`, `GiteaLinkButton`, `CopyableCredential`)
**Files:**
- Modify: `src/components/PreviewLinkButton.jsx` (alles ab L7), `src/components/GiteaLinkButton.jsx`, `src/components/CopyableCredential.jsx` (L93-152)
- Modify: `src/pages/AdminPage.css` (L330-345: `.credential-action`, `.credential-action:hover`, `.credential-check` löschen, `.credential` und `.credential .link` anpassen)
**Interfaces:**
- Consumes: `.link` aus P1, `createPreviewLink` aus `lib/teamApi`, `openInNewTab` aus `lib/openInNewTab`.
- Produces:
- `PreviewLinkButton({ href, subdomain, label = 'Vorschau öffnen' })` rendert `<a className="link" href target="_blank" rel="noopener noreferrer" onClick>{label}</a>`. Ohne `href` kommt `null`. Die Logik (Einmal-Link, `window.alert` bei Fehler) bleibt.
- `GiteaLinkButton({ href, label = 'In Gitea öffnen' })` rendert `<a className="link" href target rel>{label}</a>`.
- `CopyableCredential({ value, secret = false, label = 'Wert' })` (Signatur unverändert) mit Textknöpfen.
- Für P8: Von `.credential*` bleiben `.credential`, `.credential-value`, `.credential-value.is-masked`, `.credential-empty` und `.credential .link`. P8 zieht sie nach `global.css`.
- [ ] **Step 1: Prüfen, dass `title` und `size` nirgends übergeben werden**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "<(PreviewLinkButton|GiteaLinkButton)[^>]*(title|size)=" src`
Expected: keine Treffer. Dann entfallen die Props `title` und `size`.
- [ ] **Step 2: Umbau**
- Die Link-Bausteine verlieren Inline-Stil und Icon. Der Text ist `label`.
- `CopyableCredential`:
- Der Umschalter ist `<button type="button" className="link">` mit „Anzeigen“ bzw. „Verbergen“.
- Der Kopierknopf ist `<button type="button" className="link">` mit „Kopieren“ bzw. nach dem Klick „Kopiert“.
- `aria-label` und `title` führen die sichtbaren Wörter mit: `${label} anzeigen`, `${label} verbergen`, `${label} kopieren`, `${label} kopiert`.
- Maskierung, Timer, `stopPropagation` und `copyText` bleiben.
- Entscheidung: `aria-label` und `title` sagen „verbergen“ statt „ausblenden“, damit der zugängliche Name das sichtbare Wort enthält.
- `.credential`: `gap: 8px`, `.credential .link { flex: 0 0 auto; font-size: 12px; white-space: nowrap }`. `AdminPage.css` lädt vor `global.css`. `.credential .link` (0,2,0) schlägt `.link` und `button.link` (0,1,1) über die Spezifität und braucht kein `!important`.
- [ ] **Step 3: Prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p5 && "$W/shots.sh" p5 "$W/shots/p5-t6" --only kunde-overview,ticket-projekte --gate p5; echo "exit $?"`
Expected: `kunde-overview` besteht (Login und Passwort mit Textknöpfen, kein `svg` in `.credential`, auch nach „Anzeigen“). `ticket-projekte` darf noch Verstöße der späteren Tasks zeigen, aber „Vorschau öffnen“ und „In Gitea öffnen“ stehen als Text da.
- [ ] **Step 4: Commit**
```bash
git add src/components/PreviewLinkButton.jsx src/components/GiteaLinkButton.jsx src/components/CopyableCredential.jsx src/pages/AdminPage.css
git commit -m "Projekte: Vorschau-, Gitea- und Zugangsknoepfe als Textlinks" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 7: `PreviewBlock`
**Files:**
- Modify: `src/components/PreviewBlock.jsx` (Import L1-12 behalten, `dotColor`-Import und `STATE_OF_COLOR` L14 löschen, Markup L174-297 neu)
**Interfaces:**
- Consumes: `Button`, `Badge` aus `components/ui`, `previewColorTone` aus `./projectTones`, `previewStatus`, `modeOptionsFor`, `modeHint`, `isDevUnavailableError`, `NO_DEV_HINT` aus `lib/previewView`, Klassen `.pv-*`, `.input`, `.select`, `.msg`, `.link`, `.badge.wrap`.
- Produces: `PreviewBlock({ projectId })` (Signatur unverändert).
- [ ] **Step 1: Umbau**
Alle Zustände, Handler, Intervalle und Texte bleiben. Nur das Markup ändert sich.
| Heute | Danach |
|---|---|
| „Lädt …“ | `<p className="muted xs">Lädt …</p>` |
| Label „Modus“ und Auswahl | `.pv-row`: `<label className="muted xs" htmlFor>`, `<select className="select pv-mode">` |
| `NO_DEV_HINT`, `modeHint` | `<p className="muted xs">` |
| Punkt und Statustext | `<Badge tone={previewColorTone(status.color)} className="wrap">{status.text}</Badge>` |
| Fehlertext des Builds, `error`, `envError` | `<p className="msg err">`. `error` zusätzlich mit `role="alert"`. |
| „Preview-Runner ist nicht aktiv.“ | `<p className="muted xs">` |
| Neu bauen, Protokoll ansehen, Jetzt starten, Stoppen | `Button` (Standard), `type="button"`, in `.pv-row`, `disabled`-Bedingungen unverändert |
| Lauf-Auswahl und `<pre>` | `<select className="select">` (`aria-label="Lauf wählen"`) und `<pre className="pv-log">` |
| `<strong>Variablen</strong>` | `<h4 className="pv-title">Variablen</h4>` |
| Variablenzeile | `.pv-var`: `<code>`, „•••“, bei Bearbeitung `input.input` und `<button type="button" className="link">Übernehmen</button>`, sonst `<button type="button" className="link">Ändern</button>`, dazu `<button type="button" className="link">Entfernen</button>` |
| Neue Variable | `.pv-add`: zwei `input.input` (Enter-Handler bleiben) und `Button type="button"` „Hinzufügen“ |
| „Variablen speichern“ | `Button` (Standard), `type="button"` |
| Wurzel | `<div className="pv-block">` |
- [ ] **Step 2: Prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p5 && "$W/shots.sh" p5 "$W/shots/p5-t7" --only ticket-projekte --gate p5; echo "exit $?"`
Expected: Im Bild steht der geöffnete Preview-Block ohne Knopfrahmen-Reihe, ohne dunklen Log-Kasten. `icon-svg` an der Projektzeile und am Abschnitt (Tasks 9 und 10) darf noch melden.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "btn-sm|#[0-9a-f]{3,6}|style=\{" src/components/PreviewBlock.jsx`
Expected: keine Treffer.
- [ ] **Step 3: Commit**
```bash
git add src/components/PreviewBlock.jsx
git commit -m "Projekte: Preview-Block ohne Kleinknoepfe, Statuspunkt als Badge, Log auf hellem Grund" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 8: `AnalyseRow` (Zustimmung als Dialog)
**Files:**
- Modify: `src/components/AnalyseRow.jsx` (`COLORS` L5 löschen, Markup L58-132 neu)
**Interfaces:**
- Consumes: `Dialog`, `Button`, `Badge`, `Field` aus `components/ui` (P1), `analyseColorTone` aus `./projectTones`, alle Funktionen aus `lib/analyseView` unverändert (`analyseStatus`, `textformLabel`, `canSubmitTextform`, `channelLabel`, `legalLinks`, `chatTemplate`, `privacyHint`).
- Produces: `AnalyseRow({ projectId })` (Signatur unverändert).
- [ ] **Step 1: Umbau**
Zustände, `apply`, `cancel`, `copyTemplate`, `canSubmit` und alle Wortlaute bleiben. Neu ist das Markup:
| Heute | Danach |
|---|---|
| Zeile „Website-Analyse:“ | `<div className="pv-row">`: `<span className="muted">Website-Analyse:</span>`, dann Status |
| Status | `data`: `<Badge tone={analyseColorTone(status.color)} className="wrap">{status.text}</Badge>`; `loadError`: `<span className="msg err" role="alert">`; sonst `<span className="muted">Lädt …</span>`; `status.hint` als `<span className="muted">({status.hint})</span>` |
| „Einschalten“ (oder `status.enableLabel`) und „Ausschalten“ | `<button type="button" className="link">` mit dem Text von heute |
| Zustimmungsablauf (`mode === 'on'`) | `<Dialog title="Website-Analyse" onClose={cancel} busy={busy} footer={…}>` |
| Kästchen mit `textformLabel` | `<label className="check top">`. Die beiden Links sind `<a className="link" target="_blank" rel="noopener noreferrer">`, Wortlaut aus `label.before/avv/between/lb/after`. |
| „Datum der Zustimmung:“ | `<Field label="Datum der Zustimmung:" htmlFor>` mit `input.input` (`type="date"`, `required`, `min`, `max` wie heute) |
| „Weg:“ und die zwei Radios | `<div className="field" role="radiogroup" aria-label="Weg der Zustimmung">`, `<span className="strong">Weg:</span>`, Radios in `<label className="check">` mit `channelLabel(c)` |
| `privacyHint` | `<p className="msg err">` |
| `<details>` „Textvorlage für den Chat“ | bleibt `<details>`. Die Vorlage steht in `<p className="prose">`, „Kopieren“/„Kopiert“ ist `Button` (Standard) mit `type="button"`. |
| „Einschalten“ und „Abbrechen“ | Fuß: `<span className="grow" />`, `Button type="button"` „Abbrechen“ (`disabled={busy}`), `Button type="button" variant="primary"` „Einschalten“ (`disabled={!canSubmit \|\| busy}`) |
| Ausschalten-Rückfrage (`mode === 'off'`) | bleibt eine Zeile ohne Dialog: Text „Website-Analyse wirklich ausschalten?“, `<button type="button" className="link-danger">Ja, ausschalten</button>`, `<button type="button" className="link">Abbrechen</button>` |
| `error` | im Dialog als `<p className="msg err" role="alert">` ganz oben im Dialog-Inhalt, sonst unter der Zeile |
- Entscheidung: Der Dialogtitel ist „Website-Analyse“ (das Wort der Zeile, ohne Doppelpunkt). Ein Titel muss sein, ein neuer Wortlaut ist nicht nötig.
- Entscheidung: Die Ausschalten-Rückfrage bleibt inline, nur die Zustimmung ist ein Dialog (wie in der Spec).
- Entscheidung: Die Reihenfolge im Fuß folgt dem Prototyp (Abbrechen links vom Hauptknopf).
- [ ] **Step 2: Prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p5 && "$W/shots.sh" p5 "$W/shots/p5-t8" --only analyse-zustimmung --gate p5; echo "exit $?"`
Expected: Der Dialog hat keinen Verstoß. `icon-svg` darf noch an `AssignedProjectCard` und `TicketProjects` melden (Tasks 9 und 10). Bei 390 px liegt der Dialog unten als Sheet, „Einschalten“ ist erreichbar. Esc schließt, bei `busy` nicht. Der Text im Kästchen ist unverändert (Stichprobe: Diff gegen `textformLabel`).
- [ ] **Step 3: Commit**
```bash
git add src/components/AnalyseRow.jsx
git commit -m "Projekte: Website-Analyse-Zeile ohne Farbliterale, Zustimmung als Dialog (Wortlaut unveraendert)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 9: `AssignedProjectCard`
**Files:**
- Modify: `src/components/AssignedProjectCard.jsx` (Icon-Imports und `STATUS_COLORS`/`StatusBadge` L2-35 ersetzen, Markup L81-176 neu)
**Interfaces:**
- Consumes: `Button`, `Badge` aus `components/ui`, `statusValueTone` aus `./projectTones`, `PreviewLinkButton`, `GiteaLinkButton`, `PreviewBlock`, `AnalyseRow`, Klassen `.proj-*`, `.row-actions`, `.link`, `.msg`.
- Produces: `AssignedProjectCard({ project, onUnassign, showActions = false, customerName = '', onChanged = () => {} })` (Signatur unverändert). Aufrufer sind `TicketProjects` und der Projekte-Reiter der Kundenseite (P4).
- [ ] **Step 1: Umbau**
Handler (`openPreviewInput`, `handleEnablePreview`, `handleBackfill`) und Zustände bleiben.
| Heute | Danach |
|---|---|
| Karte mit blauem Rahmen | `<div className="proj-item">` |
| Name, `(subdomain)`, Repo mit `FaCodeBranch` | `.proj-head > .proj-main`: `<strong>`, `<span className="muted small">`, `<div className="cell-sub">{repoFullName}</div>` |
| Vorschau-, Gitea-Knopf, Lösen-Knopf | `.row-actions` mit `PreviewLinkButton`, `GiteaLinkButton` und `<Button variant="danger" type="button" title="Vom Ticket lösen" aria-label="Vom Ticket lösen">Lösen</Button>`. Er erscheint unter derselben Bedingung (`showActions && onUnassign`). |
| `StatusBadge` mit Punkt | `.proj-meta`: `<span>Status: <Badge tone={statusValueTone(value)}>{value}</Badge></span>`, ebenso „Provisioning:“. Ohne Wert nichts. |
| `AnalyseRow` | unverändert eingebunden (`project.subdomain`) |
| „Preview ▴/▾“ (`btn btn-sm`) | `<button type="button" className="link" aria-expanded>` mit demselben Text. Die Zeichen ▴ und ▾ bleiben, sie sind Text. Der `PreviewBlock` steht darunter. |
| „Preview einschalten“, „Einschalten“, „Pushes nachtragen“ | `.row-actions` (linksbündig): zwei `<button type="button" className="link">`, `input.input` mit `Button type="button"` „Einschalten“ (`disabled={!subdomain \|\| previewBusy}`), `<button type="button" className="link" disabled={pushBusy}>Pushes nachtragen</button>` |
| Meldungen | `<p className="msg muted">` je `previewMessage`, `pushMessage` |
- [ ] **Step 2: Prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p5 && "$W/shots.sh" p5 "$W/shots/p5-t9" --only ticket-projekte,kunde-projects --gate p5; echo "exit $?"`
Expected: `kunde-projects` besteht. `ticket-projekte` darf noch `icon-svg` am Abschnitt melden (Task 10). Die Projektzeile am Ticket zeigt Name, Repo, drei Textlinks, Status-Badges und die Analyse-Zeile ohne Kasten.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|btn-sm|#[0-9a-f]{3,6}|style=\{" src/components/AssignedProjectCard.jsx`
Expected: keine Treffer.
- [ ] **Step 3: Commit**
```bash
git add src/components/AssignedProjectCard.jsx
git commit -m "Projekte: Projektzeile am Ticket und beim Kunden ohne Kasten, Aktionen als Textlinks" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 10: `TicketProjects`
**Files:**
- Modify: `src/components/TicketProjects.jsx` (Import L2 löschen, Markup L158-228 neu)
**Interfaces:**
- Consumes: `Card`, `Button`, `Dialog` aus `components/ui`, `AssignedProjectCard`, Klassen `.ticket-projects-assign`, `.row-buttons`, `.msg`.
- Produces: `TicketProjects({ ticket })` (Signatur unverändert, P2 bindet sie auf der Ticketseite ein). `data-testid="project-kind"` bleibt.
- [ ] **Step 1: Umbau**
Alle Zustände und Handler (`run`, `assign`, `assignPlain`, `assignWithKind`, `unassign`, `create`) bleiben.
| Heute | Danach |
|---|---|
| `ui-card pad` mit `<h5>` | `<Card title={\`Projekte (${assigned.length})\`}>` |
| „Suche neue Repos …“ | `<p className="muted">` |
| `importHint`, `error` | `<p className="msg err" role="alert">` |
| „Noch kein Projekt an diesem Ticket.“ | `<p className="muted">` |
| Art-Zeile und Karte je Projekt | `<span className="muted xs" data-testid="project-kind">` und `AssignedProjectCard` (Props wie heute) |
| Auswahl „Projekt zuweisen“ | `.ticket-projects-assign`: `<select className="select" aria-label="Projekt zuweisen">`, Optionstexte wie heute |
| „Zuweisen“ (`FaLink`) | `Button type="button"` (Standard), `disabled={!selected \|\| busy}` |
| „Neues Projekt“ (`FaPlus`) | `Button variant="quiet" type="button"`, schaltet das Formular um |
| `assignResult` | `<p className="msg">` mit den Warnungen als `<span className="muted">` |
| Anlegen-Formular | bleibt inline: `<form className="ticket-projects-assign" onSubmit>` mit zwei `input.input` (`required`) und `Button type="submit" variant="primary"` („Anlegen & zuweisen“, beim Laden „Wird angelegt …“) |
| „Was ist das?“-Overlay | `<Dialog title="Was ist das?" onClose={() => setKindDialog(false)}>`. Darin „Bitte wähle die Art des Projekts, bevor es zugewiesen wird.“ als `<p className="muted">`, der Fehler „Arten konnten nicht geladen werden – bitte neu laden.“ als `msg err`, die Arten als `.row-buttons` mit `Button type="button"` je `k.label` (`onClick={() => assignWithKind(k.key)}`). |
Danach die toten Klassen prüfen:
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "ticket-projects[^-]|ui-btn|ui-card" src/components/TicketProjects.jsx`
Expected: keine Treffer.
- [ ] **Step 2: Prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p5 && "$W/shots.sh" p5 "$W/shots/p5-t10" --only ticket-projekte,dlg-projekt-art --gate p5`
Expected: PASS. Bei 390 px ist der Dialog ein Sheet. Esc schließt ihn. Die Auswahl „Projekt zuweisen“ läuft nicht über den Rand.
- [ ] **Step 3: Commit**
```bash
git add src/components/TicketProjects.jsx
git commit -m "Projekte: Projektabschnitt am Ticket als Abschnitt, Art-Frage als Dialog" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 11: `ProjectsPage`
**Files:**
- Modify: `src/pages/ProjectsPage.jsx`
- L1-85 (Importe, `TYPES`, `typeOf`, `HOSTED_TYPES`, `isProjectReady`) durch Importe aus `lib/projectRow` ersetzen
- L358-369 (`FILTER_OPTIONS`, `typeOptions`) ersetzen
- L371-782 (Markup) neu
- `Row` (L784-791) und die tote `customerMap` (L131-137) löschen
**Interfaces:**
- Consumes:
- aus Task 1 bis 3: `PROJECT_TYPES`, `projectActions`, `projectTicketRef`, `updatedLabel`, `typeOf`, `projectTabs`
- aus P1: `PageHeader`, `Tabs`, `Button`, `Badge`, `EmptyState`, `Field`, `Dialog`
- aus P2: `ticketPath` und `isRowActivation(event)` aus `src/lib/rowClick.js`
- Hooks unverändert (`useWebsiteProjects`, `useCustomers`, `useWorkorders`, `useEmployees`)
- Klassen `.projects-table`, `.row-actions`, `.dialog-form`, `.pv-title`
- Produces: Seite `/projects`. Die Route `/projects/zuordnen/:id` wird nach „Projekt anlegen“ wie heute aufgerufen.
- [ ] **Step 1: Zustand und Laden**
- Neu: `const [loadedOnce, setLoadedOnce] = useState(false)`. Im Effekt: `fetchAllProjects().then(() => { if (!cancelled) setLoadedOnce(true) })`. Der Effekt behält sonst Reihenfolge und Import-Aufruf.
- Neu: Der Text „Wird geladen …“ (`<div className="loading">`) erscheint nur, solange `!loadedOnce`. Danach bleibt die Tabelle bei jedem Nachladen stehen, auch wenn `useWebsiteProjects` `loading` setzt. Der Hook bleibt unverändert.
- Entscheidung: Die Wurzel ist `<div className="page">` (seit P1), der Titel „Website-Projekte“ ohne Untertitel (ein neuer Untertitel wäre neuer Text).
- [ ] **Step 2: Kopf, Reiter, Filterzeile, Meldungen**
| Heute | Danach |
|---|---|
| `<h2>Website-Projekte</h2>` | `PageHeader title="Website-Projekte"` |
| Knopf „+“ (Titel „Neues Projekt“) | `Button variant="primary" type="button"` „Neues Projekt“ in `actions`, öffnet den Dialog |
| Knopf Sync (Titel „Gitea-Repos synchronisieren“, Spinner) | `Button type="button"` „Gitea-Repos synchronisieren“ in `actions`, `disabled={syncLoading}`, beim Laden Text „Wird geladen …“ |
| Ansicht-Select mit sechs Optionen | `Tabs tabs={projectTabs(counts, { isAdmin })} active={filter} onChange={setFilter}`. „Vorlagen“ nur für Admins. |
| Suchfeld | `input.input.search type="search"`, Platzhalter „Nach Titel suchen…“, in `.toolbar`, `aria-label="Nach Titel suchen"` |
| Kunde-Filter, Mitarbeiter-Filter | zwei `select.select.select-quiet` in `.toolbar`, Titel und Optionen wie heute |
| `notice` | `<p className="msg muted" role="status">` |
| „Weitere Treffer“ | `<p className="msg muted" data-hidden-hits>`: „Weitere Treffer: “, je Ansicht `<button type="button" className="link">{h.count} unter „{VIEW_LABELS[h.view]}“</button>`, getrennt durch „ · “. Klick setzt `setFilter(h.view)`. |
| `error` | `<p className="msg err" role="alert">` |
| Leere Liste | `EmptyState title` mit „Keine archivierten Projekte.“ bzw. „Keine Projekte gefunden.“ |
- [ ] **Step 3: Tabelle**
`<table className="table projects-table" role="table">`, Zeilen `role="row"`, Zellen `role="cell"` (Tabellensemantik bleibt beim Stapeln unter 860 px erhalten). `<tr className={\`row${archived ? ' is-archived' : ''}\`}>` mit `onClick={(e) => { if (isRowActivation(e)) openEdit(project) }}`. `isRowActivation` ignoriert Klicks auf `a, button, select, input, label`, Mehrfach-Modifikatoren und andere Maustasten.
| Spalte (Kopf) | Klassen | Inhalt |
|---|---|---|
| Projekt | – | `.cell-title` mit `<button type="button" className="link" onClick={() => openEdit(project)}>{projectName \|\| giteaRepoName \|\| '(ohne Name)'}</button>`, darunter `.cell-sub` `repoFullName` |
| Kunde | `data-label="Kunde"` | `select.select` mit Optionen „Kein Kunde“ und `({code}) {name}`, `aria-label={\`Kunde von ${name}\`}`, `disabled={assignBusyId === id \|\| archived}`, `onChange` ruft `handleAssignCustomer` (mit `window.confirm` beim Entfernen, unverändert) |
| Art | `hide-md` | `<Badge tone={PROJECT_TYPES[type].tone}>{short}</Badge>` |
| Adresse | `hide-md cell-address` | `<code title={subdomain}>{subdomain}</code>`, sonst `<span className="faint">ohne Website</span>` |
| Ticket | `hide-sm num` | `<Link className="link" to={ticketPath(projectTicketRef(project, ticketMap))}>`, Text derselbe Wert. Ohne Ticket `<span className="faint">-</span>`. |
| Status | `data-label="Status"` | `hostedBadge` als `Badge` (nur wenn nicht `null`), darunter `.cell-sub` mit `archived ? 'archiviert' : (status \|\| '-')` |
| Zuletzt geändert | `hide-md right cell-updated` | `updatedLabel(project.$updatedAt).text`, `title` aus `.title` |
| Aktionen | `right cell-actions` | `.row-actions` mit den vier Links unten |
Aktionen, jeweils nur wenn zutreffend (`projectActions(project)`):
| Bedingung | Link |
|---|---|
| `host` | `<button type="button" className="link" title="Projekt jetzt deployen/hosten" disabled={hostBusyId === id}>` „Hosten“, beim Laden „Wird geladen …“ |
| `preview` | `PreviewLinkButton` „Vorschau öffnen“ |
| `refresh` | `<button type="button" className="link" title="Aktualisieren – Website neu deployen" disabled={refreshBusyId === id}>` „Aktualisieren“, beim Laden „Wird geladen …“ |
| `gitea` | `GiteaLinkButton` „In Gitea öffnen“ |
- Entscheidung: Kein Tabellenfuß (die Zähler stehen in den Reitern).
- Entscheidung: Leere Werte bleiben „-“ wie heute, nicht „–“.
- Entscheidung: „Adresse“ zeigt wie die Karte heute die Subdomain.
- Entscheidung: Das Wort „Hosten“ erhält als Ladezustand den Text „Wird geladen …“ aus der Spec-Liste.
- Entscheidung: Das alte „Bearbeiten“ (Stift) entfällt als eigener Knopf. Sein Platz ist das `button.link` der ersten Zelle und der Zeilenklick.
- Entscheidung: Unter 860 px stapelt die Zeile (Projekt, dann Kunde und Status nebeneinander, dann Aktionen), weil Auswahl und Aktionen sonst keinen Platz finden. Die Kernspalten laut Spec bleiben dabei Projekt, Kunde, Status und Aktionen.
- [ ] **Step 4: Dialog „Neues Projekt“**
`Dialog as="form" onSubmit={handleCreate} title="Neues Website-Projekt" busy={createLoading} onClose={() => setShowCreateModal(false)}`. Fuß: `<span className="grow" />` und `Button type="submit" variant="primary"` („Projekt anlegen“, beim Laden „Wird angelegt...“).
Inhalt (Texte unverändert, `Field` mit `htmlFor`):
- `createError` als `<p className="msg err" role="alert">` ganz oben
- „Anzeigename“ (`required`)
- „Subdomain (optional)“ mit Platzhalter und dem Hinweis „Mit Subdomain wird die Website … als normales Projekt angelegt.“ als `hint`
- „Repo-Name (optional)“ mit Platzhalter
- „Kunde (optional)“
Der Kommentar zum Repo-Namen (nicht aus der Subdomain mitfüllen) bleibt.
- [ ] **Step 5: Dialog „Projekt bearbeiten“**
`Dialog wide title="Projekt bearbeiten" busy={editLoading \|\| archiveBusy} onClose={() => setEditProject(null)}`. Die Box ist ein `div`, damit der `PreviewBlock` außerhalb des Formulars steht.
- Inhalt in `.dialog-body`:
- `<p className="muted small">{editProject.repoFullName}</p>`
- `editError` als `msg err`
- `<form id="projekt-bearbeiten" className="dialog-form" onSubmit={handleEditSave}>` mit den Feldern „Name“ (`required`), „Typ / Template“ (Optionen aus `PROJECT_TYPES`, `adminOnly` nur für Admins, `hint` ohne Icon) und, außer bei Typ `project`, „Subdomain“ (Hinweis „Ergebnis: https://…project.webklar.com - Aenderung wird neu deployt (alte Subdomain wird abgebaut).“)
- bei archiviertem Projekt `<p className="muted small">Archiviert: zum Bearbeiten oder Hosten erst reaktivieren.</p>`
- unter denselben Bedingungen wie heute (`projectType !== 'project' && subdomain && !archived`) `<section><h4 className="pv-title">Preview</h4><PreviewBlock key projectId /></section>`
- Fuß:
- links `Button variant="danger" type="button"` „Repository archivieren“ (`window.confirm` bleibt) bzw. bei archivierten Projekten `<button type="button" className="link">Reaktivieren</button>`, `disabled={archiveBusy \|\| editLoading}`
- dann `<span className="grow" />`
- rechts `Button type="submit" form="projekt-bearbeiten" variant="primary"` („Speichern & synchronisieren“, beim Laden „Speichert...“, `disabled` bei `editLoading \|\| archiveBusy \|\| archived`, `title` wie heute)
- Entscheidung: Der Speichern-Knopf steht im Fuß außerhalb des `<form>` und hängt über das Attribut `form` daran. So löst Enter im Namensfeld Speichern aus, im `PreviewBlock` nie.
- [ ] **Step 6: Alles Übrige löschen**
Entfallen: Icon-Importe, `TYPES`, `typeOf`, `HOSTED_TYPES`, `isProjectReady` (jetzt in `lib/projectRow`), `Row`, `customerMap`, `FILTER_OPTIONS`. Die Handler `handleCreate`, `handleSyncRepos`, `handleAssignCustomer`, `handleHostNow`, `handleRefresh`, `openEdit`, `handleEditSave`, `handleArchiveToggle` bleiben bis auf den Ersatz von `typeOf` durch den Import unverändert.
- [ ] **Step 7: Prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p5 && "$W/shots.sh" p5 "$W/shots/p5-t11" --only projekte,projekte-archiv,projekte-treffer,projekte-lang,projekte-viel,projekte-861,projekte-1101,projekte-aktion,projekte-tastatur,projekte-zeilenklick,fehler-projekte,dlg-projekt-neu,dlg-projekt-bearbeiten,dlg-projekt-archiv --gate p5`
Expected: PASS (Exit 0).
- Kein `overflow-x`, auch bei 861 und 1101 px, die erste Zelle ist dort (gerundet) mindestens 140 px breit.
- Bei 390 px stehen Projekt, Kunde, Status und Aktionen gestapelt in der Zeile.
- `projekte-aktion` und `projekte-tastatur` bestehen.
- Im Bild `projekte` steht höchstens ein schwarzer Knopf im Kopf („Neues Projekt“).
- [ ] **Step 8: Commit**
```bash
git add src/pages/ProjectsPage.jsx
git commit -m "Projekte: Liste als Tabelle mit Reitern, Neu- und Bearbeiten-Dialog, Laden nur beim ersten Mal" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 12: `AssignProjectPage`
**Files:**
- Modify: `src/pages/AssignProjectPage.jsx` (Import L3 löschen, Rückgaben ab L178 neu)
**Interfaces:**
- Consumes: `PageHeader`, `Card`, `Button`, `Field`, `Badge` aus `components/ui`, `CreateCustomerForm` (Dialog aus P4, Props `onCancel`, `onCreate`), `ticketPath` (P2), Klassen `.seg.wrap`, `.pick-list`, `button.list-row`, `.row-buttons`, `.summary`, `.check`, `.field-row-3`.
- Produces: Seite `/projects/zuordnen/:id`. Zustände, Effekte, `finish`, `go`, `patch`, `startNewTicket`, `setNT`, `stepReady`, die Logik aus `lib/assignFlow` und der 409-Fall bleiben unverändert.
- [ ] **Step 1: Vorher prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "ticketPath" src/pages/AssignProjectPage.jsx`
Expected: 2 Treffer (P2). Sie bleiben bis zum Schluss erhalten.
- [ ] **Step 2: Kopf und Zustände**
- Alle `Button` dieser Seite tragen `type="button"`, die Seite hat kein `<form>`.
- Ein gemeinsamer Kopf `PageHeader crumb={[{ label: 'Projekte', to: '/projects' }, { label: repoName \|\| project.projectName }]} title="Projekt zuordnen" subtitle={…}` für alle Zustände. `subtitle` ist die Zeile der bisherigen Kopf-Karte: „Repository in Gitea öffnen“ (`<a className="link" target="_blank" rel="noreferrer">`) und „ · angelegt am {Datum}“ mit „ von {createdByGitea}“, Wortlaut wie heute.
- Entscheidung: Der bisherige Untertitel mit dem Schrittnamen entfällt. Dieselbe Zeile steht direkt darunter als Abschnittstitel `h2.section` (kommt von `Card title`). `STEP_TITLES` bleibt unverändert.
- Laden: `<div className="page"><div className="loading">Wird geladen …</div></div>`. Ladefehler: `PageHeader` mit der Brotkrume nur „Projekte“ (es gibt noch kein `project`) und `<p className="msg err" role="alert">{loadError}</p>`.
- Zustand „Zugeordnet, mit Hinweisen“: `<Card title="Zugeordnet, mit Hinweisen">` mit den Warnungen als `<ul className="summary">` und `Button variant="primary"` „Weiter“.
- Zustand „Art bereits gesetzt“: Hinweis (`notice` als `msg err`), „Bereits erledigt von **…** am …“ und `TicketLink` (`Link className="link"`, Text „Zum Ticket #{woid}“) ohne Kasten.
- [ ] **Step 3: Schritte**
Je Schritt `<Card title={STEP_TITLES[state.step]}>`. „Zurück“ (`Button variant="ghost"`) und „Weiter“ (`Button variant="primary"`) bleiben mit denselben Bedingungen, ohne Pfeil-Icons. Der Fehler (`error`) ist `msg err`.
| Schritt | Danach |
|---|---|
| Art | `<div className="seg wrap" role="group" aria-label={STEP_TITLES.kind}>` mit je einem `<button type="button" aria-pressed={state.kind === k.key}>`. Das Verhalten (`patch({ kind, newTicket: null, ticketId: '' })`) bleibt. |
| „Ist das für einen Kunden?“ | `.row-buttons`: `Button variant="primary"` „Ja, für einen Kunden“, `Button` „Nein, internes Projekt“ |
| Kunde | Zeile aus `input.input.search` (Platzhalter „Kunde suchen …“) und für Admins `Button` „Neuer Kunde“ (ohne `FaPlus`). `showNewCustomer && isAdmin` rendert `CreateCustomerForm` wie P4 es vorsieht, ohne umgebendes `div`. Liste in `.pick-list`, je Kunde `<button type="button" className="list-row" aria-pressed>` mit `.cell-title` (Name, `· code` als `.faint`) und `.cell-sub` (`ort · mail` oder `-`). „Kein Kunde gefunden.“ als `<p className="muted pick-empty">`. |
| Ticket | Laden: `<p className="muted">Wird geladen …</p>`. Fehler: `msg err` mit „Die Tickets des Kunden konnten nicht geladen werden.“ und `Button` „Erneut versuchen“. Liste wie beim Kunden mit `#{woid} {topic}` und `type · status`. „Dieser Kunde hat keine offenen Tickets.“ als `pick-empty`. „Neues Ticket anlegen“ als `Button` (primary, solange `newTicket` gesetzt ist, wie heute). Das Formular: `.field-row-3` mit „Thema“, „Angefragt von“, „Start“ (`type="date"`), darunter „Details“ als `textarea.textarea`, dann „Ticket-Art: … (ergibt sich aus der Art)“ als `<p className="muted small">`. |
| Zusammenfassung | `<ul className="summary">` mit den Zeilen von heute (Art, Kunde bzw. „Internes Projekt (ohne Kunde)“, Ticket bzw. „Neues Ticket“). Preview-Hinweise als `<p className="muted">`. „Preview einschalten“ als `<label className="check"><input type="checkbox" …>`, „Subdomain“ als `Field` mit dem `hint`. „Abschließen“ als `Button variant="primary"`, beim Laden „Speichert …“. |
- Entscheidung: Die Auswahl ist nicht nur an Hintergrund und Häkchen erkennbar. Die gewählte Zeile trägt `aria-pressed="true"`, den Hintergrund `--bg-hover` und das Badge „Ausgewählt“ (`<Badge>Ausgewählt</Badge>` am Zeilenende). Das Wort ist neu und von der Spec verlangt („Auswahl auch als Text markiert“).
- Entscheidung: Die Zusammenfassung bleibt eine Liste mit dem Wortlaut von heute (kein `.kv`, damit die Doppelpunkte und Sätze unverändert bleiben).
- Entscheidung: Der Brotkrumen-Link führt immer auf `/projects`.
- [ ] **Step 4: Prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p5 && "$W/shots.sh" p5 "$W/shots/p5-t12" --only zuordnen,zuordnen-kunde,zuordnen-ticket,zuordnen-fertig --gate p5`
Expected: PASS. Bei 390 px läuft nichts über den Rand, die gewählte Zeile zeigt „Ausgewählt“.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "ticketPath" src/pages/AssignProjectPage.jsx`
Expected: weiter 2 Treffer.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{|surface-2|text-red" src/pages/AssignProjectPage.jsx`
Expected: keine Treffer.
- [ ] **Step 5: Commit**
```bash
git add src/pages/AssignProjectPage.jsx
git commit -m "Projekte: Zuordnen-Assistent mit Abschnitten, Brotkrumen und im Text markierter Auswahl" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 13: `ServicesPage`
**Files:**
- Modify: `src/pages/ServicesPage.jsx` (Importe L2-4 anpassen, Markup L30-84 neu)
**Interfaces:**
- Consumes: `PageHeader`, `Card`, `Button`, `Badge` aus `components/ui`, `previewStateTone` aus `components/projectTones`, `servicesCards` unverändert, `ticketPath` (P2, bleibt wie umgestellt), Klassen `.list-row`, `.cell-title`, `.cell-sub`, `.link`, `.msg`.
- Produces: Seite `/services`.
- [ ] **Step 1: Umbau**
- Laden: `<div className="loading">Wird geladen …</div>`.
- Fehler: `<p className="msg err" role="alert">` und `Button type="button"` „Erneut versuchen“, ohne Kasten.
- Das Kachelraster entfällt. Jede Art ist ein untereinander stehender Abschnitt `<Card title={\`${c.label} (${c.count})\`}>`.
- Beschreibung als `<p className="muted">`. Funktions-Badges bleiben `<Badge>` mit dem Text `label · statusLabel` in einer Zeile (`.pv-row`).
- Projektzeilen sind `.list-row` (kein Link, `div`): links `.cell-title` mit Projektname, dem Badge „Analyse an“ (nur bei `features.analyse === 'active'`) und davor dem Preview-Badge, darunter `.cell-sub` (`Kunde · Preview` oder „Intern“, wie heute). Rechts `Link className="link"` „Ticket #{woid}“ und in der Karte „Art fehlt“ `Link className="link" to={\`/projects/zuordnen/${p.id}\`}` „Zuordnen“.
- „Noch keine Projekte.“ als `<p className="muted pick-empty">`.
- Preview-Badge: `<Badge tone={previewStateTone(p.previewState)}>Preview: {p.previewState}</Badge>`.
- Der Import von `dotColor`, `FaSpinner`, `useNavigate` und der Aufruf `navigate` entfallen.
- Entscheidung: Das Badge trägt den Text `Preview: {Zustand}`. Das ist der Wortlaut, den der Punkt heute als Titel und für Screenreader hat. Die Wörter des Anhangs („Online“, „Wird gebaut“, „Fehler“, „Aus“) stehen nicht in der Spec-Liste „Texte“ und kommen deshalb nicht hinein.
- Entscheidung: „Zuordnen“ ist ein echter Link statt eines schwarzen Knopfes. Es ist eine Zeilenaktion, und das Ziel ist dasselbe.
- [ ] **Step 2: Prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p5 && "$W/shots.sh" p5 "$W/shots/p5-t13" --only services --gate p5`
Expected: PASS. Bei 390 px laufen Zeilen mit langem Namen nicht über den Rand.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "ticketPath" src/pages/ServicesPage.jsx`
Expected: weiter 1 Treffer.
- [ ] **Step 3: Commit**
```bash
git add src/pages/ServicesPage.jsx
git commit -m "Projekte: Services als untereinander stehende Abschnitte mit Zeilen, Preview-Zustand als Badge" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 14: Gate P5, Review, Tag
- [ ] **Step 1: Rebase**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git rebase origin/test && "$W/check.sh"`
Expected: Exit 0 und `# fail 0`. `# pass` ist die Zahl aus Task 0 plus 13 (liegen neue Commits auf `test`, kann sie höher sein). Die Bundle-Größen aus der Ausgabe werden notiert.
- [ ] **Step 2: Vollständiger Gate-Lauf**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p5 && "$W/shots.sh" p5 "$W/shots/p5" --gate p5`
Expected:
- Exit 0. Das Gate `p5` gilt für alle Ansichten (`views: '*'`), also auch für die aus P0 bis P4. So zeigt der Lauf, dass P5 sonst nichts verschlechtert hat.
- Keine Verstöße gegen `external-request`, `console-error`, `step-failed`, `dark-surface`, `low-contrast` und `overflow-x`.
- `icon-svg` ist für alle Ansichten im Umfang des Gates (P2 bis P5, ohne `ignore`) sauber.
- [ ] **Step 3: Code-Hygiene und Klassenabgleich**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|@tabler|btn-sm|style=\{|#[0-9a-fA-F]{3,6}\b|text-grey|text-red|bg-red|ui-btn|ui-card|form-control|badge-(ok|info|warn|muted|danger)|isInteractiveTarget" src/pages/ProjectsPage.jsx src/pages/AssignProjectPage.jsx src/pages/ServicesPage.jsx src/components/TicketProjects.jsx src/components/AssignedProjectCard.jsx src/components/AnalyseRow.jsx src/components/PreviewBlock.jsx src/components/PreviewLinkButton.jsx src/components/GiteaLinkButton.jsx src/components/CopyableCredential.jsx`
Expected: keine Treffer.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "btn-sm" src`
Expected: keine Treffer.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "dotColor" src`
Expected: nur noch `src/lib/previewEnv.js` (P10 löscht es samt Test).
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node "$W/css-klassen.mjs" "$WT" > "$TMPDIR/klassen.json" && node -e "const j=JSON.parse(require('fs').readFileSync(process.argv[1]));const mine=/^(projects-table|row-actions|pv-|proj-|dialog-form|pick-|summary|ticket-projects-assign)/;for (const k of ['undefinedUsed','dead']) console.log(k, j[k].filter((c)=>mine.test(c)).join(' '))" "$TMPDIR/klassen.json"`
Expected: Hinter `undefinedUsed` und hinter `dead` steht kein Name. Keine Klasse des P5-Blocks ist unbenutzt oder ohne Regel (`projects-table`, `row-actions`, `pv-*`, `proj-*`, `dialog-form`, `pick-*`, `summary`, `ticket-projects-assign`).
- [ ] **Step 4: Bundle-Größe**
Die Werte aus der `check.sh`-Ausgabe von Step 1 werden mit `design-p4` verglichen. JS ist kleiner, weil `ProjectsPage`, `AssignedProjectCard`, `GiteaLinkButton` und weitere keine Icon-Importe mehr haben.
- [ ] **Step 5: Code-Review (sonnet)**
Geprüft wird der Diff `design-p4..HEAD` gegen die Spec-Abschnitte „Bausteine“, „Ticket als eigene Seite / Route und Links“, „Texte“, „Was gleich bleibt“ und Paket P5, dazu der Review Focus oben. Besonders:
- Jede Steuerung aus den Tabellen der Tasks 7 bis 13 ist erreichbar.
- Es gibt keinen Textunterschied außer den Spec-Texten und den „Entscheidung“-Zeilen (Stichprobe: `git diff design-p4..HEAD -- src/pages src/components` auf geänderte Texte lesen).
- Alle `window.confirm`, `busyRef`-artigen Sperren (`assignBusyId`, `hostBusyId`, `refreshBusyId`, `archiveBusy`) und `data-testid`/`data-hidden-hits` sind erhalten.
Wichtige Befunde werden behoben, Kleinigkeiten notiert. Danach kommt der Rebase aus Step 1 noch einmal und Step 2 als Kurzlauf mit `--only projekte,services,ticket-projekte`.
- [ ] **Step 6: Tag und Übergabe**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" && git tag design-p5 && git log --oneline origin/test..design-p5 && git rev-parse design-p5`
Der Eigentümer bekommt:
- die Screenshots aus `$W/shots/p5/1440x900` und `$W/shots/p5/390x844` (Auswahl: `projekte`, `projekte-lang`, `projekte-861`, `dlg-projekt-neu`, `dlg-projekt-bearbeiten`, `zuordnen-kunde`, `zuordnen-fertig`, `services`, `analyse-zustimmung`, `ticket-projekte`)
- die Liste der „Entscheidung“-Zeilen dieses Plans, vor allem: neues Wort „Ausgewählt“, Preview-Badge `Preview: {Zustand}` in den Services, gestapelte Zeilen der Projekttabelle unter 860 px, Dialogtitel „Website-Analyse“
- den Hinweis, dass „Zuordnen“ in den Services jetzt ein Textlink ist
- für P8: Von `.credential*` bleiben `.credential`, `.credential-value`, `.credential-value.is-masked`, `.credential-empty`, `.credential .link`
- die Push-Zeile `git -C /home/webklar/apps/ticket.webklar.com/dev-design push origin 'design-p5^{commit}:refs/heads/test'`
- die Rücknahme `git revert --no-commit design-p4..design-p5`, Commit auf `test` und Push durch den Eigentümer (nur solange P5 das jüngste Paket ist)
- [ ] **Step 7: Nach dem Push (erst, wenn der Eigentümer gepusht hat)**
- Im Log von `webklar-ticket-build` steht „Deployment completed successfully“.
- `curl -s https://ticket.webklar.com/ | grep -o 'assets/index-[^"]*\.js'` entspricht `dist/index.html` des Tags.
- Ein lesender Sichttest im Chrome des Eigentümers: `/projects` (Reiter, eine Zeile mit „Aktualisieren“ oder „Hosten“) und die Projekte eines Tickets. Nichts anklicken, was speichert.
@@ -0,0 +1,611 @@
# WOMS „Weißes Design“ P6: Kunden-Unterseiten – Umsetzungsplan
> **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:** Chat, Gutschriften, Abo-Angebote und E-Mail-Speicher sind hell und kastenlos. Die Chat-Liste besteht aus Haarlinien-Zeilen, die drei Listen sind Tabellen mit Spaltenpriorität, das Angebots-Formular ist ein Dialog. Der Chat wechselt bei 860 px zwischen Liste und Verlauf. `admin-areas.css` ist am Ende gelöscht, weil P4 die Kundenseite davon gelöst hat und P6 die letzten Nutzer umbaut.
**Architecture:**
- `chat.css` wird neu geschrieben. Jede Regel für Elemente im Chat-Container trägt das Präfix `.woms-chat` und ist strikt spezifischer als die Fremdregel des Portal-Bausteins. Das Seitenlayout (`.chat-page` usw.) bleibt ohne Präfix.
- Die Listen nutzen `Tabs`, `.table` mit `.hide-md`/`.hide-sm`, `Badge`, `EmptyState`. `CreditDetail` wird `.two-col` mit `Card`-Abschnitten und `.kv`. `OfferForm` wird ein `Dialog`.
- Neue reine Helfer liegen in `src/lib` und sind per Test gesichert. Das Screenshot-Werkzeug bekommt den echten Portal-Chat-Baustein, damit die Chat-Regeln gegen das Fremd-CSS geprüft werden.
**Tech Stack:** React 18, react-router-dom 6, reines CSS, `node --test`, Prüf- und Screenshot-Werkzeug aus P0 (`env.sh`, `check.sh`, `shots.sh`).
**Spec:** `docs/superpowers/specs/2026-10-04-weisses-design-design.md`, Abschnitte „Gestaltungsregeln“, „Bausteine“, „Übergangsschicht KOMPAT“, Paket P6, „Seiten ohne Prototyp-Vorlage“, „Texte“, „Was gleich bleibt“, Risiken (Chat-Composer, Vorschlagslisten). Anhang `…-umbaukarte.md`: Abschnitt 5 (P6), 7.2, 8.1 Punkte 13, 14, 19. Globale Regeln: `2026-10-05-weisses-design-00-uebersicht.md`.
## Global Constraints
Es gelten die Regeln der Übersicht, besonders „Befehle und Prüfungen“, „CSS-Reihenfolge“, „Parallele Arbeit an WOMS“, „Paketstart und Abnahme-Reihenfolge“ und „Bausteine benutzen“. Jeder Run-Block beginnt mit `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&`. Danach gelten `$WT` (Worktree, Arbeitsverzeichnis), `$W` (Werkzeug) und `$TMPDIR`. Für P6 kommt hinzu:
- **Voraussetzung:** Tag `design-p5` ist gesetzt. P4 hat `CustomerFunktionen`, `AboStatusCard` und `KundenansichtButton` umgebaut, alle `.funktionen-*`-Regeln nach `global.css` verlegt und die beiden Dateien von `admin-areas.css` gelöst (keine `.area-*`-Klasse, kein Import). Task 1 prüft das.
- **Dateien:** nur `CustomerChatPage`, `CreditsPage`, `OffersPage`, `StorageTasksPage`, `src/components/{chat,credits,offers}/*`, `src/styles/{chat,admin-areas,global}.css`, `src/lib/{credit,offer,storage}Format.js` samt Tests und `$W`. Nie anfassen: `loadChatComposer.js` samt Test, die Container in `ChatThread.jsx`, das Portal-Repo (nur Lesezugriff für den Snapshot in Task 2), die P4-Dateien.
- **Ladereihenfolge:** `main.jsx` importiert `App` vor `global.css`, also steht `chat.css` im Bundle vor `global.css`, und das Portal-CSS lädt zur Laufzeit zuletzt. Gleiche Spezifität verliert immer: `.btn` aus `global.css` überstimmt ein `.chat-back { display: none }`. Seitenregeln für Elemente mit globaler Klasse brauchen höhere Spezifität (`.chat-page .chat-back`), nie `!important`.
- **Texte:** Erlaubt sind nur „← Alle Unterhaltungen“ (Pfeil statt Icon), „Speichert …“ und „Wird geladen …“ (statt Spinner). Alles andere bleibt wörtlich, auch Spaltenköpfe, Hinweisabsätze und alle `confirm`/`alert`-Texte.
- **Hygiene und Inventur:** In den zehn P6-Dateien gibt es danach kein `react-icons`, kein `size="sm"`, kein `style={{` (heute 26). Treffer je Datei für `window.confirm`, `window.alert`, `busyRef`, `data-storage-done`, `id="calc-*"`, `id="credit-*"` bleiben: `CreditDetail.jsx` 16 (1 confirm, 9 busyRef, 6 IDs), `OfferList.jsx` 4, `OfferForm.jsx` 1, `StorageTasksPage.jsx` 3.
**Entscheidungen** (die Spec ist dazu still):
- **Entscheidung:** Die Prüf-Zeichen `✓`, `!`, `i` der „Automatischen Prüfung“ bleiben als Badge-Text. Die Umbaukarte sagt „ohne Glyphen“, aber die Spec „Texte“ erlaubt keine Textänderung dafür, und sonst trüge nur die Farbe den Status.
- **Entscheidung:** Die Filter sind `Tabs` ohne Zähler (die Spec nennt keine, es gibt keine neuen Abfragen).
- **Entscheidung:** Tabellenzeilen sind nicht klickbar (heute auch nicht), kein `tr.row` und damit kein `isRowActivation` (`src/lib/rowClick.js` aus P2). Aktionen stehen als `button.link` bzw. `.link-danger` in der letzten Zelle. Die offene Gutschrift trägt `aria-current="true"`.
- **Entscheidung:** Spaltenpriorität (immer / `hide-sm` / `hide-md`):
- Gutschriften: Kunde, Status, Aktion / Eingang, Rechnung / Leistung, Bezahlt bis.
- Angebote: Kunde, Angebot, Status mit Aktionen / Gültig bis / Angelegt.
- Speicher: Kunde, Aufgabe, Aktion / Fällig / Bestellt.
- **Entscheidung:** Der Chat-Zähler ist eine kleine Zahl in `--accent`, fett (wie `.nav-count.hot`), keine Pille. Die aktive Zeile hat `aria-current="true"` und `--accent-soft`.
- **Entscheidung:** Beim Portal-Composer bleibt die Geometrie (46 px, runde Knöpfe). Überschrieben werden Farben, Ring statt Rand, Hover, Fokus, Deaktiviert und unter 640 px wieder `font-size: 16px` im Eingabefeld (das heutige `font: inherit` hebt die Portal-Regel gegen das iOS-Zoomen auf). Der Datei-Chip ist weiß mit Ring, weil die Blase selbst `--bg-hover` ist. Die Anhang-Menü-Icons bleiben.
- **Entscheidung:** Lightbox und Kamera bleiben unberührt dunkel (am `body`, vom Präfix nicht erreichbar). Der Kamera-Knopf „Foto verwenden“ bleibt im Portal-Lila und geht als „bewusst offen“ in die Übergabe.
- **Entscheidung:** `OfferForm` bleibt ohne `<form>` (also kein `Dialog as="form"`, kein Enter-Submit, wie heute). Beide Fußknöpfe tragen trotzdem `type="button"`. „Abbrechen“ ist beim Speichern gesperrt, passend zu „ד und Esc.
- **Entscheidung:** `admin-areas.css` wird in P6 gelöscht. P4 hat die `.funktionen-*`-Regeln nach `global.css` verlegt und `CustomerFunktionen` und `AboStatusCard` von der Datei gelöst. Die P6-Seiten verlieren ihre `.area-*`-, `.credit-*`-, `.offer-*`- und `.storage-row`-Klassen und den Import. Danach hat die Datei keinen Nutzer mehr (Task 11).
- **Entscheidung:** Kleine Ergänzungen stehen in einem Block „Kunden-Unterseiten (P6)“ am Ende von `global.css`. `table-wrap` gilt nur für die drei P6-Tabellen, damit P2 bis P5 unberührt bleiben.
## Review Focus
Aus der Übersicht gehören zu P6:
- **Punkt 2, lange Inhalte am Handy:** Kundenname mit 60 Zeichen in allen Listen, E-Mail mit 50 Zeichen ohne Leerzeichen, URL mit 80 Zeichen im Chat, Angebotstext und interne Notiz je 200 Zeichen. `overflow-x` bei 390 px in allen P6-Ansichten.
- **Punkt 3, Teilausfall:** Das Szenario `fehler-portal` lässt alle `/api/team/*`-Anfragen mit 500 antworten. Die vier Seiten zeigen `.msg.err` und bleiben bedienbar, „Erneut versuchen“ lädt neu.
- **Punkt 4, nur Tastatur:** Tabellenaktion per Tab erreichbar. Esc schließt erst die Vorschlagsliste, dann den Dialog. Tab bleibt im Dialog, „ד und Esc sind beim Speichern gesperrt. Doppelklick auf „Genehmigen & gutschreiben“ löst einen POST aus, der 300-ms-Debounce einen Berechnungs-POST.
- **Punkt 5, viele Einträge:** `chat-viel` (150 Unterhaltungen, die Liste scrollt in sich) und `gutschriften-viel` (120 Anträge).
Dazu gilt für P6:
- **Fremd-CSS gewinnt nicht:** `chat-verlauf` liest berechnete Stile und vergleicht sie mit Sollwerten (Task 4). `.chat-back` ist bei 1440 px unsichtbar, bei 390 px sichtbar.
- **Vorschlagsliste im Dialog** ist vollständig erreichbar, der längste Speichern-Text passt bei 390 px in die Fußzeile.
- **Zwischenstufe 1000 px:** Die Ansichten `*-tablet` zeigen die Spaltenpriorität.
---
### Task 1: Ausgangslage und Lib-Helfer
**Files:**
- Modify: `src/lib/creditFormat.js`, `src/lib/offerFormat.js`, `src/lib/storageFormat.js` (neue Exporte, Stand vor P6)
- Test (neu): `tests/unit/creditFormat.test.mjs`, `tests/unit/offerFormat.test.mjs`, `tests/unit/storageFormat.test.mjs`
**Interfaces:**
- Consumes: `fmtDate`, `fmtDateTime`, `formatCents` aus `src/lib/format.js`, `isOverdue`.
- Produces:
- `creditActionLabel(status) → 'Prüfen' | 'Ansehen'`
- `creditStatusNote(r) → string`
- `checkGlyph(status) → '✓' | '!' | 'i'`
- `checkTone(status) → 'ok' | 'warn' | 'info' | 'muted'`
- `filterOffers(offers, filter) → offers[]`
- `offerRowActions(o) → { sendLabel: '' | 'E-Mail senden' | 'Erneut senden', canWithdraw: boolean }`
- `offerMailLine(o) → null | { tone: 'muted' | 'warn', text }`
- `storageDue(t, now) → { tone, label, sub, subMuted }`
- [ ] **Step 1: Paketstart und Ausgangslage prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git status --short && git fetch origin && git merge-base --is-ancestor design-p5 HEAD && git rebase origin/test && "$W/check.sh"`
Expected: Exit 0, `git status --short` zeigt keine geänderten Dateien, `# fail 0`. Die Testzahl wird als N5 notiert. Schlägt `merge-base` fehl, fehlt `design-p5` im Branch: stoppen und melden.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git log --oneline origin/test..origin/feature/terminplaner-tp2 -- src/pages/CustomerChatPage.jsx src/pages/CreditsPage.jsx src/pages/OffersPage.jsx src/pages/StorageTasksPage.jsx src/components/chat src/components/credits src/components/offers src/lib/creditFormat.js src/lib/offerFormat.js src/lib/storageFormat.js src/styles/chat.css src/styles/admin-areas.css src/styles/global.css`
Expected: keine Ausgabe. Gibt es Treffer, stoppt der Agent und fragt den Eigentümer. Er empfiehlt, TP2 zuerst nach `test` zu bringen.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|ui-btn|FaSpinner" src/components/customer/KundenansichtButton.jsx src/components/customer/AboStatusCard.jsx src/components/customer/CustomerFunktionen.jsx; rg -n "admin-areas|\barea-" src/components/customer; rg -n "funktionen-" src/styles/admin-areas.css`
Expected: keine Treffer. Ein Treffer heißt, dass P4 `CustomerFunktionen` oder `AboStatusCard` nicht vollständig von `admin-areas.css` gelöst hat: stoppen und melden, die P4-Dateien werden in P6 nicht umgebaut.
- [ ] **Step 2: Failing Tests schreiben** (Leerzeichen in Beträgen ist U+00A0)
`creditFormat.test.mjs` (4 Tests):
- `creditStatus` pinnt die Tonnamen: `offen` → `{ tone: 'warn', label: 'Zu prüfen' }`, `rueckfrage` → `muted`/„Rückfrage beim Kunden“, `genehmigt` → `ok`/„Genehmigt“, `abgelehnt` → `danger`/„Abgelehnt“, `'neu'` → `muted`/`'neu'`.
- `creditActionLabel`: `offen`, `rueckfrage` → `'Prüfen'`, sonst `'Ansehen'`.
- `creditStatusNote`: `{ status: 'genehmigt', amountCents: 1250, appliedAt: '2026-10-01T10:00:00Z' }` → `'12,50 €'`, ohne `appliedAt` → `'12,50 € · nicht gebucht'`, `{ status: 'offen', amountCents: 1250 }` → `''`.
- `checkGlyph`/`checkTone`: `ok` → `'✓'`/`'ok'`, `warn` → `'!'`/`'warn'`, `info` → `'i'`/`'info'`, `undefined` → `'i'`/`'muted'`.
`offerFormat.test.mjs` (4 Tests):
- `offerStatus`: `offen` → `warn`/„offen“, `aktiv` → `ok`/„aktiv“, `abgelaufen` → `muted`/„abgelaufen“, `zurueckgezogen` → `muted`/„zurückgezogen“.
- `filterOffers` mit Status `offen, aktiv, abgelaufen, zurueckgezogen` (ids `1` bis `4`): `'open'` → `['1','2']`, `'all'` → alle, `null` → `[]`, Eingabe unverändert.
- `offerRowActions`: `{ status: 'offen', mailSendable: true }` → `{ sendLabel: 'E-Mail senden', canWithdraw: true }`, mit `notifiedAt` → `'Erneut senden'`, `{ status: 'aktiv', mailSendable: true }` → `canWithdraw: false`, `{ status: 'abgelaufen', mailSendable: false }` → `{ sendLabel: '', canWithdraw: true }`, `{ status: 'zurueckgezogen' }` → `{ sendLabel: '', canWithdraw: false }`.
- `offerMailLine`: `{ notifiedAt: '2026-09-30T10:05:00.000Z' }` → `{ tone: 'muted', text: 'E-Mail gesendet 30.09.26, 12:05' }`, `{ mailSendable: true }` → `{ tone: 'warn', text: 'E-Mail nicht verschickt' }`, `{ status: 'offen', customerEmail: '' }` → `{ tone: 'warn', text: 'Keine E-Mail-Adresse hinterlegt' }`, `{ status: 'abgelaufen', customerEmail: '' }` und `{ status: 'offen', customerEmail: 'a@b.de' }` → `null`.
`storageFormat.test.mjs` (2 Tests, `t0 = Date.parse('2026-10-05T08:00:00Z')`):
- `storageDue`: `{ kind: 'einrichten', createdAt: '2026-10-01T08:30:00.000Z' }` → `{ tone: 'warn', label: 'jetzt einrichten', sub: 'bestellt 01.10.26, 10:30', subMuted: true }`. `{ kind: 'senken', dueAt: '2026-10-04T00:00:00.000Z' }` → `{ tone: 'warn', label: 'zum Periodenende', sub: '04.10.2026', subMuted: false }`, mit `dueAt: '2026-10-20T00:00:00.000Z'` → `tone: 'muted'`, `sub: '20.10.2026'`.
- `isOverdue`: vergangenes `dueAt` → `true`, zukünftiges oder fehlendes → `false`.
- [ ] **Step 3: Test laufen lassen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/creditFormat.test.mjs tests/unit/offerFormat.test.mjs tests/unit/storageFormat.test.mjs`
Expected: FAIL (`… is not a function`).
- [ ] **Step 4: Implementieren**
Die Regeln sind die heutigen Inline-Ausdrücke, herausgezogen: `creditActionLabel` aus `CreditList.jsx` Z. 52, `creditStatusNote` Z. 48, `checkGlyph` und `checkTone` aus `CreditDetail.jsx` Z. 11 und 211, `filterOffers` aus `OfferList.jsx` Z. 54, `offerRowActions` und `offerMailLine` Z. 93 bis 109 (Reihenfolge der Mail-Zeile: `notifiedAt`, `mailSendable`, `offen` ohne Adresse, sonst `null`), `storageDue` aus `StorageTasksPage.jsx` Z. 57 bis 59. Zeichenketten bleiben wörtlich.
- [ ] **Step 5: Tests grün und Commit**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" --nur-tests`
Expected: Exit 0, `# pass` = N5 + 10, `# fail 0`.
```bash
git add src/lib/creditFormat.js src/lib/offerFormat.js src/lib/storageFormat.js tests/unit/creditFormat.test.mjs tests/unit/offerFormat.test.mjs tests/unit/storageFormat.test.mjs
git commit -m "Design P6: reine Helfer fuer Gutschrift-, Angebots- und Speicherzeilen mit Tests" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 2: Werkzeug: echter Chat-Baustein, Fixtures, Ansichten, Gate p6
**Files** (unter `$W/`, außerhalb des Repos, kein Commit): Create `fixtures/portal/{chat-composer.css,chat-composer.js,README.md}`. Modify `backend.mjs`, `fixtures/standard.mjs`, `checks.mjs`, `views.mjs`, `gates.mjs`.
**Interfaces:**
- Consumes: `createBackend`, `VIEWS`, `GATES`, `runChecks` aus P0, Gate `p5`. `ctx.backend.calls` (Liste `{ method, url, body }` aller beantworteten Anfragen).
- Produces: Szenario `fehler-portal`, die Ansichten unten, Gate `p6`.
- [ ] **Step 1: Portal-Baustein als Snapshot**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && cp /home/webklar/apps/kundenbereich.webklar.com/build/public/chat-composer.{css,js} "$W/fixtures/portal/" && sha256sum "$W"/fixtures/portal/chat-composer.*`
Expected: css `b34fbaeb72922ddfbd57f1074e58e58dffc560cd1e9a31c98b12e66487885647`, js `ebb79581f9b307b303877f169fa5546bb98b4fc66320c40ae8fd78c965bace23`. Die README nennt Quelle, Datum 2026-10-05 und beide Hashes.
- [ ] **Step 2: Backend erweitern**
- `https://portal.test/chat-composer.css` und `.js` liefern den Snapshot (statt der leeren Dateien aus P0).
- Anhang-Adressen unter `https://portal.test/api/team/files/*` liefern ein 1×1-PNG bzw. 200 ohne Inhalt.
- `POST …/chats/:id/attachments` antwortet im Szenario `fehler-portal` und in `chat-anhang-fehler` mit 500.
- `POST …/offers` und `POST …/takeover-credits/:id/decision` antworten nach 800 ms.
- Szenario `fehler-portal`: jede Anfrage an `https://portal.test/api/team/*` bekommt 500 im Fehlerformat des Szenarios `fehler`. Deren Konsolenmeldungen gelten als absichtlich.
- [ ] **Step 3: Fixtures ergänzen** (Schlüssel nach dem Muster der vorhandenen, Formen laut Lesecode in den `*Format.js` und JSX-Dateien)
| Bereich | Fälle |
|---|---|
| Chats | 8 Unterhaltungen, 3 mit `unreadFromCustomer` (1, 12, 120), ein 60-Zeichen-Name, ein `lastText` mit 80-Zeichen-URL. Satz `viel` mit 150. |
| Verlauf Kunde A | 7 Nachrichten: Kundentext, Teamtext (`senderName` „Kenso Grimm“), Bild (`kind: 'image'`, 640×480), PDF (`Angebot-Kuehn-2026.pdf`, 183000 Byte), Sprachnachricht (`kind: 'voice'`, `durationMs: 23000`), Teamtext mit 80-Zeichen-URL, langer Kundentext. |
| Gutschriften | 5 Anträge (`offen`, `rueckfrage`, `genehmigt` gebucht, `genehmigt` ungebucht, `abgelehnt`), einer mit 60-Zeichen-Name und langem `services`/`provider`/`domain`. Satz `viel` mit 120. `getCredit` für einen offenen und einen ungebuchten genehmigten Antrag: 3 `requiredChecks`, `autoChecks` mit `ok`/`warn`/`info` (mit `detail`), `calc`, laufendes Abo, 1 weiterer Antrag, 2 Dateien, 3 Verlaufseinträge. `POST …/calc` → `{ calc }`. |
| Angebote | 6 Angebote: `offen` mit `mailSendable` ohne `notifiedAt`, `offen` ohne E-Mail, `aktiv` mit `scope: 'laufend'`, `abgelaufen` mit `mailSendable` und `notifiedAt`, `zurueckgezogen`, eins mit 60-Zeichen-Name, 50-Zeichen-E-Mail und 200-Zeichen-`internalNote`. `getOfferContext` für einen Kunden ohne Abo und einen mit laufendem Wartungs-Abo (`upcomingCharges`). |
| Speicher | `tasks`: `einrichten`, `senken` überfällig, `senken` später, einer `partial` mit `error`, einer mit 60-Zeichen-Name. `failed` mit 2 Einträgen, `allocatedGb` über `warnGb`. |
- [ ] **Step 4: Prüf-Ausnahmen erweitern**
- In `checks.mjs`: `dark-surface` nimmt zusätzlich `.woms-chat :is(.chat-send, .chat-audio-play, .chat-file-ext, .chat-tray-remove, .chat-tray-retry, .chat-tray-progress)` aus. Der Szenario-Name `fehler-portal` gilt für `console-error` wie `fehler` (erwartete 500-Meldungen ausgenommen).
- `icon-svg` ist dauerhaft für alles unter `.woms-chat` ausgenommen (Sprechblasen mit Anhängen zeichnet derselbe fremde Baustein). Das steht im Gate `p6` (Step 5) als `ignore`, nicht in `checks.mjs`.
- [ ] **Step 5: Ansichten und Gate eintragen**
`views.mjs` bekommt die Hilfe `tablet(page)`: ab 800 px Breite `setViewportSize({ width: 1000, height: 800 })`, sonst nichts. Die Schritte haben die Signatur `steps(page, ctx)`, Anfragen liest man aus `ctx.backend.calls`. Prüfungen in Schritten nutzen `assert` aus `node:assert/strict`. Eine Ausnahme erzeugt den Verstoß `step-failed` mit Ansicht und Meldung (das Gate endet mit Exit 1). Die Kundenseiten-Ansichten tragen die IDs der Übersicht (`kunde-chat`, `kunde-abo`, `kunde-gutschriften`, `kunde-funktionen`), es gibt keine zweite Ansicht für dieselbe Reiter-URL.
| ID | Pfad / Schritte | Prüfung im Schritt |
|---|---|---|
| `chat-verlauf` | `/customers/chat?kunde=<A>` | `.chat-back` bei 1440 `display: none`, bei 390 sichtbar. `.chat-side` bei 390 ausgeblendet. Stil-Sollwerte aus Task 4. |
| `chat-menue`, `chat-anhang-fehler` | wie `chat-verlauf`; Klick `.chat-attach-btn` bzw. `setInputFiles` (`x.pdf`) auf `.chat-composer input[type=file]:not([accept])` | `.chat-menu` sichtbar bzw. `.chat-tray-item.is-error` und `.chat-composer-note` sichtbar |
| `chat-tablet`, `gutschriften-tablet`, `angebote-tablet` | Ansicht mit `tablet` | Chat: Liste und Verlauf nebeneinander, `.chat-input-row` ohne Überlauf. Tabellen: `.hide-md`-Spalten aus, `.hide-sm`-Spalten sichtbar |
| `chat-viel`, `gutschriften-viel` | Satz `viel` | `.chat-list.scrollHeight > clientHeight` |
| `chat-fehler`, `gutschriften-fehler`, `angebote-fehler`, `speicher-fehler` | Szenario `fehler-portal` | `.msg.err` sichtbar; bei Chat und Speicher Klick auf „Erneut versuchen“, die Seite lebt |
| `gutschriften-antrag` | `/customers/gutschriften?antrag=<offen>` | – |
| `gutschriften-sperre` | wie oben; `page.on('dialog', d => d.accept())`; 3 Kästchen, `#credit-note` füllen, Doppelklick „Genehmigen & gutschreiben“; in `#calc-amount` 3 Ziffern mit 50 ms Abstand | nach 1500 ms genau 1 POST `…/decision` und 1 POST `…/calc` in `ctx.backend.calls` (`method`, `url`) |
| `angebote-dialog`, `angebote-vorschlag`, `angebote-laufend` | Klick „Neues Angebot“; im Kunden-Feld „Mu“ tippen; Kunde mit laufendem Abo wählen | `[role=dialog]` sichtbar; letzter `.autocomplete-item` per `elementFromPoint` erreichbar; bei 390 px `.dialog-foot.scrollWidth <= clientWidth` |
| `angebote-tastatur` | `page.on('dialog', d => d.accept())`. Bis zu 15 Mal Tab, bis der Fokus in `table` liegt. Dialog öffnen, Kunden-Feld tippen, Esc, Esc. Erneut öffnen, 30 Mal Tab. Kunde wählen, „Angebot anlegen“, während der 800 ms Esc | Fokus erreicht die Tabelle. Erstes Esc schließt nur die Liste, zweites den Dialog. Fokus bleibt im Dialog. Beim Speichern ist `.dialog-close` `disabled` und der Dialog nach Esc noch da |
Dazu die vorhandenen Ansichten `chat`, `gutschriften`, `angebote`, `speicher`, `kunde-chat`, `kunde-abo`, `kunde-gutschriften`, `kunde-funktionen`. In `gates.mjs` entsteht `p6` nach dem Schema der Übersicht: das Gate `p5` plus `icon-svg` für die Ansichten von P6 und aller früheren Pakete.
```js
const P6_VIEWS = ['chat', 'chat-verlauf', 'chat-menue', 'chat-anhang-fehler', 'chat-tablet', 'chat-viel', 'chat-fehler',
'gutschriften', 'gutschriften-tablet', 'gutschriften-viel', 'gutschriften-fehler', 'gutschriften-antrag', 'gutschriften-sperre',
'angebote', 'angebote-tablet', 'angebote-fehler', 'angebote-dialog', 'angebote-vorschlag', 'angebote-laufend', 'angebote-tastatur',
'speicher', 'speicher-fehler', 'kunde-chat', 'kunde-abo', 'kunde-gutschriften', 'kunde-funktionen']
const prev = GATES.p5.scoped['icon-svg']
GATES.p6 = {
...GATES.p5,
scoped: { ...GATES.p5.scoped, 'icon-svg': { views: [...new Set([...prev.views, ...P6_VIEWS])], ignore: [...(prev.ignore ?? []), '.woms-chat'] } },
}
```
`step-failed`, `external-request` und `console-error` sind wie in jedem Gate aktiv, `dark-surface`, `low-contrast` und `overflow-x` kommen über `GATES.p5.checks` für alle Ansichten.
- [ ] **Step 6: Rot-Beweis**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p6 && "$W/shots.sh" p6 "$W/shots/p6-t2" --only chat-verlauf,angebote --gate p6; echo "exit $?"`
Expected: `exit 1` mit `icon-svg` mindestens in `angebote` (`FaPlus`). Das Bild `chat-verlauf` zeigt den echten Composer mit Anhängen.
### Task 3: Globale Ergänzungen
**Files:**
- Modify: `src/styles/global.css` (neuer Block am Ende)
**Interfaces:**
- Produces: `.table.table-wrap td` (lange Wörter brechen), `.table tr[aria-current="true"] td`, `.dialog-foot { flex-wrap: wrap }`, `.autocomplete .input { width: 100% }` (P7 baut auf die beiden letzten).
- [ ] **Step 1: Block anlegen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "aria-current|dialog-foot|\.autocomplete \.input" src/styles/global.css`
Expected: zeigt, welche Regeln P1 bis P5 schon angelegt haben. Der Block steht am Dateiende, hinter allen anderen Regeln, damit er bei gleicher Spezifität gewinnt. Unter `/* ===== Kunden-Unterseiten (P6) ===== */` stehen nur die fehlenden:
- `.table.table-wrap td { overflow-wrap: anywhere }`
- `.table tr[aria-current="true"] td { background: var(--bg-subtle) }`
- `.dialog-foot { flex-wrap: wrap }`
- `.autocomplete .input { width: 100% }`
- [ ] **Step 2: Bauen und Commit**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" --nur-build`
Expected: Exit 0, die Bundle-Größen werden ausgegeben.
```bash
git add src/styles/global.css
git commit -m "Design P6: kleine Tabellen-, Dialog- und Autocomplete-Ergaenzungen" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 4: `chat.css` neu, Fehlerzeile im Verlauf
**Files:**
- Modify: `src/styles/chat.css` (komplett neu), `src/components/chat/ChatThread.jsx` (Import Z. 2, Fehlerzeile Z. 120 bis 125)
- Test: `tests/unit/chatCss.test.mjs` (neu)
**Interfaces:**
- Consumes: Tokens der Spec (`--bg`, `--bg-hover`, `--bg-subtle`, `--grey-bg`, `--text*`, `--line*`, `--accent*`, `--red-fg`, `--danger`, `--radius*`, `--ring`, `--shadow-pop`), `.msg.err`, `Button`.
- Produces: `.woms-chat` mit `--chat-bubble-in`, `--chat-bubble-out`, `--chat-track`; Seitenklassen `.chat-page`, `.chat-side`, `.chat-list`, `.chat-list-item`, `.chat-list-top`, `.chat-list-last`, `.chat-list-time`, `.chat-unread`, `.chat-main`, `.chat-back`.
- [ ] **Step 1: Failing Test schreiben**
`chatCss.test.mjs` liest `chat.css` als Quelltext (Vorbild `adminRoute.test.mjs`), entfernt Kommentare und zerlegt per `/([^{}]+)\{/g` in Selektoren (ohne `@…`, Kommas gesplittet). 5 Tests:
1. Jeder Selektor mit `/\.chat-(bubble|meta|messages|notice|input-row|icon-btn|send|menu|tray|rec|audio|att|file|composer|drop-active)\b/` beginnt mit `.woms-chat` (`deepEqual(bad, [])`).
2. Kein `!important`.
3. `match(/@media \(max-width: 860px\)/)` und `doesNotMatch(/720px/)`.
4. `ChatThread.jsx` enthält unverändert `className="woms-chat" ref={zoneRef}`, `className="chat-messages" ref={boxRef}` und `<div ref={composerRef} />`.
5. `match(/\.chat-page \.chat-back\s*\{[^}]*display:\s*none/)`.
- [ ] **Step 2: Test laufen lassen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/chatCss.test.mjs`
Expected: FAIL bei 3 (alter 720-px-Wert) und 5. Test 4 besteht schon.
- [ ] **Step 3: `chat.css` neu schreiben**
Der Kopfkommentar nennt: Das Portal-CSS lädt zur Laufzeit zuletzt, Gleichstand verliert, deshalb ist jede Regel strikt spezifischer und es gibt kein `!important`. Alle P1-Variablen und -Fixes aus P1 Task 9 Step 2 bleiben unverändert. Neu sind nur `--chat-bubble-in: var(--bg-hover)`, `--chat-bubble-out: var(--grey-bg)` und `--chat-track: rgba(26,31,54,.2)`. Maße, Abstände, Schriftgrößen und Radien folgen dem Prototyp und den Tokens der Spec und entstehen beim Bauen. Verbindlich sind die Spezifitäten und Abweichungen der Tabellen sowie die Messwerte aus Step 6.
Überschreibungen des Portal-Bausteins. Die Spalte „Eigen > Fremd“ nennt die Spezifität der eigenen Regel vor der überstimmten Portal-Regel:
| Ziel | Selektor (Eigen > Fremd) | Abweichung vom Portal-Stil |
|---|---|---|
| Container, Verlauf | `.woms-chat`, `.woms-chat .chat-messages` | Flex-Spalte, `min-width: 0`. Verlauf scrollt in sich (`max-height: 60vh`), ohne Rand und Hintergrund. |
| Blase, Zeit | `.woms-chat .chat-bubble` (0,2,0 > 0,1,0), `.chat-bubble.mine`, `.chat-meta`, `.chat-notice` | Fremde Blase `--chat-bubble-in`, eigene `--chat-bubble-out` (rechts), kein Rand. `max-width: 78%`, `overflow-wrap: anywhere`. Zeit und Hinweis 12 px in `--text-2`. |
| Datei-Chip | `.woms-chat .chat-att-file` (0,2,0 > 0,1,0), `:hover` (0,3,0 > 0,2,0) | `var(--bg)`, `border: 0`, `box-shadow: var(--ring)`. Hover `--bg-hover`. |
| Audio, Fokus | `.chat-audio-bar` (0,2,0 > 0,1,0); `.chat-att-image:focus-visible`, `.chat-audio-bar:focus-visible` (0,3,0 > 0,2,0); `.chat-bubble.mine .chat-audio-play` (0,4,0 > 0,3,0); `.chat-audio-dl:hover` (0,3,0 > 0,2,0); `.chat-audio.is-error .chat-audio-time` (0,4,0 > 0,3,0) | Leiste `--chat-track`. Fokusring `2px solid var(--accent)` statt weiß. Abspielen `--chat-accent` mit `--chat-on-accent`. Download-Hover `--accent-soft`. Fehlerzeit `--red-fg`. |
| Eingabefeld | `.woms-chat .chat-input-row textarea` (0,2,1 > 0,1,1), `:focus` (0,3,1), Media `max-width: 640px` | `border: 0`, `box-shadow: var(--ring)`, `font: inherit`. Fokus `0 0 0 1px var(--accent), 0 0 0 4px var(--accent-ring)` ohne `outline`. Handy `font-size: 16px` (gegen das iOS-Zoomen). Höhe und Radius des Portals bleiben. |
| Icon-Knöpfe, Senden | `.chat-icon-btn` (0,2,0 > 0,1,0), `:hover:not(:disabled)` (0,4,0 > 0,3,0), `[aria-expanded="true"]` (0,3,0 > 0,2,0), `.chat-icon-btn:disabled`, `.chat-send:disabled` (0,3,0 > 0,2,0), `.chat-send.is-busy:disabled` (0,4,0 > 0,3,0) | `border: 0`, `box-shadow: var(--ring)`, `var(--bg)`. Hover `--bg-hover`. Offen `--accent-soft` mit 1-px-Akzentring. Deaktiviert `opacity: .55; cursor: not-allowed`, beschäftigt `opacity: .8; cursor: progress`. Senden bleibt über die P1-Variablen schwarz mit weißer Schrift. |
| Aufnahme | `.chat-rec .chat-icon-btn` (0,3,0 > 0,2,0), `:hover` (0,4,0 > 0,3,0); `.chat-rec-stop`, `.chat-rec-wave` (hinter der Icon-Knopf-Regel), `.chat-rec-dot` (0,2,0 > 0,1,0) | Knöpfe ohne Ring und Hintergrund, Hover `--accent-soft`. Farbe bzw. Punkt `--danger`. |
| Anhang-Menü | `.chat-menu` (0,2,0 > 0,1,0); `.chat-menu button` (0,2,1 > 0,1,1), `:hover`, `:focus-visible` (0,3,1 > 0,2,1) | Menü ohne Rand, mit `--shadow-pop`. Hover und Fokus `--bg-hover`, Fokus zusätzlich `outline: 2px solid var(--accent); outline-offset: -2px` (die Fremdregel setzt `outline: none`). |
| Vorschau, Hinweise | `.chat-tray-item` (0,2,0 > 0,1,0); `.chat-tray-item.is-error .chat-file-size` (0,4,0 > 0,3,0); `.chat-composer-note` (0,2,0 > 0,1,0), `.is-info` (0,3,0 > 0,2,0) | Vorschau `--bg-hover` mit transparentem Rand. Die rote Fehlervariante der Fremdregel (gleiche Spezifität, später geladen) bleibt absichtlich. Fehlertexte `--red-fg`, Info `--text-2`. |
| Ablegen | `.woms-chat.chat-drop-active::after` (0,2,1 > 0,1,1) | helle Fläche `rgba(255,255,255,.92)`, Schrift `--text`, Rand `--text-3` |
Seitenlayout ohne Präfix. Es folgt dem Prototyp, verbindlich sind:
- `.chat-page` ist ein Grid mit 320 px für die Liste und `minmax(0, 1fr)` für den Verlauf. Bei `max-width: 1100px` ist die Liste 260 px breit.
- `.chat-side` und `.chat-main` haben `min-width: 0`. Zwischen ihnen steht eine senkrechte Haarlinie (`--line`).
- `.chat-list` scrollt in sich (`max-height: 70vh; overflow-y: auto`). `.chat-list-item` ist eine Haarlinien-Zeile ohne Kasten und Rand, Hover `--bg-subtle`, `[aria-current="true"]` `--accent-soft`.
- `.chat-list-last` kürzt mit Ellipsis. `.chat-unread` ist eine kleine Zahl in `--accent`, fett, 12 px, ohne Pille.
- `.chat-page .chat-back { display: none }`, Spezifität (0,2,0) > `.btn` (0,1,0).
- Bei `max-width: 860px` (nicht 720 px): einspaltig. `.chat-page.has-thread .chat-side` und `.chat-page:not(.has-thread) .chat-main` sind `display: none`, Seite und Haupt ohne Rand und Innenabstand, `.chat-page .chat-back { display: inline-flex }`, `.woms-chat .chat-bubble { max-width: 90% }`.
- [ ] **Step 4: Fehlerzeile im Verlauf**
In `ChatThread.jsx` bleibt der Zeilencontainer `flex items-center gap-2 wrap`. Der Text wird `<span className="msg err" role="alert">`, der Knopf `<Button onClick={…}>Erneut versuchen</Button>` ohne Icon. Der Import `react-icons/fa6` entfällt. Container, Effekte und `chat-notice muted` bleiben.
- [ ] **Step 5: Test grün**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/chatCss.test.mjs tests/unit/loadChatComposer.test.mjs`
Expected: PASS.
- [ ] **Step 6: Gegen das echte Fremd-CSS prüfen**
`chat-verlauf` bekommt einen Schritt, der `getComputedStyle` liest und mit `assert.equal` vergleicht:
- `.chat-bubble.theirs` `background-color` = `rgb(246, 248, 250)`, `.chat-bubble.mine` = `rgb(235, 238, 241)`.
- `.chat-send` `background-color`/`color` = `rgb(17, 17, 17)`/`rgb(255, 255, 255)`.
- `.chat-attach-btn` `border-top-width` = `0px`, `box-shadow` ≠ `none`.
- `.chat-att-file` `background-color` = `rgb(255, 255, 255)`, `border-top-width` = `0px`.
- `.chat-audio-bar` `background-color` = `rgba(26, 31, 54, 0.2)`.
- `.chat-input-row textarea` `border-top-width` = `0px`.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p6 && "$W/shots.sh" p6 "$W/shots/p6-t4" --only chat-verlauf,chat-menue,chat-anhang-fehler --gate p6; echo "exit $?"`
Expected:
- Schritt-Prüfungen bestehen (kein `step-failed`). Der Bericht enthält höchstens `icon-svg` am alten Zurück-Pfeil von `CustomerChatPage` (Task 5). Nur dann ist `exit 1` hier erlaubt.
- `chat-verlauf` bei 1440 px: Blasen hellgrau, Senden schwarz mit weißer Schrift, Icon-Knöpfe weiß mit Ring, Datei-Chip weiß.
- `chat-anhang-fehler`: rote Fehlerzeile und rot umrandete Vorschau.
- [ ] **Step 7: Commit**
```bash
git add src/styles/chat.css src/components/chat/ChatThread.jsx tests/unit/chatCss.test.mjs
git commit -m "Design P6: chat.css neu (Praefix .woms-chat, Umschaltung bei 860 px), Fehlerzeile ohne Icon" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 5: Chat-Seite
**Files:**
- Modify: `src/pages/CustomerChatPage.jsx` (Z. 3, 61 bis 110), `src/styles/global.css` (KOMPAT-Regel `.side-count` löschen)
**Interfaces:**
- Consumes: `PageHeader`, `Card`, `Button`, `EmptyState`, `AutocompleteInput` (Klasse `input`), `.msg.err`, Klassen aus Task 4, `lastLine`, `sortChats`.
- Produces: Seite und Adresse `/customers/chat?kunde=<id>` unverändert.
- [ ] **Step 1: Seite umbauen**
- `react-icons`, `size="sm"` und `Card pad={false}` um die Liste entfallen. Die Liste ist `.chat-list` direkt unter dem Suchfeld.
- Das Suchfeld bleibt `AutocompleteInput` („Neue Nachricht an Kunde…“, `onPick` öffnet nur die volle Beschriftung).
- Fehlerzeile: `div.flex.items-center.gap-2.wrap` mit `span.msg.err[role=alert]` und `Button` „Erneut versuchen“. Der Leerzustand „Noch keine Unterhaltungen“ bleibt `EmptyState`.
- Jede Unterhaltung ist ein `button.chat-list-item` mit `aria-current={c.customerId === kunde ? 'true' : undefined}`. Zeile 1 (`.chat-list-top`): `<strong>` Name und `.chat-list-time`. Zeile 2: `.chat-list-last` mit `lastLine(c)` und bei `unreadFromCustomer > 0` `span.chat-unread`.
- Der Zurück-Knopf steht vor der Karte: `<Button className="chat-back" onClick={() => setParams({})}>← Alle Unterhaltungen</Button>`, darunter `<Card title={nameOf(kunde)}><ChatThread … /></Card>`. Ohne Auswahl bleibt die Karte mit `EmptyState` („Unterhaltung links auswählen“ / „Oder oben eine neue Nachricht an einen Kunden beginnen.“) und ohne Titel. Eine `Card` ohne Titel und ohne Aktionen rendert kein `h2` (Übersicht).
- [ ] **Step 2: `.side-count` löschen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "side-count" src`
Expected: nur noch die KOMPAT-Regel in `global.css`. Sie wird gelöscht (P6 ist ihr letzter Nutzer), danach 0 Treffer.
- [ ] **Step 3: Prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p6 && "$W/shots.sh" p6 "$W/shots/p6-t5" --only chat,chat-verlauf,chat-tablet,chat-viel,chat-fehler,kunde-chat --gate p6`
Expected: Exit 0. Bei 390 px zeigt `chat` nur die Liste ohne Überlauf, `chat-verlauf` nur den Verlauf mit „← Alle Unterhaltungen“. Bei 1440 px zwei Spalten mit senkrechter Trennlinie, ohne Zurück-Knopf. `chat-tablet`: Liste 260 px.
- [ ] **Step 4: Commit**
```bash
git add src/pages/CustomerChatPage.jsx src/styles/global.css
git commit -m "Design P6: Chat-Liste als Haarlinien-Zeilen, Zaehler und Zurueck-Pfeil als Text, side-count entfaellt" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 6: Gutschriften-Liste
**Files:**
- Modify: `src/pages/CreditsPage.jsx` (Z. 10 bis 12), `src/components/credits/CreditsView.jsx` (Z. 25 bis 29), `src/components/credits/CreditList.jsx` (Z. 2, 7, 24 bis 62)
**Interfaces:**
- Consumes: `Tabs`, `Badge`, `EmptyState`; `.table`, `.table-wrap`, `.hide-sm`, `.hide-md`, `.right`, `.num`, `.cell-title`, `.cell-sub`, `.link`, `.loading`, `.msg.err`, `.table-foot`; Helfer aus Task 1.
- Produces: `CreditList({ customerId, selectedId, onSelect, reloadKey })` mit unveränderten Props.
- [ ] **Step 1: `CreditList` umbauen**
- Filter `<Tabs tabs={[{ id: 'open', label: 'Offen' }, { id: 'all', label: 'Alle' }]} …/>`. State, Effekt und `listCredits` bleiben.
- Laden `div.loading` „Wird geladen …“, Fehler `div.msg.err[role=alert]`, leer `EmptyState` mit „Keine offenen Anträge.“ bzw. „Noch keine Anträge.“.
- `table.table.table-wrap` mit denselben Spalten und Zellinhalten wie heute. `<br />` plus `area-muted` wird `.cell-sub`, der Kundenname `.cell-title`. Klassen auf `th` und `td`: Eingang `hide-sm`, Leistung / bisher bei `hide-md`, Rechnung `hide-sm right num`, Bezahlt bis `hide-md`.
- Statuszelle: `Badge` und, wenn nicht leer, `.cell-sub` mit `creditStatusNote(r)`. Letzte Zelle ohne Kopftext: `<button type="button" className="link" onClick={() => onSelect(r.id)}>{creditActionLabel(r.status)}</button>`.
- Die Zeile der offenen Gutschrift trägt `aria-current="true"`. Imports `FaSpinner`, `Card`, `admin-areas.css` und die zwei Inline-Styles entfallen.
- [ ] **Step 2: `CreditsView` und `CreditsPage`**
`CreditsView`: Das `div` mit `marginBottom` um `CreditDetail` entfällt, die Liste steht in `<div className={selected ? 'mt-3' : undefined}>`. `CreditsPage`: Der Absatz „Übernahmegutschrift: …“ bleibt wörtlich als `<p className="table-foot">` ohne Inline-Style.
- [ ] **Step 3: Prüfen und Commit**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p6 && "$W/shots.sh" p6 "$W/shots/p6-t6" --only gutschriften,gutschriften-tablet,gutschriften-viel,gutschriften-fehler,kunde-gutschriften --gate p6`
Expected: Exit 0. 390 px: Kunde, Status, Aktion, kein Überlauf mit dem 60-Zeichen-Namen. 1000 px: Eingang und Rechnung sichtbar, Leistung und Bezahlt bis aus. 1440 px: alle sieben Spalten.
```bash
git add src/pages/CreditsPage.jsx src/components/credits/CreditsView.jsx src/components/credits/CreditList.jsx
git commit -m "Design P6: Gutschriften-Liste als Tabelle mit Reitern und Spaltenprioritaet" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 7: `CreditDetail`
**Files:**
- Modify: `src/components/credits/CreditDetail.jsx` (Imports Z. 1 bis 9, Markup Z. 116 bis 284)
**Interfaces:**
- Consumes: `Card`, `Badge`, `Button`, `Field`; `.two-col`, `.kv`, `.field-row`, `.check`, `.list-row`, `.cell-sub`, `.prose`, `.msg`, `.loading`, `.link`; `checkGlyph`, `checkTone`.
- Produces: `CreditDetail({ id, onClose, onChanged, onOpen })` unverändert.
- [ ] **Step 1: Logik unverändert**
Nicht angefasst werden `load`, `figures`, `setFigure` (300-ms-Timer), `decide` (`decisionError`, `window.confirm`, `busyRef`), `apply`, `canApprove`, `amountTouched`. Neu ist nur der Zustand `busyAction` (`''`, `'approve'`, `'question'`, `'reject'`, `'apply'`), den `decide` und `apply` neben `busy` setzen und zurücksetzen, damit der gedrückte Knopf „Speichert …“ bzw. „Sendet …“ zeigen kann.
- [ ] **Step 2: Markup umbauen**
- **Kopf:** `<Card title={`Antrag ${r.ref} · ${c.name || c.companyName}`} actions={<Button onClick={onClose}>Schließen</Button>}>`, darunter `p.muted` „eingegangen … “ mit `Badge` und `p.msg.ok[role=status]` für `message`.
- **Aufbau:** `div.two-col` mit zwei Spalten-`div`. Jede Gruppe ist ein `<Card title="…">` mit dem heutigen `h4`-Text. Den Abstand zwischen den Karten einer Spalte liefert `.page section + section` aus P1, P6 baut dafür keine eigene Regel. Die Reihenfolge der Abschnitte bleibt: links Antrag, Grund / Nachricht, Nachweise, Verlauf, rechts Kunde & Vertrag, Weitere Anträge, Automatische Prüfung, Prüfung durch dich, Berechnung bzw. Entscheidung.
- **Ersetzungen:**
- `dl.area-kv` wird `dl.kv`.
- Mehrzeiler (`<br />` plus `area-muted`) werden `.cell-sub`.
- Nachweise, Verlauf, Weitere Anträge und Prüfpunkte werden `.list-row`. Text aus `r.message` und Verlaufstexten ist `.prose`.
- Alle Links (`domain`, Nachweise) bekommen `className="link"`. Die Nummer in „Weitere Anträge“ ist `<button type="button" className="link" onClick={() => onOpen?.(o.id)}>` statt `<a href="#">`.
- Prüfpunkte tragen `<Badge tone={checkTone(k.status)}>{checkGlyph(k.status)}</Badge>`.
- „In Stripe öffnen“ ist `Button as="a" …` ohne Icon, `KundenansichtButton` ohne `variant`.
- **Formular:** Die manuellen Punkte stehen als `div.field` mit je einem `label.check`. „Berechnung“ besteht aus zwei `.field-row` mit `Field label htmlFor` (IDs `calc-amount`, `calc-months`, `calc-paid-until`, `calc-from`, Klassen `input`/`select`). `calcText` ist `p.strong`, der Hinweis `p.muted.small`. „Gutschrift (€)“ (`#credit-amount`) und das Nachrichtenfeld (`textarea#credit-note.textarea`, `maxLength={4000}`, langes Label wörtlich) sind `Field`.
- **Knopfreihe** `div.flex.items-center.gap-3.wrap`:
- `Button variant="primary"` „Genehmigen & gutschreiben“ (`disabled={!canApprove}`),
- `Button` „Rückfrage senden“,
- `Button variant="danger"` „Ablehnen“.
- Bei `busyAction` zeigt der gedrückte Knopf „Speichert …“ (genehmigen, ablehnen) bzw. „Sendet …“ (Rückfrage).
- „Jetzt in Stripe gutschreiben“ ist `variant="primary"` mit „Speichert …“ bei `apply`.
- **Meldungen:** `actionError` als `p.msg.err[role=alert]`. Ladefehler `<Card><p className="msg err" role="alert">…</p><Button onClick={onClose}>Schließen</Button></Card>` (ohne Titel und Aktionen, also ohne `h2`). Laden `div.loading`.
- `react-icons`, `admin-areas.css` und alle 9 Inline-Styles entfallen.
- [ ] **Step 3: Prüfen und Commit**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p6 && "$W/shots.sh" p6 "$W/shots/p6-t7" --only gutschriften-antrag,gutschriften-sperre --gate p6`
Expected: Exit 0, die Sperr-Prüfung besteht (1 POST `…/decision`, 1 POST `…/calc`). `gutschriften-antrag` bei 1440 px zweispaltig mit genau einem schwarzen Knopf und „Ablehnen“ als rotem Text, bei 390 px einspaltig ohne Überlauf.
```bash
git add src/components/credits/CreditDetail.jsx
git commit -m "Design P6: Gutschrift-Detail zweispaltig mit Abschnitten und kv-Listen, Verhalten unveraendert" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 8: Abo-Angebote: Seite, Ansicht, Liste
**Files:**
- Modify: `src/pages/OffersPage.jsx` (Z. 10), `src/components/offers/OffersView.jsx` (Z. 2, 14 bis 34), `src/components/offers/OfferList.jsx` (Z. 2, 7, 54 bis 118)
**Interfaces:**
- Consumes: `Tabs`, `Badge`, `Button`, `EmptyState`; Klassen wie Task 6; `filterOffers`, `offerRowActions`, `offerMailLine`.
- Produces: `OfferList({ customerId, reloadKey, onMessage, onChanged })` und `OffersView({ customerId, customerName })` unverändert.
- [ ] **Step 1: `OfferList` umbauen**
- Tabs „Offen & aktiv“ (`open`) und „Alle“ (`all`), `shown = filterOffers(offers, filter)`. Laden, Fehler und Leerzustand wie bei den Gutschriften („Keine offenen oder aktiven Angebote.“ / „Noch keine Angebote.“).
- `table.table.table-wrap` mit denselben Spalten und Zellinhalten wie heute, Mehrzeiler als `.cell-sub`. Klassen: Angelegt `hide-md`, Gültig bis `hide-sm`. Stripe-Links `a.link`.
- Statuszelle: `Badge`, `.cell-sub` „seit …“ / „wartet auf Abschluss“, „Stripe: Coupon · Abo“, danach `offerMailLine(o)` als `.cell-sub` (`warn` als `span.msg.warn`).
- Aktionen unter den Status-Zeilen, `div.flex.gap-3.wrap.mt-1`, nur wenn `sendLabel` oder `canWithdraw`:
- `<button type="button" className="link" title="Angebots-E-Mail an … senden" onClick={() => sendMail(o)} disabled={busyId === o.id}>{sendLabel}</button>`
- `Button variant="danger"` „Zurückziehen“
- `withdraw` und `sendMail` bleiben unverändert (2 `confirm`, 2 `alert`).
- Imports `FaSpinner`, `Card`, `admin-areas.css` und die drei Inline-Styles entfallen.
- [ ] **Step 2: `OffersView` und `OffersPage`**
`OffersView`: Meldung als `<p className="msg ok mb-2" role="status">`, dann `<div className="toolbar"><Button variant="primary" …>Neues Angebot</Button></div>` (ohne `FaPlus`, auch bei offenem Dialog sichtbar), dann `OfferList`, am Ende `{showForm && <OfferForm … />}` ohne Stil-`div`. `OffersPage`: Der Absatz „Ein Angebot legt sofort …“ bleibt wörtlich als `<p className="table-foot">`.
- [ ] **Step 3: Prüfen und Commit**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p6 && "$W/shots.sh" p6 "$W/shots/p6-t8" --only angebote,angebote-tablet,angebote-fehler,kunde-abo --gate p6`
Expected: Exit 0. 390 px: Kunde, Angebot, Status, die 50-Zeichen-E-Mail bricht, „E-Mail senden“ und „Zurückziehen“ sind erreichbar. 1440 px: alle fünf Spalten.
```bash
git add src/pages/OffersPage.jsx src/components/offers/OffersView.jsx src/components/offers/OfferList.jsx
git commit -m "Design P6: Angebote-Liste als Tabelle mit Reitern, Aktionen als Textlinks" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 9: `OfferForm` als Dialog
**Files:**
- Modify: `src/components/offers/OfferForm.jsx` (Imports Z. 1 bis 8, Markup Z. 85 bis 143)
**Interfaces:**
- Consumes: `Dialog({ title, onClose, busy, footer, children })`, `Field`, `Button`, `AutocompleteInput` (Prop `id`), `.field-row`, `.check`, `.msg.err`.
- Produces: `OfferForm({ fixedCustomerId, fixedCustomerName, onCreated, onCancel })` unverändert. `onCancel` ist zugleich `onClose` des Dialogs.
- [ ] **Step 1: Markup als Dialog**
Kontext-Effekt, `evaluation`, `save` (`window.confirm(offerConfirm(…))`), `suggestions` und `onPick` bleiben. Der Rückgabewert wird `<Dialog title="Neues Angebot" onClose={onCancel} busy={busy} footer={…}>` ohne `<form>`. Felder in der heutigen Reihenfolge und mit den heutigen Labeltexten, alle als `Field` mit neuen IDs `offer-*` für die Label-Bindung:
- „Kunde“: mit `fixedCustomerId` `<p className="strong">{fixedCustomerName || 'Kunde'}</p>`, sonst `AutocompleteInput id="offer-customer"`.
- `.field-row` „Produkt“ (`select.select`) und „Preis pro Monat im Angebot (€)“ (`input.input`).
- `.field-row` „für“ (`select.select`) und „Angebot gültig bis“ (`input.input type="date"`).
- „Hinweis für den Kunden …“ und „Interne Notiz …“ (`input.input`, `maxLength` 500 bzw. 2000).
- `label.check` „Kunden per E-Mail über das Angebot informieren“.
- Vorschau: Fehlerzeilen `p.msg.err`, Vorschauzeilen als `p`, die Platzhaltertexte wörtlich. Bei `error` `p.msg.err[role=alert]`.
`footer`: `Button variant="primary" type="button"` (`onClick={save}`, `disabled={!evaluation || evaluation.errors.length > 0 || busy}`) mit `busy ? 'Speichert …' : evaluation?.saveLabel || 'Angebot anlegen'` und `Button type="button"` „Abbrechen“ (`onClick={onCancel}`, `disabled={busy}`). `FaSpinner`, `Card`, `admin-areas.css`, die vier Inline-Styles und die Klasse `offer-form` entfallen.
- [ ] **Step 2: Clipping und Tastatur prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p6 && "$W/shots.sh" p6 "$W/shots/p6-t9" --only angebote-dialog,angebote-vorschlag,angebote-laufend,angebote-tastatur,kunde-abo --gate p6`
Expected: Exit 0, alle Schritt-Prüfungen bestehen. `angebote-vorschlag`: Liste vollständig erreichbar. `angebote-laufend` bei 390 px: Der lange Speichern-Text bricht in eine zweite Zeile der Fußzeile. `angebote-tastatur`: Esc schließt erst die Liste, dann den Dialog, beim Speichern wirken „ד und Esc nicht.
Besteht die Clipping-Prüfung nicht, bekommt `.dialog-body .autocomplete-list` im Block aus Task 3 `max-height: 200px` (nie `!important`, die Komponente bleibt unberührt).
- [ ] **Step 3: Commit**
```bash
git add src/components/offers/OfferForm.jsx
git commit -m "Design P6: Angebots-Formular als Dialog (Esc, Sperre beim Speichern, Vorschlagsliste geprueft)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
War die Abhilfe nötig, kommt `src/styles/global.css` ausdrücklich in dieses `git add`.
### Task 10: E-Mail-Speicher
**Files:**
- Modify: `src/pages/StorageTasksPage.jsx` (Imports Z. 1 bis 8, Markup Z. 38 bis 98)
**Interfaces:**
- Consumes: `PageHeader`, `Card`, `Badge`, `Button`, `EmptyState`; `.table.table-wrap`, `.hide-sm`, `.hide-md`, `.cell-sub`, `.link`, `.msg`, `.list-row`, `.table-foot`, `.loading`; `storageDue`.
- Produces: Seite `/customers/email-speicher` unverändert.
- [ ] **Step 1: Markup umbauen**
- Fehler `div.flex.items-center.gap-2.wrap` mit `span.msg.err[role=alert]` und `Button` „Erneut versuchen“ (`onClick={load}`). Laden `div.loading` „Wird geladen …“.
- Zusammenfassung `<p className={summary.warn ? 'msg warn mb-2' : 'muted mb-2'}>`.
- Bei null Aufgaben `EmptyState` „Keine offenen Speicher-Aufgaben.“, sonst `table.table.table-wrap` mit den heutigen Spalten und Zellinhalten, Mehrzeiler als `.cell-sub`. Klassen: Fällig `hide-sm`, Bestellt `hide-md`.
- Fällig-Zelle: `Badge tone={due.tone}` mit `due.label` und `due.sub` (`.cell-sub`, wenn `due.subMuted`). Kundenname `Link className="link"` auf `/customers/${t.customerId}`. Die Teilfehler-Zeile `partialText(t)` ist `span.msg.err`.
- Letzte Zelle ohne Kopftext: `<button type="button" className="link" data-storage-done={t.id} onClick={() => done(t)} disabled={busyId === t.id}>{busyId === t.id ? 'Speichert …' : 'Erledigt'}</button>`.
- „Gescheiterte Änderungen (letzte 30 Tage)“ bleibt `Card title`, die Zeilen sind `.list-row` (Text `failedText` als `span.muted`). Der lange Hinweisabsatz bleibt wörtlich als `<p className="table-foot">`.
- `done`, `window.confirm('Stripe-Menge von Hand korrigiert?')` und `window.alert` bleiben. `react-icons`, `admin-areas.css` und beide Inline-Styles entfallen.
- [ ] **Step 2: Prüfen und Commit**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p6 && "$W/shots.sh" p6 "$W/shots/p6-t10" --only speicher,speicher-fehler --gate p6`
Expected: Exit 0. 390 px: Kunde, Aufgabe und „Erledigt“ ohne Überlauf. 1440 px: alle fünf Spalten, Fällig als Badge mit Datum.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -c "data-storage-done" src/pages/StorageTasksPage.jsx`
Expected: `1`.
```bash
git add src/pages/StorageTasksPage.jsx
git commit -m "Design P6: E-Mail-Speicher als Tabelle, Erledigt als Textlink mit data-storage-done" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 11: `admin-areas.css` löschen, KOMPAT-Reste
**Files:**
- Delete: `src/styles/admin-areas.css`
- Modify: `src/styles/global.css` (KOMPAT-Zeilen `.credit-cols a`, `.area-rows a`)
**Interfaces:**
- Consumes: `$W/css-klassen.mjs` aus P1. P4 hat die `.funktionen-*`-Regeln nach `global.css` verlegt, die Tasks 6 bis 10 haben die Importzeilen der P6-Dateien entfernt.
- Produces: keine Datei `admin-areas.css`, keinen Import und keine Klasse aus ihr.
- [ ] **Step 1: Restnutzer ermitteln**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "\barea-|credit-(row|cols|files|checks|history|manual|calc|field)|offer-(row|form)|storage-row" src --glob '!*.css'; rg -n "admin-areas" src`
Expected: keine Treffer, auch nicht in `CustomerFunktionen.jsx` und `AboStatusCard.jsx`. Ein Treffer in einer P6-Datei wird dort behoben. Ein Treffer in einer P4-Datei heißt, dass P4 sie nicht vollständig gelöst hat: stoppen und melden. Das Muster enthält bewusst kein `funktionen-`, denn diese Klassen leben in `global.css`.
- [ ] **Step 2: Datei löschen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git rm -q src/styles/admin-areas.css && rg -n "admin-areas" src`
Expected: keine Treffer.
- [ ] **Step 3: KOMPAT-Reste löschen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "credit-cols|area-rows|side-count" src/styles`
Expected: genau die beiden Zeilen in `global.css` (KOMPAT „Links in Bestandsklassen“) für `.credit-cols a` und `.area-rows a`. Beide werden gelöscht, `.side-count` ist schon seit Task 5 weg. Danach liefert derselbe Befehl keine Treffer.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node "$W/css-klassen.mjs" "$WT" | node -e "const j=JSON.parse(require('fs').readFileSync(0));console.log(j.undefinedUsed.join('\n'))"`
Expected: nur die in P1 erlaubten Namen (`p-4`, `btn-sm`, `status-filter`, `supporters-editor`, `timeline`, Bootstrap-Namen in `WorksheetList.jsx`). Meldet das Skript die gelöschte Datei als fehlend, wird sie in der Dateiliste von `css-klassen.mjs` (unter `$W`) gestrichen.
- [ ] **Step 4: Bauen und Commit**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p6 && "$W/shots.sh" p6 "$W/shots/p6-t11" --only kunde-abo,kunde-gutschriften,kunde-funktionen,gutschriften,angebote,speicher --gate p6`
Expected: Exit 0. Die Kundenseiten-Reiter `kunde-abo` und `kunde-funktionen` sehen aus wie nach P4 (Vergleich mit `$W/shots/p4`), die drei Listen wie nach Task 6, 8 und 10.
```bash
git status --short
git add src/styles/global.css
git commit -m "Design P6: admin-areas.css geloescht, KOMPAT-Reste entfernt" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
`git status --short` vor dem Commit darf nur `D src/styles/admin-areas.css` (vom `git rm` schon vorgemerkt) und `M src/styles/global.css` zeigen. Die Löschung steht damit im selben Commit.
### Task 12: Gate P6, Review, Tag, Übergabe
- [ ] **Step 1: Rebase und Ausgangswerte**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git rebase origin/test && "$W/check.sh"`
Expected: Exit 0, `# fail 0`, `# pass` = N5 + 15 (10 Lib-Tests, 5 `chatCss`). Liegen neue Commits auf `test`, ist die Zahl entsprechend höher und wird notiert. Die Bundle-Größen aus der Ausgabe werden mit P5 verglichen, das CSS sollte kleiner sein (`admin-areas.css` ist weg).
- [ ] **Step 2: Vollständiger Gate-Lauf**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p6 && "$W/shots.sh" p6 "$W/shots/p6" --gate p6`
Expected: Exit 0, in allen Ansichten kein Verstoß gegen `dark-surface`, `low-contrast`, `overflow-x`, `console-error`, `external-request`, `step-failed`. In den P6-Ansichten dazu kein `icon-svg` außerhalb von `.woms-chat`.
- [ ] **Step 3: Inventur und Hygiene**
Run:
```bash
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && for f in src/components/credits/CreditDetail.jsx src/components/offers/OfferList.jsx src/components/offers/OfferForm.jsx src/pages/StorageTasksPage.jsx; do echo "$f $(rg -o 'window\.confirm|window\.alert|busyRef|data-storage-done|id="(calc|credit)-[a-z-]+"' $f | wc -l)"; done
rg -n "react-icons|size=\"sm\"|style=\{\{|!important" src/pages/{CustomerChat,Credits,Offers,StorageTasks}Page.jsx src/components/{chat,credits,offers} src/styles/chat.css
rg -n "admin-areas" src
git diff --stat design-p5..HEAD -- src/lib/loadChatComposer.js tests/unit/loadChatComposer.test.mjs
test ! -e src/styles/admin-areas.css && echo "admin-areas.css weg"
```
Expected: Zählung 16, 4, 1, 3. Die Befehle zwei bis vier sind leer, am Ende steht „admin-areas.css weg“.
- [ ] **Step 4: Code-Review (sonnet)**
Geprüft wird der Diff `design-p5..HEAD` gegen die Spec-Abschnitte „Bausteine“, „Seiten ohne Prototyp-Vorlage“ und P6. Schwerpunkte:
- Jede Chat-Regel hat Präfix und strikt höhere Spezifität (Tabelle in Task 4).
- `admin-areas.css` ist gelöscht, kein Import und keine ihrer Klassen ist übrig, `.funktionen-*` stehen unverändert in `global.css`.
- `CreditDetail` verhält sich unverändert (`busyRef`, Debounce, `confirm`).
- Der Textdiff (`git diff design-p5..HEAD -w`) zeigt nur Textänderungen, die die Spec unter „Texte“ erlaubt (darunter „Speichert …“, „Sendet …“, „Wird geladen …“) oder die ein Icon durch ein Zeichen ersetzen (z. B. „← Alle Unterhaltungen“).
- Jede Aktion der alten Seiten ist erreichbar: Prüfen/Ansehen, Schließen, Kundenansicht öffnen, In Stripe öffnen, Weitere Anträge, Genehmigen, Rückfrage, Ablehnen, Jetzt in Stripe gutschreiben, Neues Angebot, E-Mail senden/Erneut senden, Zurückziehen, Coupon- und Abo-Link, Erledigt, Kundenlink, Erneut versuchen, Neue Nachricht an Kunde, Unterhaltung öffnen, Zurück.
Wichtige Befunde werden behoben. Danach kommt der Rebase aus Step 1 noch einmal und Step 2 als Kurzlauf mit `--only chat-verlauf,gutschriften,angebote,speicher`.
- [ ] **Step 5: Tag und Übergabe**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" && git tag design-p6 && git log --oneline origin/test..design-p6 && git rev-parse design-p6`
Der Eigentümer bekommt:
- die Screenshots aus `shots/p6/1440x900` und `shots/p6/390x844`. Auswahl: `chat`, `chat-verlauf`, `chat-menue`, `chat-anhang-fehler`, `chat-tablet`, `gutschriften`, `gutschriften-antrag`, `gutschriften-tablet`, `angebote`, `angebote-dialog`, `angebote-vorschlag`, `speicher`, `kunde-chat`, `kunde-abo`.
- die bewusst offenen Punkte:
- Lightbox und Kamera bleiben dunkel, „Foto verwenden“ bleibt im Portal-Lila (nicht aus WOMS änderbar).
- Die Anhang-Menü-Icons des Portal-Bausteins bleiben.
- Die Blasenfarben `--bg-hover` und `--grey-bg` sind auf Weiß sehr zart. Die Stellschrauben sind `--chat-bubble-in` und `--chat-bubble-out` in `chat.css`.
- den Hinweis zu tp2: `admin-areas.css` ist gelöscht. Ändert `feature/terminplaner-tp2` die Datei (Anleitungs-Panel der Funktionen), gibt es beim Zusammenführen einen Konflikt, und die Panel-Regeln gehören dann in den Block „Kunden (P4)“ von `global.css`.
- die Push-Zeile `git -C /home/webklar/apps/ticket.webklar.com/dev-design push origin 'design-p6^{commit}:refs/heads/test'`
- die Rücknahme `git revert --no-commit design-p5..design-p6` als Commit auf `test`, durch den Eigentümer. Sie gilt nur, solange P6 das jüngste Paket ist.
- [ ] **Step 6: Nach dem Push (erst, wenn der Eigentümer gepusht hat)**
- Im Log von `webklar-ticket-build` steht „Deployment completed successfully“.
- `curl -s https://ticket.webklar.com/ | grep -o 'assets/index-[^"]*\.js'` stimmt mit `dist/index.html` des Tags überein.
- Lesender Sichttest im Chrome des Eigentümers: `/customers/chat` mit geöffnetem Verlauf (Anhang-Menü öffnen), `/customers/gutschriften` und `/customers/angebote`.
@@ -0,0 +1,537 @@
# WOMS „Weißes Design“ P7: Finanzen – Umsetzungsplan
> **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:** Nach P7 sind alle Finanzseiten ohne Karten, Kästen und Icons gestaltet: Übersicht, Einstellungen, Bankrückkehr und der Reiter „Rechnungen“ am Kunden. Die Dialoge „Buchung zuordnen“ und „Rechnung schreiben“ laufen auf `Dialog`. `Kpi` ist gelöscht. Funktion, Rückfragen und Texte bleiben.
**Architecture:**
- Der Finanzblock in `src/styles/global.css` (heute ab `/* --- Finanzen --- */`, ca. 320 Zeilen) wird Region für Region neu geschrieben, jeweils zusammen mit den Komponenten der Region. Er enthält nur Tokens, keine Farbliterale und kein `!important`. Task 3 rahmt ihn mit zwei Marken, weil P5 und P6 hinter ihm eigene Blöcke anhängen.
- Die Container-Queries (560 Vorschau, 640 Buchungen, 860 Rechnungen) bleiben. `container-type` sitzt auf einem neuen Wrapper `.fin-body` unter der Abschnittsüberschrift, Dialoge stehen nie darin.
- Neue Datei `src/lib/financeUi.js` mit zwei reinen Helfern: Vorzeichen-Rot und Meldungsklassen.
**Tech Stack:** React 18, react-router-dom 6, Vite 5, reines CSS. Tests mit `node --test`, Screenshot-Werkzeug aus P0.
**Spec:** `docs/superpowers/specs/2026-10-04-weisses-design-design.md`, Abschnitte „Gestaltungsregeln“, „Bausteine“ (Abschnitt, Knöpfe, Badge, Tabelle, Schlüsselwerte, Formular, Dialog, Meldungen, Laden), „Bewusste Änderungen“ (Esc), „Risiken“ (Scroll-Anker, Vorschlagslisten in Dialogen) und Paket P7. Dazu Anhang `…-umbaukarte.md`, Abschnitte 3, 4, 5/P7, 7.8. Globale Regeln und Schnittstellen: `2026-10-05-weisses-design-00-uebersicht.md`.
## Global Constraints
Es gelten die Regeln der Übersicht. Für P7 kommt hinzu:
- **Befehle:** Jeder Run-Block beginnt mit `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&`. Danach gelten `$WT`, `$W` und `$TMPDIR`. Tests und Build laufen nur über `"$W/check.sh"`, Browser-Läufe nur über `"$W/shots.sh"`. `FIN` ist die kommagetrennte Liste aller Ansichten-IDs der Tabelle aus Task 2 Step 2 (Shell-Variable, gesetzt in Task 2 Step 4).
- **Gate-Läufe:** `--gate p7` schaltet `icon-svg` für die `FIN`-Ansichten scharf. Die Finanzseite ist erst nach Task 5 ohne Icons, deshalb laufen Task 3 und 4 mit `--gate p6` (dieselben übrigen Prüfungen). Ab Task 5 gilt `--gate p7`.
- **Dateien:** nur `src/pages/FinancePage.jsx`, `FinanceSettingsPage.jsx`, `BankCallbackPage.jsx`, alle 17 Dateien in `src/components/finance/`, `src/components/ui/Kpi.jsx` (löschen), `src/components/ui/index.js` (Export entfernen), `src/lib/financeUi.js` (neu), `src/styles/global.css` (nur der Finanzblock). Alle `src/lib/finance*.js`, `src/hooks/*`, `CustomerDetailPage.jsx` und alle bestehenden Tests bleiben unberührt.
- **Texte wörtlich.** Erlaubt sind nur `▾`/`▴` statt Chevron-Icons, „+ Position“ (Plus-Icon wird Zeichen), „Entfernen“ in der Positionszeile (heute nur Icon) und „← Zurück zu den Finanzen“. Ladetexte („Speichere …“, „Sende …“, „Rechnungen werden geladen …“) bleiben, nur Spinner und Icons entfallen.
- **Tonnamen** `muted|info|ok|warn|danger` aus `financeFormat.js`/`financeView.js` bleiben, gemappt wird nur in `Badge`.
- **Beträge** behalten das Vorzeichen (U+2212). Rot (`--red-fg`) gilt genau dann, wenn der angezeigte Text mit „−“ beginnt: Klasse `.fin-neg`, bei Kennzahlen `tone="neg"`. Wo die Lib schon markiert (`saldoSicherNegative`, `saldoErwartetNegative`, `low.negative`, `shortfall`), gilt diese Markierung weiter.
- **Container-Falle:** `container-type: inline-size` macht das Element zum Containing Block für `position: fixed`. Es steht nur auf `.fin-body`, `AssignDialog` und `InvoiceCreateModal` sind nie Nachfahren davon.
- **Hygiene in allen P7-Dateien:** keine Icon-Importe, kein `style={{…}}`, kein `size="sm"`, keine Farbliterale, keine Klassen `ui-btn`, `form-control`, `form-group`, `form-label`, `overlay-content`, `overlay-close`, `spinner`, `kpi`. `lockRef`/`busyRef` und alle `window.confirm`-Rückfragen bleiben.
**Gemeinsame Ersetzungen** (Komponenten und CSS):
| Alt | Neu |
|---|---|
| Spinner plus Text im `.empty` | `<div className="loading" role="status">Text wörtlich</div>` |
| `fin-hint` / `fin-hint-warn` / `fin-dialog-*` | `msg muted` / `msg warn` (`role="alert"` bleibt) / Fehler im Dialog `msg err` |
| tonabhängige Meldung | `<p {...noticeProps(tone, 'fin-notice')}>` |
| `Button variant="ghost"`, `size="sm"` | `Button` (Standard), `size` entfällt |
| Aktion in Zeile, Liste oder Meldung | `<button type="button" className="link">`, Links `<a className="link">`, destruktiv `Button variant="danger"` |
| `form-control` / `form-group` + `form-label` | `input`/`select`/`textarea` / `Field` bzw. `FormField` |
| CSS-Farben und -Maße | `--text-muted`, `--text-faint` → `--text-2`; `--warn` → `--orange-fg`; Text-`--danger` → `--red-fg` (Formularfehler bleibt `--danger`); `--border` → `--line`; Flächen und Kartenrahmen entfallen; Gewicht 700/800 → 600 (Tabellentitel 500); Großbuchstaben entfallen; Media-Queries 640/700/720 → 860 |
Die CSS-Schritte nennen nur Abweichungen, Spezifitäts- und Reihenfolgeregeln sowie Maße, die ein Gate-Schritt prüft. Werte, die dort fehlen, bleiben wie im heutigen Finanzblock und folgen der Ersetzungstabelle.
**Steuerelemente, die erreichbar bleiben** (Namen unverändert):
- Übersicht: „Einstellungen“, „Aktualisieren“, „Aufschlüsselung“, der eine Knopf je Aufgabe (bank-verbinden, bank-aktualisieren, rechnung-oeffnen, zuordnen, kunde-oeffnen, fixkosten-uebernehmen, einstellungen), „Alle N anzeigen“/„Weniger anzeigen“, „Schließen“ (Zuordnen-Hinweis), jedes „Erneut versuchen“, das heute steht. Rechnungen: „Rechnung schreiben“, Filter Offen/Überfällig/Bezahlt/Entwürfe/Alle, Aktionen Zahlungsseite, PDF, Senden, Erinnerung senden, Stornieren, Löschen. Buchungen: „Zuordnen“/„Ändern“. Pager: „Zurück“/„Weiter“.
- Dialoge: „ד, „Abbrechen“, „Schließen“, „Zuordnen“, „Zuordnung lösen“, „Zurück“, „Ja, als bezahlt markieren“, „Als Entwurf speichern“, „Senden“, „+ Position“, „Entfernen“, „Zur Übersicht“.
- Einstellungen und Rest: „Speichern“, „Jetzt abrufen“, „Neu verbinden“/„Bank verbinden“, „Neue Position“, „Bearbeiten“, „Aktivieren“/„Auf inaktiv setzen“, „Löschen“, „Abbrechen“, „Zu den Finanzen“, „Daten erfassen“, „← Zurück zu den Finanzen“.
**Entscheidungen** (die Spec schweigt; nächste Lösung zu heute):
- Entscheidung: `.fin-stack` ist ein normaler Block ohne `gap` und ohne Flex. Der Abstand der Seitenteile ist `margin-top: 36px` auf jedem Geschwister nach dem ersten (`.fin-stack > * + *`). Das ist derselbe Wert wie `.page section + section` aus P1. Beide Regeln setzen dieselbe Eigenschaft am selben Element, also addiert sich nichts. Ein `gap: 36px` ergäbe zwischen zwei Abschnitten 72 px. Als Block lässt `.fin-stack` den oberen Rand von `h2.section:first-child` (8 px, weil `Card` die Überschrift als erstes Kind in ein `<section>` setzt) mit dem Abschnittsabstand verschmelzen. Eine Sonderregel für die Überschrift entfällt.
- Entscheidung: Finanztabellen sind keine `tr.row` (Zeilen nicht klickbar, Aktionen sind echte Knöpfe und Links). Es gibt kein `.hide-sm`/`.hide-md`, schmal stapeln die Container-Queries die Zeilen wie heute.
- Entscheidung: Die Aufgaben-Aktion je Zeile und „Alle N anzeigen“ sind `.link` (bis zu sieben Ringknöpfe untereinander wären zu unruhig). „Aktualisieren“, allein stehendes „Erneut versuchen“, „Jetzt abrufen“ und „Neue Position“ sind Standardknöpfe.
- Entscheidung: Pro Abschnitt höchstens ein schwarzer Knopf (Rechnung schreiben, Speichern, Jetzt abrufen bzw. Bank verbinden, Zuordnen/Senden im Dialog). Die Seitenköpfe haben keinen.
- Entscheidung: Die Aufschlüsselungszeilen („− Steuer-Rücklage …“) bleiben neutral, weil sie Abzüge sind. Rot sind Summen, Salden sowie Posten- und Buchungsbeträge.
- Entscheidung: Der Altersstreifen ist `<ul class="stats fin-ages">` mit `Stat` je Gruppe. Er überschreibt das Flex-Layout durch 4 Spalten, ab Container ≤ 860 px 2 Spalten (die Handy-Regel von `.stats` ergäbe 3 + 1).
- Entscheidung: Der Dialog-Fuß ist `<div className="fin-foot">` (Flex, umbrechend, rechtsbündig, auf dem Handy gleich breite Knöpfe) statt `<span className="grow" />`. Mit `.dialog-foot { flex-wrap: wrap }` aus P6 allein stünde der dritte Knopf bei 390 px linksbündig in einer zweiten Zeile.
## Review Focus
Aus der Übersicht gehören diese Punkte zu P7:
- **Punkt 2, lange Inhalte am Handy:** Testdaten `finanz-minus` mit Kundenname (60 Zeichen), Verwendungszweck (140 Zeichen ohne Leerzeichen) und Gegenpartei (80 Zeichen). `overflow-x` ist im Gate `p7` für alle Ansichten scharf.
- **Punkt 3, Teilausfall:** `finanzen-teilausfall` (Stripe aus, Abos nicht ladbar, Rechnungen und Buchungen antworten 500). Jeder Block zeigt seine Meldung mit „Erneut versuchen“, die Seite bleibt bedienbar.
- **Punkt 4, nur Tastatur:** Esc, Tab-Falle, gesperrtes „ד bei `busy`, „Enter sendet nie“ und „Esc schließt zuerst die Vorschlagsliste“ sind Schritte in `dlg-zuordnen-*` und `dlg-rechnung-*`.
- **Punkt 5, viele Einträge:** 120 Buchungen, 60 Rechnungen, 50 Zeilen je Seite (`finanzen-pager`).
Zusätzlich für P7 (Risiken der Spec): Scroll-Anker `finanzen-rechnungen` und `finanzen-buchungen` liegen nicht unter der Mobil-Leiste (`finanzen-anker`). Die Vorschlagsliste im Kundenfeld wird im scrollenden `.dialog-body` nicht abgeschnitten (`dlg-rechnung-kunde`). Negative Beträge sind rot und tragen ihr Minus (`finanzen-minus`, `finanzen-tabellen`).
---
### Task 1: Hilfsfunktionen `financeUi`
**Files:**
- Create: `src/lib/financeUi.js`
- Test: `tests/unit/financeUi.test.mjs` (neu)
**Interfaces:**
- Consumes: `euro`, `euroWhole`, `signedEuro` aus `src/lib/financeFormat.js` (nur im Test).
- Produces:
- `isNegText(text) → boolean`: wahr, wenn `text` ein String ist und mit „−“ (U+2212) beginnt.
- `noticeProps(tone, extra = '') → { className, role }`: `'ok'` → `msg ok`/`status`, `'warn'` → `msg warn`/`alert`, sonst `msg muted`/`status`. `extra` hängt mit Leerzeichen an `className`.
- [ ] **Step 1: Paketstart und Ausgangslage**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git merge-base --is-ancestor design-p6 HEAD && git rebase origin/test && git status --short && "$W/check.sh"`
Expected: Der Rebase läuft ohne Konflikt, `git status` ist leer, `# fail 0`. Ist `design-p6` kein Vorfahr von `HEAD`, stoppt der Agent und meldet das. Testzahl N sowie JS- und CSS-Größe aus der `check.sh`-Ausgabe werden notiert (Vergleich in Task 9).
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git log --oneline origin/test..origin/feature/terminplaner-tp2 -- src/pages/FinancePage.jsx src/pages/FinanceSettingsPage.jsx src/pages/BankCallbackPage.jsx src/components/finance src/components/ui/Kpi.jsx src/components/ui/index.js src/lib/financeUi.js src/styles/global.css`
Expected: keine Ausgabe. Gibt es Treffer, stoppt der Agent und fragt den Eigentümer. Er empfiehlt dabei, TP2 zuerst nach `test` zu bringen.
- [ ] **Step 2: Failing Test schreiben**
```js
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
const { isNegText, noticeProps } = await loadModule('src/lib/financeUi.js')
const { euro, euroWhole, signedEuro } = await loadModule('src/lib/financeFormat.js')
test('isNegText: nur das angezeigte Minus zählt', () => {
assert.equal(isNegText(euro(-449)), true)
assert.equal(isNegText(signedEuro(-449)), true)
assert.equal(isNegText(euroWhole(-123456)), true)
assert.equal(isNegText(euroWhole(-40)), false) // „0 €“: Rundung auf 0 trägt kein Vorzeichen
assert.equal(isNegText(signedEuro(16879)), false)
assert.equal(isNegText(euro(null)), false) // „–“ (U+2013)
for (const v of ['-4,49 €', '', undefined, null, 5]) assert.equal(isNegText(v), false)
})
test('noticeProps: Klasse und Rolle je Ton', () => {
assert.deepEqual(noticeProps('ok'), { className: 'msg ok', role: 'status' })
assert.deepEqual(noticeProps('warn'), { className: 'msg warn', role: 'alert' })
assert.deepEqual(noticeProps('info'), { className: 'msg muted', role: 'status' })
assert.deepEqual(noticeProps(undefined), { className: 'msg muted', role: 'status' })
assert.deepEqual(noticeProps('warn', 'fin-notice'), { className: 'msg warn fin-notice', role: 'alert' })
})
```
- [ ] **Step 3: Test laufen lassen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/financeUi.test.mjs`
Expected: FAIL (Modul `financeUi.js` fehlt).
- [ ] **Step 4: `financeUi.js` implementieren**
Zwei benannte Exporte nach der Signatur oben, deutscher Kommentar im Stil der Nachbardateien, keine Abhängigkeit.
- [ ] **Step 5: Test grün**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/financeUi.test.mjs && "$W/check.sh" --nur-tests`
Expected: PASS, `# pass` = N + 2, `# fail 0`, Exit 0.
- [ ] **Step 6: Commit**
```bash
git add src/lib/financeUi.js tests/unit/financeUi.test.mjs
git commit -m "Finanzen: Helfer fuer Vorzeichen-Rot und Meldungsklassen" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 2: Werkzeug – Finanzdaten, Ansichten, Gate `p7`
**Files** (unter `$W/`):
- Create: `fixtures/finanz-minus.mjs`, `fixtures/finanz-teilausfall.mjs`
- Modify: `views.mjs`, `gates.mjs`, bei Bedarf `backend.mjs` (`POST …/finanzen/buchungen/:id/zuordnen` → `{ buchung }`, `POST …/finanzen/bank/rueckkehr` → `{ ok: true, validUntil: '2027-03-29' }`)
**Interfaces:**
- Consumes: `VIEWS`- und `GATES`-Schema aus der Übersicht (`id`, `path`, `fixtures`, `scenario`, `steps(page, ctx)`). Schritte werfen bei Abweichung (`node:assert/strict`), `shots.mjs` meldet das als `step-failed`. `ctx.backend.calls` listet alle beantworteten Requests. Portal-Antworten unter `https://portal.test/api/admin/finanzen/…`.
- Produces: die Ansichten der Tabelle unten und das Gate `p7` (Aufbau nach der Übersicht, Step 3).
- [ ] **Step 1: Testdaten anlegen**
Beide Dateien leiten von `standard.mjs` ab und ersetzen nur die Finanz-Antworten (gleiche Form).
- `finanz-minus`:
- Kopf: Kontostand −1.234,56 €, `verfuegbarCents` < 0, `tiefsterSicher` negativ, `tiefsterErwartet` positiv (beweist Rot je Betrag).
- Vorschau mit 8 Posten und negativen Salden. 9 Aufgaben, darunter je eine mit `rechnung-oeffnen` ohne `hostedUrl`, `zuordnen`, `bank-aktualisieren`, `fixkosten-uebernehmen`.
- Ein Abo `past_due` (gefährdet), alle 4 Altersgruppen mit Werten.
- 60 Rechnungen aller Status (eine Abo-Rechnung, ein Kundenname mit 60 Zeichen). 120 Buchungen: Ein- und Ausgänge, `PDNG`, Verwendungszweck mit 140 Zeichen ohne Leerzeichen, Gegenpartei mit 80 Zeichen, offene Eingänge mit `vorschlaege` (Rechnung und Auszahlung).
- Ein fester Kunde mit E-Mail, aber ohne Straße, PLZ und Ort.
- `finanz-teilausfall`: `stand.stripe = { ok: false, fehler: 'Stripe ist gerade nicht erreichbar.' }`, `abos = { fehler: 'Die Abos konnten nicht geladen werden.' }`, Kopf ohne Kontostand.
- [ ] **Step 2: Ansichten anlegen oder anpassen**
| ID | Aufbau | Prüfung (Schritt wirft bei Abweichung) |
|---|---|---|
| `finanzen` (vorhanden) | `/finance` | Alle Namen aus „Steuerelemente, die erreichbar bleiben“ (Global Constraints) per `getByRole` auffindbar. Mindestens ein `button.link`, alle mit transparentem `background-color` und `border-top-width: 0px` (so auch jedes `button.link-danger` in `finanzen-tabellen`). |
| `finanzen-minus` | `finanz-minus`; Klick „Aufschlüsselung“ | Text in `.stat .v` bzw. `.fin-low .fin-amount`, der mit „−“ beginnt, trägt `.v.neg` bzw. `.fin-neg`. Kein `.v.neg` ohne „−“ (außer in `.fin-abo-stats`). Kein `.fin-breakdown li` mit `.fin-neg`. |
| `finanzen-tabellen` | `finanz-minus` | `.fin-forecast-amount`, `.fin-tx-amount` mit „−“ tragen `.fin-neg`, die anderen nicht. `thead` von `.fin-forecast`, `.fin-invoices`, `.fin-tx` ist bei 1440 nicht `position: absolute`, bei 390 `absolute`. Dreimal `.fin-body` mit `container-type: inline-size`, kein `.overlay` darin. Zwischen `#finanzen-rechnungen` und `#finanzen-buchungen` liegen 36 px (± 1 px), nicht 72. Bei 1440 liegen die Oberkanten der beiden Abschnitte in `.fin-cols` gleich hoch (± 1 px). |
| `finanzen-pager` | `finanz-minus`, zu `#finanzen-buchungen`, „Weiter“ | Fuß „Seite 2 von 3 · 120 Buchungen“, 50 `.fin-tx tbody tr`. |
| `finanzen-anker` | `finanz-minus`, Klick „Zu den Rechnungen“, 800 ms | `#finanzen-rechnungen` hat `scroll-margin-top` 16px (1440) bzw. 68px (390), seine Oberkante liegt dort (± 1 px). |
| `finanzen-teilausfall` | `finanz-teilausfall`, Szenario `fehler`, `/rechnungen` und `/buchungen` → 500 (`page.route`), Reload | Mindestens 2 `[role=alert]` und 2 „Erneut versuchen“, h1 „Finanzen“ sichtbar, „Aktualisieren“ nicht `disabled`. |
| `finanzen-fehler` | Szenario `fehler`, `/uebersicht` → 500, Reload | `[role=alert]` und klickbares „Erneut versuchen“. |
| `dlg-rechnung` (vorhanden) | Klick „Rechnung schreiben“ | `role=dialog` mit Name „Rechnung schreiben“, Klasse `dialog-wide`. Overlay-Rechteck so groß wie das Fenster, kein Vorfahr `.fin-body`. Fokus im Dialog. |
| `dlg-rechnung-kunde` | „Mü“ in `#fin-invoice-customer` | Feldbreite ≥ 90 % von `.dialog-body`. `[role=listbox]` bei 1440 ganz im `.dialog`, bei 390 ≥ 120 px sichtbar. Esc schließt nur die Liste, das zweite Esc den Dialog. |
| `dlg-rechnung-enter` | Fokus in Beschreibung, Enter; `ctx.backend.calls` zählt `POST …/rechnungen` | Dialog bleibt, 0 Requests. „Als Entwurf speichern“ ohne Kunde zeigt `.msg.err[role=alert]` im sichtbaren Teil von `.dialog-body`. |
| `dlg-zuordnen-zahlung` (vorhanden) | Klick „Zuordnen“ an erster offener Eingangsbuchung | Vorschläge sind `label.check` mit Radio. 12 × Tab und 12 × Umschalt+Tab bleiben im `.dialog`. |
| `dlg-zuordnen-bestaetigung` | Vorschlag mit Rechnung, „Zuordnen“ | `.fin-confirm` nennt „in Stripe als bezahlt markiert“, Fuß „Zurück“ und „Ja, als bezahlt markieren“, `ctx.backend.calls` enthält kein `POST …/zuordnen`. Esc schließt ohne Request. |
| `dlg-zuordnen-busy` | wie oben, `POST …/zuordnen` 1500 ms verzögert, „Ja, …“ | Währenddessen `.dialog-close` `disabled`, Esc lässt den Dialog offen. Danach schließt er. |
| `finanzen-einstellungen` (vorhanden) | `/finance/einstellungen` | h2 „Einstellungen“, „Bankzugang“, „Fixkosten“, Link „← Zurück zu den Finanzen“. |
| `finanzen-einstellungen-fehler` | Mindestpuffer „abc“, „Speichern“, dann „Neue Position“ | `[aria-invalid="true"]` hat `box-shadow` mit `rgba(192, 18, 60`, `.msg.err[role=alert]` am Feld. h3 „Neue Position“, Fokus in `#fin-fix-name`. |
| `finanzen-bank-callback`, `-ok` | `/finanzen/bank-callback`, bzw. mit `?code=x&state=y` | „Zu dieser Seite liegt keine Rückmeldung der Bank vor.“ bzw. `.msg.ok` mit „Bankzugang erneuert, gültig bis“. |
| `kunde-invoices` (vorhanden), `kunde-invoices-adresse` | Kunde ohne Anschrift aus `finanz-minus` (andere URL als `kunde-invoices`) | `.fin-address-warn-page` mit Rolle alert und „Zur Übersicht“. „Senden“ eines Entwurfs über 250 € ist `disabled`. |
- [ ] **Step 3: Gate-Eintrag**
Ein Eintrag nach dem Schema der Übersicht. `p7` ist `p6` plus `icon-svg` für die `FIN`-Ansichten und die aller früheren Pakete. Eine `ignore`-Liste des Vorgängers bleibt unverändert.
```js
p7: {
...GATES.p6,
scoped: {
...GATES.p6.scoped,
'icon-svg': { ...GATES.p6.scoped['icon-svg'], views: [...GATES.p6.scoped['icon-svg'].views, /* IDs aus FIN */] },
},
},
```
Die absichtlichen 500er der zwei Fehleransichten schreibt der Browser als `console.error` („Failed to load resource“). Deshalb laufen sie im Szenario `fehler`, dessen Meldungen `console-error` ausnimmt. Nimmt `checks.mjs` nur Meldungen der eigenen Fehlerantworten aus, wird die Ausnahme um Meldungen zu `…/api/admin/finanzen/` erweitert.
- [ ] **Step 4: Vorher-Lauf**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && FIN=finanzen,finanzen-minus,finanzen-tabellen,finanzen-pager,finanzen-anker,finanzen-teilausfall,finanzen-fehler,finanzen-einstellungen,finanzen-einstellungen-fehler,finanzen-bank-callback,finanzen-bank-callback-ok,dlg-rechnung,dlg-rechnung-kunde,dlg-rechnung-enter,dlg-zuordnen-zahlung,dlg-zuordnen-bestaetigung,dlg-zuordnen-busy,kunde-invoices,kunde-invoices-adresse && "$W/build.sh" "$WT" p7 && "$W/shots.sh" p7 "$W/shots/p7-vorher" --only "$FIN" --gate p7`
Expected: Exit 1. `icon-svg` meldet `finanzen`, `finanzen-einstellungen`, `dlg-rechnung`, `dlg-zuordnen-zahlung`. Die Schritte schlagen an in `finanzen`, `finanzen-minus`, `finanzen-tabellen`, `finanzen-anker` (390 px), `finanzen-einstellungen-fehler`, `dlg-rechnung*` und `dlg-zuordnen-zahlung`. `finanzen-pager`, `finanzen-teilausfall`, `finanzen-fehler`, `dlg-zuordnen-bestaetigung` und `dlg-zuordnen-busy` sind Wächter für heutiges Verhalten und laufen schon vorher durch.
### Task 3: Seite, Statuszeile, Liquiditätskopf, Aufgaben; `Kpi` löschen
**Files:**
- Modify: `src/pages/FinancePage.jsx`, `src/components/finance/StatusLine.jsx`, `LiquidityHead.jsx`, `TodoList.jsx`, `src/components/ui/index.js`
- Delete: `src/components/ui/Kpi.jsx`
- Modify: `src/styles/global.css` (Marken um den Finanzblock, Regionen „Allgemein“ bis `Block 2: Zu erledigen`, `.kpi*`)
**Interfaces:**
- Consumes: `Stat({ label, value, sub, tone })`, `Card`, `Badge`, `EmptyState`, `PageHeader`, `Button` (P1), `isNegText`, `noticeProps` (Task 1).
- Produces: die Klassen `.fin-stack`, `.fin-top`, `.fin-block`, `.fin-body`, `.fin-table-wrap`, `.fin-notice`, `.fin-load-error`, `.fin-cols`, `.fin-neg`, `.fin-flag`, `.fin-amount`, `.fin-sr` für Task 4 bis 8.
- [ ] **Step 1: P1-Voraussetzungen prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "Stat|Dialog" src/components/ui/index.js && rg -n "\.stat \.sub|\.table-foot|\.msg\b|\.loading|\.field-row\b" src/styles/global.css | head`
Expected: `Stat` und `Dialog` sind exportiert, die Klassen definiert. Fehlt ein Export, wird er in `index.js` ergänzt.
- [ ] **Step 2: Finanzblock rahmen, CSS-Region „Allgemein“**
Zuerst die Grenzen setzen.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "^/\* (--- Finanzen|Finanzen: Einstellungen|=====|Kunden-Unterseiten|Projekte \(P5\))" src/styles/global.css`
Expected: Der Finanzblock beginnt bei `/* --- Finanzen --- */` und endet vor dem nächsten Blockkommentar (P6, KOMPAT …) bzw. am Dateiende. Davor kommt die Marke `/* ===== FINANZEN (P7) ===== */`, dahinter `/* ===== ENDE FINANZEN ===== */`.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n -A2 "===== FINANZEN \(P7\)" src/styles/global.css`
Expected: die Marke, danach `.fin-amount`.
`.fin-amount` und `.fin-sr` bleiben (Werte gleich). Neu oder geändert:
| Regel | Wert |
|---|---|
| `.fin-neg`, `.fin-flag` | `color: var(--red-fg)`, `.fin-flag` zusätzlich `font-weight: 600` |
| `.fin-stack` | keine eigene Regel (Block, kein `gap`, siehe Entscheidung oben) |
| `.fin-stack > * + *` | `margin-top: 36px` |
| `.fin-top` | `display: flex; flex-direction: column; gap: 20px` |
| `.fin-block` | `scroll-margin-top: 16px`, bei ≤ 860 px `68px` (Mobil-Leiste 52 px + 16 px) |
| `.fin-body` | `container-type: inline-size` (einzige Stelle) |
| `.fin-table-wrap` | `overflow-x: auto` |
| `.fin-notice`, `.fin-page-notice` | `margin: 0 0 8px`, `margin-bottom: 16px` |
| `.fin-load-error` | `display: grid; gap: 12px; justify-items: start; padding: 16px 0` |
| `.fin-cols` | einspaltig, `gap: 0 56px` (den Zeilenabstand liefert `.page section + section`); ab 1100 px `minmax(0,1.8fr) minmax(0,1fr)`, `.fin-cols-half` 2 × `minmax(0,1fr)` und zusätzlich `.fin-stack .fin-cols > section + section { margin-top: 0 }`, damit beide Spalten oben bündig stehen (höhere Spezifität als die globale Regel, kein `!important`). |
Gelöscht: `.fin-spin`, `.fin-hint*`, `.fin-link*`, `.fin-stack .kpi-grid`. P5 legt den Reset für `button.link` an. Zeigt die Prüfung in `finanzen` oder `finanzen-tabellen` (Task 2) Browser-Rahmen an `button.link` oder `button.link-danger`, kommt `button.link-danger` in dieselbe Regel (`background: none; border: 0; padding: 0; font: inherit; cursor: pointer`).
- [ ] **Step 3: CSS-Blöcke 0, 1, 2**
- **Block 0:** Die Statuszeile hat keinen Kasten: `.fin-status` Flex, `space-between`, umbrechend, Abstand `8px 16px`, 13 px, `--text-2`. `.fin-status-items` Flex, Abstand `4px 24px`. `.fin-status-warn` `--orange-fg`, 600.
- **Block 1:**
- `.stats.fin-head` ist ein Raster `repeat(auto-fit, minmax(220px, 1fr))`, Abstand `24px 48px`, `margin: 0`. Die Handy-Regel von `.stats` (3 Spalten) würde die breite dritte Kennzahl abschneiden. Die Doppelklasse schlägt `.stats` durch Spezifität.
- `.stats.fin-head .sub` 13 px `--text-2`. `.fin-breakdown-total` mit `border-top: 1px solid var(--line)`, 600.
- `.fin-lows` Raster `auto auto minmax(0,1fr)`. `.fin-low .fin-amount` 18/26 px, 600.
- **Block 2:** `.fin-todos` und `.fin-todo` mit Haarlinien `var(--line)`. `.fin-todo` Raster `80px minmax(0,1fr) auto`, Abstand `12px 16px`, `padding: 12px 0`, bei ≤ 860 px einspaltig. Die Streifenklassen `.fin-todo-hoch/-mittel` und `.fin-todos-more` entfallen.
- [ ] **Step 4: `Kpi` und `.kpi*` löschen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git rm -q src/components/ui/Kpi.jsx`. Dann die Zeile `export { default as Kpi }` aus `src/components/ui/index.js` und alle `.kpi`-Regeln samt `.fin-head .kpi-value` aus `global.css` entfernen (falls P1 sie nicht schon gelöscht hat).
Expected: Nach Step 5 liefert `rg -n "Kpi|kpi" src` keinen Treffer.
- [ ] **Step 5: Komponenten umbauen**
- `FinancePage`:
- Wurzel `.page`. Kopf: `PageHeader`, „Einstellungen“ als `Button as={Link}` ohne Icon. Ladezustand `loading`.
- Ohne Daten: `msg err` plus `Button variant="primary"` „Erneut versuchen“, ohne `Card` und `style`.
- Mit Daten: `.fin-stack` > `[Teilfehler: msg warn mit .link „Erneut versuchen“]`, `.fin-top` (`StatusLine`, `LiquidityHead`), `TodoList`, `.fin-cols` (`ForecastTable`, `TaxCard`), `.fin-cols.fin-cols-half` (`AboCard`), `InvoiceTable`, `TransactionTable`.
- `assignNotice` als `msg warn fin-page-notice` mit `.link` „Schließen“. `AssignDialog` bleibt am Ende der `.page`.
- `StatusLine`: Zeilen als `.fin-status-item` (Warnungen `.fin-status-warn`, ohne Icon). „Aktualisieren“/„Aktualisiere …“ ist ein Standardknopf. Hinweis über `noticeProps(hint.limited ? 'info' : 'warn')`.
- `LiquidityHead`:
- `<div className="stats fin-head">` mit drei `Stat`. Kontostand: `tone` = `isNegText(euroWhole(…)) ? 'neg' : ''`. Verfügbar: `tone` = `shortfall ? 'neg' : ''`, Flagge „Rücklagen nicht gedeckt“ als `.fin-flag`.
- Tiefster Stand ohne `tone`, Beträge in `.fin-lows` bekommen `.fin-neg` bei `low.negative`.
- „Aufschlüsselung“ ist `<button className="link">` mit `<span aria-hidden="true">▾/▴</span>`, die Zeilen von `breakdownRows` bleiben neutral.
- `TodoList`: `Card` mit Badge „n offen“ als Aktion. Zeile `.fin-todo` ohne Zusatzklasse für die Dringlichkeit, Aktion als `<button className="link">` („Moment …“ während des Laufs, alle gesperrt), Fehler als `msg warn`. „Alle N anzeigen“ als `.link` in `<div className="table-foot">`.
- [ ] **Step 6: Hygiene, Bau, Bild**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{|size=\"sm\"|ui-btn|Kpi" src/pages/FinancePage.jsx src/components/finance/{StatusLine,LiquidityHead,TodoList}.jsx; "$W/build.sh" "$WT" p7 && "$W/shots.sh" p7 "$W/shots/p7-t3" --only finanzen,finanzen-minus,finanzen-anker,finanzen-teilausfall,finanzen-fehler --gate p6`
Expected: `rg` ohne Treffer, Gate Exit 0. In `finanzen-minus.png`: drei Kennzahlen nebeneinander, Kontostand und Verfügbar rot, beim „Tiefsten Stand“ nur der negative Betrag, Aufgaben als Haarlinienzeilen ohne Streifen.
- [ ] **Step 7: Commit**
```bash
git add src/styles/global.css src/pages/FinancePage.jsx src/components/finance/StatusLine.jsx src/components/finance/LiquidityHead.jsx src/components/finance/TodoList.jsx src/components/ui/index.js
git commit -m "Finanzen: Seitenkopf, Statuszeile, Kennzahlen und Aufgaben ohne Karten; Kpi entfernt" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
(`git rm` hat `Kpi.jsx` schon gestaged.)
### Task 4: Vorschau, Steuer, Abos
**Files:**
- Modify: `src/components/finance/ForecastTable.jsx`, `TaxCard.jsx`, `AboCard.jsx`, `src/styles/global.css` (Blöcke 3, 4, 5)
**Interfaces:**
- Consumes: `.fin-body`, `.fin-table-wrap`, `.fin-neg`, `.fin-cols` (Task 3), `Stat`, `.table`, `.stats`.
- Produces: Signaturen unverändert (`ForecastTable({ vorschau, kontostandCents, stripeDown, abosFehler })`, `TaxCard({ steuer })`, `AboCard({ abos })`).
- [ ] **Step 1: CSS Blöcke 3, 4, 5**
- **Vorschau:** `.fin-forecast-label` `overflow-wrap: anywhere`. Randzellen-Padding, `th:first-child` und Hover-Regeln entfallen. `@container (max-width: 560px)` bleibt mit denselben Flächen (`date kind amount / label / sicher erwartet`), Trenner `1px solid var(--line)`, `padding: 10px 0`.
- **Steuer:** `.fin-facts` Raster, Abstand 16 px. `.fin-fact` mit Haarlinie unten `var(--line)`, der letzte ohne. `.fin-fact-big` 22/30, 600. `.fin-fact-unknown` `--orange-fg`.
- **Abos:** `.fin-abos` und `.fin-abo` mit Haarlinien `var(--line)`. `.fin-abo` Raster `minmax(0,1fr) auto`, `padding: 12px 0`.
- Gelöscht: `.fin-stats`, `.fin-stat-*`, `.fin-todo-card`, `.fin-forecast-card`, `.fin-tax-card`, `.fin-abo-card`.
- [ ] **Step 2: Komponenten umbauen**
- `ForecastTable`: `Card title` ohne `className` und `bodyStyle`. Darunter `.fin-body` > `.fin-table-wrap` > `<table className="table fin-forecast …">` (Rollen, `caption.fin-sr`, `data-label` unverändert). Betragszelle mit `fin-neg` bei `isNegText(row.amountText)`. Hinweise unter der Tabelle als `msg warn` (`role="status"`) ohne Icon, die Annahme als `msg muted`.
- `TaxCard`: gleiche Struktur ohne Card-Klasse. `v.noteText` steht als `<p className="msg muted fin-tax-note">`, Wortlaut unverändert (rechtlich relevant).
- `AboCard`: `<div className="stats fin-abo-stats">` (Haken für `finanzen-minus`, ohne eigene Regel) mit `Stat` „Abo-Einnahmen pro Monat“, „zahlende Kunden“ und, nur wenn gesetzt, „gefährdet“ mit `tone="neg"`. Liste wie bisher, Fehlerfall `msg warn` ohne Icon.
- [ ] **Step 3: Hygiene, Bau, Bild**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{|bodyStyle|Kpi" src/components/finance/{ForecastTable,TaxCard,AboCard}.jsx; "$W/build.sh" "$WT" p7 && "$W/shots.sh" p7 "$W/shots/p7-t4" --only finanzen,finanzen-minus,finanzen-teilausfall --gate p6`
Expected: `rg` ohne Treffer, Exit 0. Bild: bei 390 px Vorschau-Zeilen dreizeilig (Datum/Art/Betrag, Posten, beide Salden), bei 1440 als Tabelle; Steuer als Liste mit Haarlinien.
- [ ] **Step 4: Commit**
```bash
git add src/styles/global.css src/components/finance/ForecastTable.jsx src/components/finance/TaxCard.jsx src/components/finance/AboCard.jsx
git commit -m "Finanzen: Vorschau, Steuer und Abos ohne Karten, Container-Query auf Wrapper" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 5: Rechnungen, Buchungen, Blätterleiste, Kundenreiter
**Files:**
- Modify: `src/components/finance/InvoiceTable.jsx`, `TransactionTable.jsx`, `Pager.jsx`, `AddressWarning.jsx`, `CustomerInvoices.jsx`, `src/styles/global.css` (Block 6 und 7 ohne Dialoge)
**Interfaces:**
- Consumes: `.fin-body`, `.fin-table-wrap`, `.fin-notice`, `.fin-load-error`, `.table`, `.seg`, `.toolbar`, `.table-foot`, `.stats`, `Stat`, `noticeProps`, `isNegText`.
- Produces: Signaturen unverändert (`InvoiceTable`, `TransactionTable`, `Pager({ page, pages, total, noun, onPage })`, `AddressWarning({ customer, onShowOverview, variant })`, `CustomerInvoices({ customer, onShowOverview })`) und `export const TRANSACTIONS_ANCHOR = 'finanzen-buchungen'`.
- [ ] **Step 1: CSS Block 6 und 7**
- `.stats.fin-ages`: Liste ohne Zeichen, Raster `repeat(4, minmax(0,1fr))`, Abstand `16px 32px`, `margin: 0 0 16px`; `.fin-ages .stat .v` 18/26 px; `@container (max-width: 860px)` 2 Spalten.
- `.fin-block .seg { flex-wrap: wrap; max-width: 100% }`, bei ≤ 860 px Knopf-Padding `0 9px`. `.fin-pager` (mit `.table-foot`): `justify-content: center; gap: 16px`, `.fin-pager-info` tabular.
- Tabellen: Abstands- und Randzellenregeln entfallen (`.table` aus P1 gilt). Es bleiben `.fin-inv-customer` und `.fin-tx-counterparty` mit `overflow-wrap: anywhere`, `.fin-inv-number`, `.fin-inv-created/-due` und `.fin-tx-date` nowrap, `.fin-inv-statusbox` Raster 3 px, `.fin-actions` Flex `gap: 4px 14px`, `.fin-tx-remittance` mit 2 Zeilen Klemme, `.fin-tx-assign .badge { white-space: normal; height: auto; padding-block: 2px }` (zwei Klassen schlagen `.badge`), `.fin-tx-action` nowrap rechts.
- `@container (max-width: 860px)` (Rechnungen) und `(max-width: 640px)` (Buchungen) bleiben mit den Flächen `number status / customer / created due / total remaining / actions` bzw. `date amount / what / assign action`, Trenner `var(--line)`, `padding: 10px 0`.
- `.fin-address-warn` Flex umbrechend `4px 12px`; `-page` `margin: 0 0 16px`, `-dialog` `margin: 0`.
- [ ] **Step 2: `InvoiceTable` und `Pager`**
- Aufbau: `<section id={…ANCHOR} className="fin-block">` > `Card title="Rechnungen"` mit schwarzem „Rechnung schreiben“ als Aktion > `.fin-body` (Altersstreifen, `.toolbar` mit `.seg`, Meldung, Inhalt, `Pager`). `InvoiceCreateModal` bleibt Geschwister der `Card`.
- Filter: `.seg` mit `aria-pressed` und Klasse `on`. Meldung über `noticeProps(message.tone, 'fin-notice')`. Altersstreifen: `Stat label={cell.label} value={cell.sumText} sub={…Rechnung(en)}` je `<li>`.
- Zeilenaktionen: „Zahlungsseite“ und „PDF“ als `<a className="link" target="_blank" rel="noopener noreferrer">` mit unverändertem `aria-label`, „Senden“ und „Erinnerung senden“ als `.link`-Knopf, „Stornieren“ und „Löschen“ als `Button variant="danger"`. Sperren, Laufbeschriftungen („Sende …“, „Storniere …“, „Lösche …“) und `title` am gesperrten „Senden“ bleiben.
- Tabelle `<table className="table fin-invoices" role="table">`, Rollen, `caption.fin-sr`, `data-label` unverändert. `Pager`: `<nav className="table-foot fin-pager">` mit „Zurück“/„Weiter“ als `Button variant="quiet"`.
- [ ] **Step 3: `TransactionTable`, `AddressWarning`, `CustomerInvoices`**
- `TransactionTable`: `Card title="Buchungen"` in `<section id=TRANSACTIONS_ANCHOR className="fin-block">`, Inhalt in `.fin-body`. Laden `loading`, Fehler `fin-load-error` mit `msg warn` und Standardknopf, Stripe-Hinweis `msg warn` ohne Icon. Betrag `fin-amount fin-tx-amount` plus `fin-neg` bei `isNegText`, `fin-tx-out` bleibt. „Zuordnen“/„Ändern“ als `.link`-Knopf mit `aria-label`.
- `AddressWarning`: `<div className="msg warn fin-address-warn fin-address-warn-{variant}" role="alert">` mit `<button type="button" className="link">` bzw. `Link className="link"`. Der Knopf trägt immer `type="button"`, weil die Variante `dialog` in einem `Dialog as="form"` steht.
- `CustomerInvoices`: Sperrfall als `EmptyState` ohne `Card`, „Daten erfassen“ bleibt schwarz. Sonst `AddressWarning` und `InvoiceTable`.
- [ ] **Step 4: Hygiene, Bau, Bild**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{|size=\"sm\"|bodyStyle|FaSpinner" src/components/finance/{InvoiceTable,TransactionTable,Pager,AddressWarning,CustomerInvoices}.jsx; "$W/build.sh" "$WT" p7 && "$W/shots.sh" p7 "$W/shots/p7-t5" --only finanzen,finanzen-tabellen,finanzen-pager,finanzen-anker,finanzen-teilausfall,kunde-invoices,kunde-invoices-adresse --gate p7`
Expected: `rg` ohne Treffer, Exit 0 (kein `overflow-x` bei 390 px trotz langer Namen), alle Schritte laufen durch. Bild: Altersstreifen 4 Spalten (1440) bzw. 2 (390), Filter als graue Schiene, Tabellen mit Haarlinien und Textaktionen.
- [ ] **Step 5: Commit**
```bash
git add src/styles/global.css src/components/finance/InvoiceTable.jsx src/components/finance/TransactionTable.jsx src/components/finance/Pager.jsx src/components/finance/AddressWarning.jsx src/components/finance/CustomerInvoices.jsx
git commit -m "Finanzen: Rechnungen und Buchungen als Tabellen ohne Karten, Textaktionen, Blaetterleiste" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 6: Einstellungen, Bankzugang, Fixkosten, Bankrückkehr, `FormField`
**Files:**
- Modify: `src/pages/FinanceSettingsPage.jsx`, `src/pages/BankCallbackPage.jsx`, `src/components/finance/SettingsCard.jsx`, `BankAccessCard.jsx`, `FixedCostsCard.jsx`, `FormField.jsx`, `src/styles/global.css` (Region „Finanzen: Einstellungen“)
**Interfaces:**
- Consumes: `.field`, `.hint`, `.field-row`, `.input/.select/.textarea` mit Fehlerring bei `[aria-invalid="true"]` (P1), `.kv`, `.msg`, `noticeProps`.
- Produces: `FormField({ id, label, hint, error, children })` unverändert. Das Kind bekommt weiter `id`, `aria-invalid`, `aria-describedby`.
- [ ] **Step 1: CSS Einstellungen**
- `.fin-fieldset` Raster `gap: 16px`, ohne Rand und Innenabstand. `.fin-form-field { min-width: 0 }`. `.fin-input-amount` rechts. `.fin-counter` 12 px `--text-2` rechts, `margin-top: -8px`, `.fin-counter-over` `--danger` (Formularfehler), 600. `.fin-form-actions` Flex, `gap: 12px`, `margin-top: 16px`.
- `.fin-fixed-form` ohne Rahmen, nur `padding-bottom: 24px; margin-bottom: 8px; border-bottom: 1px solid var(--line)`. `.fin-form-title` 14/20, 600. `.fin-result` 16 px, 600.
- `.fin-fixed-list` und `.fin-fixed` mit Haarlinien `var(--line)`. `.fin-fixed` Raster `minmax(0,1fr) auto`, `padding: 12px 0`. `.fin-fixed-off` Name und Betrag `--text-2`.
- Gelöscht: `.fin-form-grid`, `.fin-bank-facts*`, `.fin-field-error`, `.fin-form-hint`, `.fin-form-notice`, `.fin-form-field .form-control[aria-invalid]`.
- [ ] **Step 2: Komponenten umbauen**
- `FormField`: `.field fin-form-field`, Hinweis `<div className="hint">`, Fehler `<div className="msg err" role="alert">`. Den Fehlerring liefert allein `aria-invalid`.
- `SettingsCard`: Feldpaare als `.field-row`, Felder `input fin-input-amount`, Textarea `textarea`, Ladefehler `fin-load-error` mit `msg warn`. „Speichern“/„Speichere …“ schwarz ohne Icon. Meldung über `noticeProps(notice.tone)`.
- `BankAccessCard`: `Card` mit Badge als Aktion. Fakten als flaches `<dl className="kv">` (`<dt>`/`<dd className="num">` ohne `<div>`-Gruppen). Schwarz ist „Jetzt abrufen“ (bei `canRefresh`), sonst „Bank verbinden“, der andere Knopf ist Standard. „Rufe ab …“ und „Leite weiter …“ bleiben.
- `FixedCostsCard`: „Neue Position“ im Kopf als Standardknopf ohne Plus. Formular mit `<h3 id="fin-fix-form-title" className="fin-form-title">`, `.field-row`, „Speichern“ schwarz, „Abbrechen“ Standard. Zeilenaktionen: „Bearbeiten“ und `row.toggleLabel` als `.link`, „Löschen“ als `Button variant="danger"`. `aria-label`, `window.confirm` und Sperren bleiben.
- [ ] **Step 3: Seiten**
- `FinanceSettingsPage`: `PageHeader` mit „← Zurück zu den Finanzen“ als `Button as={Link}` (Standard, Pfeil ist Zeichen). Inhalt `.fin-stack` mit den drei Abschnitten.
- `BankCallbackPage`: ohne `Card` und `style`. Ergebnis `<p className="msg ok fin-result">` bzw. `msg err fin-result` mit unveränderter `role`. Laden `loading` („Bankzugang wird eingerichtet …“). „Zu den Finanzen“ bleibt schwarz.
- [ ] **Step 4: Hygiene, Bau, Bild**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{|size=\"sm\"|form-control|form-group|FaSpinner" src/pages/FinanceSettingsPage.jsx src/pages/BankCallbackPage.jsx src/components/finance/{SettingsCard,BankAccessCard,FixedCostsCard,FormField}.jsx; "$W/build.sh" "$WT" p7 && "$W/shots.sh" p7 "$W/shots/p7-t6" --only finanzen-einstellungen,finanzen-einstellungen-fehler,finanzen-bank-callback,finanzen-bank-callback-ok --gate p7`
Expected: `rg` ohne Treffer, Exit 0, alle Schritte laufen durch. Bild `finanzen-einstellungen-fehler`: roter Ring und Fehlertext am Feld, keine Kästen.
- [ ] **Step 5: Commit**
```bash
git add src/styles/global.css src/pages/FinanceSettingsPage.jsx src/pages/BankCallbackPage.jsx src/components/finance/SettingsCard.jsx src/components/finance/BankAccessCard.jsx src/components/finance/FixedCostsCard.jsx src/components/finance/FormField.jsx
git commit -m "Finanzen: Einstellungen, Bankzugang, Fixkosten und Bankrueckkehr ohne Karten" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 7: `AssignDialog` auf `Dialog`
**Files:**
- Modify: `src/components/finance/AssignDialog.jsx`, `src/styles/global.css` (Dialog-Regionen für die Zuordnung)
**Interfaces:**
- Consumes: `Dialog({ title, onClose, busy, wide, footer, children })` (P1), `Field`, `.check`, `.kv`, `.msg`, `useScrollIntoView`.
- Produces: `AssignDialog({ transaction, invoices, invoicesLoading, invoicesError, invoicesLoaded, onClose, onDone })` unverändert. `onClose({ refresh })` und `onDone(buchung)` verhalten sich wie bisher.
- [ ] **Step 1: CSS Zuordnen**
- `.fin-foot`: Flex, `flex: 1`, umbrechend, `justify-content: flex-end`, Abstand 8 px; bei ≤ 860 px `.fin-foot .btn { flex: 1 1 auto }`.
- `.fin-choices`: Raster ohne Rand. `.fin-choice` (mit `.check`): volle Breite, `padding: 8px 0`, Haarlinie unten. Mit gewähltem Radio ist `.fin-choice-label` 600 (`:has(input:checked)`). `.fin-choice-off` `opacity: .55; cursor: not-allowed`. `.fin-choice-detail` 13 px `--text-2`, mit `.warn` `--orange-fg`.
- `.fin-confirm` 600 in `--orange-fg`, kein Kasten. `.fin-assigned { margin: 0 }`. `.kv .fin-tx-facts-amount { text-align: left; font-weight: 600 }`.
- Gelöscht: `.fin-tx-facts*`, `.fin-confirm:focus`, Kastenregeln von `.fin-choice`.
- [ ] **Step 2: Komponente umbauen**
- Eigener Esc-Listener, `useDialogFocus`, `dialogRef`, Overlay-Markup und „ד-Knopf entfallen. Es bleiben `lockRef`, `aliveRef`, `dirtyRef`, `useScrollIntoView(errorRef, error)` und `confirmRef`.
- Gerüst `<Dialog title="Buchung zuordnen" onClose={close} busy={disabled} footer={…}>`. `close()` ist parameterlos (das „ד ruft es mit dem Klick-Event), prüft `lockRef` und ruft `onClose({ refresh: dirtyRef.current })`.
- Body: Fakten als flaches `<dl className="kv">`, Fehler `msg err` mit `ref={errorRef}`, Hinweise `msg muted`/`msg warn` mit unveränderten Rollen. Vorschläge und Kategorien über `radio(o)` als `<label className="check fin-choice…">`. „Andere offene Rechnung“, „Fixkosten“ und „Notiz (optional)“ sind `Field` mit `select`/`textarea`. Beim Bestätigen steht der Text im `.fin-confirm` (`role="group"`, `tabIndex={-1}`, Fokus per `confirmRef` wie heute).
- Fuß je Zustand: zugeordnet „Schließen“ und, bei `canUnassign`, „Zuordnung lösen“ (beide Standard). Bestätigend „Zurück“ (Standard) und „Ja, als bezahlt markieren“ (schwarz, „Markiere …“). Sonst „Abbrechen“ (Standard) und „Zuordnen“ (schwarz, „Ordne zu …“, gesperrt ohne Auswahl).
- Der Zweischritt bleibt: Bei einer Rechnung geht vor der Bestätigung keine Anfrage raus.
- [ ] **Step 3: Hygiene, Bau, Prüfung**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|useDialogFocus|keydown|overlay|fin-overlay|fin-close|form-control|form-label" src/components/finance/AssignDialog.jsx; "$W/build.sh" "$WT" p7 && "$W/shots.sh" p7 "$W/shots/p7-t7" --only dlg-zuordnen-zahlung,dlg-zuordnen-bestaetigung,dlg-zuordnen-busy --gate p7`
Expected: `rg` ohne Treffer, Exit 0, alle drei Schritte laufen durch. Bild: weißer Dialog, Vorschläge als Radiozeilen mit Haarlinien, bei 390 px Bottom-Sheet mit umbrechendem Fuß.
- [ ] **Step 4: Commit**
```bash
git add src/styles/global.css src/components/finance/AssignDialog.jsx
git commit -m "Finanzen: Buchung zuordnen auf Dialog-Baustein (Esc, Sperre, Vorschlaege als Zeilen)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 8: `InvoiceCreateModal` auf `Dialog`
**Files:**
- Modify: `src/components/finance/InvoiceCreateModal.jsx`, `src/styles/global.css` (Rest der Dialog-Region)
**Interfaces:**
- Consumes: `Dialog`, `Field`, `AutocompleteInput` (P1, rendert `.input`), `AddressWarning`, `.fin-foot` (Task 7).
- Produces: `InvoiceCreateModal({ open, onClose, onCreated, fixedCustomer, onShowOverview })` unverändert, samt Vertrag `onCreated(rechnung, info)`.
- [ ] **Step 1: CSS Rechnung schreiben**
- `.fin-items` ohne Rand. `.fin-item` Raster `minmax(0,1fr) 80px 120px 110px auto`, Abstand `8px 12px`, `align-items: end`. `.fin-item .input { width: 100% }` (zwei Klassen schlagen `.input`), `.fin-item .fin-amount` rechts. `.fin-item-label` 12 px `--text-2`, ab der zweiten Zeile visuell versteckt. `.fin-items-foot` Flex `space-between`, `margin-top: 8px`. `.fin-total` Flex, `gap: 16px`, 600.
- `.fin-dialog-row` Raster `180px minmax(0,1fr)`, Abstand 12 px.
- Bei ≤ 860 px: `.fin-item` Raster `minmax(0,.7fr) minmax(0,1fr) minmax(0,1fr) auto` mit Flächen `desc desc desc desc / qty price sum remove`, Labels sichtbar. `.fin-dialog-row` einspaltig.
- `.autocomplete .input { width: 100% }` legt P6 an (`rg -n "\.autocomplete \.input" src/styles/global.css`). Fehlt sie, kommt sie hierher (Prüfung in `dlg-rechnung-kunde`).
- Alte Hüllenregeln löschen: `.fin-close*`, `.fin-overlay`, `.fin-dialog` samt `:focus`, `.fin-dialog-error`, `.fin-dialog-warn`, `.fin-dialog-sub`, `.fin-dialog-actions`, `.fin-icon-btn*`.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "fin-(overlay|close|icon-btn)" src; rg -n "fin-dialog" src | rg -v "fin-dialog-row"`
Expected: Vor Step 2 trifft es noch `InvoiceCreateModal.jsx`, nach Step 2 höchstens den KOMPAT-Block.
- [ ] **Step 2: Komponente umbauen**
- Eigener Esc-Listener, `useDialogFocus`, `dialogRef`, Overlay und „ד-Knopf entfallen. Es bleiben `lockRef`, `aliveRef`, `errorRef` mit `useScrollIntoView`.
- Gerüst `<Dialog as="form" wide title="Rechnung schreiben" onClose={close} busy={disabled} onSubmit={(e) => e.preventDefault()} footer={…}>`. Das Formular hat keinen Submit-Knopf. Jeder Knopf darin trägt `type="button"` (`Button` setzt keinen Standard-Typ): „Abbrechen“, „Als Entwurf speichern“, „Senden“, „+ Position“, „Entfernen“ und der Knopf in `AddressWarning`. Deshalb sendet Enter nie. `close()` prüft `lockRef` und ruft `onClose()`.
- Body: zuerst Fehler `msg err` mit `ref={errorRef}`. Kunde als `Field` mit `htmlFor="fin-invoice-customer"` (fester Kunde oder `AutocompleteInput`), Hinweise `hint`/`msg warn`, `AddressWarning variant="dialog"` mit dem bisherigen `onShowOverview`-Handler.
- Positionen als `<fieldset className="fin-items">` mit `.fin-item`-Zeilen (`input`, `fin-amount`, `aria-label`s unverändert). „Entfernen“ ersetzt den Papierkorb als `<button type="button" className="link">` mit `aria-label="Position n entfernen"`, gesperrt bei nur einer Position. „+ Position“ ist ein Standardknopf. Zahlungsziel und Notiz als `Field` in `.fin-dialog-row`.
- Fuß `<div className="fin-foot">`: „Abbrechen“ und „Als Entwurf speichern“ (Standard), „Senden“ (schwarz, `disabled` und `title` wie heute). „Speichere …“ und „Sende …“ bleiben.
- Esc: Ein offenes Vorschlagslisten-Popover ruft `preventDefault`, dann schließt `Dialog` nicht. Ohne Liste schließt Esc über `close`.
- [ ] **Step 3: Hygiene, Bau, Prüfung**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|useDialogFocus|keydown|overlay|form-control|form-group|form-label|FaTrash|FaPlus" src/components/finance/InvoiceCreateModal.jsx; "$W/build.sh" "$WT" p7 && "$W/shots.sh" p7 "$W/shots/p7-t8" --only dlg-rechnung,dlg-rechnung-kunde,dlg-rechnung-enter,kunde-invoices-adresse --gate p7`
Expected: `rg` ohne Treffer, Exit 0, alle Schritte laufen durch (Overlay in Fenstergröße, Esc-Reihenfolge, Enter ohne Request, Feldbreite). Bild bei 390 px: Positionszeilen zweizeilig, Fuß mit umbrechenden Knöpfen, Vorschlagsliste sichtbar.
- [ ] **Step 4: Commit**
```bash
git add src/styles/global.css src/components/finance/InvoiceCreateModal.jsx
git commit -m "Finanzen: Rechnung schreiben auf Dialog-Baustein (breit, Enter sendet nie, Esc schliesst Liste zuerst)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 9: Gate P7, Review, Tag
- [ ] **Step 1: Rebase**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git rebase origin/test && "$W/check.sh"`
Expected: Exit 0, `# pass` = N + 2 und `# fail 0`. Die Größen von JS und CSS aus dieser Ausgabe gehen in Step 3.
- [ ] **Step 2: Vollständiger Gate-Lauf, Hygiene und tote Klassen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p7 && "$W/shots.sh" p7 "$W/shots/p7" --gate p7`
Expected: Exit 0. Alle Prüfungen aus P1 sind für alle Ansichten leer, `icon-svg` für alle `FIN`-Ansichten und die der früheren Pakete.
Run:
```bash
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh || exit 1
F="src/components/finance src/pages/FinancePage.jsx src/pages/FinanceSettingsPage.jsx src/pages/BankCallbackPage.jsx"
finblock() { sed -n '/^\/\* ===== FINANZEN (P7)/,/^\/\* ===== ENDE FINANZEN/p' src/styles/global.css; }
rg -n "react-icons|@tabler|style=\{|size=\"sm\"|ui-btn|form-control|form-group|form-label|overlay-content|overlay-close|spinner|Kpi|kpi|#[0-9a-fA-F]{3,8}\b|rgba?\(" $F
finblock | rg -n "#[0-9a-fA-F]{3,8}\b|rgba?\(|!important|var\(--(surface|border|text-muted|text-faint|r-|shadow-[123]|warn|info|ok)\b"
finblock | rg -o "\.fin-[a-z0-9-]+" | sort -u | sed 's/^\.//' | while read c; do rg -q "(^|[^A-Za-z0-9_-])${c}([^A-Za-z0-9_-]|$)" src --glob '!*.css' || echo "TOT $c"; done
finblock | wc -l; finblock | rg -c "container-type"
git diff --name-only design-p6..HEAD
```
Expected:
- Die ersten beiden Befehle geben nichts aus.
- Der dritte nennt nur `fin-address-warn-dialog` und `fin-address-warn-page` (per Template-Präfix gebaut, erlaubt).
- Der Finanzblock hat 250 bis 360 Zeilen, `container-type` steht genau einmal da (`.fin-body`).
- Der letzte Befehl (`git diff --name-only design-p6..HEAD`) nennt nur die Dateien aus „Global Constraints / Dateien“ und `tests/unit/financeUi.test.mjs`.
- [ ] **Step 3: Bundle-Größe**
Die JS- und CSS-Größe aus der `check.sh`-Ausgabe von Step 1 wird mit Task 1 Step 1 verglichen. Keiner der beiden Werte ist größer.
- [ ] **Step 4: Code-Review (sonnet)**
Geprüft wird der Diff `design-p6..HEAD` gegen die Spec-Abschnitte „Gestaltungsregeln“, „Bausteine“ und Paket P7. Prüfaufträge:
- Verhalten gleich geblieben: Sperren, `window.confirm`, Zweischritt in `AssignDialog`, Rollen und `data-label` der Tabellen.
- `busy` und `close()` sind in beiden Dialogen richtig angeschlossen, kein Dialog sitzt in `.fin-body`.
- Jede Klasse `fin-*` ist definiert oder bewusst entfallen, Rot nur bei Minus, Texte nur wie in „Global Constraints“ geändert.
Wichtige Befunde werden behoben, Kleinigkeiten notiert. Nach Korrekturen kommen Step 1 und Step 2 noch einmal, damit die Bilder den Stand zeigen, der getaggt wird.
- [ ] **Step 5: Tag und Übergabe**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" && git tag design-p7 && git log --oneline origin/test..design-p7 && git rev-parse design-p7`
Der Eigentümer bekommt:
- die Screenshots aus `shots/p7/1440x900` und `shots/p7/390x844` (Auswahl: `finanzen`, `finanzen-minus`, `finanzen-teilausfall`, `dlg-rechnung-kunde`, `dlg-zuordnen-bestaetigung`, `finanzen-einstellungen-fehler`, `kunde-invoices-adresse`)
- den Hinweis, dass negative Beträge jetzt rot sind und beide Dialoge neu auch mit Esc schließen
- die Push-Zeile `git -C /home/webklar/apps/ticket.webklar.com/dev-design push origin 'design-p7^{commit}:refs/heads/test'`
- die Rücknahme (nur solange P7 das jüngste Paket ist): `git revert --no-commit design-p6..design-p7`, Commit und Push durch den Eigentümer
- den Hinweis für P10: Die KOMPAT-Zeilen zu `.fin-link` und `.fin-dialog` (760 px) sind tot und fallen mit dem Block weg.
- [ ] **Step 6: Nach dem Push (erst, wenn der Eigentümer gepusht hat)**
- Im Log von `webklar-ticket-build` steht „Deployment completed successfully“.
- `curl -s https://ticket.webklar.com/ | grep -o 'assets/index-[^"]*\.js'` entspricht `dist/index.html` des Tags.
- Ein lesender Sichttest im Chrome des Eigentümers: Finanzen mit Statuszeile und Rechnungsliste, Scroll von „Zu den Rechnungen“. Keinen Dialog öffnen, der etwas sendet.
@@ -0,0 +1,661 @@
# WOMS „Weißes Design“ P8: Rechtliches und Admin – Umsetzungsplan
> **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:** Nach P8 sind „Rechtliches“ und „Admin“ kastenlos und ohne Icons. Rechtstexte stehen als Abschnitte mit Haarlinien-Zeilen, die Anbieter-Verträge als Tabelle. Vertrags-, Versions- und Mitarbeiter-Formular sind Dialoge, die nur bei Erfolg schließen und Fehler im Dialog zeigen. Jede heutige Funktion bleibt erreichbar.
**Architecture:**
- Zwei neue reine Lib-Dateien mit Tests: `src/lib/legalView.js` (Anzeige-Texte der Vertragsliste, Badge-Anteil der Frist) und `src/lib/employeeForm.js` (Prüfung des Mitarbeiter-Formulars).
- Bausteine aus P1: `Dialog`, `Field`, `Card`, `Badge`, `Tabs`, `PageHeader`, `EmptyState`, `.table`, `.msg`, `.loading`, `.link`.
- CSS: Der Rechtliches-Block in `global.css` und `AdminPage.css` (416 → rund 45 Zeilen) werden neu geschrieben. `.credential*`, `.sr-only` und `.spin` ziehen nach `global.css`.
- Hooks (`useLegal`, `useEmployees`) und `lib/legal.js` bleiben unverändert.
**Tech Stack:** React 18, react-router-dom 6, reines CSS. Tests mit `node --test`, Screenshot-Werkzeug aus P0.
**Spec:** `docs/superpowers/specs/2026-10-04-weisses-design-design.md`, Abschnitte „Gestaltungsregeln“, „Bausteine“ (Abschnitt, Knöpfe, Links, Badge, Tabelle, Formular, Dialog, Meldungen, Laden), „Texte“ und Paket P8. Anhang `2026-10-04-weisses-design-umbaukarte.md`: Abschnitt 3 (Dialog-Verbraucher), 4 (Icon-Regel), 5 (P8), 7.4, 7.8. Globale Regeln und Schnittstellen: `2026-10-05-weisses-design-00-uebersicht.md`.
## Global Constraints
Es gelten die Regeln der Übersicht. Für P8 kommt hinzu:
- Jeder Run-Block beginnt mit `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh`. Danach gelten `$WT` (Worktree, Arbeitsverzeichnis), `$W` (Werkzeug) und `$TMPDIR`. Tests und Build werden nur über `"$W/check.sh"` ausgewertet, Browser-Skripte laufen nur über `"$W/shots.sh"`.
- Ausgangspunkt ist das Tag `design-p7`, alle Diff-Prüfungen vergleichen gegen `design-p7`.
- Unverändert bleiben `src/hooks/useLegal.js`, `useEmployees.js`, `useDialogFocus.js`, `useScrollIntoView.js` und `src/lib/legal.js`. Beide `window.confirm`-Rückfragen („Vertrag „…“ wirklich löschen?“, „… und dessen Login wirklich löschen?“) bleiben wörtlich.
- Sichtbare Textänderungen sind nur: „Bearbeiten“ und „Löschen“ in der Admin-Tabelle (waren Icons), „Speichert …“ im Speichern-Knopf des Admin-Formulars (war Spinner) und der Leer-Hinweis der Admin-Tabelle (Task 9).
- Dialoge schließen nur über „ד, Esc und bei Erfolg. Bei Fehlern bleiben Dialog und Eingaben offen. Solange gespeichert wird, sind „ד, Esc und „Abbrechen“ gesperrt.
- Jeder Knopf in einem `<Dialog as="form">` außer dem Absenden-Knopf trägt `type="button"` (`Button` setzt keinen eigenen Standard-Typ).
- Abstände zwischen aufeinanderfolgenden Abschnitten kommen von `.page section + section { margin-top: 36px }` aus P1. P8 baut dafür keine eigenen Regeln (kein `.legal-doc + .legal-doc`, kein `.admin-page section + section`).
- CSS-Reihenfolge: `main.jsx` importiert `App` vor `global.css`, also lädt `AdminPage.css` vor `global.css`, und bei gleicher Spezifität gewinnt `global.css`. Regeln in `AdminPage.css`, die eine Global-Klasse überstimmen, haben höhere Spezifität (0,2,0, z. B. `.page.admin-page …`, `.check.admin-check`) und nie `!important`. Deshalb wirkt die heutige Breite 1480 px nie.
- Die CSS-Tabellen dieses Plans nennen nur die Abweichungen vom Prototyp und von den Tokens. Was Basis (`*`-Reset, `button`-Reset), `.table`, `.kv`, `.field-row` und `.check` schon liefern, wird nicht wiederholt.
- In den umgebauten Dateien: keine Icon-Importe, keine neuen `style={{…}}`, keine Farbliterale.
## Review Focus
Aus der Übersicht gehören drei Punkte zu P8:
- **Punkt 2, lange Inhalte am Handy:** Testdaten `lang-recht-admin` (Anbietername 60 Zeichen, Zweck 90, Notiz 300, Dateiname 70, E-Mail 70). `overflow-x` ist seit dem Gate `p1` für alle Ansichten scharf, also auch im Gate `p8`.
- **Punkt 3, Teilausfall:** Ansicht `admin-ausfall` (Szenario `fehler`). Die Mitarbeiterliste antwortet 500, die Seite zeigt `.msg.err`, „Mitarbeiter anlegen“ öffnet weiter den Dialog.
- **Punkt 4, nur Tastatur:** Esc schließt Vertrags- und Mitarbeiter-Dialog. Bei laufendem Speichern ist „ד gesperrt und Esc wirkungslos (`dlg-vertrag-fehler`, `admin-fehler`). Aufklappzeilen sind echte `<button>` mit `aria-expanded` und eigenem `onClick` (Enter und Klick auf den Button klappen auf).
Zusätzlich gilt: Das Seiten-Banner der Admin-Seite bleibt für Erfolg und Löschen, Validierungs- und Speicherfehler des Formulars stehen im Dialog. Checkliste der Bedienelemente, nichts darf fehlen:
- Rechtliches: 2 Reiter, „Neue Version“, Verlauf auf-/zuklappen, „Live-Seite öffnen“, „Als gültig markieren“, Dateilinks mit „(herunterladen)“, „Neuer Vertrag“, Zeile aufklappen, „Bearbeiten“, „Als gekündigt markieren“ samt „Endet am“, „Speichern“, „Abbrechen“, „Löschen“ (mit Rückfrage), alle Felder und der Upload beider Dialoge.
- Admin: Reiter, „Mitarbeiter anlegen“, „Bearbeiten“, „Löschen“ (nicht in der eigenen Zeile, mit Rückfrage), Kopieren von E-Mail und User-ID, Zugangsdaten-Abschnitt mit „Anzeigen“, „Kopieren“, „ד, Admin-Checkbox (für das eigene Konto gesperrt), Kürzel in Großbuchstaben.
---
### Task 1: Paketstart und Ausgangswerte
**Files:** keine Änderung.
**Interfaces:** keine.
- [ ] **Step 1: Paketstart**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git merge-base --is-ancestor design-p7 HEAD && git rebase origin/test && "$W/check.sh" && git status --short`
Expected: Exit 0, Rebase ohne Konflikt, `# fail 0` und eine leere Statusliste. Endet `merge-base` mit Exit 1, liegt `design-p7` nicht im Branch: stoppen und melden. Die Testzahl heißt im Folgenden N0, die Bundle-Größen aus der Ausgabe von `check.sh` werden notiert.
- [ ] **Step 2: Abgleich mit TP2**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git log --oneline origin/test..origin/feature/terminplaner-tp2 -- src/pages/LegalPage.jsx src/components/legal src/lib/legal.js src/hooks/useLegal.js src/pages/AdminPage.jsx src/pages/AdminPage.css src/components/AdminRoute.jsx src/components/CopyableCredential.jsx src/styles/global.css`
Expected: keine Ausgabe. Stand 2026-10-05 berührt `225e2f1` die Rechtliches-Dateien. Gibt es Treffer, stoppt der Agent vor Task 2, nennt dem Eigentümer die Commits und fragt nach. Er empfiehlt dabei, TP2 zuerst nach `test` zu bringen.
- [ ] **Step 3: Voraussetzungen aus P1 bis P7 prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && ls src/components/ui/Dialog.jsx src/components/ui/Field.jsx src/components/ui/tones.js src/lib/rowClick.js && rg -n "isRowActivation" src/lib/rowClick.js && rg -n "button\.link" src/styles/global.css && rg -n -m 6 "\.field-row|\.dialog-foot|\.msg\.err|\.loading|\.kv\b" src/styles/global.css`
Expected: Die vier Dateien sind da. `src/lib/rowClick.js` (P2 Task 2) enthält `isRowActivation`. Fehlt die Datei, stoppt der Agent und meldet es, P8 baut keinen eigenen Zeilenklick-Helfer. `button.link` hat ein Zurücksetzen mit `background: none` und `border: 0` (P2 oder P5), und die Klassen sind definiert.
Entscheidung: Fehlt das Zurücksetzen, ergänzt P8 es im Commit von Task 5: `button.link { background: none; border: 0; padding: 0; font: inherit; text-align: left; cursor: pointer }`.
### Task 2: `legalView` (Lib, TDD)
**Files:**
- Create: `src/lib/legalView.js`
- Test: `tests/unit/legalView.test.mjs` (neu)
**Interfaces:**
- Consumes: `formatDate` und `deadlineTone` aus `src/lib/legal.js`, unverändert.
- Produces, alles in `src/lib/legalView.js`:
- `costLabel(contract)`, `termLabel(contract)`, `noticeLabel(contract)`, `deadlineLabel(result)` → String. Es sind die bisherigen lokalen Funktionen aus `ProviderContractsTab.jsx` Z. 8–36.
- `deadlineView(result, today) → { text, lead, rest, badge, muted }` mit `badge: 'danger' | 'warn' | null`.
- [ ] **Step 1: Failing Test schreiben**
```js
const { costLabel, termLabel, noticeLabel, deadlineView } = await loadModule('src/lib/legalView.js')
const NB = '\u00a0'
const today = '2026-09-30'
const active = (noticeBy, periodEnd = '2027-03-12') => ({ state: 'active', noticeBy, periodEnd })
const v = (r) => deadlineView(r, today)
test('Vertragsliste: Kosten, Laufzeit, Frist wie bisher', () => {
assert.equal(costLabel({ cost: null }), '–')
assert.equal(costLabel({ cost: 12.5, costInterval: 'month' }), `12,50${NB}€/Monat`)
assert.equal(costLabel({ cost: 120, costInterval: 'year' }), `120,00${NB}€/Jahr`)
assert.equal(termLabel({}), '–')
assert.equal(termLabel({ termMonths: 0 }), 'jederzeit kündbar')
assert.equal(termLabel({ termMonths: 12, renewalMonths: 12 }), '12 Monate, dann je 12 Monate')
assert.equal(termLabel({ termMonths: 24, renewalMonths: 0 }), '24 Monate, endet danach')
assert.equal(noticeLabel({}), '–')
assert.equal(noticeLabel({ noticeValue: 1, noticeUnit: 'months' }), '1 Monate')
assert.equal(noticeLabel({ noticeValue: 14, noticeUnit: 'days' }), '14 Tage')
})
test('deadlineView: Badge nur bei rot und orange, Text wörtlich wie bisher', () => {
assert.deepEqual(v(active('2026-10-15')), {
text: 'kündbar bis 15.10.2026 (Periode endet 12.03.2027)',
lead: 'kündbar bis 15.10.2026', rest: '(Periode endet 12.03.2027)', badge: 'danger', muted: false,
})
assert.equal(v(active('2026-10-30')).badge, 'danger') // 30 Tage
assert.equal(v(active('2026-10-31')).badge, 'warn') // 31 Tage
assert.equal(v(active('2026-12-29')).badge, 'warn') // 90 Tage
const normal = v(active('2026-12-30')) // 91 Tage
assert.deepEqual([normal.badge, normal.lead === normal.text, normal.rest], [null, true, ''])
})
test('deadlineView: Text und Dämpfung der übrigen Zustände', () => {
assert.deepEqual([v({ state: 'anytime' }).text, v({ state: 'anytime' }).muted], ['jederzeit kündbar', false])
assert.equal(v({ state: 'ends', periodEnd: '2027-01-31' }).text, 'endet am 31.01.2027')
assert.deepEqual([v({ state: 'ended', periodEnd: '2026-01-31' }).text, v({ state: 'ended', periodEnd: '2026-01-31' }).muted], ['beendet seit 31.01.2026', true])
assert.equal(v({ state: 'cancelled', periodEnd: '2027-01-31' }).text, 'gekündigt zum 31.01.2027')
assert.equal(v({ state: 'cancelled', periodEnd: null }).text, 'gekündigt')
assert.deepEqual([v({ state: 'incomplete' }).text, v({ state: 'incomplete' }).muted], ['Fristen fehlen', true])
})
```
- [ ] **Step 2: Test laufen lassen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/legalView.test.mjs`
Expected: FAIL, Modul nicht gefunden.
- [ ] **Step 3: Implementieren**
- Die vier Funktionen samt Formatierer `euro` (Z. 8–36 von `ProviderContractsTab.jsx`) wandern unverändert nach `src/lib/legalView.js`.
- `deadlineView` ergänzt sie. `text` ist `deadlineLabel(result)`. Bei `deadlineTone(result, today)` gleich `red` ist `badge` `'danger'`, bei `orange` `'warn'`, sonst `null`.
- Bei gesetztem `badge` ist `lead` `kündbar bis ${formatDate(noticeBy)}` und `rest` `(Periode endet ${formatDate(periodEnd)})`. Sonst ist `lead` gleich `text` und `rest` leer.
- `muted` ist wahr bei Ton `grey` oder `missing`.
- [ ] **Step 4: Test grün**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/legalView.test.mjs && "$W/check.sh" --nur-tests`
Expected: PASS, Exit 0, Testzahl N0 + 3, `# fail 0`.
- [ ] **Step 5: Commit**
```bash
git add src/lib/legalView.js tests/unit/legalView.test.mjs
git commit -m "Rechtliches: Anzeige-Texte der Vertragsliste als Lib mit Test" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 3: `validateEmployeeForm` (Lib, TDD)
**Files:**
- Create: `src/lib/employeeForm.js`
- Test: `tests/unit/employeeForm.test.mjs` (neu)
**Interfaces:**
- Produces: `validateEmployeeForm(form, isEdit) → string | null`. Der String ist der deutsche Fehlertext, `null` heißt gültig. Prüfungen und Texte sind die aus `AdminPage.jsx` Z. 70–82.
- [ ] **Step 1: Failing Test schreiben**
```js
const { validateEmployeeForm } = await loadModule('src/lib/employeeForm.js')
const base = { displayName: 'Max Muster', email: 'max@webklar.com', password: '', shortcode: '', isAdmin: false }
const NEED = 'Name und E-Mail sind erforderlich.'
test('validateEmployeeForm: neuer Mitarbeiter', () => {
assert.equal(validateEmployeeForm({ ...base, displayName: ' ', password: '12345678' }, false), NEED)
assert.equal(validateEmployeeForm({ ...base, email: '', password: '12345678' }, false), NEED)
assert.equal(validateEmployeeForm({ ...base, password: '1234567' }, false), 'Das Passwort muss mindestens 8 Zeichen haben.')
assert.equal(validateEmployeeForm({ ...base, password: '12345678' }, false), null)
})
test('validateEmployeeForm: Mitarbeiter bearbeiten', () => {
assert.equal(validateEmployeeForm(base, true), null) // leeres Passwort = unverändert
assert.equal(validateEmployeeForm({ ...base, password: 'abc' }, true), 'Ein neues Passwort muss mindestens 8 Zeichen haben.')
assert.equal(validateEmployeeForm({ ...base, password: 'abcdefgh' }, true), null)
assert.equal(validateEmployeeForm({ ...base, email: ' ' }, true), NEED)
})
```
- [ ] **Step 2: Test laufen lassen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/employeeForm.test.mjs`
Expected: FAIL, Modul nicht gefunden.
- [ ] **Step 3: Implementieren**
Reihenfolge der Prüfungen: erst Name und E-Mail (getrimmt), dann bei `!isEdit` die Mindestlänge 8, dann bei `isEdit` mit nicht leerem Passwort die Mindestlänge 8.
- [ ] **Step 4: Test grün**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/employeeForm.test.mjs && "$W/check.sh" --nur-tests`
Expected: PASS, Exit 0, Testzahl N0 + 5, `# fail 0`.
- [ ] **Step 5: Commit**
```bash
git add src/lib/employeeForm.js tests/unit/employeeForm.test.mjs
git commit -m "Admin: Pruefung des Mitarbeiter-Formulars als Lib mit Test" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 4: Werkzeug – Testdaten, Szenario, Ansichten, Gate `p8`
**Files** (alle unter `$W/`, kein Commit im Repo):
- Create: `fixtures/lang-recht-admin.mjs`
- Modify: `backend.mjs`, `selftest.mjs`, `checks.mjs`, `views.mjs`, `gates.mjs`
**Interfaces:**
- Consumes: `createBackend`, `VIEWS`, `GATES` und die Testdaten aus P0.
- Produces: die Ansichten aus Step 4, `GATES.p8`, das Szenario `'mitarbeiter'` und die erweiterte Konsolen-Ausnahme des Szenarios `fehler`.
- [ ] **Step 1: Testdaten `lang-recht-admin` anlegen**
Die Datei erweitert `standard` im Stil von `altdaten.mjs`. Alle Daten sind relativ zum heutigen Tag berechnet, damit die Töne stabil bleiben.
- **Rechtstexte:** Dokument A (Titel 50 Zeichen) mit gültiger Version, Entwurf und zwei abgelösten Versionen, eine mit 300-Zeichen-Notiz und eine Datei mit 70-Zeichen-Namen. Dokument B hat nur einen Entwurf, Dokument C keine Version.
- **Verträge:** Je ein Vertrag mit Kündigungstermin in 10 Tagen (rot), 60 (orange) und 200 (normal), jeweils Frist 30 Tage, Laufzeit und Verlängerung 12 Monate. Dazu ein gekündigter, beendeter, unvollständiger, jederzeit kündbarer und auslaufender Vertrag. Einer trägt Anbieter (60 Zeichen), Zweck (90), Notiz (300) und eine Datei (70).
- **Mitarbeiter:** die 4 Standard-Mitarbeiter plus einer mit Namen (40) und E-Mail (70 Zeichen). Der Mitarbeiter KNSO trägt die `userId` des angemeldeten Kontos, sonst ergänzen (Ansicht `admin-bearbeiten` braucht die eigene Zeile).
- [ ] **Step 2: Szenario `mitarbeiter` (erst Selbsttest, dann Backend)**
`selftest.mjs` bekommt die Prüfung: `GET https://ticket.test/v1/account` liefert im Szenario `mitarbeiter` 200 mit `labels: ['mitarbeiter']`.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && cd "$W" && node selftest.mjs`
Expected: FAIL. Nach der Ergänzung in `backend.mjs`: `selftest ok`.
- [ ] **Step 3: Konsolen-Ausnahme für absichtliche 500er**
Die absichtlich per `page.route` erzeugten 500er schreibt der Browser als `console.error` („Failed to load resource“). Der Check `console-error` nimmt Meldungen aus, die das Szenario `fehler` auslöst (P0).
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "fehler" "$W/checks.mjs" "$W/shots.mjs"`
- Die Ansichten mit absichtlichen 500ern (`dlg-vertrag-fehler`, `dlg-version-fehler`, `admin-fehler`, `admin-ausfall`) laufen im Szenario `fehler`.
- Deckt die Ausnahme ihre Meldungen nicht ab, wird sie in `checks.mjs` um die URL-Muster `**/collections/*/documents` (nur POST) und `**/api/admin/employees` erweitert. P7 tut dasselbe für die Finanzen, ein schon vorhandener gleichwertiger Eintrag wird mitbenutzt.
- [ ] **Step 4: Ansichten in `views.mjs` anlegen oder anpassen**
Alle nutzen `fixtures: 'lang-recht-admin'`, die vier Fehleransichten zusätzlich `scenario: 'fehler'`. Der `path` ist `/rechtliches` bzw. `/admin`. Die Schritte haben die Signatur `steps(page, ctx)`, `ctx.backend.calls` listet die vom Backend beantworteten Requests. Fehlerantworten setzen die Schritte mit `page.route(…)`. Seitenrouten haben Vorrang vor den Kontextrouten des Backends, `route.fallback()` reicht alles andere durch. Geprüft wird mit `throw new Error(…)`, das den Verstoß `step-failed` erzeugt. Die vier `dlg-*`-Ansichten behalten die Viewports `[[1440, 900], [390, 844], [1440, 700], [390, 700]]` und rufen am Ende `submitReachable(page)` auf (P0).
| ID | Schritte |
|---|---|
| `rechtliches` | `/rechtliches`. Fokus auf `.legal-doc-summary` des ersten Dokuments, Enter. Es muss `aria-expanded="true"` stehen und `.legal-history` sichtbar sein. |
| `rechtliches-anbieter` | Reiter „Verträge mit Anbietern“. Fokus auf `button.link` der ersten Zeile, Enter. `.legal-detail` muss sichtbar sein. Klick „Als gekündigt markieren“, `.legal-cancel` muss sichtbar sein. |
| `dlg-vertrag` | Reiter Anbieter, Klick „Neuer Vertrag“. `[role=dialog]` muss offen sein, Esc muss ihn schließen. Dann wieder öffnen. |
| `dlg-vertrag-fehler` | Dialog öffnen, Anbieter „Testanbieter“ eintragen. `POST **/collections/*/documents` mit 800 ms Verzögerung und 500 `{ message: 'Test: Speichern fehlgeschlagen' }` beantworten, Speichern klicken. Sofort muss `.dialog-close` `disabled` sein, Esc darf nicht schließen. Nach der Antwort steht `.msg.err[role=alert]` mit dem Text im offenen Dialog, und „Anbieter“ trägt noch „Testanbieter“. |
| `dlg-version` | Reiter Texte, Klick „Neue Version“ am ersten Dokument. Bei 390 px liegt der Speichern-Knopf nach `scrollIntoView` im Fenster. |
| `dlg-version-fehler` | Wie `dlg-vertrag-fehler` mit Version „v9“. |
| `admin` | `/admin`. |
| `admin-neu` | Klick „Mitarbeiter anlegen“. Dialog muss offen sein, Esc schließt ihn. Dann wieder öffnen. |
| `admin-fehler` | Dialog öffnen, Name, E-Mail, Passwort „abc“, absenden. Im Dialog muss „Das Passwort muss mindestens 8 Zeichen haben.“ als `.msg.err` stehen. Dann Passwort „12345678“, `POST **/api/admin/employees` mit 500 `{ error: 'Test: Login konnte nicht angelegt werden' }` beantworten, absenden. Die Meldung wechselt, der Dialog bleibt offen. |
| `admin-zugang` | Gültiges Formular absenden. Der Dialog muss zu sein, `.msg.ok` und der Abschnitt „Login wurde angelegt“ müssen sichtbar sein. |
| `admin-bearbeiten` | Klick „Bearbeiten“ in der Zeile des angemeldeten Kontos. Titel „Mitarbeiter bearbeiten“, Admin-Checkbox `disabled`. |
| `admin-ausfall` | Zusätzlich zum Szenario `GET **/api/admin/employees` mit 500 beantworten (die Seite fragt erst das Portal, dann die Collection). Ein `.msg.err` steht auf der Seite, „Mitarbeiter anlegen“ öffnet den Dialog. |
| `admin-403` | Szenario `mitarbeiter`. Die Überschrift „Zugriff verweigert“ steht da. |
- [ ] **Step 5: Gate festlegen**
In `gates.mjs` kommt nach der Deklaration von `GATES` dazu. Das Gate `p8` erbt Prüfungen und Ansichten von `p7` (die `p1`-Prüfungen für alle Ansichten, `step-failed`, `external-request` und `console-error` immer) und schaltet `icon-svg` zusätzlich für die Ansichten von P8 und aller früheren Pakete ein:
```js
const iconSvg = GATES.p7.scoped['icon-svg']
GATES.p8 = {
...GATES.p7,
scoped: {
...GATES.p7.scoped,
'icon-svg': {
...iconSvg,
views: [...iconSvg.views, 'rechtliches', 'rechtliches-anbieter', 'dlg-vertrag', 'dlg-vertrag-fehler', 'dlg-version', 'dlg-version-fehler',
'admin', 'admin-neu', 'admin-fehler', 'admin-zugang', 'admin-bearbeiten', 'admin-ausfall', 'admin-403'],
},
},
}
```
- [ ] **Step 6: Ausgangslauf**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p8-vorher && "$W/shots.sh" p8-vorher "$W/shots/p8-vorher" --only admin --gate p8`
Expected: Exit 1 mit `icon-svg` in `admin` (Icons der Zeilenaktionen und des Knopfes). Das ist der Ausgangszustand.
### Task 5: Rechtstexte-Reiter
**Files:**
- Modify: `src/pages/LegalPage.jsx` (Z. 19 Ladetext, Z. 34 Fehlerzeile)
- Modify: `src/components/legal/LegalTextsTab.jsx` (Markup Z. 23–83, Logik Z. 9–21 bleibt)
- Modify: `src/components/legal/legalUi.jsx` (`FileLinks`, Z. 8–21)
- Modify: `src/styles/global.css` (Block „Rechtliches“, `rg -n "Rechtliches ---" src/styles/global.css`; bei Bedarf das Zurücksetzen für `button.link` aus Task 1)
**Interfaces:**
- Consumes: `Card`, `Badge`, `Tabs`, `EmptyState`, `PageHeader` (P1), `.link`, `.msg.err`, `.loading`, `currentVersion` und `formatDate` aus `lib/legal.js`.
- Produces: `LegalTextsTab` und `FileLinks` behalten ihre Props. Dateilinks tragen `class="link"`, der Download-Link zusätzlich `legal-dl`.
- [ ] **Step 1: `LegalPage.jsx` anpassen**
- `<div className="faint">Lädt …</div>` wird `<div className="loading">Lädt …</div>`. `<div className="legal-error">` wird `<div className="msg err" role="alert">`.
Entscheidung: Die Ladetexte „Lädt …“ und (Task 9) „Mitarbeiter werden geladen …“ bleiben wörtlich. Die Spec ändert nur „Loading...“, hier ändert sich nur die Klasse.
- [ ] **Step 2: `LegalTextsTab.jsx` umbauen**
- Wurzel `<div className="legal-docs">`. Der Aktionsfehler wird `<div className="msg err legal-msg" role="alert">`.
- Pro Dokument `Card className="legal-doc" title={doc.title}` (ein eigener Abschnitt, den Abstand zum nächsten liefert P1). Die Aktion ist `<button type="button" className="link" onClick={() => setFormFor(doc)}>Neue Version</button>`.
- Die Zusammenfassung ist ein echter `<button type="button" className="legal-doc-summary" aria-expanded={expanded}>` statt `div role="button"`. Der Inhalt bleibt: Badge „{version} · gilt seit …“ bzw. „noch keine gültige Version“, Badges „Entwurf {version}“, dazu `<span className="legal-toggle">` mit „Verlauf ausblenden“ bzw. „Verlauf (n)“.
- „Live-Seite öffnen“ ist `<a className="link legal-live" …>`.
- Verlauf `ol.legal-history`, je Version ein `li` mit der Klasse `legal-version`, bei Status `superseded` zusätzlich `is-superseded` (kein Klassenname mehr aus dem Status). Kopf: Version fett, Zeitraum in `<span className="muted">`, Badges „gültig“ (ok) und „Entwurf“ (muted), „Als gültig markieren“ als `<button type="button" className="link">`.
- „Noch keine Versionen eingetragen.“ bleibt `.faint`, `VersionForm` wird wie bisher gerendert.
- [ ] **Step 3: `legalUi.jsx` anpassen**
Dateilink `<a className="link" …>`, Download `<a className="link legal-dl" …>(herunterladen)</a>`. „keine Datei“ bleibt `.faint` (leerer Zustand).
- [ ] **Step 4: CSS im Block „Rechtliches“ ersetzen**
Gelöscht werden `.legal-grid` samt `> .legal-error`, `.legal-version-current`, `-draft`, `-superseded` und `.legal-files a`. Neu oder geändert:
| Regel | Wert |
|---|---|
| `.legal-doc-summary` | `display: flex; flex-wrap: wrap; align-items: center; gap: 6px; width: 100%; padding: 12px 0; text-align: left; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line)` (Knopf-Reset kommt aus der Basis) |
| `.legal-toggle` | `margin-left: auto; font-size: 13px; color: var(--text-2)` |
| `.legal-live` | `display: inline-block; margin-top: 8px; font-size: 13px` |
| `.legal-history` | `list-style: none; margin: 0; padding: 0` |
| `.legal-version` | `padding: 10px 0; border-bottom: 1px solid var(--line)` |
| `.legal-version.is-superseded` | `color: var(--text-2)` |
| `.legal-version-head` | `display: flex; flex-wrap: wrap; align-items: center; gap: 8px` |
| `.legal-version-head .link` | `margin-left: auto` |
| `.legal-note` | `white-space: pre-wrap; margin: 4px 0; font-size: 13px; color: var(--text-2); overflow-wrap: anywhere` |
| `.legal-files` | `display: inline-flex; flex-wrap: wrap; gap: 4px 12px; font-size: 13px; overflow-wrap: anywhere` |
| `.legal-files .link` | `font-weight: 400` |
| `.legal-files .legal-dl` | `color: var(--text-2)` |
| `.legal-msg` | `margin: 0 0 12px` |
- [ ] **Step 5: Bauen und prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p8 && "$W/shots.sh" p8 "$W/shots/p8-t5" --only rechtliches --gate p8`
Expected: PASS. In den Bildern gilt:
- Kein Kasten um Dokumente, nur Haarlinien über und unter der Zusammenfassung und zwischen den Versionszeilen.
- „Neue Version“ steht als Textlink rechts neben der Überschrift.
- Die Zeile der gültigen Version hat keinen grünen Hintergrund (nur das Badge ist grün), die des Entwurfs keinen gestrichelten Rand.
- Dateilinks sind unterstrichen. Bei 390 px läuft nichts über.
- [ ] **Step 6: Commit**
```bash
git add src/pages/LegalPage.jsx src/components/legal/LegalTextsTab.jsx src/components/legal/legalUi.jsx src/styles/global.css
git commit -m "Design: Rechtstexte als Abschnitte mit Haarlinien, Aufklappzeile als Button" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 6: Anbieter-Reiter
**Files:**
- Modify: `src/components/legal/ProviderContractsTab.jsx` (lokale Label-Funktionen Z. 8–36 löschen, Markup Z. 75–140)
- Modify: `src/styles/global.css` (Block „Rechtliches“)
**Interfaces:**
- Consumes: `costLabel`, `termLabel`, `noticeLabel`, `deadlineView` (Task 2), `isRowActivation(event)` aus `src/lib/rowClick.js` (P2 Task 2, Prüfung in Task 1), `deadlineTone`, `nextCancellationDate` (`lib/legal.js`), `FileLinks`, `ContractForm` (Props unverändert), `Badge`, `Button`, `EmptyState`.
- Produces: `ProviderContractsTab` behält seine Props.
- [ ] **Step 1: Markup umbauen**
`TONE_ORDER`, Sortierung, `run`, `confirmCancel` und `remove` (Z. 38–73) bleiben.
- Aktionsfehler `<div className="msg err legal-msg" role="alert">`. Die `.toolbar` mit „Neuer Vertrag“ (`Button variant="primary"`) bleibt.
- `<table className="table legal-contracts">`. Die Spalten „Wofür“, „Kosten“, „Laufzeit“ und „Kündigungsfrist“ bekommen `hide-sm`.
- Zeile ohne `legal-tone-*`: `<tr className="row" onClick={(e) => { if (isRowActivation(e)) setOpen(open === c.$id ? null : c.$id) }}>`. Der Helfer ignoriert Klicks auf `a, button, select, input, label`.
- Erste Zelle: `<div className="cell-title"><button type="button" className="link" aria-expanded={open === c.$id} onClick={() => setOpen(open === c.$id ? null : c.$id)}>{c.provider}</button></div>`.
- Button und Zeilenhandler benutzen denselben Umschalter `setOpen(open === c.$id ? null : c.$id)`.
- Der Button braucht seinen eigenen `onClick`, weil `isRowActivation` Klicks auf `button` ausfiltert. Ohne ihn klappt weder Klick noch Enter auf dem Button etwas auf, und die Ansicht `rechtliches-anbieter` scheitert.
- Zelle „Nächster Termin“ mit `view = deadlineView(result, today)`: Bei `view.badge` steht `<Badge tone={view.badge}>{view.lead}</Badge>` und dahinter `<span className="muted"> {view.rest}</span>`. Sonst steht `<span className={view.muted ? 'muted' : undefined}>{view.text}</span>`.
- Detail-Zeile `tr.legal-detail > td colSpan={6}`, von oben nach unten:
- `dl.kv.legal-facts` mit „Wofür“, „Kosten“, „Laufzeit“, „Kündigungsfrist“, nur unter 860 px sichtbar.
- „Beginn: …“, Notiz `.legal-note`, `FileLinks`.
- `div.legal-actions`: `button.link` „Bearbeiten“, `button.link` „Als gekündigt markieren“ (Bedingung unverändert), `Button variant="danger"` „Löschen“.
- Beim Kündigen steht statt des Links `span.legal-cancel`: ``<label htmlFor={`cancel-${c.$id}`}>Endet am</label>``, `<input id=… type="date" className="input">`, `Button variant="primary"` „Speichern“, `Button` „Abbrechen“.
Entscheidung: Die Aufklappzeile folgt der Tabellenregel der Spec. Der echte Button in der ersten Zelle trägt die Bedienung, der Zeilenklick ist nur eine Zugabe für die Maus.
Entscheidung: Unter 860 px zeigt die Tabelle nur „Anbieter“ und „Nächster Termin“ (Kernspalten-Regel). Die übrigen Angaben stehen dann im Detail (`.legal-facts`) mit den vorhandenen Spaltenköpfen als Beschriftung. Es kommt kein neuer Text dazu, und keine Information geht verloren.
- [ ] **Step 2: CSS ergänzen und Altregeln löschen**
Gelöscht werden `.legal-tone-*`, das alte `.legal-detail td` und der `@media (max-width: 700px)`-Block. Neu oder geändert:
| Regel | Wert |
|---|---|
| `.legal-detail td` | `padding: 0 0 16px` (ohne Hintergrund) |
| `.legal-facts` | `display: none; margin-bottom: 8px`, unter 860 px `display: grid` |
| `.legal-actions` | `display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; margin-top: 8px` |
| `.legal-cancel` | `display: flex; flex-wrap: wrap; align-items: center; gap: 8px` |
| `.legal-cancel .input` | `width: 160px` |
- [ ] **Step 3: Bauen und prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p8 && "$W/shots.sh" p8 "$W/shots/p8-t6" --only rechtliches-anbieter --gate p8`
Expected: PASS. In den Bildern gilt:
- Keine getönte Zeile. Bei rot und orange steht im Terminfeld ein Badge „kündbar bis …“ mit dem Rest in Grau, beendete und gekündigte Verträge haben grauen Text im Terminfeld.
- Das Detail zeigt Textlinks, das Kündigungs-Formular steht in einer Zeile.
- Bei 390 px gibt es zwei Spalten, das Detail zeigt die vier Angaben als `.kv`.
- [ ] **Step 4: Commit**
```bash
git add src/components/legal/ProviderContractsTab.jsx src/styles/global.css
git commit -m "Design: Anbieter-Vertraege als Tabelle, Dringlichkeit nur im Terminfeld" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 7: Vertrags- und Versions-Dialog
**Files:**
- Modify: `src/components/legal/ContractForm.jsx` (Markup Z. 56–105, Import Z. 2)
- Modify: `src/components/legal/VersionForm.jsx` (Markup Z. 26–63, Import Z. 2)
- Modify: `src/styles/global.css` (Block „Rechtliches“)
**Interfaces:**
- Consumes: `Dialog`, `Field`, `Button` (P1), `useScrollIntoView(ref, trigger)` aus `src/hooks/useScrollIntoView.js`.
- Produces: `ContractForm({ contract, onSave, onClose })` und `VersionForm({ title, onSave, onClose })` behalten ihre Signaturen.
- [ ] **Step 1: `ContractForm.jsx` umbauen**
`FIELDS`, `NUMBERS`, `toForm`, `toDoc`, `set` und `submit` bleiben (Fehler landet weiter in `error`, Erfolg ruft `onClose()`).
- Der `FaTimes`-Import entfällt. Neu sind `errorRef = useRef(null)` und `useScrollIntoView(errorRef, error)`.
- Gerüst: `<Dialog as="form" wide title={contract ? 'Vertrag bearbeiten' : 'Neuer Vertrag'} busy={saving} onClose={onClose} onSubmit={submit} footer={…}>`.
- Felder als `Field label htmlFor` mit `id="contract-<key>"`. Eingaben tragen `className="input"`, Auswahlen `select`, die Notiz `textarea`. `name`, `required`, `type`, `min`, `inputMode` und `accept` bleiben.
- Paare in `<div className="field-row">`: Kosten und „pro“, Laufzeit und „Verlängert sich um“, Kündigungsfrist und „Einheit“.
- Upload: `<input type="file" className="legal-file" multiple accept=…>` in `Field label="Dateien hinzufügen"`.
- Der Fehler ist das erste Kind: `{error && <div className="msg err" role="alert" ref={errorRef}>{error}</div>}`.
- Fuß: `<span className="grow" />`, `<Button type="button" onClick={onClose} disabled={saving}>Abbrechen</Button>`, `<Button variant="primary" type="submit" disabled={saving}>{saving ? 'Speichert …' : 'Speichern'}</Button>`.
Entscheidung: „Abbrechen“ ist während des Speicherns gesperrt, wie „ד und Esc. Heute schließt es mitten im Speichern und verwirft die Rückmeldung.
- [ ] **Step 2: `VersionForm.jsx` umbauen**
Gleiches Muster, auch im Fuß (`Abbrechen` mit `type="button"`, Absenden mit `type="submit"`). Der Titel ist `Neue Version: ${title}`, der Dialog nicht `wide`, die IDs sind `version-<key>`. „Version“ behält `placeholder="z. B. v6"` und `required`, „Gültig ab“ bleibt `required`, die Notiz `rows={4}`, der Upload bekommt `legal-file`.
- [ ] **Step 3: CSS**
Gelöscht werden `.legal-error` und `.legal-form-row`. Neu:
| Regel | Wert |
|---|---|
| `.legal-file` | `font-size: 13px; color: var(--text-2)` |
| `.legal-file::file-selector-button` | `height: 28px; padding: 0 10px; margin-right: 10px; border: 0; border-radius: var(--radius); background: var(--bg); box-shadow: var(--ring); font: inherit; font-weight: 500; color: var(--text); cursor: pointer` |
- [ ] **Step 4: Hygiene und Bilder**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "overlay|react-icons|form-control|form-group|form-label|legal-error|legal-form-row|ui-btn" src/components/legal src/pages/LegalPage.jsx`
Expected: keine Treffer.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p8 && "$W/shots.sh" p8 "$W/shots/p8-t7" --only dlg-vertrag,dlg-vertrag-fehler,dlg-version,dlg-version-fehler --gate p8`
Expected: PASS, das heißt:
- Esc schließt den Dialog. Bei laufendem Speichern ist „ד gesperrt und Esc wirkungslos.
- Nach dem Fehler steht `.msg.err` im offenen Dialog, die Eingabe ist erhalten.
- Bei 390 px ist der Dialog ein Bottom-Sheet, und der Speichern-Knopf ist erreichbar.
- [ ] **Step 5: Commit**
```bash
git add src/components/legal/ContractForm.jsx src/components/legal/VersionForm.jsx src/styles/global.css
git commit -m "Design: Vertrags- und Versions-Formular als Dialoge, Fehler im Dialog" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 8: Zugangsdaten-, `sr-only`- und `spin`-Klassen nach `global.css`
**Files:**
- Modify: `src/styles/global.css` (Ende von Block 3 „Bausteine“, vor `/* ===== KOMPAT (bis P10) ===== */`)
- Modify: `src/pages/AdminPage.css` (Blöcke `.credential*`, `.sr-only`, `.spin` samt `@keyframes admin-spin` löschen)
**Interfaces:**
- Consumes: `src/components/CopyableCredential.jsx` in der Fassung von P5 (Textknöpfe „Anzeigen“/„Verbergen“, „Kopieren“/„Kopiert“).
- Produces: die Klassen für `CopyableCredential` und `.sr-only` für alle Seiten.
- [ ] **Step 1: Klassen der Komponente lesen**
Run: ``source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -o 'className=\{?[`"][^`"]*credential[^`"]*' src/components/CopyableCredential.jsx | sort -u``
Expected: `credential`, `credential-value`, `is-masked`, `credential-empty`. P5 hat `credential-action` und `credential-check` gelöscht, die Knöpfe sind `button.link` in `.credential`. Es werden nur die Regeln verschoben, die `design-p7:src/pages/AdminPage.css` noch enthält.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git show design-p7:src/pages/AdminPage.css`
Expected: Zu sehen sind die verbliebenen Blöcke `.credential*`, `.sr-only`, `.spin` und `@keyframes admin-spin`. Nur diese werden verschoben.
- [ ] **Step 2: Regeln nach `global.css` verschieben und neu einfärben**
| Regel | Änderung beim Verschieben |
|---|---|
| `.credential`, `.credential .link`, `.credential-value.is-masked` | unverändert (Stand P5: `gap: 8px`, `.link` mit `flex: 0 0 auto; font-size: 12px; white-space: nowrap`) |
| `.credential-value` | `overflow: hidden; text-overflow: ellipsis` wird `overflow-wrap: anywhere`, sonst unverändert. Lange Werte brechen um, statt abgeschnitten zu werden und die Admin-Tabelle bei 390 px zu sprengen. |
| `.credential-empty` | `color: var(--text-3)` (steht für „–“) |
| `.sr-only` | unverändert (`position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0`) |
| `.spin` | `animation: spin .8s linear infinite` (nutzt die vorhandenen `@keyframes spin`, der Block `@keyframes admin-spin` entfällt) |
Entscheidung: `.spin` zieht wie in der Spec nach `global.css`, obwohl es nach P8 keinen Aufrufer mehr hat. Der einzige war der Spinner der Admin-Seite. P10 löscht es, wenn `rg -w spin src --glob '!*.css'` dann weiter nichts findet. Das steht in der Übergabe.
- [ ] **Step 3: Prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node "$W/css-klassen.mjs" "$WT" | node -e "const j=JSON.parse(require('fs').readFileSync(0));console.log(j.undefinedUsed.filter(n=>/credential|sr-only/.test(n)).join('\n'))"`
Expected: leer.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p8 && "$W/shots.sh" p8 "$W/shots/p8-t8" --only admin,kunde-overview --gate p7`
Expected: PASS. Das Gate `p7` ist das des Vorgängerpakets, denn `icon-svg` für `admin` wird erst mit Task 9 sauber. Zugangsdaten in der Admin-Tabelle und auf der Kundenseite (Ansicht `kunde-overview`, Abschnitt „Portal-Zugang“) sind lesbar, die Textknöpfe „Kopieren“ und „Anzeigen“ unterstrichen.
- [ ] **Step 4: Commit**
```bash
git add src/styles/global.css src/pages/AdminPage.css
git commit -m "Design: Zugangsdaten-, sr-only- und spin-Klassen nach global.css" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 9: AdminPage
**Files:**
- Modify: `src/pages/AdminPage.jsx` (Importe Z. 1–13, Zustand Z. 40–45, 403-Block Z. 52–62, Formular-Logik Z. 64–112, Markup Z. 123–226; `showNotice` und `removeEmployee` Z. 114–121 bleiben)
- Rewrite: `src/pages/AdminPage.css`
- Modify: `src/components/AdminRoute.jsx`
**Interfaces:**
- Consumes: `validateEmployeeForm(form, isEdit)` (Task 3), `Dialog`, `Field`, `Button`, `Badge`, `Card`, `PageHeader`, `Tabs`, `EmptyState` (P1), `CopyableCredential` (P5), `useScrollIntoView`, `useEmployees` und `useAuth` unverändert.
- Produces: nichts für spätere Pakete.
- [ ] **Step 1: Seitengerüst**
- Alle Icon-Importe entfallen. `Notice` rendert ``<div className={`msg ${notice.type === 'error' ? 'err' : 'ok'} admin-notice`} role={notice.type === 'error' ? 'alert' : 'status'}>``.
- Kopf: `PageHeader eyebrow="Systemverwaltung" title="Admin" subtitle="Mitarbeiter und ihre Zugänge verwalten. Kunden pflegst du unter „Kunden“."`. Die Aktionen sind `<span className="muted small">Angemeldet als <strong>{user?.name || user?.email}</strong></span>` und `Button variant="primary"` „Mitarbeiter anlegen“, der einzige schwarze Knopf im Kopf.
- Der Reiter ist `Tabs` mit einem Eintrag. Das Label bleibt `Mitarbeiter (${employees.length})` ohne `count`, weil `count` den Text ändern würde.
- Die Wurzel bleibt `<div className="page admin-page">`. Der 403-Block behält sein Markup (`span` „403“, `h1` „Zugriff verweigert“, `p`).
- [ ] **Step 2: Zugangsdaten-Abschnitt**
`createdCredential` rendert `Card title="Login wurde angelegt" actions={<button type="button" className="btn btn-quiet" aria-label="Hinweis schließen">×</button>}` mit `<p className="muted small">Diese Zugangsdaten jetzt sicher weitergeben.</p>` und `<div className="credential-list">` um die zwei `CopyableCredential` (E-Mail, Passwort mit `secret`). Er steht über der Tabelle und ist kein Dialog.
- [ ] **Step 3: Tabelle**
`Card title="Mitarbeiterkonten"` mit `<p className="muted small admin-lead">Aktive Logins für das Ticketsystem</p>`:
- Ladefehler `employeesError`: `div.msg.err[role=alert]`.
- Laden: `<div className="loading">Mitarbeiter werden geladen …</div>`.
- Leer: `EmptyState title="Noch keine Mitarbeiter" hint="Lege mit „Mitarbeiter anlegen“ den ersten Login an."`.
Entscheidung: Der Hinweis „Lege links den ersten Login an.“ passt nicht mehr, weil das Formular nicht mehr links steht. Das ist die einzige Textänderung außerhalb der Spec-Liste, die Übergabe nennt sie.
Die Tabelle ist `<table className="table">` ohne `tr.row`, denn die Zeilen sind heute auch nicht klickbar. Spalten:
| Spalte | Inhalt | Klassen |
|---|---|---|
| Mitarbeiter | `strong` mit Name oder „–“ | – |
| E-Mail | `CopyableCredential` (Label „E-Mail“) | – |
| Kürzel | `<span className="admin-shortcode">` | `hide-sm` |
| Rolle | `Badge tone="ok"` „Admin“, sonst `<span className="muted">Mitarbeiter</span>` | `hide-sm` |
| User-ID | `CopyableCredential` (Label „User-ID“) | `hide-sm` |
| Aktionen (`sr-only`) | `div.admin-actions` mit `button.link` „Bearbeiten“ und `Button variant="danger"` „Löschen“, Letzteres nur wenn `employee.userId !== user?.$id` | `right` |
Die `aria-label` der Aktionen bleiben `${employee.displayName} bearbeiten` und `… löschen`. Auf dem Handy bleiben Mitarbeiter, E-Mail und Aktionen.
- [ ] **Step 4: Formular als Dialog**
- Neu sind `formOpen`, `formError`, `errorRef` und `useScrollIntoView(errorRef, formError)`. `editingSelf` ist die einmal berechnete Bedingung „Admin-Haken gehört zum eigenen Konto“ (heute zweimal inline).
- „Mitarbeiter anlegen“ und „Bearbeiten“ setzen Formular und `editingEmployee`, leeren `formError` und öffnen mit `setFormOpen(true)`.
- `submitEmployee` prüft zuerst `validateEmployeeForm(employeeForm, Boolean(editingEmployee))`. Bei einem Text setzt es `formError` (kein Banner) und kehrt zurück.
- Sonst läuft der Ablauf wie bisher: `employeeBusy` an, `updateEmployee` bzw. `createEmployee`, `employeeBusy` aus.
- Bei Misserfolg steht `result.error || 'Mitarbeiter konnte nicht gespeichert werden.'` in `formError`, der Dialog bleibt offen.
- Bei Erfolg folgen `setCreatedCredential` (nur beim Anlegen), `showNotice('success', …)` mit den bisherigen Texten, dann Formular schließen und zurücksetzen.
- Gerüst: `<Dialog as="form" title={editingEmployee ? 'Mitarbeiter bearbeiten' : 'Mitarbeiter anlegen'} busy={employeeBusy} onClose={closeForm} onSubmit={submitEmployee} footer={…}>`.
- Inhalt von oben nach unten:
- Fehler `div.msg.err[role=alert][ref=errorRef]`, dann `<p className="muted small">Die User-ID wird automatisch vergeben.</p>`.
- Vier `Field` mit `id="employee-<key>"` und `className="input"`: Name (`required`), E-Mail (`type="email"`, `required`), Passwort (Label „Neues Passwort (optional)“ bzw. „Passwort“, `autoComplete="new-password"`, `required={!editingEmployee}`, Platzhalter „Mindestens 8 Zeichen“), Kürzel (`maxLength={10}`, Platzhalter „z. B. KNSO“, Großschreibung beim Tippen).
- Admin-Checkbox: ``<label className={`check admin-check${editingSelf ? ' is-disabled' : ''}`}>`` mit `<input type="checkbox" disabled={editingSelf}>`, `<strong>Admin-Account</strong>` und `<small>Darf Systemkonfiguration und Benutzer verwalten.</small>`.
- Fuß: `<span className="grow" />`, `Button type="button" disabled={employeeBusy}` „Abbrechen“, `Button variant="primary" type="submit" disabled={employeeBusy}` mit „Speichert …“, „Änderungen speichern“ oder „Mitarbeiter anlegen“.
- [ ] **Step 5: `AdminPage.css` neu schreiben**
Die Datei wird vollständig ersetzt:
| Regel | Wert |
|---|---|
| `.page.admin-page` | `max-width: 1480px` |
| `.admin-notice` | `margin-bottom: 16px` |
| `.admin-lead` | `margin: 0 0 8px` |
| `.admin-shortcode` | `font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; letter-spacing: .04em` |
| `.admin-actions` | `display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 4px 16px` |
| `.credential-list` | `display: flex; flex-wrap: wrap; gap: 8px 32px; margin-top: 8px` |
| `.check.admin-check` | `align-items: flex-start` (Spezifität 0,2,0, sonst gewinnt `.check` aus `global.css`) |
| `.admin-check small` | `display: block; margin-top: 2px; font-size: 13px; line-height: 18px; color: var(--text-2)` |
| `.admin-check.is-disabled` | `cursor: not-allowed; opacity: .55` |
| `.admin-access-denied` | `max-width: 540px; margin: 12vh auto 0` |
| `.admin-access-denied > span` | `font-size: 12px; font-weight: 500; color: var(--red-fg)` |
| `.admin-access-denied h1` | `margin: 4px 0 8px; font-size: 28px; line-height: 34px; font-weight: 700; letter-spacing: -.02em` |
| `.admin-access-denied p` | `color: var(--text-2)` |
Weg sind alle `!important`, Kästen, Rahmen, Schatten, Farbliterale, der Split-Layout-Block und die Media-Queries. Eine Regel für den Abstand zwischen Zugangsdaten und Mitarbeiterkonten gibt es nicht, ihn liefert `.page section + section` aus P1.
- [ ] **Step 6: `AdminRoute.jsx` angleichen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n 'className="loading"' src/components/AdminRoute.jsx`
- Trifft es, hat ein früheres Paket die Datei schon umgestellt, und dieser Schritt entfällt.
- Sonst wird der Ladezweig `<div className="loading"><p>Wird geladen …</p></div>`. Das `<p>` bleibt ohne Klasse, weil `tests/unit/adminRoute.test.mjs` genau diese Zeichenfolge prüft. Der Seitenabstand kommt von `.loading`, Inline-Style, `spinner` und die Hilfsklassen entfallen.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/adminRoute.test.mjs`
Expected: PASS.
- [ ] **Step 7: Bauen und prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{|#[0-9a-fA-F]{3,8}\b|!important|admin-icon-button|admin-primary-button|form-control|spinner" src/pages/AdminPage.jsx src/pages/AdminPage.css src/components/AdminRoute.jsx`
Expected: keine Treffer.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p8 && "$W/shots.sh" p8 "$W/shots/p8-t9" --only admin,admin-neu,admin-fehler,admin-zugang,admin-bearbeiten,admin-ausfall,admin-403 --gate p8`
Expected: PASS. In den Bildern gilt:
- Keine Karten, die Tabelle hat nur Haarlinien, Aktionen sind Textlinks.
- Der Dialog zeigt Validierungs- und Speicherfehler als `.msg.err` oben und schließt nur bei Erfolg.
- Nach Erfolg steht das Banner auf der Seite, darunter der Zugangsdaten-Abschnitt.
- Bei 390 px sind Mitarbeiter, E-Mail und Aktionen sichtbar, ohne waagerechtes Scrollen.
- [ ] **Step 8: Commit**
```bash
git add src/pages/AdminPage.jsx src/pages/AdminPage.css src/components/AdminRoute.jsx
git commit -m "Design: Admin mit Tabelle, Mitarbeiter-Formular als Dialog, AdminPage.css neu" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 10: Gate P8, Review, Tag
- [ ] **Step 1: Rebase**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git rebase origin/test && "$W/check.sh"`
Expected: Exit 0, Rebase ohne Konflikt, `# fail 0`. Die Testzahl ist N0 + 5. Liegen neue Commits auf `test`, kann sie höher sein, die Zahl wird notiert. So zeigen die Bilder der folgenden Schritte den Stand, der getaggt wird.
- [ ] **Step 2: Vollständiger Gate-Lauf**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p8 && "$W/shots.sh" p8 "$W/shots/p8" --gate p8`
Expected: Exit 0. Das Gate `p8` prüft alle Ansichten mit den `p1`-Prüfungen (`overflow-x`, `dark-surface`, `low-contrast`) und `icon-svg` für die Ansichten von P2 bis P8. Die 13 P8-Ansichten haben Bilder bei 1440 und 390 px ohne Verstöße.
- [ ] **Step 3: Hygiene und unveränderte Logik**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|@tabler|style=\{|#[0-9a-fA-F]{3,8}\b|!important|overlay-close|overlay-content|form-control|ui-btn|ui-table" src/pages/AdminPage.jsx src/pages/AdminPage.css src/pages/LegalPage.jsx src/components/legal`
Expected: keine Treffer.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git diff --stat design-p7 -- src/hooks src/lib/legal.js; git diff design-p7 -- src | rg '^[-+].*window\.confirm'; rg -n "legal-tone|legal-version-(current|draft)|legal-grid|legal-form-row|legal-error|legal-doc \+|admin-page section" src`
Expected: alle drei Ausgaben leer.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node "$W/css-klassen.mjs" "$WT" | node -e "const j=JSON.parse(require('fs').readFileSync(0));console.log(j.dead.filter(n=>/^(legal|admin|credential)-/.test(n)).join('\n'))"`
Expected: leer.
- [ ] **Step 4: Bundle-Größe**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" --nur-build`
Expected: Exit 0, CSS kleiner als bei Task 1. Die Werte werden notiert.
- [ ] **Step 5: Code-Review (sonnet)**
Geprüft wird der Diff `design-p7..HEAD` gegen die Spec-Abschnitte „Bausteine“ (Dialog, Tabelle, Badge, Meldungen), „Texte“ und Paket P8. Wichtige Befunde werden behoben. Danach kommen Step 1 (Rebase) noch einmal und Step 2 als Kurzlauf mit `--only rechtliches,rechtliches-anbieter,admin`. Schwerpunkte:
- Der Dialog schließt nie bei einem Fehler, und „ד, Esc und „Abbrechen“ sind bei `busy` gesperrt.
- Die Checkliste der Bedienelemente aus „Review Focus“ ist vollständig.
- Die Bedingung „eigene Zeile“ (kein „Löschen“, Admin-Haken gesperrt) gilt weiter.
- Die Dringlichkeit steht nur im Terminfeld.
- Die Aufklappzeile der Verträge öffnet per Klick auf den Button, per Enter und per Klick auf die Zeile.
- [ ] **Step 6: Tag und Übergabe**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" && git tag design-p8 && git log --oneline origin/test..design-p8 && git rev-parse design-p8`
Der Eigentümer bekommt:
- die Screenshots aus `$W/shots/p8` (Unterordner `1440x900` und `390x844`; Auswahl: `rechtliches`, `rechtliches-anbieter`, `dlg-vertrag-fehler`, `admin`, `admin-neu`, `admin-fehler`, `admin-zugang`)
- den Hinweis auf die Textänderungen: „Bearbeiten“/„Löschen“, „Speichert …“ und der Leer-Hinweis der Admin-Tabelle
- die Notiz für P10: `.spin` hat keinen Aufrufer mehr
- die Push-Zeile `git -C /home/webklar/apps/ticket.webklar.com/dev-design push origin 'design-p8^{commit}:refs/heads/test'`
- die Rücknahme `git revert --no-commit design-p7..design-p8`, Commit und Push durch den Eigentümer (gilt nur, solange P8 das jüngste Paket auf `test` ist)
- [ ] **Step 7: Nach dem Push (erst, wenn der Eigentümer gepusht hat)**
- Im Log von `webklar-ticket-build` steht „Deployment completed successfully“.
- `curl -s https://ticket.webklar.com/ | grep -o 'assets/index-[^"]*\.js'` entspricht `dist/index.html` des Tags.
- Ein lesender Sichttest im Chrome des Eigentümers: Rechtliches mit beiden Reitern und Admin mit der Tabelle. Dabei wird nichts gespeichert oder gelöscht.
@@ -0,0 +1,390 @@
# WOMS „Weißes Design“ P9: Serverpuls – Umsetzungsplan
> **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:** Nach P9 ist Serverpuls hell und kastenlos. Der Kopf trägt ein Text-Badge statt eines Emojis, Warnungen sind Listenzeilen mit Stufen-Badge, die Kacheln sind `.stats` mit Sparkline. Deltas zeigen sich als Textfarbe, Umschalter sind `.seg`, Panels sind `h2.section`, die Projekttabelle ist eine `.table`. Die Diagramme nutzen die Farben aus `chartColors.js`. Alle `data-testid` bleiben.
**Architecture:**
- Drei reine Helfer in `src/lib/serverpulsFormat.js` mit Tests: `deltaDir`, `deltaSign`, `levelTone`.
- Bausteine aus P1: `Stat`, `Badge` (`badgeToneClass` aus `ui/tones.js`), `PageHeader`, `.stats`, `.list-row`, `.seg`, `.table`, `.msg`, `.toolbar`, `.input.search`.
- `serverpuls.css` wird neu geschrieben (74 → rund 70 Zeilen).
- `Chart.jsx` und `Sparkline.jsx` bleiben unverändert, weil P1 die Diagrammfarben schon umgestellt hat. Dasselbe prüft Task 6.
- Abfragen, 60-Sekunden-Takt, Neuladen bei Sichtbarkeit, `reqId`-Schutz, Sortierlogik und Zeitachsen bleiben unverändert.
**Tech Stack:** React 18, uPlot (Canvas), reines CSS. Tests mit `node --test`, Screenshot-Werkzeug aus P0.
**Spec:** `docs/superpowers/specs/2026-10-04-weisses-design-design.md`, Abschnitte „Gestaltungsregeln“, „Design-Tokens“ (Diagrammfarben), „Bausteine“ (Kennzahlen, Umschalter, Tabelle, Liste, Badge, Meldungen, Laden) und Paket P9. Anhang `2026-10-04-weisses-design-umbaukarte.md`: Abschnitt 5 (P9) und 7.1. Globale Regeln und Schnittstellen: `2026-10-05-weisses-design-00-uebersicht.md`.
## Global Constraints
Es gelten die Regeln der Übersicht. Für P9 kommt hinzu:
- Jeder Run-Block beginnt mit `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh`. Danach gelten `$WT` (Worktree, Arbeitsverzeichnis), `$W` (Werkzeug) und `$TMPDIR`. Tests und Build werden nur über `"$W/check.sh"` ausgewertet, Browser-Skripte laufen nur über `"$W/shots.sh"`.
- Ausgangspunkt ist das Tag `design-p8`, alle Diff-Prüfungen vergleichen gegen `design-p8`.
- CSS-Reihenfolge: `main.jsx` importiert `App` vor `global.css`, also lädt `serverpuls.css` vor `global.css`, und bei gleicher Spezifität gewinnt `global.css`. Jede Regel in `serverpuls.css`, die eine Global-Klasse überstimmt (`.stats`, `.list-row`, `.msg`, `.table`, `.seg`), hat höhere Spezifität (0,2,0, z. B. `.stats.sp-tiles`, `.sp-page .msg`) und nie `!important`.
- Abstände zwischen aufeinanderfolgenden Abschnitten kommen von `.page section + section { margin-top: 36px }` aus P1. P9 baut dafür keine eigenen Regeln: `.sp-panel` und `.stats.sp-tiles` setzen keinen Außenabstand nach oben. Eigene Abstände gibt es nur innerhalb der Abschnitte (`div`-Container).
- Die CSS-Tabellen dieses Plans nennen nur die Abweichungen vom Prototyp und von den Tokens. Was Basis (`*`-Reset, `button`-Reset), `.table`, `.stat`, `.list-row` und `.seg` schon liefern, wird nicht wiederholt.
- Canvas-Farben stehen nur in `chartColors.js`. In `ServerpulsPage.jsx`, `Chart.jsx`, `AppsTable.jsx` und `Sparkline.jsx` gibt es kein Farbliteral.
- Keine Icon-Importe, keine Emojis, keine Glyphen ▲ und ▼. Die Sortierzeichen ↑ und ↓ bleiben.
- Sichtbare Textänderungen: keine außer den Badge-Texten des Kopfes aus `LEVEL_TEXT`. Alle Ladetexte („Lädt …“) und Meldungen bleiben wörtlich.
- Keine neuen `style={{…}}`.
## Review Focus
Aus der Übersicht gehören drei Punkte zu P9:
- **Punkt 2, lange Inhalte am Handy:** Sechs Kacheln, Umschalter mit 6 und 5 Knöpfen und die Tabelle bei 390 px ohne waagerechtes Scrollen. Testdaten `serverpuls-lang`: Projektname mit 60 Zeichen ohne Leerzeichen, Warnungstitel mit 90 Zeichen. `overflow-x` ist seit dem Gate `p1` für alle Ansichten scharf, also auch im Gate `p9`.
- **Punkt 3, Teilausfall:** Ansicht `serverpuls-ausfall`. Die Übersicht antwortet 500, `sp-error` erscheint, Diagramm und Tabelle bleiben nutzbar, die Kacheln zeigen „–“.
- **Punkt 4, nur Tastatur:**
- Projektzeilen lassen sich über den Button in der ersten Zelle öffnen (`aria-pressed`).
- Sortierköpfe sind Buttons mit `aria-sort` auf dem `th`.
- Die `.seg`-Knöpfe tragen `aria-pressed`.
Zusätzlich gilt für P9:
- Checkliste der Bedienelemente, nichts darf fehlen: Metrik-Umschalter (6 Knöpfe), Zeitraum-Umschalter (5), Projektsuche, 6 Sortierköpfe (Projekt, Größe, Δ 7 T, Δ 30 T, CPU, RAM), Projektzeile (öffnet und schließt das Detail), Zeitraum-Umschalter im Detail (5).
- Die 20 `data-testid` im Serverpuls-Code bleiben identisch (Task 1 und 6). Ohne den Muster-Eintrag `sp-tile-${id}` sind es die 19 der Spec.
- Rot heißt gestiegen und Grün gesunken, das Vorzeichen steht weiter in der Zahl.
---
### Task 1: Paketstart und Ausgangswerte
**Files:** keine Änderung.
**Interfaces:** keine.
- [ ] **Step 1: Paketstart**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git merge-base --is-ancestor design-p8 HEAD && git rebase origin/test && "$W/check.sh" && git status --short`
Expected: Exit 0, Rebase ohne Konflikt, `# fail 0` und eine leere Statusliste. Endet `merge-base` mit Exit 1, liegt `design-p8` nicht im Branch: stoppen und melden. Die Testzahl heißt im Folgenden N0, die Bundle-Größen aus der Ausgabe von `check.sh` werden notiert.
- [ ] **Step 2: Abgleich mit TP2**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git log --oneline origin/test..origin/feature/terminplaner-tp2 -- src/pages/ServerpulsPage.jsx src/components/serverpuls src/styles/serverpuls.css src/lib/serverpulsFormat.js src/styles/global.css`
Expected: keine Ausgabe. Gibt es Treffer, stoppt der Agent vor Task 2, nennt dem Eigentümer die Commits und fragt nach. Er empfiehlt dabei, TP2 zuerst nach `test` zu bringen.
- [ ] **Step 3: Referenzliste der Test-IDs anlegen**
Run: ``source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -o --no-filename 'testid=("sp-[a-z-]+"|\{`sp-tile-\$\{id\}`\})' src/pages/ServerpulsPage.jsx src/components/serverpuls | sort > "$W/p9-testids.before.txt" && wc -l < "$W/p9-testids.before.txt"``
Expected: `20`.
- [ ] **Step 4: Voraussetzungen aus P1 und P2 prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && ls src/components/serverpuls/chartColors.js src/components/ui/Stat.jsx src/components/ui/tones.js src/lib/rowClick.js && rg -n "isRowActivation" src/lib/rowClick.js && rg -n -m 1 "CHART\." src/components/serverpuls/Chart.jsx src/pages/ServerpulsPage.jsx && rg -n "#[0-9a-fA-F]{3,8}\b|rgba?\(" src/components/serverpuls src/pages/ServerpulsPage.jsx --glob '!chartColors.js'`
Expected: Die Dateien sind da, `src/lib/rowClick.js` (P2 Task 2) enthält `isRowActivation(event)`, `CHART.` kommt in beiden Dateien vor, und die letzte Suche liefert keine Treffer (Exit 1). Fehlt `rowClick.js`, stoppt der Agent und meldet es, P9 baut keinen eigenen Zeilenklick-Helfer.
### Task 2: Format-Helfer `deltaDir`, `deltaSign`, `levelTone` (TDD)
**Files:**
- Modify: `src/lib/serverpulsFormat.js` (neue Exporte nach `formatNumber`, vor dem Abschnitt `// --- Zeitachsen`)
- Test: `tests/unit/serverpulsFormat.test.mjs` (Z. 5 Import, zwei neue Tests)
**Interfaces:**
- Produces:
- `deltaDir(d) → 'up' | 'down' | 'flat'` (`flat` bei Betrag unter `1e-9`, genau die Regel aus `Tile`).
- `deltaSign(dir) → '+' | '−' | ''` (U+2212).
- `levelTone(level) → 'ok' | 'warn' | 'danger' | 'muted'`. Es sind die Ton-Namen von `Badge`.
- Bestehende Exporte bleiben unverändert.
- [ ] **Step 1: Failing Test schreiben**
Die Destrukturierung in Z. 5 bekommt `deltaDir, deltaSign, levelTone`. Dazu:
```js
test('deltaDir und deltaSign: Richtung und Vorzeichen der Kachel-Deltas', () => {
assert.deepEqual([deltaDir(0), deltaDir(5e-10), deltaDir(-5e-10)], ['flat', 'flat', 'flat'])
assert.equal(deltaDir(0.4), 'up')
assert.equal(deltaDir(-0.4), 'down')
assert.deepEqual([deltaSign('up'), deltaSign('down'), deltaSign('flat')], ['+', '−', ''])
})
test('levelTone: Badge-Ton je Stufe', () => {
assert.deepEqual(['ok', 'medium', 'critical'].map(levelTone), ['ok', 'warn', 'danger'])
assert.deepEqual([levelTone('low'), levelTone(undefined), levelTone('x')], ['muted', 'muted', 'muted'])
})
```
- [ ] **Step 2: Test laufen lassen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/serverpulsFormat.test.mjs`
Expected: FAIL, `deltaDir is not a function`.
- [ ] **Step 3: Implementieren**
Die drei Funktionen in `src/lib/serverpulsFormat.js`. Die Schwelle `1e-9` und das Zeichen U+2212 sind wie in `Tile` und `formatDelta`.
- [ ] **Step 4: Test grün**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/serverpulsFormat.test.mjs && "$W/check.sh" --nur-tests`
Expected: PASS, Exit 0, Testzahl N0 + 2, `# fail 0`.
- [ ] **Step 5: Commit**
```bash
git add src/lib/serverpulsFormat.js tests/unit/serverpulsFormat.test.mjs
git commit -m "Serverpuls: Richtung, Vorzeichen und Stufen-Ton als Format-Helfer mit Test" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 3: Werkzeug – Testdaten, Ansichten, Gate `p9`, Ausnahme für Sparklines
**Files** (alle unter `$W/`, kein Commit im Repo):
- Create: `fixtures/serverpuls-ok.mjs`, `fixtures/serverpuls-veraltet.mjs`, `fixtures/serverpuls-lang.mjs`
- Modify: `views.mjs`, `gates.mjs`, `checks.mjs`
**Interfaces:**
- Consumes: `createBackend`, `VIEWS`, `GATES` aus P0 und die Konsolen-Ausnahme des Szenarios `fehler` (P0, in P8 Task 4 Step 3 erweitert).
- Produces: die Ansichten aus Step 2, `GATES.p9`, die Ausnahme `svg.sp-spark` im Check `icon-svg` und die Ausnahme für `**/api/serverpuls/**` im Szenario `fehler`.
- [ ] **Step 1: Testdaten-Varianten anlegen**
Alle erweitern `standard`.
- `serverpuls-ok`: Übersicht mit `level: 'ok'` und `alerts: []`.
- `serverpuls-veraltet`: `level: 'medium'`, eine Warnung, `lastSample` vor 40 Minuten.
- `serverpuls-lang`: `level: 'critical'` mit drei Warnungen (`critical`, `medium`, `low`), davon eine mit 90-Zeichen-Titel. Zwölf Projektzeilen, eine mit 60-Zeichen-Namen ohne Leerzeichen.
- [ ] **Step 2: Ansichten in `views.mjs` anlegen oder anpassen**
Der `path` ist `/serverpuls`. Die Schritte haben die Signatur `steps(page, ctx)`, `ctx.backend.calls` listet die vom Backend beantworteten Requests. Geprüft wird mit `throw new Error(…)`, das den Verstoß `step-failed` erzeugt.
| ID | Daten | Schritte |
|---|---|---|
| `serverpuls` | `serverpuls-lang` | Warten, bis `[data-testid="sp-chart"]` ein `canvas` mit Breite über 0 trägt und sechs `[data-testid^="sp-tile-"]` da sind. |
| `serverpuls-ok` | `serverpuls-ok` | Wie `serverpuls`. |
| `serverpuls-veraltet` | `serverpuls-veraltet` | Wie `serverpuls`. |
| `serverpuls-projekt` | `serverpuls-lang` | Fokus auf `button.link` der ersten `[data-testid="sp-app-row"]`, Enter: `sp-detail` muss sichtbar sein und drei `canvas` enthalten. Fokus auf den Sortierkopf „Größe“ (`th[data-sort="sizeBytes"] button`), Enter: `aria-sort` muss `ascending` heißen. Klick auf „RAM“ in `[data-testid="sp-metric-select"]` (nicht auf den gleichnamigen Tabellenkopf), dann muss dieser Knopf `aria-pressed="true"` tragen. |
| `serverpuls-ausfall` | `standard`, Szenario `fehler` | `GET **/api/serverpuls/overview` mit 500 `{ error: 'Test: Serverpuls ist nicht erreichbar.' }` beantworten. `sp-error` muss sichtbar sein, das Diagramm gezeichnet und die Tabelle da. |
- [ ] **Step 3: Gate festlegen**
In `gates.mjs` kommt nach der Deklaration von `GATES` dazu. Das Gate `p9` erbt Prüfungen und Ansichten von `p8` (die `p1`-Prüfungen für alle Ansichten, `step-failed`, `external-request` und `console-error` immer) und schaltet `icon-svg` zusätzlich für die Ansichten von P9 und aller früheren Pakete ein:
```js
const iconSvg = GATES.p8.scoped['icon-svg']
GATES.p9 = {
...GATES.p8,
scoped: {
...GATES.p8.scoped,
'icon-svg': {
...iconSvg,
views: [...iconSvg.views, 'serverpuls', 'serverpuls-ok', 'serverpuls-veraltet', 'serverpuls-projekt', 'serverpuls-ausfall'],
},
},
}
```
- [ ] **Step 4: Sparklines vom Icon-Check ausnehmen**
`checks.mjs`: Der Check `icon-svg` nimmt `svg.sp-spark` dauerhaft aus (wie `.brand`), nicht über `ignore` im Gate. Es sind Datengrafiken (Inline-SVG-Minikurven), keine Icons. Die Konsolen-Ausnahme des Szenarios `fehler` deckt auch die Meldungen zu `**/api/serverpuls/**` ab, falls sie das noch nicht tut.
- [ ] **Step 5: Ausgangslauf**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p9-vorher && "$W/shots.sh" p9-vorher "$W/shots/p9-vorher" --only serverpuls --gate p9`
Expected: Exit 0. Der Stand nach P1 ist hell, und die Sparklines sind ausgenommen. Die Bilder dienen als Vergleich.
### Task 4: Kopf, Warnungen, Kacheln, Umschalter, Panels
**Files:**
- Modify: `src/pages/ServerpulsPage.jsx` (`LEVEL_*` Z. 31–32, `Tile` Z. 79–100, `Pills` Z. 102–117, `Cleanup` Z. 119–139, Markup Z. 254–331; Abfragen und Hooks Z. 141–239 bleiben)
- Rewrite: `src/styles/serverpuls.css` (ohne die Tabellenregeln, die bleiben bis Task 5)
**Interfaces:**
- Consumes: `Stat({ label, value, sub, tone })`, `PageHeader`, `badgeToneClass(tone)` aus `src/components/ui/tones.js`, `Badge`, `deltaDir`, `deltaSign`, `levelTone` (Task 2), `formatDelta`.
- Produces: nichts für spätere Pakete.
- [ ] **Step 1: Kopf und Fehlerzeile**
- Fehler: `<div className="msg err" data-testid="sp-error" role="alert">`.
- Kopf: `<section data-testid="sp-head">` ohne `sp-level-*`. Die Kopfzeile `div.sp-head-top` enthält:
- `<span className={badgeToneClass(levelTone(level))} data-testid="sp-ampel" data-level={level}>{LEVEL_TEXT[level] || level}</span>`. `LEVEL_TEXT` ist die schon vorhandene, bisher ungenutzte Konstante („Alles ok“, „Warnungen“, „Critical“), das Emoji und `dot` entfallen.
- die `<strong className="sp-headline">` mit dem unveränderten Text und `sp-age`. Bei `age.stale` kommt zusätzlich die Klasse `sp-age-stale`.
- Ohne `overview` zeigt das Badge `badge plain` mit „–“, `data-level` bleibt `ok`.
Entscheidung: Das Badge nennt die Stufe aus `LEVEL_TEXT`, die Überschrift bleibt wörtlich („Alles ok“, „Critical – 2 Warnungen“, „2 Warnungen“). Die Doppelung ist gewollt. Ohne Daten steht „–“ statt „Alles ok“, damit der Fehlerzustand nicht grün aussieht.
- [ ] **Step 2: Warnungen**
`ul.sp-alerts[data-testid=sp-alerts]` mit `li.list-row.sp-alert[data-testid=sp-alert]`:
- Links `div.sp-alert-main` mit `<Badge tone={levelTone(a.level)}>{LEVEL_LABEL[a.level] || a.level}</Badge>`, `span.sp-alert-title` und `span.sp-alert-value`.
- Rechts `<span className="muted small nowrap">seit {formatSince(a.since)}</span>`.
- [ ] **Step 3: Kacheln, Umschalter, Panels, Aufräumbar**
- Kacheln: `<section className="stats sp-tiles" data-testid="sp-tiles">`. Jede Kachel ist `div.sp-tile[data-testid="sp-tile-<id>"]` mit `Stat label value sub`, `Sparkline width={120} height={32}` und der Delta-Zeile.
- `sub` ist `model?.sub || '\u00a0'`, damit die Sparklines einer Reihe fluchten.
- Die Delta-Zeile ist `div.sp-tile-delta.sp-delta-{dir}` mit `{deltaSign(dir)}{model.fmtDelta(d)} ggü. gestern`. `dir` kommt aus `deltaDir(d)`, die Glyphen ▲ und ▼ entfallen.
- `Pills` rendert `<div className="seg" data-testid={testid}>` mit `<button type="button" className={active ? 'on' : undefined} aria-pressed={active}>`.
- Panels: `<section className="sp-panel" data-testid="sp-chart-panel">` mit `<h2 className="section">Verlauf</h2>`, darunter `div.sp-controls` mit beiden `Pills`, dann das Diagramm.
- `sp-projects` und `sp-cleanup` folgen demselben Muster.
- „Größen von {Tag}“ steht als `<span className="muted small">` in `h2.section` rechts.
- Die Detail-Überschrift ist `<h3 className="sp-detail-title">`, ihre `Pills` stehen in `div.sp-controls`.
- „Aufräumbar“: `div.stats.sp-cleanup-stats` mit drei `Stat` („Ungenutzte Images“, „Build-Cache“, „Gestoppte Container“), die Liste der gestoppten Container als `li.list-row` mit Name und `<span className="num">`-Größe, der Stand-Satz als `<p className="muted small">`. „Noch keine Messung.“ bekommt `.muted`.
- Alle `sp-muted`, `sp-small` werden `muted`, `small`. `.sp-nodata` bleibt als Klasse für „Lädt …“ und „Für diesen Zeitraum …“.
Entscheidung: Die Panels sind `<section data-testid>` mit `h2.section` statt `Card`, weil `Card` kein `data-testid` weitergibt. Die Umschalter stehen unter der Überschrift, nicht darin, damit der Kopf auf dem Handy nicht überläuft.
- [ ] **Step 4: `serverpuls.css` neu schreiben**
Gelöscht werden `.sp-page`-Flex, alle Kästen, Rahmen und Literale, `.sp-level-*`, `.sp-ampel`, `.sp-tile-label/-value/-sub`, `.sp-pill*`, `.sp-panel-head`, `.sp-cleanup-grid`. Neu:
| Regel | Wert |
|---|---|
| `.sp-page .msg` | `margin: 0 0 16px` |
| `.sp-head-top` | `display: flex; align-items: center; gap: 12px; flex-wrap: wrap` |
| `.sp-headline` | `font-size: 16px; line-height: 24px; font-weight: 600` |
| `.sp-age` | `margin-left: auto; font-size: 13px; color: var(--text-2)` |
| `.sp-age-stale` | `color: var(--orange-fg); font-weight: 500` |
| `.sp-alerts` | `list-style: none; margin: 12px 0 0; padding: 0` |
| `.sp-alert-main` | `display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; min-width: 0` |
| `.sp-alert-title` | `font-weight: 500; overflow-wrap: anywhere` |
| `.sp-alert-value` | `color: var(--text-2)` |
| `.stats.sp-tiles` | `display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 24px 32px` (ohne `margin`) |
| `.sp-tile` | `min-width: 0` |
| `.sp-tile .sub` | `min-height: 16px` (Größe und Farbe kommen von `.stat .sub`) |
| `.sp-spark` | `display: block; max-width: 100%; margin-top: 6px` |
| `.sp-tile-delta` | `font-size: 12px; line-height: 16px; margin-top: 4px` |
| `.sp-delta-up` / `-down` / `-flat` | `color: var(--red-fg)` / `var(--green-fg)` / `var(--text-2)` |
| `.sp-panel` | `min-width: 0` (ohne `margin`) |
| `.sp-controls` | `display: flex; flex-wrap: wrap; gap: 8px 16px; margin-bottom: 12px` |
| `.sp-controls .seg` | `flex-wrap: wrap` |
| `.sp-chart` | `width: 100%; min-height: 200px` |
| `.sp-chart .u-legend` | `font-size: 13px; color: var(--text-2)` |
| `.sp-nodata` | `padding: 40px 0; text-align: center; color: var(--text-2)` |
| `.sp-detail` | `margin-top: 28px` (ein `div` im Panel, keine `section`) |
| `.sp-detail-title` | `margin: 0 0 8px; font-size: 15px; line-height: 22px; font-weight: 600; overflow-wrap: anywhere` |
| `.sp-detail-charts` | `display: grid; grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr)); gap: 24px` |
| `.sp-chart-title` | `font-size: 13px; color: var(--text-2); margin-bottom: 4px` |
| `.sp-cleanup-list` | `list-style: none; margin: 12px 0; padding: 0` |
| ≤ 1100 px `.stats.sp-tiles` | `grid-template-columns: repeat(3, minmax(0, 1fr))` |
| ≤ 860 px `.stats.sp-tiles` | `grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px 24px` |
| ≤ 860 px `.sp-age` | `margin-left: 0` |
| ≤ 860 px `.sp-page .sp-cleanup-stats` | `grid-template-columns: repeat(2, minmax(0, 1fr))` |
Den Abstand zwischen Kopf, Kacheln und Panels liefert `.page section + section` aus P1. `.stats.sp-tiles` und `.sp-panel` setzen deshalb keinen oberen Außenabstand. Ein `margin` auf `.stats.sp-tiles` würde den globalen Abstand wegen der höheren Spezifität überstimmen.
- [ ] **Step 5: Bauen und prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p9 && "$W/shots.sh" p9 "$W/shots/p9-t4" --only serverpuls,serverpuls-ok,serverpuls-veraltet,serverpuls-ausfall --gate p9`
Expected: PASS. In den Bildern gilt:
- Keine Kästen um Kopf, Kacheln und Panels.
- Das Badge trägt „Critical“ (rot), „Warnungen“ (orange) bzw. „Alles ok“ (grün). Veraltete Daten stehen als oranger Text.
- Warnungen sind Zeilen mit Haarlinien und Stufen-Badge (Critical rot, Medium orange, Low grau).
- Die Kacheln stehen bei 1440 px in sechs Spalten, bei 390 px in zwei. Deltas sind rot oder grün ohne Pfeil.
- Der Umschalter ist grau mit weißem aktivem Teil und bricht bei 390 px um.
- Das Diagramm hat eine schwarze Linie, die Spitze ist grau gestrichelt, es gibt nur horizontale Rasterlinien, und die Legende ist 13 px grau.
- Kopf, Kacheln und Panels haben überall denselben Abstand (36 px aus `.page section + section`).
- [ ] **Step 6: Commit**
```bash
git add src/pages/ServerpulsPage.jsx src/styles/serverpuls.css
git commit -m "Design: Serverpuls-Kopf mit Text-Badge, Warnungen als Zeilen, Kacheln als Kennzahlen" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 5: Projekttabelle
**Files:**
- Modify: `src/components/serverpuls/AppsTable.jsx` (Spaltenliste Z. 5–12, `Delta` Z. 14–18, Markup Z. 42–89; Sortier- und Filterlogik Z. 20–40 bleibt)
- Modify: `src/styles/serverpuls.css` (Tabellenregeln `.sp-search` bis `.sp-delta` ersetzen)
**Interfaces:**
- Consumes: `.table`, `tr.row`, `.toolbar`, `.input.search`, `.hide-sm`, `.num`, `.right` (P1), `isRowActivation(event)` aus `src/lib/rowClick.js` (P2 Task 2, Prüfung in Task 1), `formatBytes`, `formatDelta`, `formatPct`, `Sparkline`.
- Produces: `AppsTable({ rows, selectedId, onSelect })` behält seine Props.
- [ ] **Step 1: Markup umbauen**
- Die Wurzel `div.sp-apps[data-testid=sp-apps]` bleibt. Die Suche steht in `<div className="toolbar">` als `<input className="input search" type="search" data-testid="sp-apps-search" placeholder="Projekt suchen …">`.
- `<table className="table">` ohne `.sp-table-wrap`. Die Spaltenliste bekommt je Eintrag `num` (Größe, Δ 7 T, Δ 30 T, CPU, RAM) und `hide` (Δ 30 T, CPU und die Spalte „30 Tage“). Diese Spalten tragen `right num` bzw. `hide-sm` auf `th` und `td`.
- Sortierkopf: `<th data-sort={c.key} aria-sort={…}>` mit `<button type="button" className="sp-sort" onClick={() => toggle(c.key)}>{c.label}<span aria-hidden="true">{' ↑' | ' ↓' | ''}</span></button>`.
- `aria-sort` ist `ascending` bzw. `descending` am aktiven Kopf, sonst fehlt es.
- Die Zeichen ↑ und ↓ bleiben wie bisher.
- Zeile: ``<tr className={`row${selected ? ' sp-row-active' : ''}`} data-testid="sp-app-row" data-app-id={r.id}>``.
- Der Zeilenklick ruft `onSelect(selectedId === r.id ? null : r.id)`, aber nur wenn `isRowActivation(e)` wahr ist.
- Die erste Zelle ist `<td className="sp-name"><button type="button" className="link" aria-pressed={selected} onClick={() => onSelect(selectedId === r.id ? null : r.id)}>{r.name}</button></td>` mit demselben Aufruf. Der Button braucht ihn, weil `isRowActivation` Klicks auf `button` ausfiltert.
- `Delta` rendert ``<span className={`sp-delta sp-delta-${d.dir}`}>{d.text}</span>`` ohne Pfeil. Das Vorzeichen steckt in `d.text`.
- Die Leerzeile bleibt (`colSpan={7}`, „Keine Projekte gefunden.“, Klasse `sp-empty`).
Entscheidung: Die Zeilen folgen der Tabellenregel der Spec. Der Button in der ersten Zelle trägt die Tastaturbedienung, der Zeilenklick bleibt für die Maus. Unter 860 px bleiben Projekt, Größe, Δ 7 T und RAM.
- [ ] **Step 2: Tabellen-CSS ersetzen**
Gelöscht werden `.sp-search`, `.sp-table-wrap`, `.sp-table*`, `.sp-sortable`, `.sp-row`, `.sp-row:hover`, `.sp-delta`. Neu:
| Regel | Wert |
|---|---|
| `.sp-sort` | `display: block; width: 100%; text-align: inherit; white-space: nowrap` (Knopf-Reset und Innenabstand 0 kommen aus der Basis) |
| `.sp-name` | `overflow-wrap: anywhere` |
| `.sp-name .link` | `text-align: left; overflow-wrap: anywhere` |
| `.sp-page .table tr.sp-row-active td` | `background: var(--bg-subtle)` |
| `.sp-page .table tr.sp-row-active td:first-child` / `:last-child` | `box-shadow: -12px 0 0 var(--bg-subtle)` / `12px 0 0 var(--bg-subtle)` |
| `.sp-empty` | `text-align: center; color: var(--text-2)` |
| `.sp-delta` | `font-variant-numeric: tabular-nums` |
- [ ] **Step 3: Bauen und prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p9 && "$W/shots.sh" p9 "$W/shots/p9-t5" --only serverpuls,serverpuls-projekt --gate p9`
Expected: PASS, das heißt:
- Die Tastaturschritte laufen durch.
- Die Tabelle hat nur Haarlinien, Zahlen stehen rechts, die ausgewählte Zeile ist `--bg-subtle`.
- Deltas sind rot, grün oder grau ohne Pfeil. Das Detail zeigt drei Diagramme in Schwarz.
- Bei 390 px sind vier Spalten da und der lange Projektname bricht um.
- [ ] **Step 4: Commit**
```bash
git add src/components/serverpuls/AppsTable.jsx src/styles/serverpuls.css
git commit -m "Design: Serverpuls-Projekttabelle (Sortierkoepfe und Zeilen als Buttons, Deltas als Farbe)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 6: Gate P9, Review, Tag
- [ ] **Step 1: Rebase**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git rebase origin/test && "$W/check.sh"`
Expected: Exit 0, Rebase ohne Konflikt, `# fail 0`. Die Testzahl ist N0 + 2. Liegen neue Commits auf `test`, kann sie höher sein, die Zahl wird notiert. So zeigen die Bilder der folgenden Schritte den Stand, der getaggt wird.
- [ ] **Step 2: Vollständiger Gate-Lauf**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p9 && "$W/shots.sh" p9 "$W/shots/p9" --gate p9`
Expected: Exit 0. Das Gate `p9` prüft alle Ansichten mit den `p1`-Prüfungen (`overflow-x`, `dark-surface`, `low-contrast`) und `icon-svg` für die Ansichten von P2 bis P9. Die fünf P9-Ansichten haben Bilder bei 1440 und 390 px ohne Verstöße.
- [ ] **Step 3: Test-IDs, Farben, unveränderte Logik**
Run: ``source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -o --no-filename 'testid=("sp-[a-z-]+"|\{`sp-tile-\$\{id\}`\})' src/pages/ServerpulsPage.jsx src/components/serverpuls | sort | diff - "$W/p9-testids.before.txt"``
Expected: keine Ausgabe, also dieselben 20 Einträge.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|@tabler|style=\{|#[0-9a-fA-F]{3,8}\b|rgba?\(|!important|▲|▼|🔴|🟠|🟢" src/pages/ServerpulsPage.jsx src/styles/serverpuls.css src/components/serverpuls --glob '!chartColors.js'`
Expected: keine Treffer.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git diff design-p8 -- src/pages/ServerpulsPage.jsx | rg '^[-+].*(setInterval|60000|visibilit|reqId|serverpulsApi)'; git diff --stat design-p8 -- src/components/serverpuls/Chart.jsx src/components/serverpuls/Sparkline.jsx`
Expected: beide Ausgaben leer, weil Takt, Abfragen, `Chart.jsx` und `Sparkline.jsx` unverändert sind.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node "$W/css-klassen.mjs" "$WT" | node -e "const j=JSON.parse(require('fs').readFileSync(0));console.log(j.dead.filter(n=>/^sp-/.test(n)).concat(j.undefinedUsed.filter(n=>/^sp-/.test(n))).join('\n'))"`
Expected: leer, also keine tote und keine undefinierte `sp-*`-Klasse.
- [ ] **Step 4: Bundle-Größe**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" --nur-build`
Expected: Exit 0, CSS kleiner als bei Task 1. Die Werte werden notiert.
- [ ] **Step 5: Code-Review (sonnet)**
Geprüft wird der Diff `design-p8..HEAD` gegen die Spec-Abschnitte „Diagrammfarben“, „Bausteine“ (Kennzahlen, Umschalter, Tabelle, Liste, Badge, Meldungen) und Paket P9. Wichtige Befunde werden behoben. Danach kommen Step 1 (Rebase) noch einmal und Step 2 als Kurzlauf mit `--only serverpuls,serverpuls-projekt`. Schwerpunkte:
- Die Test-IDs und Abfragen sind unverändert.
- Der Fehlerzustand zeigt kein grünes „Alles ok“.
- Das Vorzeichen steht weiter in jeder Delta-Zahl.
- Die Tastaturbedienung funktioniert (Zeilen, Sortierköpfe, Umschalter).
- [ ] **Step 6: Tag und Übergabe**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" && git tag design-p9 && git log --oneline origin/test..design-p9 && git rev-parse design-p9`
Der Eigentümer bekommt:
- die Screenshots aus `$W/shots/p9` (Unterordner `1440x900` und `390x844`; Auswahl: `serverpuls`, `serverpuls-ok`, `serverpuls-projekt`, `serverpuls-ausfall`)
- den Hinweis, dass das Kopf-Badge „Alles ok“ und die Überschrift „Alles ok“ doppelt stehen (Entscheidung aus Task 4), falls er die Doppelung streichen will
- die Push-Zeile `git -C /home/webklar/apps/ticket.webklar.com/dev-design push origin 'design-p9^{commit}:refs/heads/test'`
- die Rücknahme `git revert --no-commit design-p8..design-p9`, Commit und Push durch den Eigentümer (gilt nur, solange P9 das jüngste Paket auf `test` ist)
- [ ] **Step 7: Nach dem Push (erst, wenn der Eigentümer gepusht hat)**
- Im Log von `webklar-ticket-build` steht „Deployment completed successfully“.
- `curl -s https://ticket.webklar.com/ | grep -o 'assets/index-[^"]*\.js'` entspricht `dist/index.html` des Tags.
- Ein lesender Sichttest im Chrome des Eigentümers: Serverpuls mit Diagramm und Projekttabelle. Der Menüpunkt zeigt weiter den Punkt `sp-nav-dot`, wenn es Warnungen gibt.