Commit Graph

469 Commits (b95cae16698e948f2ead5fa9adea7f76bbe9b265)

Author SHA1 Message Date
nexxo 6314bb60fb Schalter je Paket, ehrliche Statusanzeige, Reste der alten Leiter
Die Umschaltmigration verglich beim Erkennen eines bereits umgeschalteten
Bestands nur Kontingent und Platte. Bei Intern waren beide schon vorher
richtig (5/20 GB), also hielt sie das Paket für erledigt und ließ RAM, Kerne
und Plätze auf ihren alten Werten stehen (1024 MB/1 Kern/5 statt 4096 MB/2/3).
Die Prüfung vergleicht jetzt alle neun Vorgaben; eine zweite, kleine Migration
hebt einen Bestand nach, auf dem die erste schon lief, und tut nichts auf einer
Neuinstallation.

Die Statusanzeige unterschied bisher nicht zwischen "kein Angebot" und "läuft,
aber nicht im Laden" — ein internes Paket zeigte "Nichts verfügbar" in der
Liste und "Im Verkauf" auf der Versionsseite darunter. Vier Zustände statt
zwei, mit fester Reihenfolge: der Notausschalter (sales_enabled) schlägt die
Konsolen-Kennzeichnung (internal).

Ein zweiter Schalter je Paketfamilie nimmt sie aus dem Preisblatt, ohne sie
unverkäuflich zu machen — nach dem Vorbild des vorhandenen Verkaufsschalters,
ohne Bestätigungsmodal, weil reversibel. Enterprise wechselt von
sales_enabled=false (weder käuflich noch verschenkbar) auf internal=true, wie
das Testpaket.

Dazu die liegengebliebenen Zahlen der alten Leiter in Produktattrappe,
Mail-Vorschau, Fabrik-Vorgaben und Seedern, sowie eine Testzusicherung, die auf
eine wandernde ID statt auf den berechneten Wert hätte treffen können.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:53:06 +02:00
nexxo f8d0c2c353 Merge branch 'claude/angry-golick-187172' into feat/neue-pakete 2026-08-01 16:27:13 +02:00
nexxo ce7d0426cb Fix-Runde: preferredDatacenter() blind fuer Reservierung, Loeschen hob sie auf
preferredDatacenter() waehlte das Rechenzentrum ueber Host::availableGb() ohne
->unreserved() - ein Rechenzentrum mit einem grossen, aber komplett
reservierten Host sah geraeumiger aus als eines mit echtem allgemeinem
Bestand. Der Checkout haette die Bestellung dorthin gelegt, placeableIn()
haette dort niemanden gefunden, und sie waere geparkt, obwohl anderswo Platz
war. Jetzt ->unreserved(), derselbe Bestand wie largestPlaceableGb().

Die Migration nutzte nullOnDelete() und tat damit das Gegenteil der eigenen
Vorgabe: die Reservierung sollte einen Kundenaustritt nicht stillschweigend
ueberleben, loeste sich mit nullOnDelete() aber genau so auf, sobald der
Kunde verschwindet. restrictOnDelete() macht das Loeschen eines Kunden mit
eigener Maschine zum Fehler, bis ein Operator die Reservierung von Hand
gelöst hat - Migration und Modellkommentar sagen jetzt dasselbe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 16:21:46 +02:00
nexxo 3df7434f7a Fix-Runde 1/1: Enterprise-Schwelle folgt dem Katalog statt einer festen Zahl
Zwei Betreiber-Entscheidungen aus der Durchsicht:

1. Keine zweite, feste Untergrenze mehr im Fliesstext ("ab 500 GB") -- die
   eigene Maschine beginnt dort, wo das groesste Paket endet, ihre Groesse ist
   Hardware-Frage einer Angebotsanfrage, kein zweiter Wert auf dem Preisblatt.
2. Die Zahl in der Ueberschrift ("Mehr als :quota?") kommt jetzt aus $plans --
   derselben Liste, die baseline() und comparison() schon lesen, statt fest im
   Sprachtext zu stehen. Eine spaetere Umschaltung der Paketleiter (Task 9)
   aendert sonst, was "am groessten" ist, und der Satz haette es nicht gemerkt.

