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

221 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 „<Art-Label> – <Repo-Name>“, 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 <kindSetBy> am <Datum>“, 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.**