Der Schritt haengt jetzt zwischen RebootIntoPveKernel und
ConfigureProxmox. Nach dem Neustart, weil dort ifupdown2 steht und der
Tunnel gerade bewiesen hat, dass er einen Neustart ueberlebt. Vor
ConfigureProxmox, dessen refuseWithoutBridge() unveraendert stehen bleibt
und damit zur Nachpruefung wird — bauen UND pruefen, dasselbe Paar wie
BuildVmTemplate -> VerifyVmTemplate.
Beschriftung in beiden Sprachen, und ein Test verlangt das kuenftig von
JEDEM Pipeline-Schritt statt nur vom neuen: wer einen einhaengt und die
Sprachdateien vergisst, faellt im Test auf statt in der Konsole.
bridge.sh und bridge-run.sh gehoeren ins Bootstrap-Archiv — es ist der
einzige Weg, auf dem das Skript auf eine nackte Maschine kommt.
Der End-to-End-Test lief rot, und zu Recht: sein Attrappen-Host sagte
nichts ueber sein Netz, also lehnte der neue Schritt ab. Das war der
Beweis, dass er wirklich haengt. Er bekommt jetzt einen Host, der seine
Bruecke schon hat — den Bau-Pfad kann eine Attrappe nicht nachstellen,
er lebt davon, dass die Verbindung abreisst und wiederkommt. Dafuer gibt
es die Schritt-Tests und die drei Sandkasten-Laeufe.
scriptBridgeState/scriptBridgeStatus liegen in tests/Pest.php, nicht in
einer einzelnen Testdatei — zwei Dateien brauchen sie.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Übernahme brach in "WireGuard einrichten" ab: fünf Wiederholungen,
"WireGuard handshake not up yet". Geprüft wurde mit `ping -c1 -W2 <hub-ip>`.
Das fragte drei Dinge auf einmal und nannte nur eines: ob der Handshake steht,
ob ICMP durchkommt — und ob es `ping` auf der Maschine überhaupt GIBT.
Auf Hetzners `Debian-trixie-latest-amd64-base` gibt es das nicht. `iputils-ping`
ist im Image nicht dabei, und PrepareBaseSystem installiert
`curl gnupg ifupdown2 chrony`. Der Schritt scheiterte damit über einem Tunnel,
der stehen konnte. Auf dem alten Proxmox-Image war ping dabei, deshalb lief es
dort durch — dieselbe Pipeline, anderes Grundsystem.
Gefragt wird jetzt WireGuard selbst: `date +%s; wg show wg0 latest-handshakes`.
Die Uhr des HOSTS kommt in derselben Antwort mit, weil `latest-handshakes` eine
absolute Zeit ausgibt und ein Vergleich gegen UNSERE Uhr eine Zeitverschiebung
zwischen zwei Maschinen als Tunnelzustand läse. `date` ist in coreutils und
überall da.
Codex, zwei Runden:
- P1: `> 0` hiesse "hat jemals". WireGuard behält den Zeitstempel unbegrenzt,
also meldete ein Wiederholungslauf über einem toten Tunnel "steht", und die
Schritte danach wählten die Tunneladresse für SSH. Jetzt muss der Handshake
frisch sein (180 s) und vom KONFIGURIERTEN Hub kommen — ein fremder Peer auf
wg0 ist kein Beweis dafür, dass wir erreichbar sind.
- P1: Der neue Host-Versatz (.100) liess die Vergabe in einem Subnetz kleiner
als /26 "erschöpft" melden, obwohl unten alles frei war. Der Versatz ist eine
Bevorzugung, keine Bedingung: zweiter Durchgang von vorn.
Ausserdem, wie gewünscht: Hosts bekommen ihre Tunneladresse ab .100
(CLUPILOT_WG_HOST_OFFSET), Personen zählen weiter von unten. Fortlaufend
vergeben landete der erste Host zwischen zwei Notebooks, und wer eine Adresse
in einem Protokoll las, konnte nicht sagen, ob dahinter ein Mensch oder eine
Maschine steht.
2243 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Task 5: Billing::purchase() now checks SecretVault for a stripe.secret before
touching anything else — no customer, no plan lookup, no Order row — and the
customer reads a plain sentence instead of a 500 page. HttpStripeClient's own
secret() throws the same StripeNotConfigured exception rather than sending a
request with an empty bearer token and letting Stripe's 401 stand in for the
real cause.
isConfigured() was fixed alongside it: it used to call secret() too, which
would have turned every "is Stripe set up?" check (the checkout controller,
the catalogue sync commands) into an uncaught exception the moment a key went
missing — the opposite of what this task is for. It now reads the vault
directly.
The new guard is unconditional, including for the cart-only purchase types
(storage, addon, plan changes) that never call Stripe themselves — a cart
order nobody can ever pay for is pointless to create. That broke every
existing Billing/Cart/Downgrade test that calls purchase() without a stored
key; each affected fixture now deposits one via the new withStripeSecret()
Pest helper. A few of those tests (CustomDomainAccessTest, DowngradeTest) were
passing already but for the wrong reason — the new guard, not the check they
were named for — since both only assert an order was NOT created; they get
the same fixture fix so they still prove what they claim.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
env('MAIL_HOST')/env('MAIL_PORT') returning anything, or MAIL_URL supplying
either, now counts as "supplied" and wins over the sentinel comparison — a
relay genuinely on 127.0.0.1:2525 no longer reads as unconfigured. The
sentinel stays as a fallback for when neither signal says anything (including
under config:cache, where env() goes null and this collapses to today's
behaviour).
The seed migration also guards against SECRETS_KEY being unset at migrate
time (encrypting the .env password would otherwise throw and take the
whole migration down, on a fresh install as much as in the test suite),
and the four tests/Feature/Mail files written before this migration reset
the mailbox table in their beforeEach so they keep testing what they did
before mailboxes.key collided with the seeded rows.
DNS names are case-insensitive and Symfony's getHost() always returns lowercase,
so ADMIN_HOSTS=Admin.Example.com rejected every request and locked operators
out. Normalised in the config AND at the comparison, so the value is safe
whichever route it took in (env, cached config, runtime set).
operator()/admin() moved from RbacTest/HostManagementTest to tests/Pest.php:
they were only loaded when those files happened to be in the run, so a targeted
single-file run died on 'undefined function operator()'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- CreateCustomerAdmin puts OC_PASS on the docker invocation (survives the &&).
- RegisterBackup creates a real vzdump job via ProxmoxClient::createBackupJob.
- RegisterMonitoring registers via a new MonitoringClient service (interface +
fake + http). Both persist the external id [E] before the breadcrumb, so
acceptance reflects a real registration, not a fabricated row.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>