# 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/`. - **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//`), 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 `, 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/`. 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** ```bash 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//repos'`. - `needs_repo_webhook(owner: str, new_owner: str, legacy_owner: str) -> bool` - [ ] **Step 1: Write the failing tests** ```python 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** ```bash 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//`. 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** ```bash 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** ```bash 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** ```bash 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: ```bash 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** ```bash 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 ..HEAD` against Spec 8 and this plan (`superpowers:requesting-code-review`). --- ## Livegang – nur mit OK des Nutzers, erst nach Plan 1 L5 **L0 Sicherung** ```bash 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: ```bash 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:** ```bash 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', '

Probe

', 'Probe')`. 2. Clone the template once with `W.get_template_cache()`. This checks that the system account can read `knso/`. 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:** ```bash 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.