17 KiB
Projektarten, Zuordnungs-Ablauf und „Services“ – 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: Jedes Repo bekommt eine Art; neue Repos (Gitea oder WOMS) werden per Mail bzw. direkt durch einen Ablauf geführt, bis Art, Kunde und Ticket feststehen; WOMS bekommt den Bereich „Services“.
Architecture: Portal (Express, node --test) hält Katalog, Schema, Zuordnungs-API, Mail und Erkennung
(Webhook-Ereignis repository + 10-Minuten-Job). WOMS (React/Vite, node --test tests/unit/*.test.mjs)
bekommt die Seiten /projects/zuordnen/:id und /services und spricht nur über die Portal-Admin-API.
Tech Stack: Node 20/24, Express, Appwrite (node-appwrite über server/services/appwriteAdmin.js),
nodemailer + HTML-Vorlagen (server/mail-templates/), React 18 + react-router, Playwright (Fixture-Check).
Spec: docs/superpowers/specs/2026-09-30-projektarten-services-design.md (Commit 7b489fa)
Worktrees: Portal /home/webklar/apps/kundenbereich.webklar.com/dev-projektarten (Branch
feature/projektarten). WOMS: neu anlegen git -C /home/webklar/apps/ticket.webklar.com/build worktree add -b feature/projektarten ../dev-projektarten origin/test (Rebase-Falle beachten: getrackte node_modules ⇒
bei Konflikten cherry-pick in temporärem Worktree).
Global Constraints
- Arten-Schlüssel exakt:
website,webapp,app,migration,automation,other; Labels exakt: „Website“, „Web-App“, „App“, „Migration/Umzug“, „Automation/Skript“, „Sonstiges“. - Preview je Art: website
yes, webapppossible, alle anderenno. Funktionen: nur website ⇒['analyse'];FEATURES.analyse = { label: 'Website-Analyse', status: 'in_development' }. - Leeres
kind= „Art fehlt“. Keine automatische Einordnung bestehender Repos. - Neues Ticket entsteht nur beim Abschließen; Thema „ – “, Ticket-Art
Migrationbei Art migration sonstWebpage,status: 'Open',serviceType: 'Remote',systemType: 'n/a'. - Mail nur für Repos, deren Projekt-Dokument im aktuellen Lauf/Ereignis neu angelegt wurde und deren Gitea-
created_at< 24 h alt ist. Eine Erinnerung nach 24 h, danach nichts. Max. 3 Versandversuche. - Mail-Link exakt
https://ticket.webklar.com/projects/zuordnen/<projektId>. - Alle Texte Deutsch, Du-Form wie T15. Kein
confirm()in der UI. - Nichts pushen/live schalten ohne OK des Nutzers (Livegang L1–L5 unten).
Review Focus
- Doppel-Abschluss (zwei Tabs / zwei Mitarbeiter) ⇒ zweiter Aufruf 409, kein zweites Ticket. → Task 4
assignProjectwirft nach neu angelegtem Ticket ⇒ Ticket wird gelöscht,kindnicht gesetzt. → Task 4- Erster Job-Lauf nach Livegang findet viele Alt-Repos ⇒ keine Mails (24-h-Grenze). → Task 6
- Ticket gehört zu anderem Kunden oder ist Closed/Cancelled ⇒ 400. → Task 4
- Gitea
repository-Ereignis für ein Repo, das der Job im selben Moment importiert ⇒ genau ein Dokument, genau eine Mail. → Task 7
Portal
Task 1: Katalog projectKinds.js
Files: Create server/services/projectKinds.js, Test test/projectKinds.test.js
Interfaces – Produces:
-
KINDS: Array<{ key, label, preview: 'yes'|'possible'|'no', features: string[], ticketType: 'Webpage'|'Migration', description }> -
FEATURES: { analyse: { label, status } } -
isKind(key: string): boolean,kindLabel(key): string('' bei unbekannt),ticketTypeFor(key): 'Webpage'|'Migration' -
servicesCatalog(): { kinds: KINDS, features: FEATURES } -
Step 1: Test schreiben –
KINDS.map(k=>k.key)deepEqual der 6 Schlüssel in Reihenfolge; Labels exakt;isKind('website')===true,isKind('shop')===false,isKind('')===false;ticketTypeFor('migration')==='Migration',ticketTypeFor('app')==='Webpage';KINDS.find(k=>k.key==='website').featuresdeepEqual['analyse'], alle anderen[]; Preview-Werte wie Global Constraints. -
Step 2:
node --test test/projectKinds.test.js⇒ FAIL (Modul fehlt) -
Step 3: implementieren; Beschreibungen je eine Zeile („Was wir liefern“), z. B. Website „Internetauftritt inkl. Landingpage oder Shop“.
-
Step 4: Test ⇒ PASS
-
Step 5:
git commit -m "Projektarten: Katalog"
Task 2: Schema-Skript
Files: Create scripts/projektarten-setup.mjs, Test test/projektartenSetup.test.js
Interfaces – Produces: exportiertes PROJECT_KIND_ATTRIBUTES (Array { key, type, size?, required:false }) und
main({ check }); Muster scripts/setup-chat-collection.mjs (idempotent: vorhandene Attribute überspringen, --check nur melden).
Attribute an websiteProjects: kind string 20, forCustomer boolean, kindSetBy string 64, kindSetAt
string 40, createdByGitea string 64, assignMailSentAt string 40, assignReminderSentAt string 40,
assignMailAttempts integer.
- Step 1: Test – die 8 Schlüssel exakt, alle
required: false;mainmit gemocktem Client, derkindschon kennt, legt nur die 7 anderen an. - Step 2: FAIL · Step 3: implementieren · Step 4: PASS
- Step 5:
git commit -m "Projektarten: Schema-Skript fuer websiteProjects"
Task 3: createTicketForProject
Files: Modify server/services/ticketAdmin.js, Test test/ticketAdmin.test.js (Mock-Muster wie test/projectAssignment.test.js / test/helpers)
Interfaces – Produces: createTicketForProject({ customerId, topic, type, requestedBy, startDate, details }): Promise<{ $id, woid }>
– nutzt generateNextWoid, Felder wie ensureProjectTicket (priority 1, serviceType 'Remote', systemType 'n/a',
customerName/Location aus dem Kunden) plus requestedBy, startDate. deleteTicket(ticketId): Promise<void>.
- Step 1: Test – erzeugt Dokument mit
type,topic,status:'Open',customerNamevom Kunden, nächster WOID; unbekannter Kunde ⇒ Fehler mitstatus 400;deleteTicketruftdeleteDocument(workorders, id). - Step 2–4: FAIL → implementieren → PASS
- Step 5:
git commit -m "Tickets: createTicketForProject und deleteTicket"
Task 4: Zuordnen-Service + Route
Files: Create server/services/projectKindAssign.js, Modify server/routes/admin/websiteProjects.js,
Test test/projectKindAssign.test.js
Interfaces:
- Consumes: Task 1 (
isKind,kindLabel,ticketTypeFor), Task 3,assignProject(projectId, {customerId, ticketId}),updateWebsiteProject(id, { subdomain, projectType }),withLocks,repoLockKey. - Produces:
completeAssignment(projectId, { kind, forCustomer, customerId, ticketId, newTicket, preview }, { staffName, now }): Promise<{ ticketId: string|null }>; RoutePOST /api/admin/website-projects/:id/zuordnen⇒ 200{ ticketId }, 400{ error }, 404, 409{ error: 'bereits zugeordnet', kindSetBy, kindSetAt }.staffNameausreq.appwriteUser.name || email.
Ablauf unter withLocks([repoLockKey(repoFullName)]): Projekt laden (404) → kind gesetzt ⇒ 409 → prüfen
(Art gültig; bei forCustomer Kunde existiert, genau eins von ticketId/newTicket, vorhandenes Ticket gehört
dem Kunden und Status ∉ {Closed, Cancelled}) → ggf. Ticket anlegen → assignProject (bei Fehler: neu angelegtes
Ticket deleteTicket, Fehler weiterwerfen) → updateDocument(kind, forCustomer, kindSetBy, kindSetAt) →
wenn kind==='website' && forCustomer && preview?.subdomain: updateWebsiteProject(id, { subdomain, projectType: 'preview' }).
Intern (forCustomer:false): nur Art speichern, ticketId: null.
- Step 1: Tests (je ein
test(...)):intern speichert nur Art·vorhandenes Ticket wird zugewiesen·neues Ticket nur beim Abschliessen, Thema "Website – foo"·Migration ergibt Ticket-Art Migration·Ticket anderer Kunde ⇒ 400·Ticket Closed ⇒ 400·unbekannte Art ⇒ 400·assignProject wirft ⇒ neues Ticket geloescht, kind leer·zweiter Aufruf ⇒ 409, nur ein Ticket·Website mit Subdomain ⇒ updateWebsiteProject(preview)· Route: ohne Staff-JWT ⇒ 401. - Step 2–4: FAIL → implementieren → PASS (
npm testkomplett grün) - Step 5:
git commit -m "Projektarten: Zuordnen-Ablauf serverseitig"
Task 5: GET /api/admin/services
Files: Create server/routes/admin/services.js, Modify server/index.js (app.use('/api/admin/services', …)), Test test/servicesRoute.test.js
Interfaces – Produces: Antwort { kinds, features, groups: { [kind|'missing']: Array<{ id, projectName, repoFullName, customerId, customerName, ticketId, woid, previewUrl, features: {} }> } }.
Archivierte und isTemplate-Projekte ausgeschlossen. previewUrl = https://<subdomain>.project.webklar.com bzw. liveDomain, sonst ''. features bleibt {} (kommt mit Teilprojekt 3).
- Step 1: Test – 3 Projekte (website, leer, archiviert) ⇒
groups.website.length===1,groups.missing.length===1, archiviertes fehlt; Kundenname aufgelöst;requireAppwriteStaffaktiv. - Step 2–4: FAIL → implementieren → PASS
- Step 5:
git commit -m "Projektarten: Services-Uebersicht fuer WOMS"
Task 6: Mail „Neues Repo zuordnen“ (T16) + Auswahl-Logik
Files: Create server/mail-templates/T16 Repo zuordnen.html, Create server/services/repoAssignMail.js,
Test test/repoAssignMail.test.js
Interfaces:
- Consumes:
findEmployeeForPusher,sendStaffMail,renderMail,berlinDateTime. - Produces:
shouldSendAssignMail(project, repo, { now, createdNow }): boolean;dueReminder(project, { now }): boolean;sendRepoAssignMail(project, { giteaLogin, reminder }): Promise<{ sent, reason? }>(setztassignMailSentAtbzw.assignReminderSentAt, zähltassignMailAttempts).
Vorlage: Header-Kommentar wie T15 (Betreff/Platzhalter). Betreff „Neues Repo zuordnen: {{repo}}“, Erinnerung „Erinnerung: …“
(Block <!-- WENN erinnerung -->). Inhalt: Repo, Gitea-Link, angelegt von/am, Knopf „Jetzt zuordnen“ ⇒ Link aus Global Constraints.
- Step 1: Tests –
shouldSendAssignMailfalse beicreatedNow:false, false beicreated_at25 h alt, false bei gesetztemkind, false beiassignMailAttempts>=3, true bei neu + 1 h;dueRemindertrue genau wennassignMailSentAt≥ 24 h,kindleer,assignReminderSentAtleer; Mail enthält den exakten Link; unbekannter Login ⇒{ sent:false, reason:'no_employee' };renderMailmeldet keineproblems. - Step 2–4: FAIL → implementieren → PASS
- Step 5:
git commit -m "Projektarten: Mail T16 Repo zuordnen"
Task 7: Erkennung – Webhook-Ereignis + 10-Minuten-Job
Files: Modify server/routes/webhook/gitea.js, Modify server/services/giteaAdmin.js (importNewRepos),
Create server/services/repoAssignJob.js, Modify server/index.js, Test test/repoAssignDetect.test.js
Interfaces:
-
importNewRepos()liefert zusätzlichcreatedRepos: Array<{ projectId, repo }>(bestehendes{ created }bleibt). -
Webhook: Header
X-Gitea-Event: repositorymitpayload.action === 'created'⇒ unterrepoLockKeyDokument anlegen falls fehlt (createdByGitea = payload.sender.login), dannsendRepoAssignMailfallsshouldSendAssignMail; Antwort 200. Andererepository-Aktionen ⇒ 200 ignoriert. Push-Pfad unverändert. -
runRepoAssignJob({ now }):importNewRepos()⇒ fürcreatedReposcreatedByGitea = repo.owner.loginwenn Owner kein Org-Konto, Mail nach Regel; danach Erinnerungen (dueReminder) und Wiederholungen (Mail fehlgeschlagen, Versuche < 3). Bei Appwrite-Fehler: Lauf abbrechen, loggen. Inserver/index.jssetInterval(…, 10*60*1000).unref()+ einmal 60 s nach Start. -
Step 1: Tests –
repository/created⇒ 1 Dokument + 1 Mail; gleichzeitig laufender Job für dasselbe Repo ⇒ weiterhin 1 Dokument, 1 Mail;repository/deleted⇒ nichts; Job-Lauf mit 20 alten Repos (created_at vor 3 Tagen) ⇒ 0 Mails; Org-Repo ohne Sender im Job ⇒ keine Mail; Push-Webhook-Tests (pushTimeWebhook.test.js) weiterhin grün. -
Step 2–4: FAIL → implementieren → PASS (
npm testkomplett) -
Step 5:
git commit -m "Projektarten: neue Repos erkennen und Zuordnungs-Mail senden"
WOMS
Task 8: API-Client + Ablauf-Logik
Files: Create src/lib/projectKindsApi.js (Muster projectAdminApi.js), Create src/lib/assignFlow.js, Test tests/unit/assignFlow.test.mjs
Interfaces – Produces:
-
fetchServices(),completeAssignment(projectId, body)(⇒ Task-4-Route),fetchProject(projectId)(vorhandene Projekt-Abfrage wiederverwenden). -
nextStep(state): 'kind'|'customer?'|'customer'|'ticket'|'summary'undprevStep(state); intern springt voncustomer?direkt zusummary. -
newTicketDefaults({ kindLabel, ticketType, repoName, repoUrl, customer, today }): { topic, type, requestedBy, startDate, details }–topic"${kindLabel} – ${repoName}",requestedBy=customer.contactName || customer.name,detailsenthältrepoUrl. -
canFinish(state): boolean(Kunde ⇒ Ticket oder vollständiges newTicket nötig). -
openTicketsFor(tickets, customerId): Status ∉ {Closed, Cancelled}. -
Step 1: Tests für alle fünf Funktionen mit den Werten oben (u. a. „Migration/Umzug – foo“ ⇒ type 'Migration').
-
Step 2–4:
npm testFAIL → implementieren → PASS -
Step 5:
git commit -m "Projektarten: Ablauf-Logik und API-Client"
Task 9: Seite /projects/zuordnen/:id
Files: Create src/pages/AssignProjectPage.jsx, Modify src/App.jsx (ProtectedRoute), CSS in vorhandenem Stil.
Schritte laut Spec Abschnitt 2 mit Task-8-Logik. Kopf: Repo, Gitea-Link, angelegt von/am. „Neuer Kunde“ öffnet das
vorhandene Kunden-Formular (Modal aus CustomersPage) und übernimmt den neuen Kunden. Bei kind gesetzt: Hinweis
„Bereits erledigt von am “, Link zum Ticket, Knopf „Art ändern“ (nur Schritt 1, dann speichern).
Nach Erfolg navigate('/tickets?open=<ticketId>') (vorhandenes Öffnen-Muster der TicketsPage verwenden), intern ⇒ /services.
409 ⇒ Seite neu laden und Hinweis zeigen.
- Step 1: implementieren · Step 2:
npm run buildohne Fehler - Step 3:
git commit -m "Projektarten: Zuordnungs-Ablauf im WOMS"
Task 10: Seite /services + Navigation
Files: Create src/pages/ServicesPage.jsx, Create src/lib/servicesView.js, Modify src/App.jsx, src/components/Navbar.jsx (Eintrag „Services“ nach „Projekte“), Test tests/unit/servicesView.test.mjs
Interfaces – Produces: servicesCards(apiResponse): Array<{ key, label, description, count, features:[{label, statusLabel}], projects }>,
Karte missing („Art fehlt“) zuletzt, nur wenn nicht leer; statusLabel für in_development = „In Entwicklung“.
- Step 1: Test
servicesCards(Reihenfolge = Katalog, Zählung, Art-fehlt-Karte, Statuslabel). - Step 2–4: FAIL → implementieren (Karte je Art, Projektliste, Knopf „Zuordnen“ ⇒
/projects/zuordnen/:id) → PASS;npm run buildok - Step 5:
git commit -m "Projektarten: Bereich Services"
Task 11: Weitere Einstiege
Files: Modify src/components/TicketProjects.jsx, src/pages/ProjectsPage.jsx
- TicketProjects: beim Zuweisen eines Projekts ohne
kinderst kleiner Dialog „Was ist das?“ (6 Knöpfe), danncompleteAssignment(id, { kind, forCustomer:true, customerId: ticket.customerId, ticketId: ticket.$id })stattassignProjects. Projekte mitkindweiter wie bisher. Art je zugewiesenem Projekt als kleines Label anzeigen. - ProjectsPage „Neues Repo anlegen“: nach erfolgreichem
create-from-template⇒navigate('/projects/zuordnen/<id>')(ID aus der Antwort; fehlt sie im Portal-Result, in Task 4 ergänzen). - Step 1: implementieren · Step 2:
npm test+npm run buildgrün · Step 3:git commit -m "Projektarten: Art beim Zuweisen und nach Repo-Anlage"
Task 12: Oberflächen-Check (Playwright, Fixture)
WOMS-dist statisch ausliefern + Fixture-API (Portal-Admin-Routen gemockt). Prüfen:
Ablauf intern; Kunde + vorhandenes Ticket; Kunde + neues Ticket (Vorbelegung sichtbar, Abbruch ⇒ kein POST);
409-Hinweis; Services-Seite mit Art-fehlt-Karte; Handy-Breite 390 px ohne horizontales Scrollen.
Skript in den Scratchpad, Screenshots nach /root/reports/projektarten-2026-09-30/.
- Step 1: Checks laufen grün · Step 2: Ergebnis im SDD-Ledger festhalten
Livegang (jeder Schritt nur mit OK des Nutzers)
- L1
docker exec -w /app webklar-kundenbereich-server node scripts/projektarten-setup.mjs --check, dann ohne--check. - L2 Portal: rebase auf origin/main,
npm test,git push origin feature/projektarten:main; prüfen: Container-Start,GET /api/admin/servicesohne JWT ⇒ 401, Preview-Sync ok. - L3 Org-Hook 112: Ereignisse
push+repository(PATCH mit System-Token, vorher Stand sichern). - L4 WOMS:
git push origin feature/projektarten:test; prüfen: Bundle enthält „Services“. - L5 Live-Test mit dem Nutzer: Repo in Gitea anlegen ⇒ Mail ⇒ Ablauf mit Test-Kunde/Ticket #99999 ⇒ Ticket/Preview prüfen ⇒ aufräumen (Repo, Dokument, ggf. Ticket). Erst danach Teilprojekt 2.