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>
20 KiB
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 mitnode --testgeprüft, geladen über esbuild. Das Ticketsystem hat bisher keinen Testlauf.
- Portal-Aufrufe kommen nach
src/lib/projectAdminApi.js. - Oberfläche: Der Hook
useWebsiteProjectsund 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
appwrite13 (Web-SDK)- Node 24 auf dem Host für
node --test - esbuild aus
node_modules, genutzt über die JS-Schnittstelle, nicht überbin/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, insrc/lib/projectAssign.jsalsGITEA_ORGfestgelegt. - Portal-Schnittstellen, jeweils mit
Authorization: Bearer <Appwrite-JWT>über das vorhandeneadminFetch:POST /api/admin/gitea/import-repos→{ ok, created }POST /api/admin/website-projects/:id/assignmit{ customerId, ticketId }→{ ok, project, moved, backfilled, warnings }POST /api/admin/website-projects/:id/pushes→{ ok, moved, backfilled, warnings }PATCH /api/admin/website-projects/:idmit{ projectType: 'preview', subdomain }→{ ok, previewUrl, … }
- Texte:
Suche neue Repos …wird beim Zuweisen nach WEBklar verschobenPreview 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:
- Grundlage ist der Kundenname des Tickets, sonst der Projektname.
- Kleinschreibung.
- ä→ae, ö→oe, ü→ue, ß→ss.
- Akzente entfernen (NFD).
- Apostrophe
'’‘`entfernen. [^a-z0-9]+→-, Bindestriche am Rand entfernen.- Höchstens 63 Zeichen, danach nochmals Bindestriche am Ende entfernen.
- „Preview einschalten“ erscheint, wenn
!project.previewUrl || project.status !== 'deployed', und nur, wenn die KarteshowActionshat (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_modulesist im Repo versioniert (etwa 8900 Dateien), und im Worktree ist es ein Symlink. Deshalb nurgit add <Dateien>, niemals-Aoder..- Eine andere Sitzung arbeitet parallel in
dev-entschlackung, gerade anApp.jsx,InvoicePanel.jsx,useInvoices.js,integrationsApi.jsundFinancePage.jsx. Diese Dateien fasst Plan 2 nicht an. Vor dem Pushgit 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
- Das Portal ist noch nicht aktualisiert.
import-reposantwortet dann mit 404 oder 500. Der Dialog zeigt nur den Hinweis, und die Listen laden trotzdem. Test: Task 4, Prüfung 7. - 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“.
- 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. - Namen mit Zeichen wie „&“, „+“, „ß“ oder „é“. Das ergibt eine gültige DNS-Subdomain ohne doppelte Bindestriche. Test: Task 1, „Sonderzeichen“.
- 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
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(onlyscripts.test)
Interfaces:
-
Produces:
suggestSubdomain(customerName?: string, projectName?: string): stringGITEA_ORG = 'WEBklar'isOutsideOrg(repoFullName: string): boolean. It isfalsefor an empty name, and the comparison ignores case.needsPreviewButton(project): booleanassignSummary(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>intests/unit/load.mjs. It uses esbuildbuild({ bundle: true, format: 'esm', write: false, platform: 'neutral' })andimport()from adata:URL.
-
Step 1: Write the failing tests
// 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
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 }.resultscomes fromassignSequentiallywithassignWebsiteProject.successis true only if every result isok.erroris the first error text.
unassignProjectstays 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:
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
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 inprojectAdminApi.js) -
Produces:
AssignedProjectCardgets the new optional propscustomerNameandonChanged(). WithoutshowActionsit looks exactly as before.
-
Step 1: Implement
TicketProjectsModal
- When the dialog opens:
- set
importing = trueand showSuche neue Repos …, - call
await importNewRepos(); on an error, setimportHintto`Neue Repos konnten nicht geladen werden (${err.message}).`and carry on, - then call
loadProjects().
- set
- List entries: For
isOutsideOrg(p.repoFullName), add a grey note to the entry:wird beim Zuweisen nach WEBklar verschoben. handleAssign:- call
assignProjects(selectedIds, { customerId: ticket.customerId || '', ticketId: ticket.$id }), - build a list of
assignSummary(projectName, result.ok ? result.result : { error: result.error })fromresultsand show it under the button: one line per project, warnings indented, - clear the selection, then
loadProjects()andonUpdated?.().
- call
- Cards: Pass
customerName={ticket.customerName || ''}andonChanged={() => { loadProjects(); onUpdated?.() }}toAssignedProjectCard.
- 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 buttonEinschalten. It is disabled while the input is empty or a request is running. EinschaltencallsupdateWebsiteProject(project.$id, { projectType: 'preview', subdomain }).- On success: the message
`Preview läuft: ${r.previewUrl}`andonChanged(). - On an error:
err.message, and the input stays.
- A click shows an input prefilled with
-
„Pushes nachtragen“ calls
backfillProjectPushes(project.$id)and showsassignSummary(project.projectName, r), thenonChanged(). -
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 assetNotice(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
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:
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-corefrom/root/.npm/_npx/db89d7302a373f10/node_modules/playwright-core(load it viacreateRequire) and Chromium at~/.cache/ms-playwright/chromium-1234/chrome-linux64/chrome. Run it undersystemd-run --scope -p MemoryMax=1200M -p MemorySwapMax=0, and checkfree -mbeforehand. -
Page: served from
https://ticket.testviapage.route. -
Appwrite mock:
- a logged-in admin session and
POST /v1/account/jwts→{ jwt: 'jwt-admin' }, listDocumentsfor workorders: ticket 100115, customer name „Döner Nation“,websiteProjects:JUSN/Ben-BreidtandWEBklar/sauber, both available,WEBklar/D-ner-Nation, assigned to ticket 100115, withoutpreviewUrl, statussynced,
- plus employees and customers.
- a logged-in admin session and
-
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.mjsin scratchpadc07881f0…/wstimes), reuse its Appwrite mock.
Prüfungen (all must pass):
- 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 showsSuche neue Repos …. JUSN/Ben-Breidtcarrieswird beim Zuweisen nach WEBklar verschoben.WEBklar/sauberdoes not.- Selecting both and clicking „Zuweisen“ sends two
…/assignrequests. The second starts only after the first has answered. Two summary lines appear. The first isBen Breidt: nach WEBklar verschoben · 1 Push nachgetragen, with the mock answer to match. - The card of
D-ner-Nationhas „Preview einschalten“.- The input shows
doener-nation. - Changed to
doener-testand sent with „Einschalten“, it producesPATCH …/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.
- The input shows
- When the PATCH answers
409 { error: 'Subdomain "doener-nation" ist bereits vergeben' }, exactly that text appears and the input stays. - „Pushes nachtragen“ sends
POST …/pushesand showsDöner Nation: keine neuen Pushes. - When
import-reposanswers 404, the dialog showsNeue Repos konnten nicht geladen werden (…), and both lists still appear. - On the Projekte page, „Kunde zuordnen“ sends
…/assignwith{"customerId":"<id>","ticketId":""}, and the notice shows the summary. - 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
-
Update and test:
git -C /home/webklar/apps/ticket.webklar.com/dev-gitea-org fetch origin && git -C … rebase origin/testThen run
npm test, and the Task 4 check once more against the rebased state. -
Deploy-like check:
git archive+docker run node:20-alpine(rm -rf node_modules; npm install; npm run build) finishes without errors. -
Push:
git -C /home/webklar/apps/ticket.webklar.com/dev-gitea-org push origin feature/gitea-organisation:test. -
Check:
- the
out/bundle and the live https://ticket.webklar.com contain the stringSuche neue Repos, - the deploy log shows
npm run buildwithout errors, - opening the dialog in the live ticket 100115 lists
JUSN/Ben-Breidtand similar repos. Doing this is only reading.
- the
-
Update the memory note
woms-ticketsystem.
Rückweg: git revert the Plan 2 commits on a branch, then push to test after the OK.