Files
tickte-system/docs/superpowers/plans/2026-10-05-weisses-design-p5-projekte.md
T

69 KiB
Raw Blame History

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

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

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

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

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

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
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
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
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
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
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' : ''}`}>mitonClick={(e) => { if (isRowActivation(e)) openEdit(project) }}. isRowActivationignoriert Klicks aufa, 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}, onChangerufthandleAssignCustomer(mitwindow.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

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