Files
tickte-system/docs/superpowers/plans/2026-10-05-weisses-design-p8-rechtliches-admin.md
T

51 KiB
Raw Blame History

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.

Nachtrag aus P0 (Gate): Gate p1 und alle folgenden Gates schließen admin und admin-neu von overflow-x aus (['*', '!admin', '!admin-neu']). P8 macht die Admin-Seite bei 390 px ohne waagerechtes Scrollen und entfernt die beiden Ausschlüsse aus allen Gates ab p8 (ein Schritt im Abschluss-Task, geprüft mit "$W/shots.sh" … --only admin,admin-neu --gate p8).

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..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

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
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

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
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:

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

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

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

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
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

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.