Files
Webklar-Kundenbereich/docs/superpowers/plans/2026-09-30-projektarten-services.md
T
2026-09-30 18:25:27 +00:00

17 KiB
Raw Blame History

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, webapp possible, alle anderen no. 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 Migration bei Art migration sonst Webpage, 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

  1. Doppel-Abschluss (zwei Tabs / zwei Mitarbeiter) ⇒ zweiter Aufruf 409, kein zweites Ticket. → Task 4
  2. assignProject wirft nach neu angelegtem Ticket ⇒ Ticket wird gelöscht, kind nicht gesetzt. → Task 4
  3. Erster Job-Lauf nach Livegang findet viele Alt-Repos ⇒ keine Mails (24-h-Grenze). → Task 6
  4. Ticket gehört zu anderem Kunden oder ist Closed/Cancelled ⇒ 400. → Task 4
  5. 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').features deepEqual ['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; main mit gemocktem Client, der kind schon 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', customerName vom Kunden, nächster WOID; unbekannter Kunde ⇒ Fehler mit status 400; deleteTicket ruft deleteDocument(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 }>; Route POST /api/admin/website-projects/:id/zuordnen ⇒ 200 { ticketId }, 400 { error }, 404, 409 { error: 'bereits zugeordnet', kindSetBy, kindSetAt }. staffName aus req.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 test komplett 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; requireAppwriteStaff aktiv.
  • 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? }> (setzt assignMailSentAt bzw. assignReminderSentAt, zählt assignMailAttempts).

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 – shouldSendAssignMail false bei createdNow:false, false bei created_at 25 h alt, false bei gesetztem kind, false bei assignMailAttempts>=3, true bei neu + 1 h; dueReminder true genau wenn assignMailSentAt ≥ 24 h, kind leer, assignReminderSentAt leer; Mail enthält den exakten Link; unbekannter Login ⇒ { sent:false, reason:'no_employee' }; renderMail meldet keine problems.
  • 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ätzlich createdRepos: Array<{ projectId, repo }> (bestehendes { created } bleibt).

  • Webhook: Header X-Gitea-Event: repository mit payload.action === 'created' ⇒ unter repoLockKey Dokument anlegen falls fehlt (createdByGitea = payload.sender.login), dann sendRepoAssignMail falls shouldSendAssignMail; Antwort 200. Andere repository-Aktionen ⇒ 200 ignoriert. Push-Pfad unverändert.

  • runRepoAssignJob({ now }): importNewRepos() ⇒ für createdRepos createdByGitea = repo.owner.login wenn Owner kein Org-Konto, Mail nach Regel; danach Erinnerungen (dueReminder) und Wiederholungen (Mail fehlgeschlagen, Versuche < 3). Bei Appwrite-Fehler: Lauf abbrechen, loggen. In server/index.js setInterval(…, 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 test komplett)

  • 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' und prevStep(state); intern springt von customer? direkt zu summary.

  • newTicketDefaults({ kindLabel, ticketType, repoName, repoUrl, customer, today }): { topic, type, requestedBy, startDate, details } – topic "${kindLabel} – ${repoName}", requestedBy = customer.contactName || customer.name, details enthält repoUrl.

  • 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 test FAIL → 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 build ohne 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 build ok
  • 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 kind erst kleiner Dialog „Was ist das?“ (6 Knöpfe), dann completeAssignment(id, { kind, forCustomer:true, customerId: ticket.customerId, ticketId: ticket.$id }) statt assignProjects. Projekte mit kind weiter 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 build grü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/services ohne 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.