Commit Graph

663 Commits (55b749c129df0dcf59b2cd10d434b7d49b3f45d5)

Author SHA1 Message Date
nexxo 55b749c129 Release v1.3.94 — die Kurven zeigen wieder, wie ausgelastet der Host ist
tests / pest (push) Failing after 9m19s Details
tests / assets (push) Successful in 21s Details
tests / release (push) Has been skipped Details
Drei Punkte des Besitzers, und der dritte hat den eigentlichen Fehler
freigelegt.

Die Verlaufslinien sahen komisch aus, weil sie auf ihr EIGENES Minimum und
Maximum skalierten. Ein Host mit 0,2 % CPU, der zwischen 0,1 und 0,3 schwankt,
zeichnete damit einen Seismographen über die volle Höhe — und widersprach flach
der Zahl direkt daneben. Dieselbe Regel stand im Chart.js-Entwurf schon
ausformuliert ("eine automatisch skalierte Achse lässt 3 % wie eine Wand
aussehen") und ging beim Umstieg auf die Kacheln verloren.

x-ui.spark bekommt deshalb `min`/`max`. Ohne Angabe bleibt alles wie bisher —
das ist, was jeder bestehende Aufrufer übergibt. Die Prozent-Kacheln geben
0 und 100 mit, die Netz-Kacheln nur die 0, weil MiB/s keine natürliche
Obergrenze haben. Ein untätiger Host zeichnet jetzt vier ruhige Linien statt
zweier wogender und zweier flacher — vorher wirkte CPU (0,2 %) belebter als RAM
(3,6 %), also genau verkehrt herum.

Dazu: Werte außerhalb der Grenzen werden geklemmt statt aus dem Kasten
gezeichnet, die Fläche ist ein Verlauf statt einer harten Kante, und die
Linien sind mit 120×40 statt 80×32 lesbar.

Ich hatte max=100 zwischendurch selbst verworfen, weil es "zu tot" aussah — auf
einem Bild, auf dem die Füllung wegen eines CSS-Fehlers gar nicht gezeichnet
wurde. Ein Vergleich mit einem kaputten Bild. Der Fehler: `.spark path
{ fill: none }` schlägt als CSS-Regel das Präsentationsattribut fill="url(#…)".
`fill: none` gehört an die Linie, nicht an jeden Pfad.

Die Ausstattung ist wieder einzeilig. Sechs Felder, sechs Spalten — und die
Bau-Kennung steht nur noch im Titel: als eigene Zeile zwang sie die ganze Tafel
in eine zweite Reihe, für eine Zeichenkette, die fast niemand liest.

Und die Wartezeit: die Seite stößt beim Öffnen eine Sammlung an, wenn noch
keine Messwerte da sind, statt bis zum nächsten minütlichen Lauf leer zu
bleiben. Ein leerer Kasten liest sich als "kaputt", nicht als "gleich". Der Job
ist ShouldBeUnique, zwei geöffnete Seiten reihen also keine zwei ein.

Codex-Befund dazu (P2): das galt auch für Hosts, die der Sammler ohnehin
überspringt — ein Host mitten in der Übernahme hat keinen Token, und seine
Seite hätte die ganze Flotte abgeklappert und die eigenen Kacheln trotzdem leer
gelassen. Wer gesammelt wird, steht jetzt einmal am Modell (Host::collectable),
gelesen von Job und Seite. Zwei Fassungen dieser Frage waren genau der Grund.

Geprüft: 2307 Tests grün, Pint sauber, Codex ohne Befund. Der Test für den
Maßstab prüft jetzt auch die echten Aufrufstellen und nicht nur das Bauteil —
gegengeprobt durch Entfernen von max=100, dann fällt er. Im Browser beide
Fälle angesehen: ruhiger Host vier flache Linien, ausgelasteter Host lesbare
Form, null Konsolenfehler.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 11:56:10 +02:00
nexxo a7c7f81131 Entwurf: geprueft, dass ein laufender Vertrag seine Version einfriert
Die Frage des Betreibers war, ob die Umschaltung einem Startkunden mit 100 GB
ein Kontingent von 30 GB auf eine volle Cloud schreibt. Nachgesehen statt
vermutet: snapshotFrom() kopiert die Groessen beim Kauf auf die Vertragszeile,
CustomerStep::plan() liest die Vertragsspalten, StorageAllowance::for() nimmt
instances.quota_gb — den Katalog fragt keiner von ihnen.

Die Pakete sind also sicher, die Module nicht: dort steht die Packungsgroesse
live in der Konfiguration. Die Reihenfolge gilt fuer die Bloecke, und fuer die
gilt sie zwingend.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 11:55:30 +02:00
nexxo e4d4fd0cfb Entwurf: die neuen Pakete
Die Leiter wird auf die Maschine zugeschnitten, die dasteht. Speicher bindet die
Kundenzahl je Host, nicht Rechenleistung: die alte Leiter band bei drei Kunden je
Server und ließ sechs RAM-Plaetze verfallen. Neu sind es neun Kunden a 40 GB
Platte, 351 EUR statt 147 EUR je Server.

Kopfraum waechst mit: max(10 GB, 12 %). Eine 175-GB-Nextcloud mit 50 Nutzern hat
eine groessere Datenbank und mehr Versionsstaende als eine mit drei Leuten, und
eine volle Platte legt nicht den Upload still, sondern die Instanz.

Der Zusatzblock geht auf 20 GB fuer 15 EUR und ist bei drei gedeckelt. Bei 12 EUR
waere er mit 0,60 EUR/GB billiger gewesen als jeder Aufstieg (0,73 und 0,67) —
dann stapeln Kunden, statt umzusteigen, und belegen den knappsten Rohstoff zum
niedrigsten Preis. Ein Block belegt 22 GB Platte, damit die Kopfraum-Regel auch
fuer ein gestapeltes Paket gilt.

Drei Reparaturen haengen daran, und eine davon steht seit laengerem im Code
angeschrieben: StorageAllowance liest die Packungsgroesse live aus der
Konfiguration, mit dem Vermerk, dass sie an dem Tag auf die Buchung gehoert, an
dem sie sich bewegt. Dieser Tag ist heute — ohne die Aenderung schrumpft ein
gekaufter 100-GB-Block in der Sekunde der Umstellung auf 20 GB.

Das Testpaket heisst Intern und verschwindet aus dem Verkauf, bleibt aber
verschenkbar. Enterprise verlaesst den Shop, weil 500 GB auf 388 GB keinen Platz
finden und der Kunde das erst nach der Zahlung erfuehre. Und damit "eigener
Server" mehr ist als ein Wort: Hosts bekommen eine Reservierung — heute nimmt
placeableIn() jeden aktiven Host, und die Kapazitaetszahlen zaehlen eine exklusiv
verkaufte Maschine mit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 11:50:42 +02:00
nexxo 5cae870dd9 Entwurf: vmbr0 automatisch bauen
Der letzte Handgriff der Host-Uebernahme. Derselbe Mechanismus wie
BuildVmTemplate — bridge.sh + bridge-run.sh hochladen und abgekoppelt
fahren, nicht in PHP nachbauen.

Die tragenden Punkte:

- Abgekoppelt ist Bedingung, nicht Optimierung: ifreload -a nimmt die
  Leitung, ueber die der Befehl laeuft.
- Der Zeitgeber steht vor jeder Aenderung, und nur er stellt zurueck.
  Kein zweiter Ruecknahmeweg.
- Abbestellt wird erst, wenn BEIDE Richtungen stimmen: Internet
  erreichbar UND frischer WireGuard-Handshake. Nur die erste zu pruefen
  laesst den Fall zu, in dem der Host oeffentlich lebt, CluPilot
  ausgesperrt ist und die Rueckfahrkarte gerade weggeworfen wurde.
- Kein ping — Hetzners Debian-Basis hat keins, und network.sh:190 haette
  deshalb immer 'nicht erreichbar' gesagt.
- Bruecke schon da heisst nichts anfassen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 11:47:38 +02:00
nexxo dc35e5310f Release v1.3.93 — die Seite konnte den Host gar nicht fragen
tests / pest (push) Failing after 9m23s Details
tests / assets (push) Successful in 28s Details
tests / release (push) Has been skipped Details
Auf echter Hardware blieb jede Messkachel leer, während der Host sichtbar
online war und vor zwei Minuten geantwortet hatte. Kein Zufall und kein
Aussetzer: ein Entwurfsfehler.

Nur der queue-provisioning-Container hängt im WireGuard-Netz — NET_ADMIN,
/dev/net/tun, das wireguard-Volume. Der app-Container, der die Konsole rendert,
hat überhaupt keine Route zu 10.66.0.x. Ich hatte den Proxmox-Aufruf in
render() gelegt, also in den einen Container, der die Management-Adresse nicht
erreichen kann. Ein Aufruf von dort konnte nie etwas anderes sein als eine
Zeitüberschreitung.

PingHosts schreibt dieselbe Regel seit Langem in seinen Kopf: "runs on the
provisioning queue, which is where the Proxmox credentials are usable." Ich
habe sie gelesen und nicht angewendet.

Verschlimmert hat es mein eigenes catch (Throwable): der Grund wurde
verschluckt, und die Kachel sagte "keine Messwerte" — ununterscheidbar davon,
dass der Host schweigt. Auf dem Testhost fiel nichts auf, weil TEST-NET
ohnehin nie antwortet.

Jetzt zwei Hälften:

- HostLoadSeries::collect() holt und legt ab, aus Jobs\CollectHostLoad auf der
  provisioning-Warteschlange, minütlich — der Takt, in dem Proxmox einen
  frischen Messwert schreibt. Ein Fehlschlag wird protokolliert, mit Host,
  Node und Grund.
- HostLoadSeries::forHost() liest nur aus dem Zwischenspeicher und öffnet nie
  eine Verbindung. Ein Test hält das mit Http::assertNothingSent() fest.

Der Eintrag lebt fünf Minuten bei minütlichem Sammeln: länger als der Takt,
damit ein ausgefallener Lauf keine Seite leert, die eine Sekunde vorher in
Ordnung war — und kurz genug, dass ein stehengebliebener Sammler die Zahlen
mitnimmt, statt eine alte Stunde als aktuell stehenzulassen.

Der Sammler ist ShouldBeUnique (Codex-Befund, P1). Die provisioning-Warteschlange
ist DIESELBE, auf der Kunden-Bereitstellung läuft; ein stiller Host kostet den
vollen HTTP-Zeitablauf, und ohne diese Sperre stauten sich minütlich neue Läufe
hinter dem alten und verzögerten bezahlte Arbeit. Dasselbe Mittel, das
CollectInstanceTraffic nebenan schon benutzt.

Dazu: der Zustands-Punkt war mit 62 px so groß wie der Speicher-Ring nebenan.
Eine gefüllte Scheibe wiegt optisch weit mehr als ein dünner Ring und erschlug
die Kachel — jetzt 32 px.

Und ein Test, der aus Versehen recht behielt: die Kachel-Prüfung verglich mit
"50", was auch die 500 GB in der Instanzenliste darunter trifft. Sie prüft
jetzt Zahlen, die sonst nirgends auf der Seite vorkommen.

Noch offen, nicht hier angefasst: VmTemplateCheck fragt die Proxmox-API
ebenfalls aus dem app-Container heraus, von der Bereitschaftsseite aus. Selber
Fehler, Bestand, eigener Punkt.

Geprüft: 2299 Tests grün, Pint sauber, Codex ohne Befund. Im Browser mit
eingespielten Messwerten: sechs gefüllte Kacheln, Zustands-Scheibe in
Proportion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 11:23:00 +02:00
nexxo 589489a9fc Release v1.3.92 — sechs Kacheln statt dreier ungleicher Kästen
tests / pest (push) Failing after 10m15s Details
tests / assets (push) Successful in 28s Details
tests / release (push) Has been skipped Details
Nach der Vorlage des Besitzers. Zustand, Speicher und die gemeinsame Kurve
standen nebeneinander: zwei davon halb leer, die dritte überfüllt, und die
gemeinsame Kurve brauchte eine Legende, um zu sagen, welche Linie welche ist.

Eine Reihe je Kachel löst alle drei Beschwerden auf einmal. Die Beschriftung
der Kachel benennt die Reihe, also braucht es keine Legende mehr. Die Höhen
sind durch das Raster gleich statt zufällig. Und der leere Platz ist mit
Messwerten gefüllt, die es ohnehin schon gab: Proxmox' Aufzeichnung liefert
Netzdurchsatz in beide Richtungen mit, ungefragt.

Sechs Kacheln: CPU-Auslastung, RAM-Auslastung, Speicher (als Ring), Eingehend,
Ausgehend, Zustand. Gebaut mit x-ui.metric, x-ui.spark und x-ui.ring — die
gab es alle schon, und der Kopfkommentar von x-ui.metric sagt selbst "exactly
as the approved template draws it". Nichts daneben neu gebaut.

Die öffentliche IP steht jetzt auf der Seite. Sie stand vorher NUR klein unter
der Überschrift — die Adresse, unter der der Host wirklich erreichbar ist, war
in der Detailseite nirgends ein Feld. Sie führt jetzt die Ausstattungs-Tafel
an, und die Reserve-Eingabe ist mit dorthin gezogen: die Kacheln zeigen, was
gemessen wurde, die Tafel, was eingestellt ist. Ein Eingabefeld zwischen
Messwerten sähe aus, als ließe sich eine Messung ändern.

Alle vier Verlaufslinien tragen denselben Ton. Die Regel steht im Bauteil
selbst — "muted where the figure is observed, accent where it can be acted on"
—, und hier ist keine Zahl anzufassen. Vier verschiedene Töne nebeneinander
behaupten einen Unterschied, den es nicht gibt.

Zwei Funde aus der Prüfung
--------------------------

- x-ui.spark warf fehlende Messwerte per array_filter heraus und verband die
  Nachbarn. Zwei Fehler auf einmal: die Linie behauptete eine Messung, die es
  nicht gab, und alles danach rutschte nach links — eine Stunde mit zwei Lücken
  zeichnete sich als achtundfünfzig Minuten. Die x-Lage kommt jetzt aus dem
  Platz in der URSPRÜNGLICHEN Reihe, und zusammenhängende Messwerte werden als
  eigene Züge gezeichnet. Eine saubere Reihe ergibt genau einen Zug und
  dasselbe Bild wie vorher, was alle bisherigen Aufrufer liefern.

- Der Zwischenspeicher überlebt einen Deploy. Ein Eintrag aus v1.3.91 kennt
  netin/netout nicht, und die Host-Seite wäre 55 Sekunden lang an einem
  fehlenden Schlüssel gestorben — genau in der Minute, in der jemand nachsieht,
  ob das Update durch ist. Der Schlüssel heißt jetzt host-load:v2:<id> und
  wandert mit der Form mit.

Und einer, den kein Prüfer gemeldet hat: beim Zerlegen in Züge stand im
Flächenpfad ein `L` unmittelbar vor einem `M`. Gültig gelesen, nicht
gezeichnet — die Füllung verschwand still. Aufgefallen ist es beim Ansehen der
Seite, nicht durch eine Meldung; jetzt prüft ein Test, dass jeder Flächenpfad
mit M anfängt, mit Z endet und keinen Befehl direkt hinter einem anderen trägt.

x-ui.chart behält seinen update-on-Weg, obwohl diese Seite ihn nicht mehr
benutzt: er ist eine geprüfte Fähigkeit des gemeinsamen Bauteils, und der
darunterliegende Fix (Instanz aus dem reaktiven Alpine-Objekt) gilt für jeden
Chart.

Geprüft: 2291 Tests grün, Pint sauber, Codex ohne Befund. Im Browser mit
eingespielten Messwerten: sechs Kacheln, Lücke als echte Aussparung in Linie
UND Fläche, Leerzustand zeigt "—" statt einer Null, null Konsolenfehler über
einen vollen Poll-Zyklus.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 11:02:55 +02:00
nexxo 665a0c4e08 Release v1.3.91 — die Host-Seite zeigt Last statt Ausstattung
tests / pest (push) Failing after 9m29s Details
tests / assets (push) Successful in 20s Details
tests / release (push) Has been skipped Details
Die Karte "Rechenleistung" zeigte keine Leistung. 12 Kerne und 63 GB sind die
Ausstattung des Blechs und ändern sich nie — sie standen aber im selben
Kartenraster wie "Zustand" und "Speicher", die beide leben. Wer die Seite
öffnete, um zu sehen, wie es dem Host geht, las dort eine Zahl, die das nie
sagen konnte.

An ihrer Stelle steht jetzt die Last: CPU und RAM als Stundenkurve, beide in
Prozent auf EINER Achse, dazu die aktuellen Werte als beschriftete Zahlen.

Die Geschichte kommt aus Proxmox' eigener Aufzeichnung
(/nodes/{node}/rrddata), nicht aus einem eigenen Sampler. Ein Sampler hieße
neue Tabelle, minütlicher Job, Aufräum-Job und ~1440 Zeilen je Host und Tag —
um weniger genau nachzubauen, was ohnehin auf der Platte liegt. Die RRD ist ab
der ersten Sekunde gefüllt, auch für die Stunde vor dem ersten Hinsehen, und
kann nicht von dem abweichen, was Proxmox' eigene Oberfläche zeigt.

Eine Lücke bleibt eine Lücke: ein Punkt ohne Messwert wird null, nie 0, und
spanGaps steht auf false. Dieselbe Regel wie in instance_metrics. Antwortet der
Host gar nicht, sagt die Tafel das in einem Satz, statt eine ruhige Stunde zu
zeichnen — auf dem Testhost live bestätigt.

Der Fehler, den das ans Licht gebracht hat
------------------------------------------

x-ui.chart steht überall unter wire:ignore, sonst zerstört Livewire das Canvas.
Ein Poll erreicht den Chart also nie. Dafür bekam das Bauteil ein optionales
update-on: es hört auf ein Fenster-Ereignis und tauscht die Daten IM
bestehenden Chart.js-Objekt.

Das lief nicht. Die Zahlen neben der Kurve wanderten, die Kurve nicht, und
chart.update() starb still im Legenden-Plugin:

    TypeError: Cannot set properties of undefined (setting 'fullSize')

Grund: die Chart.js-Instanz lag als Eigenschaft im Alpine-Objekt und wurde
damit reaktiv umhüllt. Chart.js' Plugin-Innenleben überlebt das Proxy nicht.
Gemessen statt geschlossen: dieselbe Instanz wirft über das Proxy und läuft
über Alpine.raw(). Sie liegt jetzt in der Closure.

Aufgefallen ist es nie, weil bis zum ersten Live-Chart kein einziger Chart in
diesem Repo je update() gerufen hat — konstruieren und Erstzeichnen gehen durch
die Hülle noch. tests/Feature/ChartLiveUpdateTest.php hält die Regel fest,
damit der nächste Live-Chart nicht denselben Nachmittag kostet.

Der Rest
--------

- Version lesbar: "Proxmox VE 9.2.6" statt pve-manager/9.2.6/7f8d…, mitten in
  der Bau-Kennung abgeschnitten. Die Kennung steht klein darunter. Eine
  unerwartete Form wird unverändert durchgereicht statt verschluckt.
- Vier Kleinkarten (Mgmt-IP, Node, Version, Instanzen) sind eine
  Ausstattungs-Tafel geworden. Die Instanzen-Anzahl steht in der Überschrift
  der Liste, die sie ohnehin zeigt.
- Der Übernahme-Fortschritt klappt zu, sobald sie durch ist. Fünfzehn
  abgehakte Schritte sind auf einem laufenden Host kein Dauerinhalt —
  aufklappbar über <details>, ohne JavaScript.
- PlanVersion::requiredTemplateVmids() ersetzt die dritte Kopie derselben
  Fensterlogik.
- BuildVmTemplate sagt nicht mehr "this takes 10–20 minutes". Das war eine
  Schätzung vor dem ersten Lauf; gemessen waren es unter zwei. Eine Konsole,
  die falsch vorhersagt, erzieht dazu, sie zu ignorieren.

Geprüft: 2281 Tests grün, Pint sauber, Codex ohne Befund. Die Farbwahl gegen
den Validator gerechnet (ΔE 28,3 protan / 39,2 normal; der Akzent liegt unter
3:1 gegen die Fläche, deshalb tragen beide Reihen sichtbare Beschriftung). Im
Browser: null Konsolenfehler über einen vollen Poll-Zyklus, und die Kurve
wandert auf dem echten Poll ohne Neuladen — mit eingespielten Messwerten
belegt, weil die Testhosts in TEST-NET liegen und nie antworten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 10:24:21 +02:00
nexxo 5aedef2b35 Release v1.3.90 — nach dem Paket fragen, nicht nach dem Pfad
tests / pest (push) Failing after 9m12s Details
tests / assets (push) Successful in 23s Details
tests / release (push) Has been skipped Details
Der zweite Bau auf echter Hardware brach ab mit "qemu-guest-agent ist NICHT im
Abbild". Es war im Abbild. Die Prüfung suchte /usr/bin/qemu-ga, Debian legt den
Gastagenten nach /usr/sbin/qemu-ga.

Dass das Paket eingebaut war, stand im selben Protokoll: customise_cloud_image
ruft --install und danach --run-command 'systemctl enable qemu-guest-agent'. Ein
fehlgeschlagenes --run-command lässt virt-customize abbrechen, und systemctl
kann keine Unit aktivieren, die es nicht gibt. Der Lauf ging durch — also war
das Paket da.

Das ist zum zweiten Mal derselbe Fehler in dieser Datei: eine Eigenschaft über
einen NAMEN behauptet statt über die Sache. Erst die Partitionsnummer statt der
Lage auf der Platte (v1.3.89), jetzt ein Pfad statt der Verpackung. Ein Pfad ist
eine Vermutung darüber, wie ein Paket seine Dateien verteilt.

Gefragt wird jetzt /var/lib/dpkg/info/<paket>.list — die Aussage des Pakets über
sich selbst, und genau die Frage, die hier gestellt gehört: hat apt es still
übergangen? Ein einziges Verzeichnis-Listing für alle vier Pakete, weil jeder
virt-ls-Aufruf eine eigene Appliance startet.

Geprüft werden damit qemu-guest-agent (Falle 3), cloud-guest-utils für growpart
(Falle 4), docker-compose-plugin und docker-ce. Der Vergleich ist grep -qx: ein
vorhandenes docker-ce-cli.list darf nicht als docker-ce durchgehen —
durchgespielt, zusammen mit einem vollständigen und einem lückenhaften Listing.

Falle 1 bleibt eine Inhaltsprüfung, weil sie eine ist: user: www-data steht in
einer Datei, die wir selbst hineinkopieren, nicht in einem Paket.

Und die Meldung nennt jetzt die fehlenden Pakete beim Namen statt nur die Falle.

Bestätigt aus demselben Lauf: Falle 2 besteht seit v1.3.89 ("Wurzel auf
/dev/sda1 und nichts dahinter"), und der Umbau selbst — Docker, Compose,
Gastagent — lief in 38 Sekunden durch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 09:32:18 +02:00
nexxo 88dadf759a Release v1.3.89 — Falle 2 lehnte das einzige Abbild ab, für das sie gedacht war
tests / pest (push) Failing after 9m49s Details
tests / assets (push) Successful in 25s Details
tests / release (push) Has been skipped Details
Der erste echte Vorlagenbau brach ab mit:

  Die Root-Partition (/dev/sda1) ist nicht die letzte auf der Platte
  (/dev/sda15).

Das Abbild war in Ordnung, die Prüfung nicht. Sie verglich Partitions-NAMEN und
hielt für die letzte, was `sort -V` nach hinten sortiert. Debians Cloud-Abbild
legt die BIOS-Boot- und die EFI-Partition aber an den ANFANG der Platte und
nummeriert sie als 14 und 15; die Wurzel ist Nummer 1 und liegt physisch
dahinter. Gemessen: sda15 beginnt bei 4 MB, sda1 bei 128 MB.

Das ist kein Zufall dieses Abbilds, sondern genau die Eigenschaft, die Falle 2
verlangt — nur so kann growpart die Wurzel über den freien Rest ausdehnen. Die
Prüfung verweigerte also ein Abbild, weil es ihre Bedingung erfüllte.

Jetzt wird nach dem ANFANG auf der Platte gefragt statt nach der Nummer, über
`guestfish part-list`, das Offsets in Bytes liefert. "Nichts liegt dahinter" ist
eine Aussage über die Platte, nicht über die Benennung.

Die Auswertung steht in zwei eigenen Funktionen (last_partition_number,
partition_start), damit sie sich ohne Proxmox und ohne libguestfs gegen
aufgezeichnete part-list-Ausgaben durchspielen lässt — gemacht, in beide
Richtungen: Debians Anordnung besteht, eine Platte mit etwas hinter der Wurzel
fällt durch. Genau diese Gegenprobe fehlte, weshalb der Fehler bis auf echte
Hardware durchkam.

Und die Fehlermeldung trägt jetzt die Byte-Offsets beider Partitionen mit.
Scheitert das je wieder, steht die Platte im Protokoll statt einer Vermutung.

Nebenbei bestätigt: der VPN-Aussetzer vor dem Bau hat zweimal
"could not ask Proxmox about the VM templates" gemeldet und wiederholt, statt
einen Neubau auszulösen — der Codex-P1-Fix aus v1.3.88 hat am ersten Tag
verhindert, dass ein Netzausfall qm destroy --purge auslöst.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 09:21:54 +02:00
nexxo db125781ed Release v1.3.88 — die Vorlage baut sich selbst
tests / pest (push) Failing after 9m17s Details
tests / assets (push) Successful in 22s Details
tests / release (push) Has been skipped Details
Der letzte Handgriff in der Host-Übernahme fällt weg. VerifyVmTemplate meldete
bisher nur, dass eine Vorlage fehlt, weil niemand entschieden hatte, was in die
goldene Vorlage gehört. Entschieden ist es längst und steht in
deploy/bootstrap/lib/template.sh — der neue Schritt BuildVmTemplate lädt genau
diese Datei auf den Host und führt sie dort aus, statt ihre Prüfungen ein
zweites Mal in PHP zu haben.

Er läuft abgekoppelt und wird abgefragt: Abbild laden und drei
virt-customize-Läufe brauchen zehn bis zwanzig Minuten, ein einzelner
SSH-Aufruf liefe gegen den Befehlszeitablauf von 2000 s. "Läuft noch" heißt
dabei, dass der Prozess lebt (kill -0 gegen die hinterlegte PID) — in der
Statusdatei steht "running" auch dann noch, wenn niemand mehr da ist, der sie
ändert.

Fünf Fehler, die dabei aufgefallen sind und Geld gekostet hätten:

- qm importdisk hängte die Platte unter ${storage}:vm-9000-disk-0 ein. Der Name
  gilt nur bei Block-Ablagen; auf einer Verzeichnis-Ablage heißt sie
  local:9000/vm-9000-disk-0.qcow2 — also genau auf dem per Debian aufgesetzten
  Proxmox, um das es hier geht. Jetzt qm set --import-from, und Proxmox
  benennt selbst.
- growpart war nie installiert. GrowGuestFilesystem ruft es auf, und es lief
  bisher, weil Debians Cloud-Abbild es zufällig mitbringt. Fiele es heraus,
  läge jedes gekaufte Kontingent über einem Dateisystem, das nie gewachsen ist.
  Jetzt ausdrücklich eingebaut und als vierte Falle nachgewiesen.
- local nimmt ab Werk keine Platten an. Ohne das stirbt nicht nur der Bau,
  RegisterCapacity meldet danach Kapazität 0: ein Host, der fertig aussieht und
  nie einen Kunden tragen kann. ensure_image_storage greift nur ein, wenn keine
  Ablage Platten annimmt, hängt images an die vorhandene Liste an statt sie zu
  ersetzen, und schreibt über pvesm set statt in die pmxcfs-Datei.
- Ein abgebrochener Download blieb unter dem Zielnamen liegen und wäre beim
  nächsten Lauf ungeprüft weiterbenutzt worden. Jetzt .part, umbenannt erst
  nach geprüfter Summe.
- VerifyVmTemplate und VmTemplateCheck fragten nur, ob VMID 9000 existiert. Ein
  abgebrochener Bau hinterlässt eine gewöhnliche VM mit dieser Nummer, und
  beide sagten dazu "passt" — der Fehler kam beim ersten bezahlten Klon zurück.
  Jetzt template: 1.

isTemplate() stellt zwei Anfragen, weil die falsche Antwort hier etwas
zerstört: false heißt "Vorlage fehlt", und der Bau fängt mit qm destroy --purge
an. Proxmox beantwortet die Konfiguration einer nicht vorhandenen VM mit 500 —
demselben Code wie einen Knoten in Not. Die VM-Liste klärt deshalb die
Abwesenheit, alles darunter wirft und landet im Wiederholungs-Zweig.

Aufgeben beendet erst die Prozessgruppe, dann räumt es auf, und gebaut wird nur
die Fehlliste: create_proxmox_template räumt eine VMID weg, bevor es sie
anlegt, also hätte "alles Verlangte" eine gesunde zweite Vorlage auf dem Weg
zerstört.

Geprüft: 2267 Tests grün, Pint sauber, sh -n über alle drei Shell-Dateien, die
storage.cfg-Auswertung gegen eine echte Beispieldatei durchgespielt, und jeder
Befehl, den der Schritt absetzt, geht durch sh -n — keine andere Prüfung führt
diese Shell je aus. Drei Codex-Runden (R15), alle Befunde behoben.

Nicht geprüft: nichts davon lief je gegen echte Hardware. Die erste Übernahme
auf einem Proxmox-Host ist die Abnahme.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 05:01:44 +02:00
nexxo 97ac6c04e8 Release v1.3.87 — VM.Monitor gibt es in Proxmox 9 nicht mehr
tests / pest (push) Failing after 8m56s Details
tests / assets (push) Successful in 24s Details
tests / release (push) Has been skipped Details
Die Übernahme des ersten PVE-9-Hosts blieb in CreateAutomationToken stehen:
`pveum role modify` lehnt die GANZE Privilegienliste ab, sobald ein einziger
Name darin unbekannt ist — "invalid privilege 'VM.Monitor'". Die Liste stammt
aus der PVE-8-Zeit.

Ersatzlos gestrichen, nicht ersetzt: VM.Monitor gab Zugriff auf den
QEMU-Monitor, und den benutzt CluPilot nirgends. Geprüft, nicht vermutet — die
einzigen Treffer auf "monitor" im Provisioning betreffen Uptime Kuma. Auf PVE 8
fehlt damit ein Recht, das dort ohnehin niemand gebraucht hat; eine Liste passt
weiterhin auf beide Fassungen.

Dazu ein Test, der die angeforderten Namen gegen die gültigen hält — abgelesen
aus der Administrator-Rolle eines echten PVE 9. Die Liste ist die Verabredung
zwischen CluPilot und jedem Host, den es je übernimmt; sie hier zu prüfen ist
billiger als der Abbruch auf einer Maschine, die schon halb eingerichtet ist.

2245 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 03:42:37 +02:00
nexxo 4f04d9d29d Release v1.3.86 — der Grund von pveum, nicht nur die Feststellung
tests / pest (push) Failing after 8m49s Details
tests / assets (push) Successful in 21s Details
tests / release (push) Has been skipped Details
„could not converge the Proxmox automation role privileges", fünfmal
hintereinander. Die Meldung sagte, WAS scheiterte, und verschwieg das Einzige,
was weiterhilft: was `pveum` dazu geschrieben hat. Der Betreiber musste sich
auf den Host melden und den Befehl von Hand nachstellen, um eine Zeile zu
lesen, die daneben schon dastand.

Die Fehlerausgabe von `pveum role modify` steht jetzt in der Meldung — erste
Zeile, gekürzt: `pveum` stellt den Grund voran und breitet danach seine
Aufrufhilfe aus, und die wäre ein Bildschirm Text in einer Ereigniszeile.

2244 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 03:38:02 +02:00
nexxo b9fb401823 Release v1.3.85 — Handshake-Prüfung hing an einem Programm, das es nicht gibt
tests / pest (push) Failing after 8m54s Details
tests / assets (push) Successful in 22s Details
tests / release (push) Has been skipped Details
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>
2026-08-01 03:00:19 +02:00
nexxo 2f15d1e7a4 Release v1.3.84 — VPN-Zugänge nach Personen und Hosts getrennt
tests / pest (push) Failing after 9m6s Details
tests / assets (push) Successful in 20s Details
tests / release (push) Has been skipped Details
Personen und Hosts standen gemischt untereinander, nach Verbindungszustand
sortiert — ein Host-Zugang konnte also zwischen zwei Mitarbeitern stehen.
Beide sind Peers im selben Netz und haben dieselben Messwerte, aber es sind
zwei verschiedene Fragen: "wer von uns ist im Netz" und "welche Maschinen
hängen dran". Wer die eine stellt, liest die Antworten der anderen als
Rauschen.

Jetzt zwei Gruppen mit Überschrift und Anzahl. Eine Gruppe ohne Einträge wird
gar nicht erst gezeichnet — auf einer frischen Installation stünde sonst
"Hosts" über einer Lücke, bevor je einer angelegt wurde.

Getrennt wird nach der geladenen host-Beziehung, nicht nach `kind`: die
Plakette in der Zeile fragt dasselbe, und wer einen "Host-Zugang" liest, soll
ihn auch unter den Hosts finden. Ein adoptierter Peer (kind=system) mit
host_id gehört zu den Hosts, obwohl seine Art etwas anderes sagt.

Die Zeile selbst ist nach resources/views/components/admin/vpn-peer-row.blade.php
gewandert. Sie zweimal hinzuschreiben hätte geheissen, sie ab dem nächsten
Knopf an zwei Stellen zu pflegen — und die zweite fällt erst auf, wenn jemand
einen Host-Zugang sucht und ihn anders aussehen sieht als seinen eigenen.

Die Plakette bleibt trotz der Überschrift daneben: eine Zeile wandert beim
Suchen aus ihrer Überschrift heraus, und dann steht sie ohne sie da.

Codex: keine Befunde. 2237 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 02:34:15 +02:00
nexxo e0c0bb2c29 Release v1.3.83 — Fortschritt zeigte Übersetzungsschlüssel
tests / pest (push) Failing after 8m48s Details
tests / assets (push) Successful in 23s Details
tests / release (push) Has been skipped Details
Unter jedem Schritt stand `dashboard.step_state.done` statt eines Wortes —
im Adminbereich UND im Kundenportal, wo derselbe Stepper jemanden durch die
Einrichtung seiner Cloud begleitet.

Die Schlüssel gab es in keiner der beiden Sprachdateien. Die Paritätsprüfung
aus R16 konnte das nicht finden: sie vergleicht Deutsch mit Englisch, und die
beiden waren sich einig. Der Aufruf ist zusätzlich zusammengesetzt
(`__('dashboard.step_state.'.$state)`), also findet ihn auch keine Suche nach
festen Zeichenketten. Der neue Test rendert deshalb das Bauteil und prüft alle
vier Zustände in beiden Sprachen.

Dazu ein Test, der heute Nacht hochgegangen ist: PortalInvoicesTest verbot die
Zeichenkette '01.08.2026' — das Datum aus einer alten Attrappe. Heute ist der
1. August 2026, und die echten Rechnungen des Tests tragen das
Ausstellungsdatum von heute. Eine Zusicherung, die ein Datum verbietet,
verbietet auch den Tag, an dem es echt wird. Was die Attrappe ausmachte, war
ihr Satz, und den prüft der Nachbartest über 'Nächste Abbuchung' — stabil,
weil er nirgends sonst entsteht.

2235 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 02:08:28 +02:00
nexxo ac81bc5b98 Release v1.3.82 — Übernahme-Befehlszeile neu ausstellen
tests / pest (push) Failing after 8m50s Details
tests / assets (push) Successful in 23s Details
tests / release (push) Has been skipped Details
Die Zeile gab es genau einmal: auf der Seite direkt nach dem Anlegen. Wer sie
dort nicht kopierte — oder wem sie wegbrach, wie beim WireGuard-Fehler bis
1.3.80 — hatte danach einen Host in der Liste, für den es keinen Weg zu einer
Befehlszeile mehr gab. Ein zweites Anlegen scheitert an der eindeutigen IP,
also blieb nur Entfernen und von vorn.

HostTakeoverCommand behauptet in seinem Kopfkommentar, die Zeile werde "an
zwei Stellen gezeigt (beim Anlegen und beim Neuausstellen eines Codes)". Das
Zweite gab es nie — ein Kommentar, der eine Absicht beschreibt und wie eine
Beschreibung des Gebauten klingt.

Neu: ReissueTakeover als Modal auf der Host-Detailseite. Bestätigen zuerst
(R23), denn ein bisher ausgegebener Code wird dabei wertlos; danach steht die
ganze Zeile mit Kopieren-Knopf darin, nicht nur der Code.

Codex, zwei Runden, beide Male dieselbe Sorte Fehler von mir — die Ansicht
versteckt den Knopf, die Methode prüft nichts:

- P1: `issue()` erzwingt jetzt selbst, welcher Zustand zulässig ist. Ein
  Modal, das offen blieb, während der Host aktiv wurde, rief die Methode
  trotzdem; eine Livewire-Methode ist ohnehin von aussen aufrufbar.
- P2: `issue()` prüft die Tunnel-Einstellungen erneut. Sonst wird ein gültiger
  Code entwertet und dafür eine Zeile ausgegeben, der die WireGuard-Angaben
  fehlen — genau die kaputte Zeile, vor der die Warnung daneben steht.
- P1 der zweiten Runde: `disabled` gehörte nicht in die Liste der zulässigen
  Zustände. Es sieht aus wie "noch nicht fertig" und ist das Gegenteil —
  toggleMaintenance() schaltet einen LAUFENDEN Host so still. Der Knopf hätte
  eine Produktionsmaschine zur Neuinstallation angeboten.

Die Bedingung steht deshalb einmal am Bauteil (ReissueTakeover::eligible) und
wird von Ansicht und Methode gefragt: zwei Fassungen liefen auseinander,
sobald jemand eine ändert.

2231 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 01:00:53 +02:00
nexxo 8ab50f5650 Release v1.3.81 — Host anlegen scheiterte am WireGuard-Peer
tests / pest (push) Failing after 9m3s Details
tests / assets (push) Successful in 22s Details
tests / release (push) Has been skipped Details
Das Anlegen eines Hosts endete in der Konsole mit einem 500, bevor der
Betreiber den Einmal-Code je zu sehen bekam. Ursache war nicht das Anlegen,
sondern der Tunnel-Peer: HostEnrolment::issueWithKeys() rief `wg set wg0`
selbst auf — im Web-Request, also im `app`-Container. Der hat weder NET_ADMIN
noch /dev/net/tun; wg0 lebt im Provisioning-Container. Die Antwort war
"Unable to modify interface: Operation not permitted".

Der Peer geht jetzt als ApplyHostVpnPeer auf die Provisioning-Queue — genau
dorthin, wo ApplyVpnPeer es für die VPN-Zugänge längst richtig macht, mit
derselben `wireguard:hub`-Sperre und demselben Grundsatz: der Sollzustand
kommt beim Ausführen aus der Zeile, nicht aus dem beim Einreihen
festgehaltenen Wert. Der abgelöste Schlüssel reist als Wert mit, weil in der
Zeile zu diesem Zeitpunkt schon der neue steht.

Aufgefallen ist es nie, weil die Testsuite den Hub gegen FakeWireguardHub
tauscht — der bestehende Test blieb grün, während der echte Weg seit jeher
fehlschlug. Die zwei neuen Tests prüfen deshalb den WEG statt des Ergebnisses:
in der Anfrage bleibt der Hub unberührt, und der Auftrag liegt auf der
Provisioning-Queue.

Codex: 0 Fehler, 0 Sicherheitsbefunde. Dazu sein P2 — /.claude/ stand weder
im Index noch in .gitignore, ein `git add -A` hätte 153 MB als verschachteltes
Repo eingebettet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 00:43:10 +02:00
nexxo f1133edcbd Release v1.3.80
tests / pest (push) Failing after 9m8s Details
tests / assets (push) Successful in 21s Details
tests / release (push) Has been skipped Details
2026-07-31 23:02:46 +02:00
nexxo 7c4d4fd11c Zweite Fix-Runde: Regressionen der ersten (Trockenlauf, Doppelmail, Bestand, Isolierung) 2026-07-31 23:02:13 +02:00
nexxo b15fcc53a5 Fix-Runde: Reichweite der Sperre, keine doppelte Gebuehr, verlorene Nachricht 2026-07-31 22:57:56 +02:00
nexxo 4f037c8264 Mahnwesen: Einstellungen und die zwei Eingriffe von Hand 2026-07-31 22:43:52 +02:00
nexxo fb1ef3fc63 Rollen: aufklappbare Karten, gruppierte Rechte, Knopf im Raster 2026-07-31 22:39:46 +02:00
nexxo a64d41639b Mahnwesen: die sechs Kundenmails 2026-07-31 22:31:04 +02:00
nexxo a9f15449c6 Mahnwesen: Sperre und Freigabe, zusammen gebaut 2026-07-31 22:19:42 +02:00
nexxo 9899152d95 Mahnwesen: Tageslauf mit Gebuehrenrechnungen 2026-07-31 22:13:52 +02:00
nexxo 29c2718235 R20: Begruendung im Modal, und der Waechter sieht jetzt auch Listenzeilen 2026-07-31 22:09:05 +02:00
nexxo 7f7b654fcb Zahlungsprobleme: geplatzte Kaeufe aufzeichnen, beide Arten an einer Stelle 2026-07-31 21:58:52 +02:00
nexxo 9304382f62 Mahnwesen: Fall, Zeitplan und Gebuehrenstufen 2026-07-31 21:51:42 +02:00
nexxo 93e848d3f9 Rollen und Rechte in der Konsole zuweisbar 2026-07-31 21:46:22 +02:00
nexxo 6a6a672c11 Pruefergebnis als Modal statt am Seitenende 2026-07-31 21:36:43 +02:00
nexxo f91726d078 Testergebnis erscheint am Feld statt am Seitenende; DNS-Token bekommt einen Testknopf 2026-07-31 21:26:14 +02:00
nexxo b8a774b604 Portal: Karte tauschen, offene Rechnungen begleichen, Hinweis bei Faelligkeit 2026-07-31 21:12:31 +02:00
nexxo 88382069b8 Release v1.3.79
tests / pest (push) Failing after 8m59s Details
tests / assets (push) Successful in 21s Details
tests / release (push) Has been skipped Details
2026-07-31 20:58:34 +02:00
nexxo d6fd760a8b Offene Rechnungen bis zum Ende blaettern (Codex P2) 2026-07-31 20:58:21 +02:00
nexxo 0944de7cfa Passwortrichtlinie einstellbar, doppelte Anmelde-Mail behoben 2026-07-31 20:53:19 +02:00
nexxo 19ed6461cf Bereitschaft: das Absender-Postfach wird gefragt, ob es senden KANN 2026-07-31 20:35:58 +02:00
nexxo 82c6d62057 Stripe-Client: offene Rechnungen listen und einziehen 2026-07-31 20:04:09 +02:00
nexxo 0ae752d2ae Stripe-Client: SetupIntent und Vorgabe-Zahlungsmittel 2026-07-31 20:01:14 +02:00
nexxo 62e5e0c346 Spezifikation und Plan: Zahlungsmittel, Nachzahlen, Mahnlauf 2026-07-31 19:57:31 +02:00
nexxo 1e490e03ee Release v1.3.78
tests / pest (push) Failing after 8m59s Details
tests / assets (push) Successful in 22s Details
tests / release (push) Has been skipped Details
2026-07-31 19:52:20 +02:00
nexxo 200df30b9d Hetzner-Cloud-DNS, Bereitschaftsseite aus sich heraus behebbar
Die alte Hetzner-DNS-API ist abgeschaltet (301 auf die Weboberflaeche).
Jede Bereitstellung starb in ConfigureDnsAndTls, nachdem der Kunde bezahlt
hatte. HttpHetznerDnsClient und DnsTokenCheck sprechen jetzt die Cloud-API:
Bearer statt Auth-API-Token, RRSets ueber {name}/{typ} statt Record-IDs, der
Zonen-Lookup entfaellt. Gegen das Live-Konto gemessen, nicht geraten.

Drei Fallen, die am Konto gemessen wurden: TXT-Werte muessen in
Anfuehrungszeichen (sonst 422, was als read_only gemeldet worden waere), ein
Name mit Zonensuffix wird STILL angenommen (201), und der Fake gab andere IDs
aus als der echte Client -- zwoelf Tests waren gruen ueber einem Abbau, der im
Betrieb geworfen haette.

Bereitschaftspunkte, die nur eine Shell beheben konnte, sind jetzt bedienbar:
SSH-Schluesselpaar erzeugen (Ed25519 ueber phpseclib, privater Teil direkt in
den Tresor), Stripe-Katalog abgleichen (Warteschlange, Trockenlauf, Ausgabe
wortgleich), Mailzustellung als Schalter statt MAIL_MAILER, Neustart der
Arbeiterprozesse. Stripe hat jetzt alle drei Werte in der Konsole: Secret Key,
Signatur-Secret (Tresor, je Modus getrennt) und Publishable Key (Klartext).

Codex-Review (R15), zwei P1 behoben: der Signaturschluessel faellt nicht mehr
vom Live- in den Testplatz, und eine Record-ID aus der alten API macht einen
Host nicht mehr unloeschbar -- was adressierbar ist, wandelt eine Migration um,
der Rest wird beim Loeschen laut uebergangen statt geworfen.

2109 Tests gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 19:50:01 +02:00
nexxo 6d35ab9f47 Merge the handoff
tests / pest (push) Failing after 8m41s Details
tests / assets (push) Successful in 21s Details
tests / release (push) Has been skipped Details
2026-07-31 16:07:54 +02:00
nexxo 4c8840788e Hand over the three open threads, and what they have in common
Written so the next session can start without rediscovering anything: where the
code stands, the three open pieces in the order they matter, and the two release
traps that cost two version numbers today.

The second piece is the owner's own point, and it is the right one: a readiness
page that flags things only a shell can fix has moved the work rather than done
it. Three places do that today — generating the host keypair, syncing the Stripe
catalogue, and nothing else. The rest already links where it should.

The Stripe sync gets a note about not running it inside a Livewire call: it
talks to Stripe and creates products, so it belongs in a queue with a visible
result, and the dry run belongs in the interface beside it rather than only in a
terminal.

Last section names the pattern the whole session kept turning up: a display more
confident than what it rests on. A green badge over a red measurement, a claim
about somebody's Hetzner account derived from an HTML page, a comment promising
a file the installer never created. Worth reading the code with that in mind.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 16:07:45 +02:00
nexxo acc0ac0082 Merge the Hetzner DNS migration note
tests / pest (push) Failing after 8m26s Details
tests / assets (push) Successful in 26s Details
tests / release (push) Has been skipped Details
2026-07-31 15:52:56 +02:00
nexxo a91e8525d2 Write down the Hetzner DNS migration before starting it
The root cause of the zone_not_found hunt: the old DNS API is gone. It answers
301 to the web console, and the owner's token already returns 200 against
api.hetzner.cloud — so this is code work, not a credential problem.

The note holds what the next session needs and would otherwise have to rediscover:
the endpoints, taken from cloud.spec.json rather than guessed, the fact that
records are now RRSets addressed by name and type, that a zone can be addressed
by NAME directly, and two traps in the name field — it must not end with the zone
name, which is exactly what the current client sends, and PUT on an rrset sets
labels rather than records.

Recorded rather than started, deliberately. HttpHetznerDnsClient writes the A
record for every customer instance; rewriting it is not something to begin
without room left to test it properly, and a half-migrated DNS client is worse
than a documented broken one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 15:52:55 +02:00
nexxo 7880ac7cf5 Release v1.3.77
tests / pest (push) Failing after 8m33s Details
tests / assets (push) Successful in 25s Details
tests / release (push) Has been skipped Details
The DNS check now requires a real zones array before it says anything about the
account. A 200 carrying an HTML page — a portal, a filter, a proxy — was being
read as "no zones at all", which is what the live server was seeing while the
token had read and write and the zone existed with fifteen records.

An intercepted response is now reported as one, with its status and the first
characters of what actually came back.

2053 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 15:42:46 +02:00
nexxo d321719180 Only a real zones array counts as an answer
Measured on the live server: a token with read AND write, the zone
clupilot.cloud present with fifteen records — and the console insisting the
account held no zones at all. Both screenshots contradict the message, so the
message was wrong, and the previous fix did not go far enough.

successful() is not enough. Something in the middle — a portal, a filter, a
proxy — answers with 200 and an HTML page. That body has no `zones` key, `??
[]` turned it into no zones, and the display concluded the Hetzner account was
empty. A 200 is not a promise about who answered.

So the body has to answer the question, not merely arrive: `{"zones": [...]}`
with an actual array, or it is reported as something else having spoken, with
the status and the first 120 characters of what came back. That last part is
what turns it from a verdict into a diagnosis — an operator who sees "Blocked by
policy" knows in one line what nothing else here could have told them.

A genuine `{"zones": []}` still means what it always meant.

2053 tests pass, assets build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 15:42:31 +02:00
nexxo 64045e0a04 Release v1.3.76
tests / pest (push) Failing after 8m37s Details
tests / assets (push) Successful in 22s Details
tests / release (push) Has been skipped Details
The DNS check no longer reports an unclassified HTTP failure as "this account
holds no zones". Anything other than a successful zone list is now named as
what it is, with its status code, and says explicitly that it tells you nothing
about the zones.

2050 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 15:37:01 +02:00
nexxo c81a89ce5a A failed zone list is not an empty Hetzner account
The owner asked whether an empty token was being sent. It was not — an empty one
returns `missing` — but the question was worth following, and it found a fault
in the message I had just added.

The check classified 401 and 403 as `rejected` and then read the body. Every
OTHER unsuccessful response — 404, 429, 500, a cache's status page — has no
`zones` key, `?? []` turned that into no zones, and the console then stated "this
account holds no zones at all. The token probably belongs to a different Hetzner
project." A claim about somebody's account, derived from an error nobody looked
at, delivered with more confidence than the working case gets.

`successful()` is asked first now, and an unexpected status is reported as what
it is, with the number beside it: it says nothing about the zones, and it says
so. That is the distinction this check already draws between `unreachable` and
`read_only` — both are failures, only one of them tells you anything about the
token.

A genuinely empty list still means what it meant: 200 with zero zones is a token
for a project without zones.

The test uses a dataset rather than a loop. Http::fake() ADDS stubs instead of
replacing them, so a loop would have had the first status answer all four
iterations and the test would have proved one case three times over.

2050 tests pass, assets build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 15:36:47 +02:00
nexxo f9620f29a3 Release v1.3.75
tests / pest (push) Failing after 8m55s Details
tests / assets (push) Successful in 29s Details
tests / release (push) Has been skipped Details
The readiness page no longer shows a green badge over a red measurement. A
check that was actually run decides the badge; the passive "something is
configured" answer only fills in where nothing was measured.

zone_not_found now names the zone it looked for and the zones the token can
actually see, so it stops reading like "the token is wrong" — a wrong token
never gets that far.

Also: two traffic tests failed on the 29th, 30th and 31st of long months,
because they built "last month" with subMonth() and Carbon resolves that
calendrically. The production code was never affected.

2045 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 15:26:50 +02:00