16 KiB
Handoff — vom grünen Test zum ersten echten Durchlauf
Stand: 2026-07-30, main auf v1.3.44, 1686 Tests grün.
Zweck: In dieser Nacht wurden Host-Onboarding, VM-Erstellung und der Stripe-Prozess
vollständig geprüft und der größte Teil der Funde behoben. Was bleibt, ist unten
beschrieben. Die Kernaussage in einem Satz: die Testsuite war grün, und trotzdem
hätte kein Host durchinstalliert und keine VM ausgeliefert werden können.
0. Die Lektion, die den ganzen Rest erklärt
Alle Funde waren gegen eine grüne Suite unsichtbar, und zwar systematisch:
HostOnboardingEndToEndTestfährt die ganze Pipeline gegenFakeRemoteShell, dessen Standardantwort Erfolg ist. Der Test beweist, dass die Zustandsmaschine richtig durchzählt — über das Verhalten irgendeines Befehls sagt er nichts.- Rund ein Dutzend Tests schrieben in
config('provisioning.plans.*'), einen Schlüssel, den es seit dem Katalog-Umbau nicht mehr gibt (und es existiert ein Test, der genau das behauptet:expect(config('provisioning.plans'))->toBeNull()). Jede dieser Zeilen war wirkungslos; die Tests waren aus dem falschen Grund grün. - Mehrere Tests prüften eine Spalte, die der Code eine Zeile vorher selbst geschrieben hatte.
Konsequenz für die nächste Session: Ein Testlauf ist keine Abnahme. Jeder Arbeitsblock unten endet mit einem Schritt auf echter Hardware, nicht mit „grün".
1. Entscheidungen aus dem Brainstorming (verbindlich)
| Frage | Entscheidung |
|---|---|
| Woher kommt die Software im Gast? | Vorlage bringt Docker + Compose mit, Nextcloud installiert sich beim ersten Start aus NEXTCLOUD_ADMIN_*, die die Pipeline in die .env schreibt. Vorlage bleibt kundenunabhängig, Passwörter werden nie in ein Image gebacken. |
| Proxmox-Version | PVE 9 auf Debian 13 trixie. Die Codename-Tabelle in InstallProxmoxVe kennt beides; PVE 8/bookworm kann bleiben, getestet und unterstützt wird 9. |
| Traefik | Systemdienst (Binary + systemd-Unit), pro Host, installiert und geprüft von einem neuen Pipeline-Schritt. Kein Docker auf dem Hypervisor — nftables bleibt einziger Eigentümer der Host-Firewallregeln. |
vmbr0 |
Vollautomatisch, mit Selbstrücknahme-Zeitgeber (siehe Block B). |
| Testumgebung | Ein dedizierter Server mit Debian 13, neu aufsetzbar. Kein zweiter Rechner nötig. |
| Testpaket | Eigene Paketfamilie „Test": 20 GB Platte, 1 GB RAM, 1 Kern, 5 GB Kontingent. Nach dem Beweis wieder aus dem Verkauf nehmen. |
| ACME | HTTP-01 pro Host. Kein Wildcard/DNS-01 — pro Kunde entsteht genau ein Zertifikat, unabhängig von der Host-Zahl. |
Nicht geeignet: Hetzner Cloud (CPX/CX) — keine verschachtelte Virtualisierung,
also /dev/kvm nicht vorhanden, also startet kein Gast. Unabhängig von der Größe.
Bei netcup-VPS ist es produktabhängig; entscheidend ist
grep -cE 'vmx|svm' /proc/cpuinfo && ls -l /dev/kvm.
2. Was heute Nacht behoben und ausgeliefert wurde
Sechs Commits, 8440266 → 9da1358, Tag v1.3.44.
Stripe / Geld
- Nichts konnte ein Stripe-Abo beenden. Kündigung und Widerruf schrieben nur lokale Zustände; Stripe erfuhr nie davon, und die nächste Abbuchung wurde als echte Rechnung aus der lückenlosen Serie ausgestellt und versendet — monatlich, unbegrenzt, für eine abgeschaltete Leistung. Jetzt: Kündigung zum Periodenende, Widerruf sofort, und eine Rechnung wird nur noch ausgestellt, wenn ihr Abrechnungszeitraum vor dem Vertragsende begann.
- Kündigung lief auf Monatstakt auch bei Jahresverträgen (bis zu elf bezahlte
Monate verloren). Jetzt aus
current_period_end. - Einrichtungsgebühr wurde überall beworben und nie berechnet — jetzt eine Einmalposition am Checkout, brutto, auf der ersten Rechnung.
- Das Kundenportal zeigte fünf erfundene Rechnungen, eine erfundene Ausgabenkurve und eine erfundene nächste Abbuchung, während die echten Belege existierten und nirgends im Portal auftauchten. Jetzt die echten.
- Widerruf erstattet voll (Ihre Entscheidung), Stornorechnung für jede Abbuchung im Zeitfenster, nicht nur die erste.
- Ein Absturz nach dem Auftrags-Commit ließ einen bezahlten Kauf dauerhaft
unfakturiert —
resume()holt das jetzt nach, idempotent. - Klick auf „Upgrade" löschte stillschweigend eine vorgemerkte Abstufung und tat sonst nichts. Behoben.
- Reverse-Charge-Kunden zahlten 20 % zu viel: ein Stripe-Preis für alle, also
Bruttobetrag auch für den, dem keine Steuer berechnet wird — und der Beleg wies
den ganzen Betrag als Netto aus. Jetzt zwei Preise je Paket (brutto/netto),
Auswahl an der Kasse über
TaxTreatment. - UID-Prüfung gegen VIES gebaut (vorher setzte niemand
vat_id_verified_at, Reverse Charge konnte also nie auslösen). Drei Ergebnisse: bei „Register nicht erreichbar" wird nichts geschrieben.
Host-Onboarding
- Debian-Codename war fest auf
bookworm— ein heutiger Server hätte PVE-8-Quellen gegen Debian 13 bekommen. Jetzt aus/etc/os-release, unbekannte Releases brechen ab. - WireGuard-Sackgasse: Die Tunnel-IP wurde gespeichert, bevor der Tunnel bewiesen war; danach wählte jeder Verbindungsversuch inklusive Konsolen-Wiederholung diesen kaputten Tunnel, und der reparierende Code war unerreichbar. Rettung nur per Datenbank-Handarbeit. Jetzt gilt der Tunnel erst mit bewiesenem Handshake.
- wg0 hätte den Neustart nicht sicher überlebt (
||-Fallback verdeckte die fehlende systemd-Aktivierung); der Neustart konnte eine nicht mehr bootende Maschine hinterlassen;ConfigureProxmoxtat nichts (keinvmbr0, Datacenter- Firewall aus → die „nur 80/443"-Regeln der Kunden-VMs waren wirkungslos); der Rolle fehlteSys.Modify(jede Kundenbereitstellung wäre am Backup-Schritt gestorben); nftables verwarf jedes ICMP (IPv6 und PMTUD gebrochen);api_token_refhing anAPP_KEYstatt amSECRETS_KEY. - Neu:
VerifyVmTemplate— ein Host geht nicht mehr auf „aktiv", wenn die Vorlage fehlt. Vorher starb der erste bezahlte Auftrag nach der Zahlung.
VM-Erstellung
- Das 14-tägige Kapazitätsparken war unerreichbar: Abfrageintervall 120 s gegen Schritt-Zeitlimit 60 s, also lief jeder Wiedereinstieg in eine Zeitüberschreitung. Ein bezahlter Auftrag ohne freien Host war nach sechs Minuten ein Fehlerfall. Die ganze Warteschlange war toter Code.
- Jeder
occ-Aufruf lief als root stattwww-data; Nextcloud verweigert das. Jetzt ein gemeinsamer Erbauer (App\Support\NextcloudOcc) plus ein Test, der jede neuedocker compose exec-Stelle außerhalb davon verbietet. - Ein einzelner zuckender HTTPS-Test verurteilte eine fertige Cloud zum Fehlerfall.
- Eine nachgewiesene eigene Domain wurde nie geroutet, wenn zufällig ein Neustart lief; gekaufter Speicher wurde abgerechnet und nie geliefert. Der „läuft schon etwas?"-Schutz fragt jetzt, ob der laufende Lauf die Arbeit tatsächlich abdeckt.
- Die
address-Pipeline lief in der falschen Reihenfolge (Zertifikat vor vertrauenswürdigem Hostnamen → gültiges Zertifikat auf Nextclouds „untrusted domain"-Fehlerseite). - Eine geänderte Gast-IP war unreparierbar (502 ohne Weg zurück).
- Der Hetzner-DNS-Abgleich las nur die erste Seite → ab ~100 Einträgen ein zweiter A-Record für denselben Namen.
3. Was noch zu tun ist — vier Blöcke, in dieser Reihenfolge
Block A — Goldene VM-Vorlage (kritischer Pfad, blockiert alles)
Es gibt keine Vorlage auf template_vmid 9000. Ohne sie kann nichts ausgeliefert
werden. Zu tun:
- Debian-13-VM auf dem dedizierten Server, ~20 GB Platte, cloud-init-fähig
(
qemu-guest-agentmuss drin sein —WaitForGuestAgenthängt daran). - Docker + Compose,
/opt/nextcloud/mitdocker-compose.yml, dasNEXTCLOUD_ADMIN_USER/NEXTCLOUD_ADMIN_PASSWORD/MYSQL_PASSWORDaus.envliest und beim ersten Start selbst installiert. user: www-dataamapp-Dienst — sonst schlagen alleocc-Aufrufe fehl.- Partition als letzte auf der Platte, kein LVM —
GrowGuestFilesystemkann eine nicht-letzte Partition nicht vergrößern und erkennt LVM bewusst nicht (es würde sonst ein Kontingent über ein nicht gewachsenes Dateisystem schreiben). - Als Proxmox-Vorlage konvertieren, vmid 9000.
- Schriftliches Runbook dazu, versioniert im Repo. Die Vorlage ist ab dann Ihr wichtigstes Artefakt und gehört nicht in mündliches Wissen.
Block B — vmbr0 vollautomatisch (riskantester Code des Vorhabens)
Eigener Schritt, eigener Abnahmetest. Muster:
- Aktuelle Netzkonfiguration sichern.
- Systemd-Timer auf 5 Minuten, der sie zurückspielt und neu startet.
- Brücke aktivieren.
- Von außen prüfen, ob der Host antwortet.
- Erst dann den Timer abbestellen.
Der Schritt muss erkennen, was er vorfindet — DHCP auf einer Schnittstelle
(Cloud) gegen geroutete Einzel-IP mit Gateway außerhalb des Subnetzes
(Hetzner-Dedicated) gegen netcup. Eine Brücke, die für den einen Fall richtig ist,
nimmt den anderen vom Netz. ConfigureProxmox bricht heute bewusst mit einer
Anleitung ab; dieser Block ersetzt das.
Block C — Traefik als Systemdienst
Neuer Schritt zwischen SecureHostFirewall und CompleteHostOnboarding:
- Binary installieren, systemd-Unit, statische Konfiguration mit
websecure-Entrypoint undletsencrypt-certResolver, deren Namen zu dem passen müssen, wasSshTraefikWriter::render()schon ausgibt — hier zuerst nachlesen, nicht raten. - Dynamisches Verzeichnis anlegen (
TRAEFIK_DYNAMIC_PATH, Vorgabe/etc/traefik/dynamic), Dateiprovider darauf zeigen. - Prüfen, nicht nur installieren: Dienst läuft, 80/443 belegt, Ping-Endpunkt antwortet. Ein Host darf nicht „aktiv" werden, wenn das Routing nicht steht.
- ACME braucht Port 80 erreichbar — die Firewall öffnet ihn, aber Traefik muss lauschen, bevor das erste Zertifikat angefordert wird.
Block D — Kleinigkeiten und eine kaufmännische Entscheidung
- Rollen-Konvergenz auf bereits aktiven Hosts.
Sys.Modifyerreicht sie nicht: ein abgeschlossenes Onboarding kann nicht wiederholt werden, ein Kommando fehlt. - Traffic-Pakete (Ihre Entscheidung): Der Auftrag ist einmalig, das Feld
traffic_addonsist ein dauerhafter Zähler. Hochzählen = 1000 GB für immer aus einer Zahlung; nichts tun = der Kunde zahlte für nichts. Der Code protokolliert das derzeit laut, statt still eines von beiden zu tun. pveum acl modify … || trueundpveum user add … || trueverdecken weiter Fehler — dieselbe Klasse wie der behobenerole add.VerifyVmTemplateprüft nur Existenz, nichttemplate: 1.applyFirewall()setzt nichtfirewall=1annet0der VM — ohne das greifen die Gastregeln trotz Datacenter-Firewall nicht. Prüfen, ob die Vorlage es mitbringt.Erledigt — eindeutig seitstripe_addon_prices.stripe_price_idist nicht eindeutig (Plan-Seite schon): zwei Zeilen könnten einen Preis teilen, Archivieren würde den anderen mitentziehen.2026_07_31_210000_one_row_per_stripe_price; zugleich das Netz unter dem Wiedererkennungsschritt, siehedocs/superpowers/specs/2026-07-30-stripe-price-adoption-design.md.clupilot:end-due-servicesschließt den Vertrag nicht — ein geschenkter Vertrag bleibt nach Ende der Instanz für immeractiveund zählt in Umsatz/Dashboard mit.- Eine Abbuchung, die vor dem Widerruf entstand und danach bezahlt wird, bekommt einen Beleg, wird aber nicht storniert oder erstattet — ohne Signal.
Livewire\Admin\Provisioning::isStale()erbt das neue 14-Tage-Budget: ein geparkter Lauf mit totem Worker wird zwei Wochen nicht als hängend gemeldet.
4. Was gesetzt sein muss, bevor ein echter Lauf Sinn hat
Auf diesem Entwicklungsserver ist alles vorhanden; auf dem Live-Server ist es ungeprüft. Vor dem Durchlauf einzeln bestätigen — jedes fehlende Stück bricht die Kette an einer anderen Stelle, und mehrere brechen sie nach der Zahlung.
Geheimnisse (Vault, Konsole → Integrationen):
ssh.private_key— ohne:EstablishSshTrustscheitert mit einem phpseclib-Parserfehlerdns.token— Hetzner-DNS-Token. Ohne: der A-Record der VM entsteht nicht, der Kunde hat keine Adresse. Braucht Schreibrecht auf die Zone.stripe.secretundSTRIPE_WEBHOOK_SECRET— ohne: kein Auftrag entsteht überhauptmonitoring.token— Uptime-Kuma-Brücke,RegisterMonitoringSECRETS_KEY— seit heute Nacht auch fürhosts.api_token_ref. Fehlt er, ist jeder Host-Token unlesbar und keine Bereitstellung läuft.VPN_CONFIG_KEY
Einstellungen:
CLUPILOT_DNS_ZONE=clupilot.cloud(nicht.com). Vorgabe im Code ist richtig; auf diesem Server stand in der.envversehentlich.com.CLUPILOT_WG_HUB_PUBKEY,CLUPILOT_WG_ENDPOINT,CLUPILOT_WG_SUBNET— und der Hub muss laufen und auf 51820/udp erreichbar sein. Die Hosts sind Initiatoren; der offene UDP-Port ist eine Anforderung an den Hub, nicht an die Hosts.CLUPILOT_DNS_HOSTS_DIR— dnsmasq-Verzeichnis für die internen Hostnamen, geteiltes Volume mit demvpn-dns-ContainerTRAEFIK_DYNAMIC_PATH- Firmendaten vollständig (Name, Adresse, PLZ, Ort, UID) und Steuersatz —
sonst verweigert
IssueInvoice, und der Kunde bekommt keinen Beleg - Mindestens ein Rechenzentrum angelegt —
ValidateHostInputverlangt es - Postfächer eingerichtet — sonst erreicht die Zugangsdaten-Mail den Kunden nicht
Betrieb:
queue,queue-provisioning,schedulerundreverblaufen — die Pipeline ist warteschlangengetrieben, ohne Worker passiert schlicht nichts- Stripe-Katalog nach dem Update:
stripe:sync-catalogueundstripe:reprice-subscriptions(je zuerst--dry-run). Der Abgleich legt jetzt zwei Preise je Paket an; bis er lief, kann ein geprüfter EU-Firmenkunde nicht bestellen — das ist Absicht.
5. Reihenfolge des ersten echten Durchlaufs
- Testpaket „Test" in der Konsole anlegen (20 GB / 1 GB / 1 Kern / 5 GB) und veröffentlichen.
- Abschnitt 4 durchgehen und jedes Stück bestätigen.
- Debian 13 auf dem dedizierten Server, Host in der Konsole anlegen, onboarden.
Erwartung nach Block B/C: läuft bis
activedurch. Ohne Block B bricht es beivmbr0ab — das ist derzeit gewollt. - Vorlage bauen (Block A),
VerifyVmTemplatemuss sie finden. - Testpaket kaufen. Erwartung: 16 Schritte bis
CompleteProvisioning, Nextcloud erreichbar unter<subdomain>.clupilot.cloudmit gültigem Zertifikat, Speicherkontingent greift, Zugangsdaten-Mail kommt an. - Eigene Domain hinterlegen, TXT-Nachweis, prüfen, dass sie wirklich ausgeliefert wird (nicht nur „nachgewiesen").
- Kündigen. Prüfen, dass Stripe es erfährt, die Leistung zum Periodenende endet, Route und DNS-Eintrag verschwinden.
- Zweiten Kauf als Privatperson, innerhalb 14 Tagen widerrufen. Prüfen: volle Erstattung, Stornorechnung, Leistung endet.
Jeder Schritt, der scheitert, ist ein Fund — und der Server darf dabei kaputtgehen, Debian ist neu eingespielt in Minuten.
6. Arbeitsweise in dieser Codebasis (für die neue Session)
- Eine zweite Claude-Session arbeitet im selben Arbeitsverzeichnis und im selben
Git-Index. In dieser Nacht hat ein einziges
git commitohne Pfadangabe deren vorgemerkte Dateien mitgenommen und dabeiVERSIONsowie zwei ihrer Dateien zurückgedreht. Immergit add -- <pfade>undgit commit -F - -- <pfade>, niegit add -A,git add .,git commit -aoder ein nacktesgit commit. pintnur auf eigene Pfade, nie--dirty— das Repo ist unter der Standardvorgabe nicht durchgängig formatiert und würde fremde Dateien umschreiben.- Regeln R11–R24 in
CLAUDE.mdsind nicht verhandelbar und werden per Test erzwungen. R22 besonders: eine Prüfrunde, eine Fix-Runde, dann parken. - Tests laufen auf SQLite im Speicher (
phpunit.xmlerzwingt es), gleichzeitige Läufe kollidieren also nicht, und die Entwicklungsdatenbank wird nie angefasst. - Bricht die ganze Suite auf einmal zusammen, ist das fast immer eine Datei der anderen Session mitten im Schreiben — einmal neu laufen lassen, bevor man sucht.
- Push:
git push "https://x-access-token:$TOKEN@git.bave.dev/boban/CluPilotCloud.git" mainmitGIT_ACCESS_TOKENaus der.env; ein einfachesgit pushscheitert. - Ein Update auf dem Live-Server greift nur bei einem
v*-Tag —VERSIONanheben, taggen, Tag mitpushen.