max() auf einer leeren Plan-Liste (Katalog nicht lesbar) waere ein Fatal
gewesen -- enterprise() gibt in diesem Fall jetzt null zurueck, mit Test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 16:15:59 +02:00
nexxo 4ad1828f27 Ein reservierter Host gehoert seinem Kunden
placeableIn() nahm jeden aktiven Host im Rechenzentrum, und hasRoomFor() sowie
largestPlaceableGb() zaehlten eine exklusiv verkaufte Maschine obendrein mit —
der Shop versprach damit Platz, der bereits vergeben war. Ohne diese Markierung
ist 'eigener Server' ein Satz im Angebot und nirgends eine Tatsache.

hosts.reserved_for_customer_id (nullable, ueberlebt den Kunden) markiert die
Maschine; Host::placeableIn() und HostCapacity zaehlen sie nur noch fuer den
eigenen Mieter oder gar nicht mehr zum allgemeinen Bestand. Auf der
Host-Detailseite kann ein Operator reservieren und wieder loesen — Loesen
laeuft ueber ein eigenes Bestaetigungsmodal (R23), das selbst nichts aendert,
sondern nur an HostDetail::releaseReservation() zurueckmeldet. Host-Liste und
Kapazitaetsseite weisen eine reservierte Maschine als solche aus, statt sie
kommentarlos verschwinden zu lassen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 16:06:14 +02:00
nexxo 1692fd3977 Enterprise wird angefragt, nicht ausgepreist
Ab 500 GB ist es eine eigene Maschine — im Shop faende placeableIn() auf 388 GB
vergebbarem Platz keinen Host, und der Kunde erfuehre das nach der Zahlung.
Der Anfrage-Block erscheint als vierter Bestandteil neben der
Vergleichstabelle, sobald PlanCatalogue::sellable() Enterprise nicht mehr
liefert; die Familie selbst bleibt bestehen, nur sales_enabled entscheidet
(gesetzt wird das erst in Task 9).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 15:48:41 +02:00
nexxo 3b5dee8f0d Ein Ausweg wird nur angeboten, wenn er auch buchbar ist
DowngradeCheck baute seine Empfehlung aus packsToCover(), und die rechnet
bloss. BookAddon lehnt aus ZWEI Gruenden ab - der Deckel bei drei Bloecken
und der Ausschluss von Enterprise -, und die Empfehlung kannte keinen davon.
Solange ein Block 100 GB brachte, war das unerreichbar; seit er 20 GB bringt,
landet jede Ueberschreitung ueber 60 GB dort.

Business -> Team mit 600 GB belegt bot "5 x Zusatzspeicher buchen (+100 GB)"
an. Das Modal klemmte still auf drei, den vierten haette BookAddon abgelehnt,
und der Kunde stand nach 45 Euro im Monat genau dort, wo er vorher stand - auf
der Karte, die sein Abo billiger machen sollte. Enterprise -> Business empfahl
Bloecke, die es fuer dieses Paket ueberhaupt nicht gibt.

AddonCatalogue::bookableQuantity() antwortet jetzt auf beide Gruende. Es gab
einem Enterprise-Vertrag "3" auf die Frage, die sein eigener Docblock stellt.

DowngradeCheck stellt diese eine Frage und klemmt daran: `packs` ist die
gebrauchte Zahl nur, wenn der Vertrag sie auch buchen darf, sonst null - und
dann traegt `short`, was nach allen buchbaren Bloecken uebrig bliebe, gemessen
an DEREN Restmenge statt am Deckel, damit ein Kunde mit einem Block nicht mehr
zu loeschen bekommt als noetig. Kein halbes Angebot: eine Dauerbuchung, die
den Wechsel trotzdem nicht freigibt, ist kein Ausweg, sondern der ausgegraute
Knopf mit Preisschild.

packsToCover() bleibt reine Arithmetik, mit einem Kommentar, der sagt warum:
die kaufmaennische Grenze steht in AddonCatalogue, und ein Klemmen an dieser
Stelle zoege eine Vertragsabfrage in jeden Kontingent-Schritt und ins Portal,
die beide keine Verkaufsfrage stellen.

Der Knopf haengt schon an `packs > 0` und verschwindet von selbst; der Satz
wechselt auf downgrade_escape.capped, der die verbleibende Luecke nennt und
nicht den Grund - "hoechstens drei Bloecke" waere im Enterprise-Fall falsch,
wo es gar keine gibt.

