Files
Webklar-Kundenbereich/docs/superpowers/plans/2026-09-27-gitea-organisation-2-ticketsystem.md
T
ClaudeandClaude Opus 5.5 a3faa1aff1 Umsetzungspläne: Kundenportal, Ticketsystem, Lead-Automatik
Plan 1 Kundenportal (Gitea-Grundlage, Umzug, Nachtragen, Livegang),
Plan 2 Ticketsystem (Dialog, Knöpfe), Plan 3 Lead-Automatik
(neue Lead-Repos in WEBklar über das System-Konto).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:21:34 +00:00

345 lines
20 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.
# Plan 2 – Ticketsystem: neue Repos sehen, Zuweisen mit Umzug, Preview und Pushes per Knopf
> **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.
**Ziel:** Der Dialog „Projekte bearbeiten“ zeigt neue Gitea-Repos sofort. „Zuweisen“ läuft über das Portal, das das Repo verschiebt und Pushes nachträgt, und zeigt danach eine Zusammenfassung. Zugewiesene Projekte bekommen die Knöpfe „Preview einschalten“ und „Pushes nachtragen“. „Kunde zuordnen“ auf der Projekte-Seite nutzt denselben Weg.
**Aufbau:**
- **Reine Logik** in zwei neuen Dateien:
- `src/lib/subdomain.js`: Subdomain-Vorschlag,
- `src/lib/projectAssign.js`: Organisation, Texte der Zusammenfassung, Zuweisen nacheinander.
Beide werden mit `node --test` geprüft, geladen über esbuild. Das Ticketsystem hat bisher keinen Testlauf.
- **Portal-Aufrufe** kommen nach `src/lib/projectAdminApi.js`.
- **Oberfläche:** Der Hook `useWebsiteProjects` und drei Komponenten nutzen das.
- **Oberflächen-Prüfung:** Playwright gegen einen echten vite-Build mit nachgebautem Appwrite und Portal. Sie liegt im Scratchpad und wird nicht committet.
**Technik:**
- React 18, Vite 5
- `appwrite` 13 (Web-SDK)
- Node 24 auf dem Host für `node --test`
- esbuild aus `node_modules`, genutzt über die JS-Schnittstelle, nicht über `bin/esbuild`
**Spec:** `docs/superpowers/specs/2026-09-27-gitea-organisation-design.md` im Kundenportal-Repo. Maßgeblich sind die Abschnitte 5.7, 6 und 11.
**Voraussetzung:** Plan 1 ist live (dort Livegang L5), sonst fehlen die Schnittstellen. Der Dialog muss trotzdem bedienbar bleiben, wenn `import-repos` scheitert.
## Global Constraints
**Werte und Texte, wörtlich**
- Organisation `WEBklar`, in `src/lib/projectAssign.js` als `GITEA_ORG` festgelegt.
- Portal-Schnittstellen, jeweils mit `Authorization: Bearer <Appwrite-JWT>` über das vorhandene `adminFetch`:
- `POST /api/admin/gitea/import-repos` → `{ ok, created }`
- `POST /api/admin/website-projects/:id/assign` mit `{ customerId, ticketId }` → `{ ok, project, moved, backfilled, warnings }`
- `POST /api/admin/website-projects/:id/pushes` → `{ ok, moved, backfilled, warnings }`
- `PATCH /api/admin/website-projects/:id` mit `{ projectType: 'preview', subdomain }` → `{ ok, previewUrl, … }`
- Texte:
- `Suche neue Repos …`
- `wird beim Zuweisen nach WEBklar verschoben`
- `Preview einschalten`, `Einschalten`, `Pushes nachtragen`
- `` `Preview läuft: ${previewUrl}` ``
- `` `Neue Repos konnten nicht geladen werden (${message}).` ``
- `` `${projectName}: Zuweisen fehlgeschlagen – ${error}` ``
- Die Zusammenfassung (`assignSummary`) besteht aus zwei Teilen, getrennt durch ` · `:
- „nach WEBklar verschoben“, wenn verschoben wurde,
- danach „keine neuen Pushes“, „1 Push nachgetragen“ oder `` `${n} Pushes nachgetragen` ``.
- Beispiel aus dem Spec: `Döner Nation: nach WEBklar verschoben · 1 Push nachgetragen`.
- **Subdomain-Vorschlag:**
1. Grundlage ist der Kundenname des Tickets, sonst der Projektname.
2. Kleinschreibung.
3. ä→ae, ö→oe, ü→ue, ß→ss.
4. Akzente entfernen (NFD).
5. Apostrophe `'` `’` `‘` `` ` `` entfernen.
6. `[^a-z0-9]+` → `-`, Bindestriche am Rand entfernen.
7. Höchstens 63 Zeichen, danach nochmals Bindestriche am Ende entfernen.
- **„Preview einschalten“** erscheint, wenn `!project.previewUrl || project.status !== 'deployed'`, und nur, wenn die Karte `showActions` hat (also im Dialog).
**Git und Deploy – wichtig**
- Das Deploy-Webhook des Ticketsystems setzt **jeden** gepushten Branch live. Den Feature-Branch deshalb niemals pushen. Live geht es nur mit OK des Nutzers per `git push origin feature/gitea-organisation:test`.
- `node_modules` ist im Repo **versioniert** (etwa 8900 Dateien), und im Worktree ist es ein Symlink. Deshalb nur `git add <Dateien>`, niemals `-A` oder `.`.
- Eine andere Sitzung arbeitet parallel in `dev-entschlackung`, gerade an `App.jsx`, `InvoicePanel.jsx`, `useInvoices.js`, `integrationsApi.js` und `FinancePage.jsx`. Diese Dateien fasst Plan 2 nicht an. Vor dem Push `git rebase origin/test`.
- Neue Texte werden in UTF-8 mit echten Umlauten geschrieben. Die berührten Dateien sind bisher reines ASCII und bleiben gültiges UTF-8.
## Review Focus
1. **Das Portal ist noch nicht aktualisiert.** `import-repos` antwortet dann mit 404 oder 500. Der Dialog zeigt nur den Hinweis, und die Listen laden trotzdem. Test: Task 4, Prüfung 7.
2. **Beim Zuweisen mehrerer Projekte scheitert eines.** Die anderen werden trotzdem zugewiesen, und die Zusammenfassung nennt den Fehler beim betroffenen Projekt. Test: Task 1, „nacheinander und weiter nach Fehler“.
3. **Das Ticket hat keinen Kundennamen.** Der Vorschlag nimmt dann den Projektnamen. Sind beide leer, bleibt das Feld leer, und „Einschalten“ ist gesperrt, bis etwas eingetragen ist. Tests: Task 1 (`suggestSubdomain('', '')`) und Task 4, Prüfung 4.
4. **Namen mit Zeichen wie „&“, „+“, „ß“ oder „é“.** Das ergibt eine gültige DNS-Subdomain ohne doppelte Bindestriche. Test: Task 1, „Sonderzeichen“.
5. **Die Subdomain ist schon vergeben (409 vom Portal).** Die Meldung des Portals erscheint, und die Eingabe bleibt stehen. Test: Task 4, Prüfung 5.
---
## Dateien
| Datei | Aufgabe |
|---|---|
| `src/lib/subdomain.js` (neu) | `suggestSubdomain` |
| `src/lib/projectAssign.js` (neu) | `GITEA_ORG`, `isOutsideOrg`, `needsPreviewButton`, `assignSummary`, `assignSequentially` |
| `src/lib/projectAdminApi.js` | `importNewRepos`, `assignWebsiteProject`, `backfillProjectPushes` |
| `src/hooks/useWebsiteProjects.js` | `assignProjects` über das Portal, nacheinander |
| `src/components/TicketProjectsModal.jsx` | Import beim Öffnen, Hinweis, Zusammenfassung, Daten für die Karte |
| `src/components/AssignedProjectCard.jsx` | Knöpfe „Preview einschalten“ und „Pushes nachtragen“ (nur mit `showActions`) |
| `src/pages/ProjectsPage.jsx` | „Kunde zuordnen“ zeigt die Zusammenfassung |
| `package.json` | Skript `"test": "node --test tests/unit/*.test.mjs"` (Muster statt Ordner, damit Node 20 und 24 gleich laufen) |
| `tests/unit/load.mjs`, `tests/unit/*.test.mjs` (neu) | Laden der Quellen über esbuild, Tests |
---
### Task 0: Worktree anlegen
- [ ] **Step 1: Create the worktree from the current `test`**
```bash
cd /home/webklar/apps/ticket.webklar.com/build && git fetch -q origin
git worktree add -b feature/gitea-organisation ../dev-gitea-org origin/test
cd ../dev-gitea-org && git branch --unset-upstream; rm -rf node_modules && ln -s ../build/node_modules node_modules
git log --oneline -1
```
Expected: HEAD = `origin/test`. `git status --short | grep -v node_modules` shows nothing.
---
### Task 1: Reine Logik mit Tests
**Files:**
- Create: `src/lib/subdomain.js`, `src/lib/projectAssign.js`, `tests/unit/load.mjs`, `tests/unit/subdomain.test.mjs`, `tests/unit/projectAssign.test.mjs`
- Modify: `package.json` (only `scripts.test`)
**Interfaces:**
- Produces:
- `suggestSubdomain(customerName?: string, projectName?: string): string`
- `GITEA_ORG = 'WEBklar'`
- `isOutsideOrg(repoFullName: string): boolean`. It is `false` for an empty name, and the comparison ignores case.
- `needsPreviewButton(project): boolean`
- `assignSummary(projectName: string, result: { moved, backfilled, warnings } | { error: string }): { text: string, warnings: string[] }`
- `assignSequentially(projectIds: string[], ctx, assignFn: (id, ctx) => Promise<object>): Promise<Array<{ projectId, ok: boolean, result?, error? }>>`
- `loadModule(relPathFromRepoRoot): Promise<module>` in `tests/unit/load.mjs`. It uses esbuild `build({ bundle: true, format: 'esm', write: false, platform: 'neutral' })` and `import()` from a `data:` URL.
- [ ] **Step 1: Write the failing tests**
```js
// tests/unit/subdomain.test.mjs
const { suggestSubdomain } = await loadModule('src/lib/subdomain.js')
test('Kundenname vor Projektname', () => assert.equal(suggestSubdomain('Döner Nation', 'D-ner-Nation'), 'doener-nation'))
test('Apostroph', () => assert.equal(suggestSubdomain("Emma's Schatzkiste", ''), 'emmas-schatzkiste'))
test('Sonderzeichen', () => assert.equal(suggestSubdomain('', 'Café Müller & Söhne + Straße'), 'cafe-mueller-soehne-strasse'))
test('leer', () => assert.equal(suggestSubdomain('', ''), ''))
test('63 Zeichen ohne Bindestrich am Ende', () => assert.equal(suggestSubdomain('a'.repeat(62) + ' b', ''), 'a'.repeat(62)))
// tests/unit/projectAssign.test.mjs
test('isOutsideOrg', () => {
assert.equal(isOutsideOrg('JUSN/D-ner-Nation'), true)
assert.equal(isOutsideOrg('webklar/x'), false)
assert.equal(isOutsideOrg(''), false)
})
test('needsPreviewButton', () => {
assert.equal(needsPreviewButton({ previewUrl: 'https://x', status: 'deployed' }), false)
assert.equal(needsPreviewButton({ previewUrl: '', status: 'synced' }), true)
assert.equal(needsPreviewButton({ previewUrl: 'https://x', status: 'pending' }), true)
})
test('assignSummary', () => {
assert.deepEqual(assignSummary('Döner Nation', { moved: { from: 'JUSN/D-ner-Nation', to: 'WEBklar/D-ner-Nation' }, backfilled: 1, warnings: [] }),
{ text: 'Döner Nation: nach WEBklar verschoben · 1 Push nachgetragen', warnings: [] })
assert.deepEqual(assignSummary('X', { moved: null, backfilled: 0, warnings: ['Internes Repo, bleibt wo es ist.'] }),
{ text: 'X: keine neuen Pushes', warnings: ['Internes Repo, bleibt wo es ist.'] })
assert.equal(assignSummary('X', { moved: null, backfilled: 3, warnings: [] }).text, 'X: 3 Pushes nachgetragen')
assert.deepEqual(assignSummary('X', { error: 'boom' }), { text: 'X: Zuweisen fehlgeschlagen – boom', warnings: [] })
})
test('nacheinander und weiter nach Fehler', async () => {
const log = []
const fn = async (id) => { log.push(id + ':start'); await sleep(10); log.push(id + ':end'); if (id === 'b') throw new Error('boom'); return { backfilled: 0 } }
const r = await assignSequentially(['a', 'b', 'c'], { ticketId: 't1' }, fn)
assert.deepEqual(log, ['a:start', 'a:end', 'b:start', 'b:end', 'c:start', 'c:end'])
assert.deepEqual(r.map((x) => [x.projectId, x.ok, x.error]), [['a', true, undefined], ['b', false, 'boom'], ['c', true, undefined]])
})
```
- [ ] **Step 2: Run to verify it fails**
Run: `npm test`
Expected: FAIL, because the modules are missing. The script is already added.
- [ ] **Step 3: Implement both modules**
Follow the rules in Global Constraints. `assignSequentially` runs the calls strictly one after another. It catches every error as `{ ok: false, error: err.message }` and continues with the next ID.
- [ ] **Step 4: Run to verify it passes**
Run: `npm test`
Expected: PASS
- [ ] **Step 5: Commit**
```bash
git add src/lib/subdomain.js src/lib/projectAssign.js tests/unit/load.mjs tests/unit/subdomain.test.mjs tests/unit/projectAssign.test.mjs package.json
git commit -m "Subdomain-Vorschlag und Zuweisungs-Logik mit Tests"
```
---
### Task 2: Portal-Aufrufe und Hook
**Files:**
- Modify: `src/lib/projectAdminApi.js`, `src/hooks/useWebsiteProjects.js`
**Interfaces:**
- Consumes: `assignSequentially` (Task 1)
- Produces:
- `importNewRepos(): Promise<{ ok, created }>`
- `assignWebsiteProject(id, { customerId, ticketId }): Promise<{ ok, project, moved, backfilled, warnings }>`
- `backfillProjectPushes(id): Promise<{ ok, moved, backfilled, warnings }>`
- `useWebsiteProjects().assignProjects(projectIds, { customerId, ticketId })` returns `{ success: boolean, error?: string, results }`.
- `results` comes from `assignSequentially` with `assignWebsiteProject`.
- `success` is true only if every result is `ok`.
- `error` is the first error text.
- `unassignProject` stays as it is, directly in Appwrite.
- [ ] **Step 1: Implement**
Use the existing `adminFetch` pattern, with method POST and body `JSON.stringify(...)`. `assignProject`, the single direct Appwrite update, is removed from the hook. It has no other callers: check with `git grep -n "assignProject\b" src`.
- [ ] **Step 2: Build check**
Run:
```bash
node -e "require('esbuild').build({entryPoints:['src/hooks/useWebsiteProjects.js'],bundle:true,write:false,format:'esm',platform:'browser',external:['react','appwrite'],define:{'import.meta.env.VITE_PROJECT_ADMIN_URL':'\"https://project.webklar.com\"'},loader:{'.js':'jsx','.css':'empty','.ttf':'empty','.woff2':'empty','.png':'empty','.svg':'empty'}}).then(()=>console.log('ok'))"
```
Expected: `ok`. `npm test` stays green.
- [ ] **Step 3: Commit**
```bash
git add src/lib/projectAdminApi.js src/hooks/useWebsiteProjects.js
git commit -m "Zuweisen über das Portal, Import neuer Repos, Pushes nachtragen"
```
---
### Task 3: Dialog, Projektkarte, Projekte-Seite
**Files:**
- Modify: `src/components/TicketProjectsModal.jsx`, `src/components/AssignedProjectCard.jsx`, `src/pages/ProjectsPage.jsx`
**Interfaces:**
- Consumes: Tasks 1 and 2; `updateWebsiteProject` (already in `projectAdminApi.js`)
- Produces:
- `AssignedProjectCard` gets the new optional props `customerName` and `onChanged()`. Without `showActions` it looks exactly as before.
- [ ] **Step 1: Implement `TicketProjectsModal`**
1. **When the dialog opens:**
- set `importing = true` and show `Suche neue Repos …`,
- call `await importNewRepos()`; on an error, set `importHint` to `` `Neue Repos konnten nicht geladen werden (${err.message}).` `` and carry on,
- then call `loadProjects()`.
2. **List entries:** For `isOutsideOrg(p.repoFullName)`, add a grey note to the entry: `wird beim Zuweisen nach WEBklar verschoben`.
3. **`handleAssign`:**
- call `assignProjects(selectedIds, { customerId: ticket.customerId || '', ticketId: ticket.$id })`,
- build a list of `assignSummary(projectName, result.ok ? result.result : { error: result.error })` from `results` and show it under the button: one line per project, warnings indented,
- clear the selection, then `loadProjects()` and `onUpdated?.()`.
4. **Cards:** Pass `customerName={ticket.customerName || ''}` and `onChanged={() => { loadProjects(); onUpdated?.() }}` to `AssignedProjectCard`.
- [ ] **Step 2: Implement `AssignedProjectCard`**
These controls appear only with `showActions`:
- **„Preview einschalten“** appears if `needsPreviewButton(project)`.
- A click shows an input prefilled with `suggestSubdomain(customerName, project.projectName)` and the button `Einschalten`. It is disabled while the input is empty or a request is running.
- `Einschalten` calls `updateWebsiteProject(project.$id, { projectType: 'preview', subdomain })`.
- On success: the message `` `Preview läuft: ${r.previewUrl}` `` and `onChanged()`.
- On an error: `err.message`, and the input stays.
- **„Pushes nachtragen“** calls `backfillProjectPushes(project.$id)` and shows `assignSummary(project.projectName, r)`, then `onChanged()`.
- Messages appear inside the card, below the status line. Buttons use `className="btn btn-sm"`, like the existing trash button.
- [ ] **Step 3: Implement `ProjectsPage`**
`handleAssignCustomer`:
- With a customer: the result of `assignProjects([project.$id], …)` shows as `setNotice(assignSummary(project.projectName, r.results[0].ok ? r.results[0].result : { error: r.results[0].error }).text)`. Warnings are appended with ` – `.
- Without a customer: unchanged.
- [ ] **Step 4: Build check**
Run the same esbuild call as in Task 2 Step 2, once each for `src/components/TicketProjectsModal.jsx` and `src/pages/ProjectsPage.jsx`, with `external:['react','react-dom','react-icons/*','appwrite','react-router-dom','react-markdown','@tabler/icons-react']`.
Expected: `ok` both times. `npm test` stays green.
- [ ] **Step 5: Commit**
```bash
git add src/components/TicketProjectsModal.jsx src/components/AssignedProjectCard.jsx src/pages/ProjectsPage.jsx
git commit -m "Dialog: neue Repos, Zuweisen mit Umzug, Preview einschalten, Pushes nachtragen"
```
---
### Task 4: Oberflächen-Prüfung (Scratchpad, nicht committen)
**Files:** Scratchpad `…/scratchpad/woms-ui/` (build script, `ui.mjs`)
**Setup:**
- **vite-Build** like the deploy:
```bash
git archive HEAD | tar -x -C <tmp>
docker run --rm -v <tmp>:/w -w /w node:20-alpine sh -c "rm -rf node_modules && npm install && npm run build"
```
- **Browser:** `playwright-core` from `/root/.npm/_npx/db89d7302a373f10/node_modules/playwright-core` (load it via `createRequire`) and Chromium at `~/.cache/ms-playwright/chromium-1234/chrome-linux64/chrome`. Run it under `systemd-run --scope -p MemoryMax=1200M -p MemorySwapMax=0`, and check `free -m` beforehand.
- **Page:** served from `https://ticket.test` via `page.route`.
- **Appwrite mock:**
- a logged-in admin session and `POST /v1/account/jwts` → `{ jwt: 'jwt-admin' }`,
- `listDocuments` for workorders: ticket 100115, customer name „Döner Nation“,
- `websiteProjects`:
- `JUSN/Ben-Breidt` and `WEBklar/sauber`, both available,
- `WEBklar/D-ner-Nation`, assigned to ticket 100115, without `previewUrl`, status `synced`,
- plus employees and customers.
- **Portal mock** under `https://project.webklar.com/api/admin/**`. It records every request with its start and end time.
- If the Entschlackung harness still exists (`appsmoke.mjs` in scratchpad `c07881f0…/wstimes`), reuse its Appwrite mock.
**Prüfungen** (all must pass):
1. Opening „Projekte zuweisen / bearbeiten“ on ticket 100115 sends exactly one `POST /api/admin/gitea/import-repos`. While the answer is pending (delayed by 500 ms), the dialog shows `Suche neue Repos …`.
2. `JUSN/Ben-Breidt` carries `wird beim Zuweisen nach WEBklar verschoben`. `WEBklar/sauber` does not.
3. Selecting both and clicking „Zuweisen“ sends two `…/assign` requests. The second starts only after the first has answered. Two summary lines appear. The first is `Ben Breidt: nach WEBklar verschoben · 1 Push nachgetragen`, with the mock answer to match.
4. The card of `D-ner-Nation` has „Preview einschalten“.
- The input shows `doener-nation`.
- Changed to `doener-test` and sent with „Einschalten“, it produces `PATCH …/website-projects/<id>` with the body `{"projectType":"preview","subdomain":"doener-test"}`.
- The card then shows `Preview läuft: https://doener-test.project.webklar.com`.
- Once the field is cleared, „Einschalten“ is disabled.
5. When the PATCH answers `409 { error: 'Subdomain "doener-nation" ist bereits vergeben' }`, exactly that text appears and the input stays.
6. „Pushes nachtragen“ sends `POST …/pushes` and shows `Döner Nation: keine neuen Pushes`.
7. When `import-repos` answers 404, the dialog shows `Neue Repos konnten nicht geladen werden (…)`, and both lists still appear.
8. On the Projekte page, „Kunde zuordnen“ sends `…/assign` with `{"customerId":"<id>","ticketId":""}`, and the notice shows the summary.
9. The ticket's Details tab (`TicketAssignedProjectsPanel`) and the customer page show the card without the new buttons.
Expected: the script prints `9/9 ok`.
---
## Livegang – nur mit OK des Nutzers, erst nach Plan 1 L5
1. Update and test:
```bash
git -C /home/webklar/apps/ticket.webklar.com/dev-gitea-org fetch origin && git -C … rebase origin/test
```
Then run `npm test`, and the Task 4 check once more against the rebased state.
2. Deploy-like check: `git archive` + `docker run node:20-alpine` (`rm -rf node_modules; npm install; npm run build`) finishes without errors.
3. Push: `git -C /home/webklar/apps/ticket.webklar.com/dev-gitea-org push origin feature/gitea-organisation:test`.
4. Check:
- the `out/` bundle and the live https://ticket.webklar.com contain the string `Suche neue Repos`,
- the deploy log shows `npm run build` without errors,
- opening the dialog in the live ticket 100115 lists `JUSN/Ben-Breidt` and similar repos. Doing this is only reading.
5. Update the memory note `woms-ticketsystem`.
**Rückweg:** `git revert` the Plan 2 commits on a branch, then push to `test` after the OK.