Files
Onyscribe-Admin/docs/RUNBOOK_PARALLELTEST.md

291 lines
19 KiB
Markdown
Raw Permalink 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.
# Runbook: Admin-Plattform parallel zum Altbetrieb testen (ohne Merge, nur mit Demo-Tenant)
Ziel: Die neue Admin-Plattform und den neuen Core **parallel** zu den laufenden Tenants betreiben – mit **einem** neuen Test-Tenant
`demo-admin`. Nichts Bestehendes wird angefasst, es wird **nichts gemergt**, und jeder Schritt lässt sich einzeln zurücknehmen.
---
## 0. So läuft das Ganze parallel (und warum der Altbetrieb sicher ist)
```
ALTBETRIEB (unverändert) NEUBETRIEB (Test)
──────────────────────── ─────────────────
Gitea: main / cortecs-llm-migration Gitea: Branch admin-platform-integration
└ build-deploy.yml → :latest + deploy-all └ build-branch-image.yml → NUR Image :<sha> (kein :latest, kein Deploy)
Mittwald: app-demo, app-rotary, app-sprinz, … Mittwald: app-demo-admin ← neuer, eigener Service + Volume + Ingress
Domain: demo.<DOMAIN> … Domain: demo-admin.<DOMAIN>
Daten: ins Image gebacken (tenants/) Daten: Bundle von der Admin-Plattform (TENANT_SOURCE=admin)
OIDC-Client: von Hand angelegt OIDC-Client: automatisch über die Login-API
Admin-Plattform auf Coolify (eigene Domain, eigene Postgres)
```
Schutzmechanismen (alle eingebaut und getestet):
| Schutz | Wirkung |
|---|---|
| Branch-Trennung | `build-deploy.yml` läuft nur auf `main`/`cortecs-llm-migration` (auch nicht per „Run workflow“ vom Test-Branch). Der Test-Branch baut nur ein Image mit Tag = Git-SHA. |
| Eigener Tenant | `demo-admin` hat eigenen Service `app-demo-admin`, eigenes Volume `vol-demo-admin`, eigenen Host und eigenen OIDC-Client. `app-demo` bleibt unberührt. |
| `DEPLOY_ALLOWED_TENANTS=demo-admin` | Die Admin-Plattform darf **nur** diesen Tenant live anfassen (Mittwald **und** Login-API). Auch „Alle neu deployen“ trifft nur ihn. |
| `DEPLOY_LIVE=false` zum Start | Erst nur Dry-Run (liest den Stack, schreibt nichts). |
| Differenzielles PATCH + Guard | Es wird nur `app-demo-admin` geschrieben; danach prüft der Admin, dass alle fremden Services noch da sind. |
| Default `TENANT_SOURCE=git` im Core | Der neue Core verhält sich ohne Env wie der alte. |
Rückgängig machen: siehe **Abschnitt 11**.
---
## 1. Werte zusammentragen (einmalig)
| Wert | Woher | Verwendung |
|---|---|---|
| `DOMAIN` | Gitea → Repo `transkriptipt-platform` → Einstellungen → Actions → **Variablen** | Basis-Domain in der Admin-Plattform |
| `PROJECT_ID`, `STACK_ID` | dito | Mittwald-Ziel |
| `REGISTRY` (z. B. `gitea.onyva.dev/onyva`) | dito | Registry in der Admin-Plattform |
| `MITTWALD_API_TOKEN` | Gitea → **Secrets** (du kennst den Wert; derselbe Token) | Env der Admin-Plattform |
| Weitere Variablen: `OIDC_ISSUER`, `AUTH_ENABLED`, `APP_COOKIE_SECURE`, `MAX_FILE_SIZE_MB`, `CORTECS_BASE_URL`, `LLM_MODEL`, `LLM_MAX_TOKENS`, `LLM_TEMPERATURE`, `SMTP_HOST/PORT/USER/FROM`, `MISTRAL_*`, `COST_*` | dito | in **Einstellungen** der Admin-Plattform (Abschnitt 6) |
| `CORTECS_API_KEY`, `SMTP_PASSWORD` | `cd ~/PycharmProjects/transkript-platform && sops -d --extract '["CORTECS_API_KEY"]' secrets/shared.enc.yaml` (analog `SMTP_PASSWORD`) | Einstellungen → Geteilte Secrets |
| `COST_INGEST_KEY` von `demo` | `sops -d --extract '["COST_INGEST_KEY"]' tenants/demo/secrets.enc.yaml` | Secret des Test-Tenants (Abschnitt 7, Punkt 7.4) |
| **`<ADMIN_HOST>`** | du wählst: Domain, unter der Coolify die Admin-Plattform ausliefert, z. B. `admin.deine-domain.de` | muss **öffentlich per HTTPS** erreichbar sein (der Tenant-Container holt von dort sein Bundle) |
| **`<DEINE_MAIL>`** | deine Login-E-Mail bei login.onyva.dev (Konto muss dort existieren) | Admin-Zugriff + Freigabe im Test-Tenant |
Werte **nicht** in Chats oder Tickets kopieren (Keys, Secrets).
---
## 2. Core-Branch pushen (Image bauen lassen)
```bash
cd ~/PycharmProjects/transkript-platform
git branch --show-current # → admin-platform-integration
git log --oneline -3 # 13055b8 CI: build-deploy nur von Produktions-Branches …
git rev-parse HEAD # → voller SHA; z. B. 13055b811ca0030841f41ce1f10e4f30cba1a7db ← MERKEN (= CORE_SHA)
git push -u origin admin-platform-integration
```
Was jetzt in Gitea passiert (Repo → **Actions**):
| Workflow | Erwartung |
|---|---|
| `test` | grün (Schema ↔ Code ↔ Tenants, Bundle-Loader) |
| `build-branch-image` | grün; Log-Zeile „Image gebaut: `<registry>/transkript-platform:<SHA>`“ |
| `build-deploy` | **läuft nicht** (Branch nicht gelistet) – wenn doch: sofort abbrechen und melden |
Prüfen: Gitea → Repo → **Packages** → `transkript-platform` hat einen neuen Tag = voller SHA. Der Tag `latest` ist **unverändert** (Zeitstempel!).
> ✅ Ergebnis: `CORE_SHA` (voller 40-stelliger SHA). Lege ihn in deine Notizen.
---
## 3. Admin-Plattform ins eigene Repo
1. Gitea → **Neues Repository**: Organisation `Onyva`, Name `onyscribe-admin`, **privat**, leer (ohne README/.gitignore/Lizenz).
2. Lokal:
```bash
cd ~/PycharmProjects/onyscribe-admin
git status --short | head # .env und .data dürfen NICHT erscheinen (sind ignoriert)
git ls-files | grep -E '(^|/)\.env$|\.db$' || echo "ok: keine Secrets/DB im Index"
git add -A
git commit -m "Onyscribe Admin: Initialer Stand"
git remote add origin https://gitea.onyva.dev/Onyva/onyscribe-admin.git
git push -u origin main
```
Hinweis: `seed/tenants.seed.json.gz` enthält die Tenant-Konfigurationen inkl. Zusatzdateien (z. B. `namensliste.txt` – personenbezogen). Das liegt heute schon im Altrepo; Repo privat halten.
---
## 4. Login-System: Client + API-Key für die Admin-Plattform
Im Login-Dashboard (login.onyva.dev), als Admin:
1. **API-Keys → neuer Key** („onyscribe-admin“, ggf. Ablaufdatum). Klartext `onyva_…` **einmalig** kopieren → später `ONYVA_LOGIN_API_KEY`.
2. Client für die Admin-Plattform selbst anlegen (Dashboard **oder** per API):
```bash
KEY=onyva_… # der Key aus 1.
curl -sX POST https://login.onyva.dev/api/v1/apps -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"name":"Onyscribe Admin","redirect_uris":["https://<ADMIN_HOST>/auth/callback"],"auth_mode":"required","totp_mode":"disabled"}'
# → client_id + client_secret SOFORT sichern (nur einmal sichtbar)
curl -sX PUT https://login.onyva.dev/api/v1/apps/<client_id>/memberships -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"user_email":"<DEINE_MAIL>","role":"admin"}'
```
Ergebnis: `client_id`, `client_secret` (Admin-Login), `ONYVA_LOGIN_API_KEY`.
---
## 5. Coolify: Admin-Plattform deployen
**Neue Ressource → Docker Compose (aus Git)**: Repo `Onyva/onyscribe-admin`, Branch `main`, Compose-Datei `docker-compose.yml`
(Zugriff auf das private Repo per Deploy-Key/Token einrichten).
**Umgebungsvariablen** (Coolify → Ressource → Environment):
| Variable | Wert | Phase |
|---|---|---|
| `POSTGRES_PASSWORD` | langes Zufallspasswort | immer |
| `SECRET_KEY` | `python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"` – **sichern! ohne ihn sind die Secrets in der DB unlesbar** | immer |
| `AUTH_ENABLED` | `true` | immer |
| `OIDC_ISSUER` | `https://login.onyva.dev` | immer |
| `OIDC_CLIENT_ID` / `OIDC_CLIENT_SECRET` | aus Abschnitt 4 (Punkt 2) | immer |
| `OIDC_REDIRECT_URI` | `https://<ADMIN_HOST>/auth/callback` | immer |
| `OIDC_LOGOUT_REDIRECT_URI` | `https://<ADMIN_HOST>/` | immer |
| `APP_COOKIE_SECURE` | `true` | immer |
| `ADMIN_ALLOWED_EMAILS` | `<DEINE_MAIL>` (weitere kommagetrennt) | immer – ohne Regel wird jeder Login abgelehnt |
| `MITTWALD_API_TOKEN` | derselbe Token wie in Gitea | immer |
| `ONYVA_LOGIN_API_KEY` | aus Abschnitt 4 (Punkt 1) | immer |
| `DEPLOY_ALLOWED_TENANTS` | `demo-admin` | immer |
| `DEPLOY_LIVE` | **`false`** | Phase 1 (Dry-Run) → in Abschnitt 8 auf `true` |
| `OIDC_SYNC_LIVE` | `false` | optional `true`, um den Login-Abgleich separat vor dem Deploy zu testen |
| `CORE_RELEASE_TOKEN` | leer | erst in Abschnitt 10 |
| `CORTECS_API_KEY` | leer (wird in der UI als Secret gesetzt) | optional |
**Domain**: Service `admin` → Domain `https://<ADMIN_HOST>:8300` (Port 8300 = Container-Port). **Persistente Daten**: Volume `pgdata` (Postgres) – Backup einrichten.
Deploy starten. Der erste Build nutzt das normale `Dockerfile` (mitgelieferter Core-Snapshot, **keine Registry nötig**).
Prüfen:
- [ ] `https://<ADMIN_HOST>/healthz` → `{"ok":true}`
- [ ] `https://<ADMIN_HOST>/` → Weiterleitung zu login.onyva.dev → Login → Dashboard mit **7 Tenants** (Seed).
- [ ] Seitenleiste unten: „Deploy: gesperrt · Dry-Run“, „<DEINE_MAIL>“.
- [ ] `https://<ADMIN_HOST>/auth/me` zeigt `"auth_enabled": true`. Steht dort `false` (Seitenleiste: rot „Login aus – offen!“), ist die Plattform für jeden offen: `AUTH_ENABLED=true` (ohne Anführungszeichen) und die OIDC-Variablen prüfen. Ohne Login sind Live-Aktionen automatisch gesperrt.
- Probleme: „Kein Zugriff“ → `ADMIN_ALLOWED_EMAILS`; `redirect_uri`-Fehler → Redirect-URI im Login-Client **exakt** wie in `OIDC_REDIRECT_URI`.
---
## 6. Admin-Plattform konfigurieren (Menü „Einstellungen“)
Felder (Spalte „Gitea“ = dieselbe Variable wie bisher), dann oben **„Alles speichern“**:
| Feld in der Admin-Plattform | Wert / Gitea |
|---|---|
| Basis-Domain | `DOMAIN` |
| Mittwald PROJECT_ID / STACK_ID | `PROJECT_ID` / `STACK_ID` |
| Registry | `REGISTRY` |
| Image-Name | `transkript-platform` |
| **Öffentliche URL der Admin-Plattform** | `https://<ADMIN_HOST>` |
| **Core kann Bundle laden** | ✔ aktivieren (betrifft nur Tenants mit Quelle „Admin“, also nur `demo-admin`) |
| Login (OIDC) & Limits | `OIDC_ISSUER`, `AUTH_ENABLED` (true), `APP_COOKIE_SECURE` (true), `MAX_FILE_SIZE_MB` |
| LLM & Transkription | `CORTECS_BASE_URL`, `LLM_MODEL`, `LLM_MAX_TOKENS`, `LLM_TEMPERATURE`, `MISTRAL_*` |
| E-Mail (SMTP) | `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_FROM` |
| Kostenerfassung | `COST_API_BASE`, `COST_TRACKING_ENABLED` |
| „Weitere Core-Einstellungen“ (aus dem Schema) | nur ausfüllen, was du in Gitea auch gesetzt hast (`COST_LLM_*`, `COST_STT_PER_MINUTE`, `STT_*` …); leer = Standard des Cores |
| Rollout → Canary-Tenants | `demo-admin` |
| Geteilte Secrets → `CORTECS_API_KEY` | **Wert setzen** (aus `sops -d …`) |
| Geteilte Secrets → `SMTP_PASSWORD` | **Wert setzen** (nur nötig, wenn der Test E-Mail-Versand prüfen soll) |
Dann: Karte „Login-API“ → **„Verbindung testen“** → `✔ Verbindung ok – N App(s) im Login`.
---
## 7. Test-Tenant `demo-admin` anlegen und konfigurieren
**7.1 Anlegen**: Dashboard → **+ Neuer Tenant** → Name `Onyva Demo (Admin-Test)`, ID `demo-admin`, Ausgangspunkt „Kopie von Onyva Demo“ → Anlegen.
(Quelle ist automatisch „Admin“, OIDC-Verwaltung „managed“.)
**7.2 Spracherkennung**: Tenant → *Funktion & STT* → Provider `cortecs`, Modell `voxtral-mini-transcribe-2602`
(dann reicht der geteilte Cortecs-Key; `demo` nutzt sonst Mistral und bräuchte einen eigenen `MISTRAL_API_KEY`).
**7.3 Optisch unterscheidbar machen**: *Marke & Farben* → Akzent z. B. `#E8590C` → „Töne aus Akzent ableiten“. Oben rechts **Prüfen & veröffentlichen** → Nachricht „Test-Tenant“ → **v2**.
**7.4 Secrets**: Tenant → *Secrets* → `COST_INGEST_KEY` **Wert setzen** (der Wert von `demo`; Kosten laufen dann unter `demo`, für den Test okay).
`SESSION_SECRET` und `ADMIN_BUNDLE_TOKEN` sind schon erzeugt. `OIDC_CLIENT_ID/SECRET` legt der Deploy automatisch an.
**7.5 Login**: Tenant → *Domain & Login*: Subdomain `demo-admin`; OIDC-Verwaltung `managed`, Zugriff `required`; **Freigaben**: `<DEINE_MAIL>` als `editor` (Nutzer muss im Login existieren) → *Speichern*.
**„Gegen Login prüfen“** (nur lesend) → erwartet: „würde App ‚Onyscribe – Onyva Demo (Admin-Test)‘ anlegen (Redirect: https://demo-admin.<DOMAIN>/auth/callback …)“ und „würde <DEINE_MAIL> als editor freigeben“.
**7.6 Core pinnen**: Tenant → *Übersicht* → Karte „Core-Version“ → Pin = **voller SHA aus Abschnitt 2** → *Pin setzen*.
(Der Pin sorgt dafür, dass genau das Branch-Image läuft und der Health-Check die Core-Version prüfen kann.)
**7.7 Vorflug-Check + Dry-Run**: Tenant → *Deploy* → **Vorflug-Check → Prüfen**.
Erwartet in Phase 1: alles grün/gelb, **gelb** nur „DEPLOY_LIVE nicht aktiv“; keine roten Punkte. Typische rote Punkte und Fix stehen direkt dabei
(z. B. Registry/PROJECT_ID/Token fehlen, `CORTECS_API_KEY`/`COST_INGEST_KEY` fehlen, „Core kann Bundle laden“ aus, Admin-URL nicht erreichbar).
Dann **Dry-Run** → Log lesen: „geschützte bestehende Services (bleiben unangetastet): [app-demo, app-rotary, …]“, `[DRY-RUN] PATCH …` für Volume und Service, kein Schreibzugriff.
---
## 8. Live-Deploy des Test-Tenants
1. Coolify → Admin-Ressource → `DEPLOY_LIVE=true` setzen (`DEPLOY_ALLOWED_TENANTS=demo-admin` bleibt) → neu deployen (Daten bleiben).
2. Admin → `demo-admin` → *Deploy* → Vorflug-Check → muss **„Bereit“** zeigen → **Deployen**.
Was passiert (Log): OIDC-Client im Login anlegen und Secret speichern → Pflicht-Secrets prüfen → Volume + Service per PATCH → Service neu erzeugen → Ingress
`demo-admin.<DOMAIN>` → Health-Check (bis ~90 s Wartezeit für Start + Zertifikat).
Abnahme-Checkliste:
- [ ] Deploy-Log endet mit `Health-Check … ✔ · Core 13055b811c · Bundle v2` und `✔ Fertig`
- [ ] mStudio: Service `app-demo-admin` läuft; **alle anderen Services unverändert** (Image-Tags wie vorher)
- [ ] `https://demo-admin.<DOMAIN>/health` → `core.version` = dein SHA, `bundle` = `{"source":"admin","version":2,"stale":false}`
- [ ] Seite öffnen → Login → freigegebener Nutzer kommt rein → Oberfläche in der **neuen Farbe**
- [ ] Protokoll erzeugen (kleines Transkript hochladen) → Zusammenfassung → PDF/Word → Aussehen = Vorschau in der Admin-Plattform
- [ ] Admin → Dashboard → **Zustand prüfen**: `demo-admin` grün. Die Altbetrieb-Tenants zeigen „Core ohne Vertrag (alte Version)“ – **erwartet und harmlos** (sie laufen noch auf dem alten Image)
- [ ] Altbetrieb: `demo.<DOMAIN>` und die anderen Kunden funktionieren unverändert
---
## 9. Änderung ohne Core-Änderung testen
1. Admin → `demo-admin` → *Marke & Farben*: Akzent ändern → **Prüfen & veröffentlichen** (v3) → *Deploy* → **Deployen** (nur Neustart, kein Image-Build).
2. Erwartung: Health-Check `Bundle v3 ✔`; Seite neu laden → neue Farbe.
3. Agent: *KI-Agent* → „Setze die Akzentfarbe auf #0B6E4F“ → Live-Vorschau beobachten → „In Entwurf übernehmen“ → veröffentlichen → deployen.
4. Ausfallsicherheit (nur `demo-admin`!): Coolify → Admin-Ressource **stoppen**; in mStudio den Container `app-demo-admin` **neu starten** → er startet mit gecachtem Bundle:
`https://demo-admin.<DOMAIN>/health` → `bundle.source = "cache"`, `stale = true`. Admin wieder starten → nächster Container-Neustart holt wieder `admin`.
---
## 10. (Optional) Den kompletten Core-Release-Weg testen
1. Coolify: `CORE_RELEASE_TOKEN` = langes Zufallstoken, Admin neu deployen. Gitea → Repo → Einstellungen → Actions: **Variable** `ADMIN_URL` = `https://<ADMIN_HOST>`, **Secret** `ADMIN_RELEASE_TOKEN` = dasselbe Token.
(Wirkt nur auf Workflows des Test-Branches; `main` kennt den `notify-admin`-Job nicht.)
2. Im Core-Repo eine Kleinigkeit committen und auf den Branch pushen → `build-branch-image` baut und meldet das Release.
3. Admin → Menü **Core**: neues Release, Badge **„Admin älter/anders“** (die Admin-Plattform enthält noch den alten Core-Snapshot).
4. Angleichen: `cd ~/PycharmProjects/onyscribe-admin && scripts/sync_core.sh && git add -A && git commit -m "Core-Snapshot aktualisiert" && git push` → Coolify deployt → Badge **„gleicher Core“**.
*(Später ersetzt `Dockerfile.fromcore` diesen Schritt: Admin wird direkt auf dem Core-Image gebaut.)*
5. **Prüfen** → Matrix mit allen 7 Tenants (beim ersten Mal „neu (keine Baseline)“) → **Freigeben** → **Dry-Run** → *Tenant `demo-admin`: Pin aufheben* → **Ausrollen** → nur `demo-admin` wird live ausgerollt (Allowlist), Health-Check prüft die neue Core-SHA.
6. Rollback testen: **Zurückrollen** (stellt den vorherigen Tag wieder her).
---
## 11. Abbruch / Rückbau (jederzeit)
| Was | Wie |
|---|---|
| Test-Tenant live entfernen | Admin → `demo-admin` → Übersicht → Gefahrenbereich → **Offboard-Dry-Run** prüfen → **Offboard (live)** (entfernt Service, Volume, Ingress) |
| OIDC-Client des Test-Tenants | Gefahrenbereich → **OIDC-Client im Login löschen** |
| Admin-Plattform | Coolify-Ressource stoppen/löschen; Login-Client „Onyscribe Admin“ und API-Key im Login löschen |
| Branch/Image | Gitea: Branch `admin-platform-integration` löschen, Package-Tag (SHA) löschen |
| Alles Live sperren | `DEPLOY_LIVE=false` (und/oder `DEPLOY_ALLOWED_TENANTS` leeren) → Admin neu deployen |
Der Altbetrieb braucht in keinem dieser Fälle eine Änderung.
---
## 12. Fehlersuche
| Symptom | Ursache / Fix |
|---|---|
| Vorflug: „Mittwald-Stack lesbar“ rot | Token-Rechte, `PROJECT_ID`/`STACK_ID` in Einstellungen prüfen (dieselben Werte wie Gitea) |
| Deploy: „Pflicht-Secrets ohne Wert: …“ | `CORTECS_API_KEY` (Einstellungen → Geteilte Secrets), `COST_INGEST_KEY` (Tenant → Secrets), ggf. `MISTRAL_API_KEY` (nur bei STT-Provider mistral) |
| Deploy: „… nicht für Live-Schreibzugriffe freigegeben“ | `DEPLOY_ALLOWED_TENANTS` enthält die Tenant-ID nicht |
| Deploy: „Quelle ‚Admin‘ ist noch gesperrt“ | Einstellungen → „Core kann Bundle laden“ aktivieren |
| Deploy: „OIDC-Abgleich fehlgeschlagen: API-Key ungültig (401)“ | `ONYVA_LOGIN_API_KEY` falsch/abgelaufen; Key im Login neu erzeugen |
| Deploy: „… Freigaben: <mail> existiert im Login nicht“ (Warnung) | Nutzer im Login anlegen, dann *Domain & Login → Jetzt abgleichen* |
| Health-Check: „Core X statt Y“ | Pin/Tag stimmt nicht mit dem Image überein oder Image fehlt in der Registry (mStudio-Container-Log) |
| Health-Check: „Bundle cache v… (veraltet)“ | Container erreicht die Admin-URL nicht: Domain/HTTPS/Firewall, `admin_public_url`, Bundle-Token |
| Container-Log: „Tenant-Bundle … nicht verfügbar“ | wie oben; zusätzlich `ADMIN_URL`/`ADMIN_BUNDLE_TOKEN` im Plan (Deploy → Service-Definition) prüfen |
| Login der Tenant-Seite: `redirect_uri`-Fehler | *Domain & Login → Gegen Login prüfen → Jetzt abgleichen* (ergänzt die Redirect-URI) |
| Login: `invalid_client` | `OIDC_CLIENT_SECRET` fehlt/ist veraltet → *Jetzt abgleichen* bzw. neu deployen |
| Zertifikat/Ingress fehlt | Token braucht Domain/Ingress-Recht (Log: „Ingress … NICHT angelegt“); Service läuft trotzdem, nach Rechteänderung erneut deployen |
| Admin „Zustand prüfen“: „nicht erreichbar“ | Tenant noch nicht deployt bzw. DNS/Zertifikat noch nicht fertig |
---
## 13. Danach (nicht Teil dieses Tests)
Erst wenn der Test-Tenant mehrere Tage sauber läuft: Core-Branch reviewen und mergen (→ `build-deploy` baut dann den neuen Core für alle),
Secrets der echten Tenants importieren (`python scripts/import_tenants.py --with-secrets`), pro Tenant die **Cut-over-Checkliste** abarbeiten und
`DEPLOY_ALLOWED_TENANTS` schrittweise erweitern. Die echten Tenants stellst du **einzeln** um – Rückweg: Quelle zurück auf „Git“.