Zwei bestehende Tests hingen an 600 und 800 GB aus der 100-GB-Zeit, beide
inzwischen nicht mehr deckbar; einer haette das Modal gesucht, das die Karte
zu Recht nicht mehr oeffnet. Auf 550 und 560 GB umgestellt, wo sie das pruefen,
wofuer sie geschrieben wurden.

Dazu ein Test, der belegt statt annimmt, warum das max(1, min(...)) in
ConfirmBookStorage stehen bleiben darf: ein Modal ist per openModal direkt
erreichbar, aber bookStoragePacks() legt weder am Deckel noch bei Enterprise
eine Bestellzeile an. Die bestehenden Tests deckten purchase() ab, nicht diese
Weiterleitung.

Voller Testlauf: 2405 bestanden. Pint sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 15:40:26 +02:00
nexxo c3c05ff9f8 Interne Pakete verschwinden aus dem Laden und bleiben in der Konsole
sales_enabled waere der falsche Hebel gewesen: es schaltet auch das
Verschenken ab. Das Testpaket ist fuer Abnahmelaeufe da und muss verschenkbar
bleiben, also zwei Felder fuer zwei Fragen. Der Checkout lehnt einen internen
Schluessel ausdruecklich ab, ueber denselben Fang wie einen unbekannten — eine
URL ist keine Liste, und die Antwort darf keinen Unterschied verraten.

GrantPlan las bislang dieselbe sellable()-Liste wie Preisblatt und Warenkorb,
sowohl fuer sein Dropdown als auch fuer die Validierung des Formularfelds —
ein interner Schluessel waere dort ebenso abgelehnt worden wie im Checkout,
und das Verschenken haette sein einziges Tor verloren. PlanCatalogue bekommt
deshalb grantable() als zweiten Leser derselben Abfrage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 15:31:46 +02:00
nexxo afa6183903 Zusatzspeicher: 20 GB fuer 15 Euro, hoechstens drei, nicht bei Enterprise
100 GB fuer 10 Euro waren 0,10 Euro/GB — unter Einstandspreis und billiger als
jeder Aufstieg. Wer stapelt, belegt dann den knappsten Rohstoff zum niedrigsten
Preis. 0,75 Euro/GB liegen ueber beiden Aufstiegen (0,73 und 0,67), und der
Deckel bei drei liegt dort, wo Aufsteigen billiger UND besser wird.

BookAddon haelt den Ausschluss durch AddonCatalogue::availabilityRefusal()
(gleiches Muster wie CustomDomainAccess), damit greift er auch beim
Verschenken durch den Betreiber. Billing::purchase() und storageLimitNote
fragen dieselbe Regel VOR der Zahlung, sonst haette ein Enterprise-Kunde einen
zahlbaren Auftrag anlegen koennen, den BookAddon erst danach abgelehnt haette.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 15:09:02 +02:00
nexxo 4cd848eead Fix-Runde 2: Warnung fuer nicht reparierte Hosts, case-sichere Vorabpruefung, wiederholbare Migration
Vier Befunde aus dem Abschluss-Review ueber d62a2c8/c815aee/11ba7ee:

- Die Migration renamt nur Hosts mit dns_name; pve-fsn-1/pve-hel-1 blieben
  unrepariert und stumm. Sie werden jetzt gesammelt und gemeldet (Log +
  Konsole), ohne die Migration abzubrechen - diese Hosts laufen weiter.
- Die Vorabpruefung verglich in PHP byteweise, die Spalte liegt auf
  utf8mb4_unicode_ci. Umgestellt auf GROUP BY/HAVING in SQL, damit dieselbe
  Kollation entscheidet, die spaeter den Unique-Index baut.
- Ein Fehlschlag nach der Vorabpruefung liess einen zweiten Anlauf sofort an
  "Duplicate column name" sterben. Die beiden betroffenen Schema-Schritte
  stehen jetzt hinter Schema::hasColumn(), macht den Kommentar darueber wahr.
- Seeder (DatabaseSeeder, DemoCustomerSeeder) sind auf pve-*-Namen sitzen
  geblieben, weil sie dns_name nie benutzt hatten. Auf fsn-01/hel-01
  umgestellt, next_host_number entsprechend vorbelegt.

