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

20 KiB
Raw Blame History

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
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

// 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 }.
      • 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:

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 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
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-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:

    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.