Umsetzungsplaene je Paket mit gemeinsamen Regeln (env.sh/check.sh, Gate-Schema, CSS-Reihenfolge, TP2-Pruefung vor jedem Paket) und den Schnittstellen aus P1. Gegen Spec und untereinander geprueft (zwei Pruefer, Korrekturrunde, Nachpruefung). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
69 KiB
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.jsxsteckt (Projektart, Hosting-Zustand, Aktionen), wandert mit Tests nachsrc/lib/projectRow.js.src/lib/projectList.jsbekommt den neuen ExportprojectTabs. - Die Töne der Status-Vokabulare (Preview, Analyse, Projektstatus) werden in
src/components/projectTones.jsauf Badge-Töne abgebildet.previewEnv.js,previewView.jsundanalyseView.jsbleiben unverändert. global.cssbekommt 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-p1bisdesign-p4existieren. Task 0 prüft das und rebased auforigin/test. - Dateien dieses Pakets:
ProjectsPage,AssignProjectPage,ServicesPage,TicketProjects,AssignedProjectCard,AnalyseRow,PreviewBlock,PreviewLinkButton,GiteaLinkButton,CopyableCredential, dazuglobal.cssundAdminPage.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.jsund die Signaturen vonAssignedProjectCard,TicketProjects,AnalyseRow,PreviewBlock,CopyableCredential,copyText,getCustomerPortalPassword.lib/projectList.jsbekommt 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 inprojectTones.js. - Jeder
Buttonund jeder Textlink-Knopf (button.link) bekommt ein explizitestype(buttonodersubmit), weil P1 keinen Standard setzt. In einem<Dialog as="form">trägt jeder Knopf außer dem Absenden-Knopftype="button". - Zeilenklick: Klickbare Tabellenzeilen prüfen Klicks mit
isRowActivation(event)aussrc/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, keinbtn-sm, keine Icon-Importe in den Dateien dieses Pakets. window.confirmundwindow.alertbleiben an ihren Stellen.- Ticketlinks über
ticketPathaussrc/lib/ticketRoutes.js(P2). Die drei Stellen inAssignProjectPage(2×) undServicesPagesind seit P2 umgestellt und bleiben es. - Testzahl: Stand
design-p4plus 13 neue Tests (7 inprojectRow, 2 inprojectList, 4 inprojectTones).
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). Dazuprojekte-861undprojekte-1101(Menü fest, aber wenig Platz). Erwartet: keinoverflow-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
PreviewBlockspeichert nie. - Klicks auf Link, Auswahl und Knopf in einer Zeile öffnen den Dialog nicht.
- Das
- 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-
$idohne Treffer in der Ticketliste (Rohwert als Link),$updatedAtunlesbar oder leer („-“), Status leer („-“). Sie stehen inprojekte-lang. - Kein Flackern: Nach Hosten, Aktualisieren, Kundenwechsel und Speichern bleibt die Tabelle stehen. Der Text „Wird geladen …“ erscheint nur beim ersten Laden.
- Jede heutige Steuerung ist erreichbar: Die Zuordnungen stehen in den Tabellen der Tasks 7 bis 13. Task 14 prüft sie anhand der Ansichten.
Task 0: Paketstart
- Step 1: Stand und Rebase
Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git merge-base --is-ancestor design-p4 HEAD && git rebase origin/test && "$W/check.sh"
Expected: Exit 0 und # fail 0. Die Zahl # pass wird notiert (Basis für Task 14). Ist design-p4 kein Vorfahre von HEAD, stoppt der Agent und meldet es dem Eigentümer.
- Step 2: Parallele Arbeit an TP2
Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git log --oneline origin/test..origin/feature/terminplaner-tp2 -- src/pages/ProjectsPage.jsx src/pages/AssignProjectPage.jsx src/pages/ServicesPage.jsx src/components/TicketProjects.jsx src/components/AssignedProjectCard.jsx src/components/AnalyseRow.jsx src/components/PreviewBlock.jsx src/components/PreviewLinkButton.jsx src/components/GiteaLinkButton.jsx src/components/CopyableCredential.jsx src/pages/AdminPage.css src/styles/global.css src/lib/projectList.js
Expected: keine Ausgabe. Gibt es Treffer, stoppt der Agent und fragt den Eigentümer. Er empfiehlt dabei, TP2 zuerst nach test zu bringen.
- Step 3: Bausteine aus P2
Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && ls src/lib/rowClick.js src/lib/ticketRoutes.js && rg -n "isRowActivation" src/lib/rowClick.js
Expected: Beide Dateien sind da, isRowActivation steht in rowClick.js (P2, Task 2). Fehlt etwas, stoppt der Agent.
Task 1: Projekt-Helfer projectRow.js
Files:
- Create:
src/lib/projectRow.js - Test:
tests/unit/projectRow.test.mjs(neu)
Interfaces:
-
Consumes:
fmtDateTime(value)aussrc/lib/format.js(liefert05.10.26, 09:00),formatDistanceausdate-fns,deausdate-fns/locale. -
Produces (1:1 aus
ProjectsPage.jsxL35-85 herausgezogen, Töne stattbadge-*-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)liefertsrc/lib/rowClick.js(P2, Task 2).projectRow.jsbaut 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.jsimplementieren -
PROJECT_TYPES,typeOf,isProjectReadystehen wörtlich inProjectsPage.jsxL38-85. Übernommen werdenlabel,short,hint,adminOnly.iconentfällt.badge: 'badge-info'wirdtone: 'info'(badge-ok→ok,badge-warn→warn,badge-muted→muted). -
projectActions:host = hosted && !ready && !archivedpreview = !host && Boolean(project.previewUrl)refresh = hosted && ready && !archivedgitea = 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 leererticketId''. -
updatedLabel: Text mitformatDistance(new Date(iso), now, { addSuffix: true, locale: de }), Titel mitfmtDateTime(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 nachVIEW_LABELS) - Test:
tests/unit/projectList.test.mjs(Importzeile umprojectTabsergänzen, zwei Tests anhängen)
Interfaces:
-
Consumes:
projectCounts(projects, { isAdmin })undVIEW_LABELSaus derselben Datei. -
Produces:
projectTabs(counts, { isAdmin = false }) → [{ id, label, count }]fürTabsaus 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ürpreviewStatus().color.previewStateTone(state)fürpreviewStateder Services-Antwort.analyseColorTone(color)füranalyseStatus().color.statusValueTone(value)fürstatusundprovisioningStatuseines 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 Bedarffixtures/standard.mjs
Interfaces:
-
Consumes: Aufbau von
fixtures/altdaten.mjs(Standarddaten plus überschriebene Listen),VIEWSundGATESaus P0, Schritt-Funktionensteps: async (page, ctx) => voidmitnode:assert/strict.ctx.backend.callslistet alle beantworteten Requests. -
Produces: Ansichten und Gate
p5für die Tasks 6 bis 14. -
Step 1: Testdaten anlegen
projekte-lang: Standarddaten plus- Projekt A:
projectNamemit 120 Zeichen,repoFullNameWEBklar/plus 82 Zeichen, Kunde mit 60 Zeichen im Namen,ticketIdeines Tickets der Liste - Projekt B: ohne
projectNameundgiteaRepoName,ticketIdohne Treffer in der Ticketliste,$updatedAt: 'kaputt',status: '',subdomainundpreviewUrlgesetzt, nicht deployt - Projekt C: archiviert,
subdomain,status: 'ready'
- Projekt A:
projekte-viel: Standarddaten plus 300 Projekte und 40 Kunden.fixtures/standard.mjsbraucht ein Projekt an Ticket 100131 mitsubdomain,status: 'ready',previewUrlundgiteaRepoUrl, mindestens ein Projekt je Typ (Preview, Website, Vorlage, intern) und eines ohnekind. Der Kunde, denkunde-projectsöffnet, hat dieses Projekt zugeordnet. Fehlt etwas, wird es ergänzt.
-
Step 2: Ansichten in
views.mjsanlegen 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-controllö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>"
Task 6: Link-Bausteine (PreviewLinkButton, GiteaLinkButton, CopyableCredential)
Files:
- Modify:
src/components/PreviewLinkButton.jsx(alles ab L7),src/components/GiteaLinkButton.jsx,src/components/CopyableCredential.jsx(L93-152) - Modify:
src/pages/AdminPage.css(L330-345:.credential-action,.credential-action:hover,.credential-checklöschen,.credentialund.credential .linkanpassen)
Interfaces:
-
Consumes:
.linkaus P1,createPreviewLinkauslib/teamApi,openInNewTabauslib/openInNewTab. -
Produces:
PreviewLinkButton({ href, subdomain, label = 'Vorschau öffnen' })rendert<a className="link" href target="_blank" rel="noopener noreferrer" onClick>{label}</a>. Ohnehrefkommtnull. Die Logik (Einmal-Link,window.alertbei 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-emptyund.credential .link. P8 zieht sie nachglobal.css.
-
Step 1: Prüfen, dass
titleundsizenirgends ü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-labelundtitleführen die sichtbaren Wörter mit:${label} anzeigen,${label} verbergen,${label} kopieren,${label} kopiert.- Maskierung, Timer,
stopPropagationundcopyTextbleiben.
- Der Umschalter ist
- Entscheidung:
aria-labelundtitlesagen „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.csslädt vorglobal.css..credential .link(0,2,0) schlägt.linkundbutton.link(0,1,1) über die Spezifität und braucht kein!important.
- Die Link-Bausteine verlieren Inline-Stil und Icon. Der Text ist
-
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 undSTATE_OF_COLORL14 löschen, Markup L174-297 neu)
Interfaces:
-
Consumes:
Button,Badgeauscomponents/ui,previewColorToneaus./projectTones,previewStatus,modeOptionsFor,modeHint,isDevUnavailableError,NO_DEV_HINTauslib/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(COLORSL5 löschen, Markup L58-132 neu)
Interfaces:
-
Consumes:
Dialog,Button,Badge,Fieldauscomponents/ui(P1),analyseColorToneaus./projectTones, alle Funktionen auslib/analyseViewunverä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 undSTATUS_COLORS/StatusBadgeL2-35 ersetzen, Markup L81-176 neu)
Interfaces:
-
Consumes:
Button,Badgeauscomponents/ui,statusValueToneaus./projectTones,PreviewLinkButton,GiteaLinkButton,PreviewBlock,AnalyseRow, Klassen.proj-*,.row-actions,.link,.msg. -
Produces:
AssignedProjectCard({ project, onUnassign, showActions = false, customerName = '', onChanged = () => {} })(Signatur unverändert). Aufrufer sindTicketProjectsund 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,Dialogauscomponents/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 auslib/projectRowersetzen - L358-369 (
FILTER_OPTIONS,typeOptions) ersetzen - L371-782 (Markup) neu
Row(L784-791) und die totecustomerMap(L131-137) löschen
- L1-85 (Importe,
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:
ticketPathundisRowActivation(event)aussrc/lib/rowClick.js - Hooks unverändert (
useWebsiteProjects,useCustomers,useWorkorders,useEmployees) - Klassen
.projects-table,.row-actions,.dialog-form,.pv-title
- aus Task 1 bis 3:
-
Produces: Seite
/projects. Die Route/projects/zuordnen/:idwird 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 wennuseWebsiteProjectsloadingsetzt. 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).
- Neu:
-
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.linkder 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):
createErrorals<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>editErroralsmsg err<form id="projekt-bearbeiten" className="dialog-form" onSubmit={handleEditSave}>mit den Feldern „Name“ (required), „Typ / Template“ (Optionen ausPROJECT_TYPES,adminOnlynur für Admins,hintohne Icon) und, außer bei Typproject, „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.confirmbleibt) 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...“,disabledbeieditLoading \|\| archiveBusy \|\| archived,titlewie heute)
- links
-
Entscheidung: Der Speichern-Knopf steht im Fuß außerhalb des
<form>und hängt über das Attributformdaran. So löst Enter im Namensfeld Speichern aus, imPreviewBlocknie. -
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-aktionundprojekte-tastaturbestehen. -
Im Bild
projektesteht 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,Badgeauscomponents/ui,CreateCustomerForm(Dialog aus P4, PropsonCancel,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 auslib/assignFlowund 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
Buttondieser Seite tragentype="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.subtitleist 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 vonCard title).STEP_TITLESbleibt unverändert. - Laden:
<div className="page"><div className="loading">Wird geladen …</div></div>. Ladefehler:PageHeadermit der Brotkrume nur „Projekte“ (es gibt noch keinproject) 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">undButton variant="primary"„Weiter“. - Zustand „Art bereits gesetzt“: Hinweis (
noticealsmsg err), „Bereits erledigt von … am …“ undTicketLink(Link className="link", Text „Zum Ticket #{woid}“) ohne Kasten.
- Alle
-
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-hoverund 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,Badgeauscomponents/ui,previewStateToneauscomponents/projectTones,servicesCardsunverä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">undButton 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 Textlabel · statusLabelin einer Zeile (.pv-row). - Projektzeilen sind
.list-row(kein Link,div): links.cell-titlemit Projektname, dem Badge „Analyse an“ (nur beifeatures.analyse === 'active') und davor dem Preview-Badge, darunter.cell-sub(Kunde · Previewoder „Intern“, wie heute). RechtsLink 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,useNavigateund der Aufrufnavigateentfallen.
- Laden:
-
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
p5gilt 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-contrastundoverflow-x. -
icon-svgist für alle Ansichten im Umfang des Gates (P2 bis P5, ohneignore) 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/componentsauf geänderte Texte lesen). - Alle
window.confirm,busyRef-artigen Sperren (assignBusyId,hostBusyId,refreshBusyId,archiveBusy) unddata-testid/data-hidden-hitssind 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/1440x900und$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 auftestund 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-buildsteht „Deployment completed successfully“. -
curl -s https://ticket.webklar.com/ | grep -o 'assets/index-[^"]*\.js'entsprichtdist/index.htmldes 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.