diff --git a/docs/superpowers/plans/2026-09-30-projektarten-services.md b/docs/superpowers/plans/2026-09-30-projektarten-services.md new file mode 100644 index 0000000..df60093 --- /dev/null +++ b/docs/superpowers/plans/2026-09-30-projektarten-services.md @@ -0,0 +1,220 @@ +# 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/`. +- 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`. + +- [ ] **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://.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 ``). 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=')` (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 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.**