Files
Webklar-Kundenbereich/docs/superpowers/plans/2026-09-27-gitea-organisation-3-lead-automatik.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

16 KiB
Raw Blame History

Plan 3 – Lead-Automatik: neue Lead-Repos in WEBklar, Zugang über das System-Konto

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: Die Lead-Automatik legt neue Lead-Previews in der Organisation WEBklar an und arbeitet mit dem Schlüssel lead-automatik des Kontos webklar-system. Bestehende Lead-Repos bei knso werden weiter unter ihrem bisherigen Pfad bedient.

Aufbau:

  • Neues Modul repo_paths.py: reine Funktionen, die entscheiden:

    • wem ein Lead-Repo gehört,
    • wie Pfad, Web-Adresse und Git-Adresse lauten,
    • über welche Gitea-Schnittstelle ein Repo angelegt wird,
    • ob es einen Einzel-Webhook braucht.

    Es wird mit pytest geprüft.

  • Bestehende Dateien: worker.py, processor.py, enrich.py, backfill_leads.py und migrate_previews.py nutzen diese Funktionen statt GITEA_USER/<repo_name>.

  • Unveränderter Start: Ohne neue Einstellungen verhält sich alles wie heute. Der Code geht deshalb zuerst live, die Umstellung folgt danach über config.env.

Technik:

  • Python 3 (System), pytest 6.2.5
  • systemd-Dienste webklar-automation-hook und webklar-automation-processor
  • Cron-Lauf worker.py um 06:30 Uhr (Europe/Berlin)

Spec: docs/superpowers/specs/2026-09-27-gitea-organisation-design.md im Kundenportal-Repo. Maßgeblich sind die Abschnitte 8, 9, 11 und 12.

Voraussetzung: Plan 1 ist live (dort L5). Erst dann erzeugen Pushes des System-Kontos keine Arbeitsblätter mehr. Mit dem alten Portal-Code bekämen Lead-Tickets sonst Arbeitsblätter auf „webklar-system“.

Global Constraints