Dazu vier Kleinigkeiten: ein Test nagelte den falschen Config-Schluessel fest
(dns.zone statt platform_zone), HostName::claim() erzwingt jetzt wirklich
eine Transaktion statt es nur zu verlangen, down() vergisst nicht mehr den
Settings-Cache, und der Kommentar ueber HostName::free() nennt jetzt ehrlich
die Einschraenkung auf einen einzelnen Thread.

Alle vier Migrationslaeufe (Vorabpruefung-Kollision, Meldung fuer
unreparierte Hosts, Fehlschlag-und-erneuter-Anlauf, Rueckbau mit
Cache-Invalidierung) gegen echtes MariaDB auf einer Scratch-Datenbank
geprueft, um die parallele Billing-Session nicht zu beruehren. Voller
Testlauf: 2395 bestanden. Bericht mit allen Befehlen und Ausgaben unter
.superpowers/sdd/2026-08-01-hostname-vergabe/final-fix-report.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:52:15 +02:00
nexxo d6f2aa1c9e Fix-Runde 1: Formel-Test raus, eigene Testhilfe, Docblock nachgezogen
Der Formel-Test rechnete dieselbe Formel nach und wich dabei vom echten Code
ab (kein max(0, ...) um den neuen Summanden) — ein Test, der die Implementierung
nachrechnet und dabei abweicht, ist schlechter als keiner. Der Verhaltenstest
deckt die Anforderung bereits ab und bleibt als einziger Test der Datei.

StoragePackHeadroomTest.php hing außerdem an reservedRun() aus
CustomerStepsTest.php und brach einzeln gefahren mit einem PHP-Fatal ab, statt
mit einem ehrlichen Fehlschlag. Eigene, in sich geschlossene Hilfsfunktion
(packHeadroomRun()) nach dem Muster der Nachbardateien (hostRun(),
restartableInstance()) — die Datei läuft jetzt auch allein grün.

