diff --git a/docs/handoffs/2026-07-25-clupilot-engine-v1.0-handoff.md b/docs/handoffs/2026-07-25-clupilot-engine-v1.0-handoff.md new file mode 100644 index 0000000..c0415eb --- /dev/null +++ b/docs/handoffs/2026-07-25-clupilot-engine-v1.0-handoff.md @@ -0,0 +1,210 @@ +# Handoff — CluPilot Provisionierungs-Engine v1.0 + +**Erstellt:** 2026-07-25 +**Für:** frische Claude-Code-Session +**Art:** Feature-Bau-Handoff (TDD) +**Zuerst lesen:** `docs/handoffs/2026-07-25-clupilot-state-handoff.md` (Projekt-Zustand, Workflow, Regeln, Gotchas). Dann dieses Dokument. +**Design-Spec:** die ausführliche Engine-Design-Spec liegt beim **Nutzer** (nicht im Repo). **Vor Baubeginn anfordern** — sie ist bei Detail-Konflikten der Vertrag; dieses Dokument ist der Bau-Fahrplan. + +--- + +## 1. Mission + +Aus einer **bezahlten Bestellung** wird vollautomatisch eine **laufende, gesicherte, überwachte Nextcloud-Instanz** — mit persistentem Status, Retry, Timeout, sofort gespeicherten externen IDs und Acceptance-Gate. Ausführungsmodell: **DB-State-Machine + Tick-Orchestrator** (kein monolithischer Job). + +## 2. Arbeitsweise (verbindlich) + +- **TDD.** Das Superpowers-`test-driven-development`-Skill ist auf der VM installiert (ab dieser Session verfügbar) — nutzen. Jede Step-Klasse + der Orchestrator-Kern test-first. **`ProxmoxClient` in Step-Tests mocken.** +- **Tests laufen gegen sqlite `:memory:`** (siehe State-Handoff §5/§6 — Test-Isolation ist bereits korrekt eingerichtet; nicht kaputt machen). +- **Regeln R1–R17** gelten (State-Handoff §9). R15 (Codex-Review clean) vor „fertig". Bei Spec-Konflikt: Design-Spec → `CLAUDE.md`/Regeln → Nachfrage. +- **Kleine, fokussierte Klassen** — eine Step-Klasse = eine Verantwortung. + +--- + +## 3. Topologie-Entscheidung (verbindlich) ⭐ + +Zwei **getrennte** Ebenen — nicht verwechseln: + +### 3.1 Software-Modell = **Standalone-Flotte** (Option 1) +CluPilot ist die **zentrale Konsole** und spricht **jeden Host einzeln per Proxmox-API-Token** an; die Flotte lebt in **CluPilots eigener DB**. **Kein corosync im Steuerungs-Layer.** „Cluster" ist zunächst nur CluPilots **logische Sicht**. + +> Konsequenz für die Engine: Ein Host wird **immer** über seine eigene `wg_ip` + Token adressiert. Ob dieser Host physisch allein steht oder Teil eines Proxmox-Clusters ist, ist der Engine **egal**. Damit läuft v1.0 **heute mit einem einzigen Host** und bleibt zukunftsoffen. + +### 3.2 Wachstumsziel Infrastruktur = **Cluster pro Rechenzentrum** (Option 3) +Sobald pro Standort ≥2–3 Hosts stehen und **Wartung ohne Downtime** (Live-Migration) bzw. **HA** gewünscht ist: die Hosts **innerhalb eines RZ** zu einem echten **corosync-Cluster** verbinden (Falkenstein-Cluster, Helsinki-Cluster). **Niemals** ein Cluster über WAN (fsn↔hel) — corosync braucht niedrige Latenz. + +Fußangeln fürs spätere Clustern (jetzt nur im Datenmodell vorsehen, nicht bauen): +- **Quorum:** 2 Nodes sind fragil → 3 Nodes oder QDevice. +- **Storage:** Live-Migration braucht shared/replizierten Storage (Start: ZFS+Replikation; groß: Ceph). +- Für reine Wartungs-Umzüge reicht auch offline `qm remote-migrate` zwischen Standalone-Nodes (Proxmox 8) — Cluster erst für **Live**-Migration/HA nötig. + +### 3.3 Was daraus fürs Datenmodell folgt (jetzt umsetzen) +- `hosts` bekommt **`datacenter`** (z. B. `fsn`, `hel`) und ein **nullable `cluster`** (Cluster-Name/-ID; v1.0 überall `null` = standalone). Beides jetzt anlegen, damit späteres Clustern **additiv** ist. +- **Placement v1.0 = einfach:** erster **aktiver** Host **im Ziel-Rechenzentrum** mit genug **freier Zusage** (`total_gb − reserve − Σ instances.quota_gb`). Cluster/Live-Migration/HA/Rebalancing sind **v1.1+**. +- Migration/HA werden **nicht** in v1.0 gebaut, aber das Modell verbaut sie nicht. + +--- + +## 4. Scope + +**IN v1.0 (Happy Path + Robustheit):** +1. Datenmodell (Migrations + Models). +2. Orchestrator-Kern: Run-State-Machine, Step-Kontrakt (Advance/Retry/Fail), Tick-Scheduler, `AdvanceRunJob`, Per-Run-Lock, Timeout, append-only Event-Log. +3. `ProxmoxClient`-Service (**NEU**). +4. Die **15 Kunden-Pipeline-Schritte** als idempotente Step-Klassen, inkl. Backup- + Monitoring-Registrierung + Acceptance. +5. Bestell-Eingang: Stripe-Webhook → `orders` mit **Idempotency-Key** → Run-Anlage. +6. Live-Fortschritt: die bereits existierenden Views an echte Daten binden (s. §9). + +**NICHT in v1.0 (= v1.1, bewusst weglassen):** +- **Host-Pipeline** / Panel-Host-Onboarding / Hetzner-Robot-Client. +- **Live-Migration, HA, Cluster-Bildung, Rebalancing, Reconciliation-Loop** (Waisen-Abgleich). +- Admin-Skip/Resume-Cleanup, Abort-Cleanup-Pipeline (simples Retry + Pause reicht). +- Kompletter Kündigungs-/Löschprozess. +- Multi-Host-Placement über simple Zusage-Rechnung hinaus. + +--- + +## 5. Voraussetzungen (vor echtem End-to-End-Lauf) + +1. **Versioniertes Nextcloud-Blueprint-Template in Proxmox** (einmal manuell bauen → Template; muss **qemu-guest-agent + docker** enthalten). `CloneVirtualMachine` klont daraus; Template-VMID in Config/DB hinterlegen. Fehlt es → Step failt (`Fail`, nicht Retry). +2. **Dev-Proxmox-Host erreichbar** über WG-Tunnel (`hosts.wg_ip:8006`) mit `automation@pve`-Token (Minimal-Rolle). Ohne echten Host läuft nur die **gemockte** Test-Suite. +3. **MariaDB-Entscheidung** betrifft den **Nextcloud-Stack im Blueprint** (Compose im Gast), **nicht** die Panel-DB — nicht verwechseln. Die Engine schreibt nur Config, baut die DB nicht selbst. +4. Panel läuft (ist gebaut, s. State-Handoff). +5. **Stripe** nur Test-Modus in Dev, keine echten Zahlungen. + +--- + +## 6. Bestandscode: Reuse / Neubau / Kollision + +**Reuse:** +- `phpseclib3` (SSH/SFTP) — für host-nahe Kommandos falls nötig. Gast-interne occ/Compose-Kommandos laufen aber über **qemu-guest-agent (`guestExec`)**, nicht SSH-Warten. +- `Illuminate\Support\Facades\Crypt` — Muster für verschlüsselte Secrets. Proxmox-Token/Keys ebenso verschlüsselt ablegen (`hosts.api_token_ref`). +- **Laravel Reverb** (konfiguriert) → Live-Fortschritt pushen. +- **`scheduler`-Service** (`schedule:work`) → **Tick** hier registrieren (`routes/console.php` Schedule, jede Minute). +- **`redis`-Queue** → `AdvanceRunJob` (eigene Queue `provisioning` denkbar). +- Livewire-Konventionen + das bestehende **Design-System/Komponenten** (State-Handoff §4) für Admin-Ansichten. + +**Neubau (existiert NICHT):** +- **`App\Services\ProxmoxClient`** — Proxmox VE REST (`/api2/json`), Token-Auth `PVEAPIToken=automation@pve!=`, Basis-URL aus `hosts.wg_ip`. Methoden v1.0: `cloneVm, setCloudInit, resizeDisk, startVm, guestAgentPing, guestExec, getTaskStatus` (UPID-Polling), `applyFirewall`. + > ⚠️ **`HetznerClient` existiert nicht hier** (frischer Build). Falls du irgendwo Hetzner-Cloud-Code als Vorlage suchst: Proxmox-API ist **komplett anders** — nicht vermischen. +- Orchestrator-Kern (Runner, Step-Interface, Result-Objekte, Tick, Lock). +- Engine-Tabellen + Models. + +**Kollisionen bewusst trennen:** Es gibt hier **keine** Alt-Domäne (`Server`/`Cluster`/`ProvisioningLog`/`ProvisionGaleraNode`) — der frühere „Server-Manager" wurde **nicht** mitgebaut. Die Engine ist ein sauberer Neuaufbau. Die neue `provisioning_step_events`-Tabelle ist **run-scoped & reich** (nicht mit irgendeinem simplen Log verwechseln). + +--- + +## 7. Datenmodell (Migrations + Models) — Kern zuerst + +Neu anlegen (Reihenfolge: runs/events/resources zuerst, dann Fach-Tabellen): +- `customers`, `orders` (`stripe_event_id` **unique** = Idempotency; `status`, `plan`, `amount`). +- `hosts` — **inkl. `datacenter`, `cluster` (nullable)**, `wg_ip`, `api_token_ref` (encrypted), Kapazität `total_gb / total_ram / cpu_weight / reserve_pct`, `status`. +- `instances` — `vmid`, `quota_gb`, `disk_gb`, `subdomain`, `custom_domain`, `nc_admin_ref`, `route_written`, `cert_ok`, `status`, FK `host_id`, `customer_id`. +- `provisioning_runs` — **polymorph** `subject_type`/`subject_id`, `pipeline`, `current_step`, `status`, `attempt`, `next_attempt_at`, `started_at`, `error`, `context` (json). +- `provisioning_step_events` — **append-only:** `run_id, step, attempt, outcome, message, external_ref, created_at`. +- `run_resources` — `run_id, host_id, kind, external_id` (Brotkrumen für Idempotenz + spätere Reconciliation). +- `dns_records`, `backups`, `monitoring_targets`, `onboarding_tasks`. + +**Kapazität = berechnet** (SUM `instances.quota_gb` je Host), kein separater Ledger. **Placement-Query** liest `total_gb − reserve − zugesagt`, **gefiltert auf `datacenter` und `status='active'`** (Cluster ignoriert die Query in v1.0). + +**R11:** in URLs adressierbare Records (customers/instances/runs) per **UUID**, nicht Integer-PK. + +--- + +## 8. Orchestrator-Kern (Herzstück, zuerst + test-first) + +**Run-Status:** `pending → running ⇄ waiting → completed`, plus `paused` (Admin), `failed` (Retries erschöpft/fatal). + +**Step-Interface (Vorschlag):** +```php +interface ProvisioningStep { + public function execute(ProvisioningRun $run): StepResult; // Advance | Retry | Fail +} +// StepResult::advance() | ::retry(int $afterSeconds, string $reason) | ::fail(string $reason) +``` + +**Runner-Pflichten je Ausführung:** +- **Vor Arbeit: Idempotenz-Check** (externe ID / DB-State „schon getan?"). +- **Externe IDs sofort** in `run_resources`, **BEVOR** weitergemacht wird (vmid, UPID, hetzner_record_id, backup_job_id). +- Ergebnis in `provisioning_step_events` (append-only) + `provisioning_runs` fortschreiben. +- **Timeout** je Schritt (`started_at + max_duration`); Polling-Schritte mit Gesamt-Deadline. +- **Retry** mit Backoff, `attempt++`, `max_attempts` → sonst `Fail`. +- **Fatal vs. retrybar** entscheidet der Schritt je Fehler (Proxmox-5xx/Timeout = Retry; „Template not found"/ungültige Order = Fail). + +**Advancement:** +- **Tick** (Scheduler, jede Minute): Runs `status IN (running,waiting) AND next_attempt_at <= now` → `AdvanceRunJob` dispatchen. +- **Sofort-Dispatch** bei Run-Anlage + nach erfolgreichem Schritt (nicht auf Tick warten). +- **Per-Run-Lock** (`Cache::lock("run:{id}")`) → nie zwei Worker auf demselben Run. + +--- + +## 9. Die 15 Kunden-Schritte + +Namespace `App\Provisioning\Steps\Customer\`. Reihenfolge: + +`ValidateOrder → ReserveResources → CloneVirtualMachine → ConfigureCloudInit → StartVirtualMachine → WaitForGuestAgent → ConfigureNetwork → DeployApplicationStack → ConfigureNextcloud → CreateCustomerAdmin → ConfigureDnsAndTls → RegisterBackup → RegisterMonitoring → RunAcceptanceChecks → CompleteProvisioning` + +Per-Schritt-Verhalten (Idempotenz-Check, **[E]** externe ID sofort, **[P]** Polling, **[F]** Fail) steht in der **Design-Spec Sektion 3** — dort nachschlagen, nicht neu erfinden. Harte Regeln: +- **Kundenzugang ERST nach `RunAcceptanceChecks` (14).** `CompleteProvisioning` (15) schaltet frei. +- Backup (12) + Monitoring (13) sind **Teil** der Bereitstellung, nicht danach. +- Zahlung ok trotz Fehler → Run `failed` + **manueller** Retry, **kein** Auto-Refund. +- `CreateCustomerAdmin`: Initial-Credential sicher an Kunde, **nie plaintext** persistieren. +- **`ReserveResources`** nutzt das Placement aus §7 (Host im Ziel-`datacenter`). +- occ/Compose im Gast via **guest-agent** (`guestExec`), nicht SSH-Warten. +- **DNS/TLS:** Hetzner-DNS-API legt `kundeXY.clupilot.com` an **[E]** `hetzner_record_id`; Cert via **HTTP-01** (kein DNS-01); Traefik-YAML in File-Provider-Verzeichnis schreiben, dann Cert-Ausstellung pollen **[P]** (Deadline großzügig — DNS-Propagation + Port 80 erreichbar). + +--- + +## 10. Bestell-Eingang (Stripe) + +- Webhook-Endpoint (Test-Modus). **Idempotency-Key = `stripe_event_id` unique** → verhindert Doppel-Provisionierung bei Webhook-Retries. +- Bezahlt → `orders`-Row → `provisioning_runs`-Eintrag (`pipeline=customer`, Subject = Order/Customer) anlegen + **sofort dispatchen**. + +## 11. Live-Fortschritt (an bestehende UI binden) + +- **Admin `/admin/provisioning`** ist aktuell eine **Fixture-View** (`app/Livewire/Admin/Provisioning.php`) → an echte `provisioning_runs` + `provisioning_step_events` binden, live via **Reverb** bei jedem `step_event`. Der `x-ui.progress-stepper` existiert bereits. +- **Kundendashboard**: der Bereitstellungs-Stepper auf `/dashboard` (aktuell Fixtures) → an den Run des Kunden binden; Zugang zur Cloud erst nach Abschluss. +- Panel = reine **View** auf die Tabellen (kein State im Frontend). Broadcast-Kanäle **privat** (authentifiziert), pro-Kunde scopen. + +--- + +## 12. Build-Reihenfolge (jede Phase grün testen) + +1. Migrations + Models (runs, step_events, run_resources, instances, hosts[+datacenter/cluster], customers, orders). +2. Orchestrator-Kern test-first (StepResult, Interface, Runner Advance/Retry/Fail/Timeout/Lock, `AdvanceRunJob`, Tick) mit 2–3 Fake-Steps. +3. `ProxmoxClient` + Fake/Mock-Doppel. +4. Steps 1–2 (ValidateOrder, ReserveResources — reine DB/Placement, früh testbar, inkl. datacenter-Filter). +5. Steps 3–7 (VM-Lebenszyklus) gegen gemockten ProxmoxClient; externe-ID-Persistenz + Idempotenz je Step. +6. Steps 8–10 (Stack/Nextcloud/Admin) via guestExec-Mock. +7. Step 11 (DNS/TLS) — Hetzner-DNS-Client + Traefik-YAML-Writer + Cert-Polling. +8. Steps 12–13 (Backup/Monitoring registrieren). +9. Steps 14–15 (Acceptance + Complete). +10. Stripe-Webhook + Idempotenz + Run-Anlage. +11. Live-Fortschritt an Admin+Kundendashboard binden (Reverb). +12. End-to-End gegen Dev-Host + Template (falls verfügbar), sonst voll gemockt grün. + +## 13. Definition of Done (v1.0) + +- [ ] Bezahlte Test-Bestellung erzeugt einen Run, der ohne manuelles Zutun bis `completed` läuft (gegen Dev-Host+Template) ODER voll gemockt grün. +- [ ] Jeder Schritt idempotent: künstlicher Abbruch + erneuter Tick → **keine** Doppel-Ressourcen (verifiziert über `run_resources`). +- [ ] Retry mit Backoff + Timeout + Per-Run-Lock getestet. +- [ ] Externe IDs (vmid/UPID/dns/backup) werden **vor** Weitergabe persistiert (Test simuliert Crash danach). +- [ ] Kundenzugang erst nach bestandener Acceptance. +- [ ] Stripe-Doppel-Webhook → nur EINE Provisionierung. +- [ ] Placement respektiert `datacenter` + freie Zusage; `cluster`-Feld existiert (nullable, ungenutzt in v1.0). +- [ ] Admin- + Kunden-Fortschritt live (Reverb). +- [ ] Pest grün; R12 (Admin/Kunden-Views 0 Konsolenfehler); R15 Codex clean; keine Secrets im Repo. + +## 14. Watch-Items + +1. **Kein Proxmox-Code vorhanden** → `ProxmoxClient` inkl. Auth/UPID-Polling ist echter Neubau. Früh isoliert testen. +2. **Guest-Agent statt SSH** — Nextcloud-VM hat evtl. keinen SSH-Zugang; occ/Compose via `guestExec`. Blueprint muss guest-agent + docker enthalten. +3. **Traefik File-Provider-Pfad** muss beschreibbar sein (wo läuft Traefik, wie schreibt die Engine die YAML? — konkreten Schreibweg festlegen, ggf. via Host/guest). +4. **HTTP-01 braucht Port 80** auf Traefik für die neue Subdomain, bevor das Cert kommt → DNS-Propagation abwarten (Polling-Deadline großzügig). +5. **Secrets** (Proxmox-Token, Hetzner-DNS-Token, Backup-Key-Refs) verschlüsselt (`Crypt`)/aus `.env`, nie in Git. +6. **Topologie:** `cluster` bleibt v1.0 `null`. Nicht verleiten lassen, Cluster/Migration/HA einzubauen — das ist v1.1 und würde v1.0 aufblähen. +7. Gotchas aus dem State-Handoff §6 beachten (`@js()`/Test-Isolation/CSRF/Locale/Vite-Reload). + +## 15. Danach (v1.1, NICHT jetzt) + +Host-Pipeline (Panel-Onboarding + Hetzner-Robot-Client) · **Cluster-pro-RZ-Bildung + Live-Migration/HA** (Topologie-Wachstumsziel) · Reconciliation-Loop · Admin-Skip/Resume-Cleanup · Kündigungs-/Löschprozess. Die Architektur (polymorpher Run, `run_resources`, Step-Kontrakt, `datacenter`/`cluster` im Host) ist so gebaut, dass das **rein additiv** ist.