Plan: Projektarten, Zuordnungs-Ablauf und Services
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7a4af8e3c8
commit
b98562bb4f
1 file changed
+220
@@ -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 „<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.**
|
||||
Reference in new issue
Block a user