Der Docblock von growDisk() sprach noch vom Kopfraum, der "unchanged"
mitfährt, und widersprach damit dem neuen Summanden direkt darunter. Nachgezogen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:41:39 +02:00
nexxo 3ed80a05e0 Ein Block bringt seinen eigenen Kopfraum mit
Ein Block gibt 20 GB und belegt 22. Ohne das haette ein Start mit drei Bloecken
90 GB auf 100 GB Platte gehabt — zehn Gigabyte Kopfraum, wo die Regel bei dieser
Plattengroesse zwoelf verlangt, und der gestapelte Tarif waere genau der Fall
geworden, den die Regel verhindern soll.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:31:52 +02:00
nexxo af05ed6694 Kapazitaetspruefung hinter den Idempotenz-Kurzschluss verschoben
Durchsicht (R22): die Pruefung stand in __invoke(), vor book()s Kurzschluss
fuer einen wiederholt zugestellten Webhook ("Idempotent against a retried
webhook"). Damit fragte jede Wiederholung erneut "passt NOCH ein Block
drauf" - obwohl keiner hinzukommt - und ein laengst bezahlter, laengst
gebuchter Vorgang quittierte die Wiederholung mit einem Fehler, sobald der
Host zwischenzeitlich eng geworden war. Genau das Szenario, fuer das diese
Aufgabe gebaut wurde, nur gegen den eigenen Kunden gerichtet.

Jetzt sitzt die Pruefung in book(), hinter dem order_id+addon_key-Kurzschluss:
eine Wiederholung bekommt ihre bestehende Buchung zurueck, ohne die Frage
erneut zu stellen. Eine echte neue Buchung durchlaeuft die Pruefung wie
zuvor. Deckel (quantityRefusal) und Domain-Ausschluss bleiben unangetastet -
sie haben dasselbe Muster im Kleinen, sind aber nicht Gegenstand dieses
Befundes.

Neuer Test: derselbe Auftrag wird zweimal gebucht, der Host wird zwischen
den beiden Aufrufen eng - der zweite Aufruf gibt die vorhandene Buchung
zurueck statt zu werfen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:18:01 +02:00
nexxo 11ba7ee34f Hostdetailseite: die Adresse steht neben den IPs und ist ein Klick zur Proxmox-Oberflaeche 2026-08-01 14:12:03 +02:00
nexxo 01bcc29d13 Ein Block wird nur gebucht, wenn der Host ihn tragen kann
HostCapacity wurde im Checkout, in der Bestellung und im Konsolenbereich
gefragt — beim Aufstocken nicht. Weil qcow2 duenn belegt ist, gelingt die
Ueberbuchung sofort und faellt erst auf, wenn die Gaeste wirklich schreiben.
Gefragt wird der Host der Instanz, denn eine laufende Instanz zieht nicht um.

StorageAllowanceTest: der Host in storageFixture() war mit 1000 GB (active()
Standard) schon fuer eine reine Business-Instanz (1050 GB disk_gb) zu klein —
placeableIn() haette sie nie dort platziert. Ohne Kapazitaetspruefung fiel das
nie auf; jetzt schon, deshalb auf 2000 GB angehoben.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:03:07 +02:00
nexxo c6ead1afc9 Deckel haelt auch am Rand: Warenkorb und Bestaetigungsdialog
Zwei Luecken aus der Durchsicht von Task 2, beide am selben Rand: genau am
erreichten Deckel.

purchase() klemmte $lines mit max(1, min(..., bookableQuantity, ...)) — bei
bookableQuantity=0 floort das auf eine Bestellzeile statt auf keine, und der
Speicher-Knopf war bedingungslos gerendert. Jetzt bricht purchase() fuer
type=storage frueh ab, wenn AddonCatalogue::quantityRefusal() ablehnt, und der
Knopf verschwindet zugunsten des Ablehnungssatzes.

ConfirmBookStorage klemmte an der absoluten Grenze (3), nicht an der
Restmenge — ein Vertrag mit einem gebuchten Block haette drei weitere
versprochen bekommen, obwohl nur zwei noch buchbar sind. Das Modal loest jetzt
seinen eigenen Vertrag auf (wie ConfirmCancelAddon und ConfirmRevokeSeat) und
klemmt an bookableQuantity().

Beide Faelle waren ungetestet; zwei neue Tests in StoragePackLimitTest.php
belegen sie und wurden vor dem Fix gegen den alten Stand als rot verifiziert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 13:50:07 +02:00
nexxo d62a2c8ff8 Hostnamen vergibt CluPilot: ein Name statt zweier, und der Zaehler ueberlebt das Loeschen
HostName::claim() haengt den Namen jetzt an die Rechenzentrums-Zeile
(next_host_number), nicht an MAX(hosts.name)+1: der Zaehler ueberlebt so
das Loeschen des zuletzt angelegten Hosts. hosts.dns_name faellt weg -
name ist ab jetzt der einzige Name, den Konsole, DNS, /etc/hosts und
Proxmox-Node teilen. RegisterHostDns veroeffentlicht nur noch, was
StartHostOnboarding beim Anlegen vergeben hat, statt selbst zu
nummerieren; PrepareBaseSystem baut den FQDN ueber HostName::fqdn()
statt ueber die nirgends konfigurierte clupilot.net.

Zwei Testdateien ausserhalb der Aufgabenliste (DatacenterTest,
HostTakeoverPageTest) setzten ->set('name', ...) auf HostCreate, das
Feld jetzt aber nicht mehr hat - im vollen Testlauf nachgezogen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 13:45:43 +02:00
nexxo 1f32a2d5b3 Hoechstens drei Bloecke, geprueft in der Buchung
Die Ansicht durfte 50 in den Warenkorb legen, die Aktion pruefte nichts. Der
Deckel liegt kaufmaennisch dort, wo Aufsteigen billiger wird als Stapeln, und
gehoert deshalb dorthin, wo gebucht wird — gezaehlt ueber alle laufenden
Buchungen, denn drei Bestellungen a einem Block sind drei Bloecke.

StorageAllowanceTest schrieb die alte Notbremse (50) als erwartete Zahl fest;
angepasst auf die jetzt engere kaufmaennische Grenze (3), wie im Aufgabenblatt
fuer DowngradeTest vorgezeichnet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 13:33:36 +02:00
nexxo 9888f898b8 EnsureNetworkBridge: abbestellen nach der Nachpruefung, aufgeben ohne die Rueckfahrkarte wegzuwerfen
Drei Enden, und jedes hat eine Reihenfolge:

- Abbestellt wird erst, NACHDEM die Bruecke nachgeprueft ist. Dass der
  Zweig erreicht wird, ist der Beweis: SSH kam ueber den Tunnel an und
  vmbr0 traegt Standardroute und Adresse — bridge_proven, von aussen
  gefragt. Und nur, wenn DIESER Lauf den Zeitgeber auch gestellt hat;
  ohne Termin im Kontext gehoeren die Unit-Dateien jemand anderem.
- awaitRollback pollt, bis 'rolled-back' liegt, statt sofort zu
  scheitern. Solange der Zeitgeber aussteht, steckt die Maschine mitten
  in einer Umstellung, und ein fail() liesse sie dort liegen. Gedeckelt
  durch die Schritt-Frist, die das Dreifache der Zeitgeber-Frist ist.
- giveUp loescht den Termin (sonst wird jedes Retry nach einem
  Fehlschlag zum sofortigen zweiten Fehlschlag), behaelt bridge_attempts
  (sonst ist der Deckel von zwei Versuchen keiner) und bestellt den
  Zeitgeber NICHT ab — er ist die Rueckfahrkarte, und steht er noch, hat
  er seinen Grund.

Der sh-n-Test ist gegengeprobt: mit absichtlich kaputtem Quoting faellt
er.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 13:05:58 +02:00
nexxo 2e511899f8 EnsureNetworkBridge: Start abgekoppelt, und der Verbindungsabriss wird gepollt statt gezaehlt
Die tragende Zeile ist das try/catch um keyLogin. RunRunner:102
verwandelt jede geworfene Ausnahme in ein retry(), und retry verbraucht
das Versuchskonto — ungefangen brennt der erwartete Verbindungsabriss die
fuenf Versuche in wenigen Minuten durch und laesst den Lauf scheitern,
BEVOR der Host wieder da ist. Genau der Unterschied zwischen
'Wiederholung' und 'toter Server'.

Unterschieden wird am Termin im Run-Kontext: ohne ihn hat dieser Lauf
nichts angefasst, dann ist ein Verbindungsfehler ein gewoehnlicher und
darf einen Versuch kosten. Mit ihm laeuft gerade eine Umstellung, dann
wird gepollt.

bridge.sh und bridge-run.sh gehen wortgleich hoch (Test vergleicht Byte
fuer Byte gegen die Repo-Datei), env traegt den Hub-Schluessel mit, damit
der Treiber den Handshake gegen den RICHTIGEN Peer prueft. Start per
nohup setsid, PID-Datei abgewartet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 13:04:03 +02:00
nexxo 91ecb382f5 EnsureNetworkBridge: schon da heisst nichts anfassen, und keine Bruecke auf einer Bruecke
Der Schritt, aber erst sein billigster Teil. Zwei Zusicherungen:

- Traegt vmbr0 schon Standardroute UND Adresse, dann advance() ohne einen
  einzigen veraendernden Befehl. pve-fns-1 hat die Bruecke von Hand; ein
  Wiederanlauf, der sie umbaut, baut ein funktionierendes Netz um.
- Ist die Karte mit der Standardroute keine physische (Bond, Bridge,
  VLAN), dann fail() mit Klartext statt bauen. bridge_ports darauf waere
  falsch, und was dabei herauskommt, ist aus der Ferne nicht mehr zu
  reparieren.

readBridgeState fragt beides in einem Rundlauf und leitet aus dem
LAUFENDEN Zustand ab (ip, /sys/class/net), nie aus der Datei des
Anbieters. 'up' sind absichtlich alle drei Fakten: eine vmbr0 ohne
Adresse und ohne Standardroute ist eine Bruecke im Sinne von 'ip link'
und sonst nichts.

Start, Poll und Abbestellen folgen. Bis dahin steht dort ein fail() —
gefahrlos, weil der Schritt noch nicht in der Pipeline haengt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 13:02:23 +02:00
nexxo 2ab6a92fd8 Die Packungsgroesse steht auf der Buchung, nicht in der Konfiguration
Bisher las StorageAllowance sie bei jeder Anzeige neu aus config. Solange es
eine Groesse gab, war das folgenlos — beim Zuschnitt von 100 GB auf 20 GB waere
es der stille Verlust von 80 GB je gekauftem Block gewesen, bei einem Kunden,
der bereits Daten darin liegen hat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 12:25:30 +02:00
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 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 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 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 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 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 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