Werte, die wörtlich gelten

  • Neue Schlüssel in config.env:

    • GITEA_AUTH_USER=webklar-system. Das ist der Benutzer in Git-Adressen.
    • GITEA_NEW_REPO_OWNER=WEBklar
  • Unveränderte Schlüssel: GITEA_USER=knso bleibt, als Besitzer der bestehenden Lead-Repos. TEMPLATE_OWNER und TEMPLATE_REPO bleiben ebenfalls.

  • GITEA_TOKEN wird der Schlüssel lead-automatik des Kontos webklar-system, mit den Rechten write:repository,write:organization,read:user.

  • Voreinstellungen:

    • Fehlt GITEA_AUTH_USER, gilt GITEA_USER.
    • Fehlt GITEA_NEW_REPO_OWNER, gilt GITEA_USER.
    • Ohne die neuen Schlüssel ist das Verhalten also genau wie heute.
  • Besitzer eines Lead-Repos (lead_repo_owner), in dieser Reihenfolge:

    1. lead["repo_owner"], falls gesetzt,
    2. sonst der Besitzer aus lead["repo_url"] (https://host/<besitzer>/<repo>),
    3. sonst GITEA_NEW_REPO_OWNER.

    Das präzisiert Spec 8 („bestehende Leads gelten als knso“): Alle bestehenden Leads mit Repo tragen repo_url auf knso, und nur Leads ohne Repo kommen neu nach WEBklar.

  • Einzel-Webhook: Er wird nur dann nicht angelegt, wenn der Besitzer GITEA_NEW_REPO_OWNER ist und dieser von GITEA_USER abweicht. Das ist der Fall der Organisation, in dem ihr eigener Webhook greift.

Sicherheit und Betrieb

  • Schlüssel stehen nie in Logs, Tests oder Ausgaben.
  • config.env wird nie committet.
  • Der Verarbeitungsdienst arbeitet alle 5 Minuten. Umgestellt wird darum nur, während er gestoppt ist (siehe Livegang).
  • Nach jeder Code-Änderung werden beide Dienste neu gestartet, weil sie die Module beim Start laden.

Git

  • /opt/webklar-automation ist bisher kein Git-Repo. Task 1 legt ein lokales Repo ohne Remote an, nur für Nachvollziehbarkeit und Review. .gitignore schützt Geheimnisse und Daten.
  • Immer git add <Dateien>, nie -A.

Review Focus

  1. Alter Lead mit repo_url auf knso, ohne repo_owner. Weitere Pushes, etwa aus der Anreicherung, gehen weiter nach knso/<repo>. Test: Task 2, „alter Lead bleibt bei knso“.
  2. Ein neuer Lead wird erneut verarbeitet, und sein Repo existiert schon in WEBklar (409). Das Repo wird weiterverwendet, ohne Einzel-Webhook. Tests: Task 2 (needs_repo_webhook) und Task 3 (Prüfschritt).
  3. Git-Adresse. Benutzer ist GITEA_AUTH_USER, der Pfad kommt aber vom Besitzer des Leads, nicht vom Benutzer. Test: Task 2, git_url.
  4. Ohne die neuen Einstellungen bleiben Pfade, Anlage über /user/repos und Einzel-Webhook genau wie heute. Test: Task 2, „ohne neue Einstellungen wie heute“.
  5. Die Vorlage liegt bei knso (TEMPLATE_OWNER) und wird mit dem System-Konto geklont. Geprüft wird das im Livegang, Probelauf L3.

Dateien

Datei Aufgabe
.gitignore (neu) schließt config.env*, Daten, Logs und Sicherungen aus
repo_paths.py (neu) lead_repo_owner, repo_full, web_url, git_url, repo_create_path, needs_repo_webhook
tests/test_repo_paths.py (neu) Tests
worker.py Einstellungen, ensure_repo(repo_name, firma, owner), Git- und Web-Adressen, Lookups, _mark_processed(lead_id, repo_url, repo_owner)
processor.py, enrich.py Adressen mit Besitzer
backfill_leads.py, migrate_previews.py Pfade über W.lead_repo_full(lead)

Task 1: Lokales Git-Repo und Ausgangsstand

  • Step 1: Write .gitignore
config.env*
*.bak*
logs/
queue/
status/
tmp/
backups/
master*.json
master*.xlsx
standorte.*
__pycache__/
.pytest_cache/
  • Step 2: Create the repo and commit the baseline
cd /opt/webklar-automation && git init -q
git add .gitignore *.py DOKUMENTATION.md ROUTINE_ANREICHERUNG_PROMPT.txt fallback-assets
git -c user.name=Claude -c user.email=claude@webklar.local commit -qm "Ausgangsstand Lead-Automatik"
git ls-files | grep -c "config.env\|master\|\.bak"

Expected: 0. git status --short shows no .py file as untracked.


Task 2: Pfad-Regeln als reine Funktionen

Files:

  • Create: repo_paths.py, tests/test_repo_paths.py

Interfaces:

  • Produces:

    • lead_repo_owner(lead: dict, legacy_owner: str, new_owner: str) -> str
    • repo_full(owner: str, repo_name: str) -> str
    • web_url(base_url: str, owner: str, repo_name: str) -> str
    • git_url(base_url: str, auth_user: str, token: str, owner: str, repo_name: str) -> str
    • repo_create_path(owner: str, auth_user: str, legacy_owner: str) -> str | None. The cases, checked in this order and ignoring case:
      1. owner == auth_user → '/user/repos',
      2. owner == legacy_owner → None, meaning the repo must already exist,
      3. otherwise → '/orgs/<owner>/repos'.
    • needs_repo_webhook(owner: str, new_owner: str, legacy_owner: str) -> bool
  • Step 1: Write the failing tests

B = "https://git.webklar.com"
def test_alter_lead_bleibt_bei_knso():
    lead = {"repo_name": "preview-x", "repo_url": B + "/knso/preview-x"}
    assert lead_repo_owner(lead, "knso", "WEBklar") == "knso"
def test_repo_owner_hat_vorrang():
    assert lead_repo_owner({"repo_owner": "WEBklar", "repo_url": B + "/knso/x"}, "knso", "WEBklar") == "WEBklar"
def test_neuer_lead_kommt_nach_webklar():
    assert lead_repo_owner({"repo_name": "preview-neu", "repo_url": ""}, "knso", "WEBklar") == "WEBklar"
def test_ohne_neue_einstellungen_wie_heute():
    assert lead_repo_owner({"repo_name": "preview-neu"}, "knso", "knso") == "knso"
    assert repo_create_path("knso", "knso", "knso") == "/user/repos"
    assert needs_repo_webhook("knso", "knso", "knso") is True
def test_adressen():
    assert repo_full("WEBklar", "preview-x") == "WEBklar/preview-x"
    assert web_url(B, "WEBklar", "preview-x") == B + "/WEBklar/preview-x"
    assert git_url(B, "webklar-system", "TOKEN", "knso", "preview-x") == "https://webklar-system:TOKEN@git.webklar.com/knso/preview-x.git"
def test_anlegen():
    assert repo_create_path("WEBklar", "webklar-system", "knso") == "/orgs/WEBklar/repos"
    assert repo_create_path("knso", "webklar-system", "knso") is None
    assert repo_create_path("Webklar-System", "webklar-system", "knso") == "/user/repos"
def test_einzel_webhook():
    assert needs_repo_webhook("WEBklar", "WEBklar", "knso") is False
    assert needs_repo_webhook("webklar", "WEBklar", "knso") is False
    assert needs_repo_webhook("knso", "WEBklar", "knso") is True
  • Step 2: Run to verify it fails

Run: cd /opt/webklar-automation && python3 -m pytest -q tests Expected: FAIL (ModuleNotFoundError: repo_paths)

  • Step 3: Implement repo_paths.py

  • It uses only the standard library and loads no configuration.

  • git_url builds the host from base_url without the scheme.

  • The owner is extracted from repo_url with ^https?://[^/]+/([^/]+)/[^/]+/?$.

  • Step 4: Run to verify it passes

Run: python3 -m pytest -q tests Expected: 7 passed

  • Step 5: Commit
git add repo_paths.py tests/test_repo_paths.py && git -c user.name=Claude -c user.email=claude@webklar.local commit -qm "Pfad-Regeln für Lead-Repos mit Tests"

Task 3: worker.py umstellen

Files:

  • Modify: worker.py

Interfaces:

  • Consumes: repo_paths (Task 2)

  • Produces, for the other files:

    • W.GITEA_AUTH_USER
    • W.NEW_REPO_OWNER
    • W.lead_repo_owner(lead) -> str
    • W.lead_repo_full(lead) -> str
    • W.lead_git_url(owner, repo_name) -> str
    • W.lead_web_url(owner, repo_name) -> str
    • W.ensure_repo(repo_name, firma, owner)
    • W._mark_processed(lead_id, repo_url, repo_owner="")
  • Step 1: Implement

  1. Settings: GITEA_AUTH_USER = cfg.get("GITEA_AUTH_USER") or GITEA_USER and NEW_REPO_OWNER = cfg.get("GITEA_NEW_REPO_OWNER") or GITEA_USER. Add the four lead_* wrappers, which fill the repo_paths functions with these values.
  2. ensure_repo(repo_name, firma, owner):
    • path = repo_create_path(owner, GITEA_AUTH_USER, GITEA_USER).
    • If path is None, check GET /repos/<owner>/<repo_name>. On 200, reuse the repo; otherwise raise RuntimeError("Repo %s/%s fehlt und kann dort nicht angelegt werden" % (owner, repo_name)).
    • Otherwise POST to path. As today, 409 means reuse.
    • Only call ensure_repo_webhook(repo_full(owner, repo_name)) if needs_repo_webhook(owner, NEW_REPO_OWNER, GITEA_USER).
  3. Git and web addresses:
    • The push URL in the fill step (today line 531) becomes lead_git_url(owner, repo_name), so that function gets owner as a parameter.
    • get_template_cache uses GITEA_AUTH_USER as the user; owner and repo stay TEMPLATE_OWNER/TEMPLATE_REPO.
    • Every "%s/%s/%s" % (GITEA_URL, GITEA_USER, repo_name) becomes lead_web_url(owner, repo_name).
  4. Lookups: In the three functions at the former lines 331, 373 and 415, use lead_repo_full(lead).
  5. process_lead:
    • owner = lead_repo_owner(lead), set lead["repo_owner"] = owner, then ensure_repo(repo_name, firma, owner).
    • _mark_processed(lead_id, repo_url, owner) also stores repo_owner in master.json.
    • The caller around the former line 621 passes the owner along.
  • Step 2: Check
python3 -m py_compile worker.py && python3 -m pytest -q tests
grep -n "GITEA_USER, repo_name\|GITEA_USER, (lead" worker.py

Expected: compiles, 7 passed, and grep finds nothing.

  • Step 3: Commit
git add worker.py && git -c user.name=Claude -c user.email=claude@webklar.local commit -qm "worker: Repo-Besitzer pro Lead, System-Konto für Git-Adressen"

Task 4: processor.py, enrich.py und Wartungsskripte umstellen

Files:

  • Modify: processor.py, enrich.py, backfill_leads.py, migrate_previews.py

Interfaces:

  • Consumes: the W.* functions from Task 3

  • Step 1: Implement

  • processor.py:

    • _repo_url(owner, repo_name) returns worker.lead_git_url(owner, repo_name).
    • _clone_repo(owner, repo_name).
    • In process_lead: owner = worker.lead_repo_owner(lead), then worker.ensure_repo(repo_name, firma, owner), worker._mark_processed(lead_id, repo_url, owner), and repo_url = worker.lead_web_url(owner, repo_name).
  • enrich.py:

    • _repo_push_url(owner, repo_name) and _clone_repo(owner, repo_name).
    • _apply_to_repo(owner, repo_name, p, ctx=None). The caller determines owner = worker.lead_repo_owner(lead) inside the master_lock block, together with repo_name.
    • In get_file, the API path uses worker.lead_repo_owner(lead) instead of worker.GITEA_USER.
    • repo_url (the former line 1049) becomes worker.lead_web_url(owner, repo_name).
  • backfill_leads.py and migrate_previews.py: W.lead_repo_full(lead) instead of "%s/%s" % (W.GITEA_USER, repo_name).

  • Step 2: Check

python3 -m py_compile processor.py enrich.py backfill_leads.py migrate_previews.py hook_server.py && python3 -m pytest -q tests
grep -n "GITEA_USER" processor.py enrich.py backfill_leads.py migrate_previews.py

Expected: compiles, 7 passed. GITEA_USER appears in these four files nowhere, or only in comments.

  • Step 3: Restart with unchanged config.env (the code alone changes nothing)

With the user's OK:

systemctl restart webklar-automation-hook webklar-automation-processor
systemctl is-active webklar-automation-hook webklar-automation-processor

Expected: active twice, and the log files in logs/ show no traceback in the next 10 minutes.

  • Step 4: Commit
git add processor.py enrich.py backfill_leads.py migrate_previews.py && git -c user.name=Claude -c user.email=claude@webklar.local commit -qm "processor/enrich: Adressen mit Repo-Besitzer des Leads"
  • Step 5: Review

Run a fresh code review of git diff <Ausgangsstand>..HEAD against Spec 8 and this plan (superpowers:requesting-code-review).


Livegang – nur mit OK des Nutzers, erst nach Plan 1 L5

L0 Sicherung

cp /opt/webklar-automation/config.env /opt/webklar-automation/config.env.bak-gitea-org

L1 Schlüssel und Einstellungen. The processor is stopped, so no lead is in the middle of processing:

systemctl stop webklar-automation-processor
docker exec -u git webklar-gitea gitea admin user generate-access-token --username webklar-system --token-name lead-automatik --scopes write:repository,write:organization,read:user --raw \
 | python3 -c 'import re,sys; t=sys.stdin.read().strip(); p="/opt/webklar-automation/config.env"; s=open(p).read(); s=re.sub(r"^GITEA_TOKEN=.*$", "GITEA_TOKEN="+t, s, flags=re.M); open(p,"w").write(s.rstrip("\n")+"\nGITEA_AUTH_USER=webklar-system\nGITEA_NEW_REPO_OWNER=WEBklar\n")'
grep -c "^GITEA_AUTH_USER=webklar-system$\|^GITEA_NEW_REPO_OWNER=WEBklar$" /opt/webklar-automation/config.env
systemctl start webklar-automation-processor && systemctl restart webklar-automation-hook

Expected: 2, and both services active. The token only passes through the pipe.

L2 Zugang prüfen:

cd /opt/webklar-automation && python3 -c "import worker as W; c,u=W.gitea_api('GET','/user'); print(c,u.get('login'),u.get('is_admin'))"

Expected: 200 webklar-system True.

L3 Probelauf mit Wegwerf-Repo. Only with explicit OK:

  1. With the worker functions, create WEBklar/preview-probe-gitea-org (W.ensure_repo('preview-probe-gitea-org', 'Probe', W.NEW_REPO_OWNER)), then W.set_repo_file('WEBklar/preview-probe-gitea-org', 'index.html', '<h1>Probe</h1>', 'Probe').
  2. Clone the template once with W.get_template_cache(). This checks that the system account can read knso/<TEMPLATE_REPO>.
  3. Expected:
    • the repo exists in WEBklar,
    • GET /repos/WEBklar/preview-probe-gitea-org/hooks returns [],
    • within a minute the portal has a websiteProjects entry WEBklar/preview-probe-gitea-org with status synced, created by the organisation webhook,
    • no new arbeitsblatt.
  4. Clean up:
    • DELETE /repos/WEBklar/preview-probe-gitea-org,
    • delete the websiteProjects entry with a script in the portal container,
    • check that both are gone.

L4 Beobachten. Check the next real lead: its repo is created in WEBklar, the preview deploys, and there is no per-repo webhook. Update the memory note.

Rückweg:

cp /opt/webklar-automation/config.env.bak-gitea-org /opt/webklar-automation/config.env
systemctl restart webklar-automation-hook webklar-automation-processor

The code can stay: without the new keys it behaves as it does today. Remove the lead-automatik token in Gitea under Settings → Applications of the account, as an admin.