Commit Graph

928 Commits (23027ac3c7f1cb599a52a0ca9e123eb5a0dc0f48)

Author SHA1 Message Date
nexxo 23027ac3c7 Umsetzungsplan: die oeffentlichen Seiten im Tunnel, Startskript vorher geprueft
Das Startskript des Gateways ist im Plan keine Skizze: es lief. Mit einem Namen
mit Zertifikat, einem ohne, einem mit Zertifikat aber ohne Schluessel und einem
leeren Feld — die drei letzten werden ausgelassen und benannt, der
Gesundheits-Port steht unabhaengig davon, Rueckgabewert 0.

Dabei drei Stellen nachgezogen: eine if-Abfrage statt der AND-OR-Liste, an der
install-agent.sh sich schon einmal selbst beendet hat, und "find -print -quit"
mit "|| true" statt einer Pipe nach head, die SIGPIPE liefern kann.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 19:07:05 +02:00
nexxo afc2f90dac Entwurf: die oeffentlichen Seiten im Tunnel erreichbar
Am Telefon mit aktivem VPN zeigt app.clupilot.com die Platzhalterseite. Nicht
falsch eingestellt, sondern nie gebaut: der Tunnel traegt nur 10.66.0.0/24, und
der interne Resolver biegt genau einen Namen um — den der Konsole. Portal,
Website und Statusseite loesen oeffentlich auf und gehen am Tunnel vorbei.

Der Gateway sucht seine Zertifikate kuenftig selbst und laesst Namen ohne
Zertifikat aus, statt gar nicht zu starten. Sonst nimmt ein fehlendes Zertifikat
fuer www. den Tunnel-Zugang zur Konsole mit — genau der Ausfall, vor dem
update.sh heute warnt.

files. bleibt absichtlich draussen: ein Server im Rettungssystem holt dort sein
Archiv und ist per Definition nicht im Tunnel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 19:07:05 +02:00
nexxo c68985f160 Version 1.10.2 — eine Leiste, die nicht schrumpfen darf, laeuft irgendwann ueber
tests / pest (push) Waiting to run Details
tests / assets (push) Waiting to run Details
tests / release (push) Blocked by required conditions Details
Der Festnageln-Knopf stand weiter rechts als der Bildschirm hergibt. Zweiter
Anlauf am selben Fehler: Ich hatte shrink-0 von sm auf lg verschoben, statt es zu
entfernen. Bei 1024 px nimmt die Seitenleiste 248 — fuer zwei Knoepfe, ein
Auswahlfeld und Festnageln bleibt zu wenig.

shrink-0 ist jetzt ganz weg. flex-wrap allein genuegt: passt es nebeneinander,
steht es nebeneinander; passt es nicht, bricht es um. Keine geratene
Breakpoint-Zahl mehr, die beim naechsten Geraet wieder danebenliegt.

148 Layout-Tests gruen.
2026-08-04 19:02:48 +02:00
nexxo 15230a01fa Version 1.10.1 — die Aktionsleiste bricht bis zum grossen Schirm um
tests / pest (push) Waiting to run Details
tests / assets (push) Waiting to run Details
tests / release (push) Blocked by required conditions Details
Auf dem Tablet lief die Zeile ueber den Rand: vier Elemente — zwei Knoepfe, das
Auswahlfeld und der Festnageln-Knopf — in einem Container, der ab sm auf
shrink-0 stand und damit nicht mehr schrumpfen durfte.

Das war mein eigener Fix von vorhin. sm greift schon bei 640 px, ein Tablet hat
768 — genau dazwischen sass der Fehler.

Jetzt wird die Leiste erst ab lg starr. Bis dahin bleibt sie volle Breite und
umbricht, was auf Telefon UND Tablet richtig ist. Das Auswahlfeld bekommt
zusaetzlich eine Obergrenze, damit eine lange Versionsliste die Zeile nicht
aufblaeht, und dieselbe Hoehe wie die Knoepfe daneben.

150 Layout-Tests gruen.
2026-08-04 18:57:35 +02:00
nexxo 8c1985d924 Sechs Folgepunkte aus den heutigen Pruefungen ins Register
tests / pest (push) Waiting to run Details
tests / assets (push) Waiting to run Details
tests / release (push) Blocked by required conditions Details
Sie standen bisher nur in Arbeitsdateien unter .superpowers/, und die sind
nicht Teil des Repos — beim naechsten Aufraeumen waeren sie weg. Das Register
ist der Ort, an dem der Betreiber sie sieht.

Zwei davon sind echte Fehler im laufenden Betrieb:

Die Zahlungssperre ruft shutdownVm() unbedingt, ohne zu fragen, ob der Gast
laeuft. Ein Kunde, dessen Maschine aus anderem Grund steht, bekommt seine
Sperre nie — der Lauf wirft, jede Nacht neu, und die Nachricht geht nie hinaus.
Gefunden, als eine geschaerfte Proxmox-Attrappe aufdeckte, dass neun
Mahnwesen-Pruefungen nur deshalb gruen waren, weil die alte Attrappe die Bitte
gegen eine stehende Maschine klaglos annahm.

Und ein abgebrochener vzdump laeuft nach dem Wurf weiter und schreibt sein
Vollarchiv fertig — auf dieselbe Ablage, auf der die naechtlichen Sicherungen
aller anderen Kunden dieses Hosts liegen.

Damit stehen zwoelf Punkte im Register, aber KEINER mehr in der Gruppe
"verkauft und nicht geliefert". Das war heute Morgen anders.
2026-08-04 17:43:34 +02:00
nexxo 100bc7e2f6 Wächter-Attrappe: ps-Ausgabe zitieren, damit der Zeilenumbruch den case-Zweig nicht spaltet
tests / pest (push) Waiting to run Details
tests / assets (push) Waiting to run Details
tests / release (push) Blocked by required conditions Details
$running steht mit "app\nredis" vorbelegt — ein echter Zeilenumbruch. Ohne
Anführungszeichen in den case-Zweig interpoliert, zerfiel der Zweig in zwei
Shell-Anweisungen: `printf '%s\n' app` und ein eigenständiges `redis`. Die
Attrappe meldete auf `ps` also nur einen Dienst, während `config` weiterhin
beide nannte — der Wächter sah eine Lücke, die es nicht gab, und heilte sie.

Hier bislang folgenlos, weil keine Zusicherung an der Entscheidung des Wächters
hängt. Die nächste hätte still danebengelegen und nach einem Wächter-Fehler
ausgesehen, nicht nach einem Attrappen-Fehler.

Dieselbe Stelle wurde in daeea1d (feature/wirt-konsole) für
WatchdogVisibilityTest.php auf demselben Weg gerichtet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 17:30:01 +02:00
nexxo 9f356ad923 Version 1.10.0 — was nicht geliefert wird, wird nicht mehr verkauft
tests / pest (push) Waiting to run Details
tests / assets (push) Waiting to run Details
tests / release (push) Blocked by required conditions Details
Zwei Zusatzmodule waren kaeuflich und lieferten nichts:

Office mit vollem Funktionsumfang (22,80 EUR/Monat) — in den Kundeninstanzen
laeuft ueberhaupt keine Office-Software. Die Vorlage traegt nur nextcloud und
db, die Bereitstellung installiert keine Office-App, und Nextcloud bringt von
sich aus keine mit. Beworben wurde es an drei Stellen: als Merkmal auf der
Landingpage, als Paketmerkmal in Team und Business, und als Zusatzmodul.

Zweiter Sicherungsort (6 EUR/Monat) — die naechtliche Sicherung laeuft, aber
der zweite Ort, mit dem geworben wird, existiert nicht.

Beide sind jetzt gesperrt, und zwar an BEIDEN Wegen: sie verschwinden von der
Preistafel UND eine Buchung am Formular vorbei wird abgewiesen. Ein Modul, das
die Seite nicht mehr zeigt und das ein direkter Aufruf noch buchen kann, waere
nicht geschlossen.

Jedes hat seinen EIGENEN Schalter, weil sie an verschiedenen Voraussetzungen
haengen: Office am Dokumentenserver, der Sicherungsort am Speicherserver. Sie
werden zu verschiedenen Zeitpunkten wieder freigegeben.

Bestandsbuchungen sind unberuehrt — der Riegel gilt nur fuer neue. Auf dieser
Maschine gab es keine; der Live-Betrieb ist von hier aus nicht einsehbar und
gehoert nachgesehen.

Office wird gebaut: ONLYOFFICE ueber einen gemeinsamen Dokumentenserver.
ONLYOFFICE hat im Mai 2026 die Verbindungsgrenze der freien Fassung gestrichen,
es faellt also keine Lizenzgebuehr an.
2026-08-04 17:28:55 +02:00
nexxo bf95f9da8a Zweiter Sicherungsort ebenfalls aus dem Verkauf nehmen, eigener Schalter
tests / pest (push) Waiting to run Details
tests / assets (push) Waiting to run Details
tests / release (push) Blocked by required conditions Details
extra_backups verspricht einen zweiten, getrennten Sicherungsort, den es
nicht gibt (nur die erste, tägliche Sicherung läuft). Gesperrt nach dem
Muster von collabora_pro, aber mit eigener Konstante
(EXTRA_BACKUPS_ON_SALE statt OFFICE_ON_SALE) und eigenem Abweisungssatz,
weil beide Module aus unterschiedlichen Gründen fehlen und zu
unterschiedlichen Zeitpunkten zurückkommen. Keine Bestandsbuchung
betroffen (subscription_addons enthielt auf dieser Maschine keine Zeile
dazu). OpenWork-Eintrag von SOLD auf PLANNED umgestellt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 17:28:03 +02:00
nexxo d3ac839b1e Office (collabora_pro) auch über die Abrechnungsseite sperren
tests / pest (push) Waiting to run Details
tests / assets (push) Waiting to run Details
tests / release (push) Blocked by required conditions Details
Der Vortag nahm Office nur von der Preistafel — ein eingeloggter Team-
oder Business-Kunde hätte collabora_pro technisch weiterhin über
Billing::purchase() buchen können, weil AddonCatalogue den Preis
unverändert kannte und die Abrechnungsseite nie danach fragte. Der
Schalter (OFFICE_ON_SALE) wandert deshalb vom LandingController in
AddonCatalogue, der einzigen Stelle, die Preistafel UND Abrechnungsseite
kennen; eine neue Methode saleRefusal() weist die Buchung an beiden
Enden ab (Kauf-Aufruf und Karten-Anzeige), ohne BookAddon/GrantAddon
anzufassen — Bestandsfälle und Geschenke des Betreibers bleiben möglich.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 17:10:07 +02:00
nexxo 42fd059fdd Office aus dem Verkauf nehmen, abschaltbar statt gelöscht
tests / pest (push) Waiting to run Details
tests / assets (push) Waiting to run Details
tests / release (push) Blocked by required conditions Details
Office (ONLYOFFICE über einen künftigen gemeinsamen Dokumentenserver) wurde
an drei Stellen beworben, ohne dass irgendwo im Repository ein Dokumentenserver
existiert: der Kachel "Office im Browser" auf der Preistafel, dem Paketmerkmal
`office` in Team und Business, und dem Zusatzmodul `collabora_pro` für
22,80 €/Monat. Alle drei sind jetzt stillgelegt, aber nicht gelöscht — ein
einziger dokumentierter Schalter (LandingController::OFFICE_ON_SALE) plus eine
neu veröffentlichte Planversion holen das Versprechen zurück, sobald der
Dokumentenserver steht.

Das Paketmerkmal wird über das im Katalog bereits etablierte Handover-Muster
entfernt (neue Migration, analog zu switch_to_new_plan_ladder): die laufende
Version von Team/Business wird geschlossen und durch eine identische ohne
`office` ersetzt. Bestehende Verträge bleiben auf ihrer alten, eingefrorenen
Version stehen und behalten das Merkmal unverändert.

Die dritte Planversion für Team/Business hat 24 Bestandstests berührt, die
eine feste Versionsnummer oder eine feste Preis-/Versionszahl annahmen —
repariert, überwiegend durch dynamisches Lesen der aktuellen Version statt
eines eingetippten Werts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 16:49:32 +02:00
nexxo 033578d75d Release-Decke: einen Server aus der Konsole auf eine Version festnageln
Bei zehn Servern liess sich eine Auslieferung nicht staffeln: entweder alle
nehmen die neueste Version oder niemand. Der einzige Griff, der eine bestimmte
setzt, war RELEASE=vX.Y.Z bash deploy/update.sh auf der Kommandozeile.

Der Kern ist eine geklemmte Variable. Der Agent uebergab in Zeile 669 ohnehin
schon RELEASE="$TARGET_RELEASE"; wird die an der Decke geklemmt, faellt
`behind` aus derselben Rechnung, und Knopf wie Wartungsfenster folgen von
selbst. Task 4 belegt genau das mit einem Test, der KEINEN Produktivcode
braucht: es gibt keinen zweiten Weg in eine Auslieferung.

Die Decke faellt zu, nicht auf. Unlesbar, formwidrig oder ins Leere zeigend
heisst: nichts wird angeboten. Ein Rueckfall auf "neueste Version"
installierte genau das, wovon weggenagelt wurde.

Nicht enthalten: Zurueckrollen. Das ist verboten (update.sh:222), und der
Datenbank-Schnappschuss, auf den die Fehlermeldung dort verweist, wird
nirgends genommen. Eigene Baustelle, ihr fehlendes Stueck ist der
Schnappschuss, nicht der Knopf.

Unterwegs gefunden und mitbehoben: zwei Stellen, an denen eine Zuweisung aus
einer Kommandoersetzung unter set -e + pipefail den Agenten toetete, BEVOR er
eine Statusdatei schreiben konnte (sync_vpn_certificate, release_manifest_
version) — dieselbe Ausfallart, die die Konsole eine nie endende Pruefung
zeigen laesst.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 16:37:33 +02:00
nexxo 24eb7b3d80 Fix-Welle: Schlussreview-Befunde 1-6 zur Release-Decke
Sechs Befunde aus dem Schlussreview, in einer Welle behoben:

- BEFUND 1 (Important): eine leere Auswahl im Festnageln-Feld liess
  pinRelease() ueber `$this->ceilingChoice ?: null` in setCeiling(null)
  laufen — die Gegenhandlung (Decke abnehmen) — und meldete dabei die
  Erfolgsmeldung des Festnagelns. ConfirmPinRelease::confirm() schickt die
  Version jetzt als Event-Nutzlast (wie ConfirmSaveSecret den Schluessel),
  und pinRelease(string $version) weist eine leere Version ausdruecklich
  ab, mit einer eigenen Meldung (release_pin_empty).
- BEFUND 2 (Minor, durch 1 miterledigt): Modal und Seite lasen bisher zwei
  getrennte Eigenschaften ($version vs. $ceilingChoice). Der Fix oben
  beseitigt die Trennung.
- BEFUND 3 (Important, Text only): der Kommentar bei release_tag_exists()
  in deploy/lib/release.sh und der Fehlerbehandlungs-Abschnitt der Spec
  behaupteten, ceiling_missing schuetze gegen einen vom Release-Prozess
  geloeschten Tag. Tut es nicht: `git fetch --tags --force` (ohne
  --prune-tags, bewusst) entfernt keine lokal bereits geholten Tags, die
  drueben verschwunden sind. Beide Stellen beschreiben jetzt, wogegen die
  Pruefung tatsaechlich schuetzt (ein nie geholter oder nie existierender
  Tag) und wogegen nicht. Kein --prune-tags hinzugefuegt.
- BEFUND 4 (Minor): ConfirmPinRelease hatte keinen Test. Zwei neue Tests
  nach dem Vorbild von ConfirmSaveSecret in IntegrationsPageTest.
- BEFUND 5 (Minor): ceilingChoice wurde nie aus dem gesetzten Zustand
  vorbelegt. UpdateChannel::ceiling() ist jetzt public, Settings::mount()
  belegt das Feld damit vor.
- BEFUND 6 (Minor): eine von Hand geleerte Deckendatei liest die Konsole
  als "keine Decke" (ceiling() -> null), der Agent meldet dafuer aber
  ceiling_error. Der "Decke abnehmen"-Knopf stand nur hinter
  @if($update['ceiling']) und verschwand damit genau in dem Zustand, aus
  dem er zurueckfuehren muesste. Bedingung erweitert auf
  ($update['ceiling'] || $update['ceiling_error']).

Jeder Befund traegt einen eigenen Test in ReleaseCeilingConsoleTest.php.
Volle Suite: 2973 passed (10389 assertions).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 16:25:48 +02:00
nexxo 1567f33b49 Version 1.9.0 — jede Route gehoert zu genau einem Hostnamen
tests / pest (push) Waiting to run Details
tests / assets (push) Waiting to run Details
tests / release (push) Blocked by required conditions Details
2026-08-04 15:56:29 +02:00
nexxo 8b631acb72 Fix-Welle Ganz-Branch-Review: weltlesbare .env-Sicherung, blinder Test, drei Installer-Texte
Fuenf Befunde aus der abschliessenden Review, in einer Runde behoben:

- EnvFileEditor::backup() liess PHPs copy() die Umask entscheiden statt den
  Modus der Quelle zu uebernehmen — .env stand mit deploy/install.sh auf 0600,
  jede Sicherung landete trotzdem weltlesbar bei 0644, mit APP_KEY,
  DB_PASSWORD, VPN_CONFIG_KEY und STRIPE_SECRET darin. Reproduziert (per
  kurzzeitigem git stash des Fixes: 420 statt 384) und jetzt durch einen
  expliziten chmod nach dem Kopieren sowie einen neuen Test verhindert.

- HostSeparationTest pruefte "jede Route hat einen Hostnamen" nur scheinbar
  allgemein — deploy/install.sh schreibt ADMIN_HOST_EXCLUSIVE=false als
  Vorgabe, und im nicht-exklusiven Fallback registriert routes/web.php jede
  /admin/*-Route absichtlich ohne Domain. Der Test setzte zwar
  ADMIN_HOST_EXCLUSIVE=true, sagte aber nirgends, dass genau das die
  Voraussetzung der Pruefung ist. hostSeparationTable() gibt AdminArea::
  isExclusive() jetzt als Out-Parameter zurueck, gelesen waehrend die zweite
  Anwendung noch gebootet ist, und der Test besteht darauf.

- clupilot:bind-hosts existierte, aber nichts sagte einem Operator, dass es
  ihn braucht. deploy/update.sh druckt jetzt einen eigenen Hinweis, wenn
  APP_HOST in .env leer ist — mit der vollen docker-compose-Zeile statt der
  internen in_app-Abkuerzung, weil der Operator sie in seiner eigenen Shell
  eintippt.

- ask STATUS_DOMAIN und ask FILES_DOMAIN versprachen "blank to keep it auf
  jedem Host", fuellten Enter aber ueber den dritten ask()-Parameter mit dem
  Default. Fuer FILES_DOMAIN war das kein Schoenheitsfehler: der Default
  verschiebt /bootstrap.tar.gz vom Portal weg, bevor DNS fuer den neuen Namen
  existiert. Beide Defaults entfernt.

- ask WWW_DOMAIN erklaerte nicht, dass SITE_HOST mehrere kommagetrennte Namen
  traegt (der erste kanonisch, der Rest leitet dauerhaft um) — ein Operator,
  der die Apex-Domain zusaetzlich zu www. binden wollte, hatte keinen Weg,
  das aus dem Prompt zu erfahren. Nur der Prompt-Text geaendert, kein neuer
  Prompt, Default unveraendert.

Voller Testlauf: 2953 passed (Baseline 2952 + der neue Backup-Berechtigungs-
Test), 0 failed. routes/web.php, RestrictAdminHost, config/fortify.php und
PublicSiteGate unangetastet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 15:56:21 +02:00
nexxo 432ecfbf4d Plan: der Rueckverweis-Test braucht eine vorhandene Zeile, sonst prueft er nichts
Wie zuerst geschrieben haette er den Anhaenge-Zweig getroffen und waere auch
gegen die fehlerhafte Fassung gruen gewesen. Der Implementer hat das gemerkt und
SITE_HOST= leer vorbelegt — genau die Form, die .env.example ausliefert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 15:56:21 +02:00
nexxo 6bb705e566 Fix-Runde 1: bind-hosts schreibt keine Rueckverweise mehr in die .env
apply() ersetzte eine vorhandene Zeile per preg_replace($pattern, $line, ...)
- $line kommt von der Befehlszeile, und preg_replace deutet $1/\1/\\ im ERSATZ
als Rueckverweis, auch ohne Klammern im Muster. Ein Hostname mit solchen
Zeichen wuerde still verstuemmelt in die Datei geschrieben, die jedes
Geheimnis der Installation haelt - und EnvFileEditor::isValidLine() kann das
nicht fangen, das Ergebnis ist syntaktisch weiter KEY=value. apply() arbeitet
jetzt zeilenweise ohne jede Regex im Ersatzpfad.

Zusaetzlich: EnvFileEditor::write() wirft InvalidEnvContentException, wenn der
neue Inhalt nicht parst - das war ungefangen und zeigte dem Betreiber einen
Stapelabzug auf der Zugangsdatendatei statt eines Satzes wie jeder andere
Fehlerpfad in diesem Befehl.

Neuer Test deckt den Rueckverweis-Fall ab, ueber die bereits vorhandene
(leere) SITE_HOST-Zeile - der Pfad, den preg_replace tatsaechlich traf.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 15:56:21 +02:00
nexxo 5c9ee2c6d5 Plan: apply() darf den Wert nicht als Regex-Ersatz behandeln
preg_replace deutet $1, \1 und \\ im ERSATZ als Rueckverweise, auch ohne
Gruppen im Muster. Der Wert kommt von der Befehlszeile, das Ziel ist die Datei
mit allen Zugangsdaten, und das Ergebnis waere still verstuemmelt statt
abgelehnt — isValidLine() sieht weiterhin ein gueltiges KEY=value.

Jetzt zeilenweise, ohne Regex auf dem Schreibweg. Dazu faengt der Befehl
InvalidEnvContentException ab, statt dem Betreiber einen Stapelabzug zu zeigen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 15:56:21 +02:00
nexxo d90676632e clupilot:bind-hosts traegt die Hostnamen in eine bestehende .env nach 2026-08-04 15:56:21 +02:00
nexxo 0d7950c464 Der Installer schreibt die Hostnamen, nach denen er fragt
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 15:56:21 +02:00
nexxo 4200056697 Fix: HandleExceptions auf die Originalanwendung zurückstellen 2026-08-04 15:56:21 +02:00
nexxo 3a71cdc7cf Plan: Tests laufen im Worktree, nicht im geteilten Haupt-Checkout
Am Haupt-Checkout arbeiten parallel andere Sitzungen. Deren unfertige Aenderungen
gerieten in den eigenen Testlauf und haben eine Fehlersuche in die falsche
Richtung geschickt. Der Worktree ist jetzt eigenstaendig: vendor hartverlinkt,
.env kopiert, storage-Verzeichnisse angelegt.

Ein Symlink auf vendor taeugt dafuer nicht — Composer leitet seine Basispfade aus
dem aufgeloesten Verzeichnis ab und laedt dann die fremden Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 15:56:21 +02:00
nexxo 53f1293f09 Fix: alle drei Env-Kanäle einzeln sichern, sonst kippt phpunit.xml's ADMIN_HOSTS/FILES_HOST 2026-08-04 15:56:21 +02:00
nexxo 9d3f3c27b5 Plan: das Zurueckstellen der Umgebung war falsch, und die Pruefhuerde zu niedrig
Der Testcode im Plan sicherte $_SERVER und stellte daraus zurueck. phpunit haelt
seine <env>-Werte aber in $_ENV und putenv() — gemessen: ADMIN_HOSTS und
FILES_HOST fehlen in $_SERVER voellig. Das Zurueckstellen loeschte sie damit,
und jeder Test danach las ADMIN_HOSTS aus der echten .env des Containers. Die
Suite fiel an zwei Stellen, die mit Hostnamen nichts zu tun haben.

Gefunden hat es nicht der Filterlauf, den der Plan verlangte — der war gruen.
Gefunden hat es der Vergleich zweier voller Laeufe, mit und ohne die Aenderung:
2939 gruen gegen zwei Fehlschlaege. Deshalb verlangt Schritt 5 jetzt den vollen
Lauf und nennt den Kontrollversuch beim Namen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 15:56:21 +02:00
nexxo 4f57435c4d Jede Route an einen Hostnamen, und storage/{path} zugemacht 2026-08-04 15:56:21 +02:00
nexxo 78d6c9c320 Plan: Merge im Haupt-Checkout, Versionsuntergrenze auf v1.8.2
Beides beim Vorabcheck aufgefallen: git switch main scheitert in einem
Worktree, in dem main anderswo ausgecheckt ist, und main ist waehrend des
Planens auf v1.8.2 weitergezogen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 15:56:21 +02:00
nexxo 5a2e6564db Umsetzungsplan Hostnamen-Trennung: vier Aufgaben, Testcode im Container gelaufen
Der Testcode im Plan ist keine Skizze. Er lief: 123 Routen, zwoelf ohne Domain,
und die Original-Anwendung danach lesend UND schreibend intakt. Letzteres nicht
selbstverstaendlich — die zweite Anwendungsinstanz zeigt Eloquents statischen
Connection-Resolver auf ihre eigene, leere :memory:-Datenbank, und der halbe
Testlauf faende danach keine Tabelle mehr. Genau so ist es beim ersten Versuch
passiert; das Zurueckstellen steht deshalb mit Begruendung im Plan.

Ebenfalls vorher geprueft statt geraten: dass Pest-Expectations eine eigene
Fehlermeldung als zweites Argument nehmen, und dass dynamische Properties in
beforeEach hier Hausstil sind.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 15:56:21 +02:00
nexxo 295444f432 Zwei Entwuerfe: Hostnamen-Trennung, und der Helfer, der sich selbst nachzieht
Die Hostnamen-Trennung ist gebaut und wird nie eingeschaltet. Gemessen: mit
gesetzten APP_HOST/SITE_HOST/STATUS_HOST/FILES_HOST bleiben von 123 Routen
zwoelf ohne Domain, und davon sind zehn die begruendeten. Fortify haengt
laengst an fortify.domain, livewire/* bleibt geteilt, PublicSiteGate behaelt
seine Routennamen. Die .env der laufenden Maschine kennt die vier Schluessel
nur nicht — weil install.sh nach den Namen fragt und die Antworten wegwirft,
und dabei an einer Funktion stirbt, die erst dreizehn Zeilen spaeter definiert
wird.

Der zweite Entwurf haelt eine Entscheidung fest, bevor sie gebaut wird: wer ein
Release veroeffentlichen kann, kann damit die sudoers aller Server aendern. Das
ist der Preis dafuer, dass der root-Helfer sich selbst nachzieht, und er wird
bewusst bezahlt. Das Dienstkonto bekommt dabei nichts dazu — deshalb ein
root-eigener Spiegel und nicht der Checkout, den es besitzt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 15:56:21 +02:00
nexxo 073e147ff8 Festnageln aus der Konsole
Task 5: Bedienoberfläche zum Festnageln einer Release-Version, in der
bestehenden Update-Handlungsleiste statt einer zweiten daneben.
Bestätigt im Modal (R23), Auswahlfeld selbst ohne Modal (R20).
2026-08-04 15:53:36 +02:00
nexxo d90137c521 Beleg: das Wartungsfenster achtet die Decke ohne eigene Regel 2026-08-04 15:39:02 +02:00
nexxo 5fe33553ad Zertifikats-Spec auf Weg B umgeschrieben, Registereintrag gestrichen
tests / pest (push) Waiting to run Details
tests / assets (push) Waiting to run Details
tests / release (push) Blocked by required conditions Details
Die Spec fuer die Host-Konsole (docs/superpowers/specs/2026-08-01-host-
konsole-zertifikat-design.md) war auf einer eigenen DNS-Zone und Proxmox'
Alias-Modus fuer DNS-01 gebaut. Hetzner kennt keine Subzonen (docs.hetzner.
com/networking/dns/faq/zones/, Artikel NE-7597D: "Subzones are not
supported") -- damit entfallen eigene Zone, CNAME je Host, Alias-Modus und
der pro Host verteilte DNS-Token ersatzlos. Der Grund bleibt in der Spec
stehen, statt geloescht zu werden, damit niemand denselben Weg in einem
halben Jahr erneut vorschlaegt.

Weg B, wie im Register vorgegeben: CluPilot stellt zentral aus, DNS-01
ueber den vorhandenen kontoweiten Hetzner-Token, Zertifikat per SSH
(RemoteShell::putFile + `pvenode cert set --force --restart`, geprueft
gegen die Proxmox-Dokumentation) auf den Host, Erneuerung als geplanter Job
auf der Bereitstellungs-Warteschlange (dieselbe Grenze wie SyncVpnPeers --
nur queue-provisioning erreicht einen Host ueber den Tunnel). Dazu ein
Vergleich mit der Kundeninstanz (ConfigureDnsAndTls, HTTP-01) und eine
genaue Bestandsaufnahme der Bereitschaftsseite: sie kennt heute kein
Zertifikat, weder fuer Hosts noch, trotz ersten Anscheins, uebertragbar
fuer die Plattform -- CertificateSweep/ProxyHost misst nur oeffentlich
erreichbare Namen und laeuft im falschen Container fuer einen Host-FQDN.

Im Code bestaetigt und in der Spec vermerkt: RrsetId::zone() ist heute fest
auf die Kundenzone verdrahtet, ein Host-FQDN liegt aber in der
Plattformzone -- das ist Bauarbeit, keine offene Entscheidung. Offen bleibt
nur, welches Werkzeug das ACME-Protokoll auf CluPilot-Seite spricht (keine
Bibliothek/kein Tool dafuer im Repo) und ob Plattform- und Kundenzone im
selben Hetzner-Projekt liegen -- beides als offene Fragen benannt, keine
davon blockiert den Rest des Ablaufs.

Registereintrag in OpenWork.php gestrichen: die Spec beschreibt keinen
toten Weg mehr, und genau das war der einzige Punkt, den der Eintrag
festhielt.

Getestet: php artisan test --filter=OpenWork, 8 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 15:31:28 +02:00
nexxo c93510ffe4 Fix-Runde 1: writeAtomic prueft Rueckgabewerte, ceiling() faengt Throwable
Zwei Important-Befunde aus dem Code-Review zu Task 3:

- writeAtomic() ignorierte den Rueckgabewert von File::put()/File::move()
  und meldete setCeiling() als "true", selbst wenn ein I/O-Fehler (volle
  Platte, Rechteproblem) nichts geschrieben oder eine .tmp liegen gelassen
  hatte. Beide Rueckgabewerte werden jetzt geprueft, eine liegen gebliebene
  .tmp wird im Fehlerfall aufgeraeumt, und der Fehlschlag wird bis zu
  setCeiling() durchgereicht (Rueckgabe false).

- ceiling() konnte state() doch werfen lassen: zwischen File::exists() und
  File::get() liegt ein Zeitfenster, und File::get() wirft eine
  FileNotFoundException, wenn die Datei dazwischen verschwindet. readJson()
  und lastLog() kapseln genau dieses Muster schon in try/catch(Throwable);
  ceiling() zieht jetzt nach.

Beide Befunde tragen einen eigenen Test: ein Verzeichnis an der Ceiling-
Datei-Stelle (exists() wahr, get() wirft) fuer den zweiten, eine echte
Rechteverweigerung (chmod 0500 als nicht-root Testbenutzer) fuer den
ersten -- kein Facade-Mock noetig.
2026-08-04 15:30:10 +02:00
nexxo 367198459c Der Kanal schreibt die Decke atomar und reicht sie durch 2026-08-04 15:13:43 +02:00
nexxo fd216be623 Der Hostnamen-Zähler übersteht jetzt das Löschen eines Rechenzentrums
Der Zähler lag auf der Rechenzentrums-ZEILE (next_host_number). Ein leeres
Rechenzentrum liess sich löschen - bewusst so entschieden, was nichts mehr
hält, soll entfernbar bleiben -, aber die Zeile nahm den Zähler mit. Wer
denselben Code neu anlegte, bekam eine frische Zeile mit dem Schema-Default
1, und der nächste Host hiess wieder <code>-01, obwohl dieser Name schon in
alten Protokollen, Sicherungen und DNS-Zwischenspeichern auf eine ANDERE
Maschine zeigt.

Der Zähler zieht deshalb in eine eigene Tabelle host_name_sequences um,
geführt über den rohen Code statt über die id der Rechenzentrums-Zeile.
ConfirmDeleteDatacenter bleibt unangetastet: das Löschen war nie das
Problem, nur was es mitriss. Die Migration überträgt den Bestand (fsn/hel)
vor dem Löschen der alten Spalte und ist gegen echtes MariaDB in beide
Richtungen geprüft (hoch, Werte kontrolliert, zurück, wieder hoch).

Neuer Test in HostNamingTest stellt den ganzen Bruch nach: Rechenzentrum
anlegen, Host vergeben, Host entfernen, über den echten Bestätigungsdialog
löschen, mit demselben Code neu anlegen - der nächste Name bleibt fortlaufend
statt wieder bei 01 zu beginnen. Gegen den unveränderten Code lief er rot
(HostName::preview lieferte nbg-01 statt nbg-02).

Registereintrag "Ein Zähler kann durch Löschen eines Rechenzentrums
zurückfallen" gestrichen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 15:11:58 +02:00
nexxo 4b2c2cd315 release_manifest_version ueberlebt ein fehlendes Manifest 2026-08-04 15:06:12 +02:00
nexxo 63328d44d4 Der Agent klemmt Ziel und Zaehler an der Decke 2026-08-04 14:53:14 +02:00
nexxo 507636f38f Die abgebuchte Domain wird jetzt wirklich von der Maschine genommen
tests / pest (push) Waiting to run Details
tests / assets (push) Waiting to run Details
tests / release (push) Blocked by required conditions Details
Der Registereintrag nannte den falschen Grund: das Deaktivieren startet sehr
wohl eine Provisionierung. CustomDomainAccess::deactivate() ruft seit Langem
ReapplyInstanceAddress, das legt einen Lauf der `address`-Pipeline an und
schickt AdvanceRunJob auf die provisioning-Warteschlange; ConfigureNextcloud
loescht dort trusted_domains 2 und ConfigureDnsAndTls schreibt den Router ohne
den Namen neu. Das ist gebaut und geprueft.

Der Schaden war trotzdem echt, nur eine Tuer weiter. Erreicht wurde deactivate()
allein ueber PlanChange::settleCustomDomain, also ueber den Paketwechsel. Der
zweite und haeufigere Weg, auf dem das Recht endet — der Kunde bucht das Modul
in der Abrechnung ab, clupilot:end-cancelled-addons haelt den Termin am Ende des
bezahlten Zeitraums — ging an dieser Stelle vorbei: BookAddon::cancel() lieferte
Speicher nach und sprach mit Stripe, fragte aber niemanden nach der Adresse. Die
Domain verschwand aus jeder Ansicht und blieb auf der Maschine stehen.

BookAddon::cancel() fragt jetzt CustomDomainAccess::enforce() — die ganze Regel,
nicht den Modulschluessel: wer von Team auf Business aufgestuft hat und sein
altes Modul loswird, behaelt die Domain, weil das Paket sie selbst traegt.

Und der Anstoss darf die Entscheidung nicht kippen. deactivate() faengt jetzt
einen Fehlschlag der Nachfuehrung ab und schreibt ihn als Fehler ins Log: die
Wahrheit steht in der Datenbank, die Maschine zieht nach, und eine Kuendigung
haengt nicht daran, ob ein fremder Host gerade antwortet.

Die Gegenrichtung brauchte nichts: der Entzug loescht die Domain-Spalte, also
traegt der Kunde sie nach der Neubuchung neu ein und weist sie neu nach — und
genau dort haengt seit jeher der Lauf, der sie wieder ausliefert. Ein Test haelt
das fest, damit es keine Einbahnstrasse wird.

Registereintrag gestrichen.

Rot gesehen: ohne den settleCustomDomain-Aufruf fallen drei der vier neuen
Tests; ohne das try/catch faellt der vierte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 14:50:46 +02:00
nexxo cfae6e990a Versions-Arithmetik kennt eine Decke 2026-08-04 14:41:18 +02:00
nexxo d4a9166c7a Plan-Korrektur vor der Umsetzung: Task-2-Test isolieren
Zwei Defekte im Vorab-Abgleich gefunden, beide im Testaufbau von Task 2:

- Der Test haette echte Tags (v9.9.8, v9.9.9) im GETEILTEN Repository
  angelegt. Tags liegen im gemeinsamen .git und sind damit auch fuer den
  Hauptbaum und jede Parallelsitzung sichtbar. Stirbt der Test vor seinem
  Aufraeumen, beantwortet ein liegengebliebenes v9.9.9 die Frage
  `git tag -l 'v*' --sort=-v:refname | head -1` falsch — und die entscheidet,
  wohin ein Server aktualisiert. Jetzt laeuft der Agent in einem Wegwerf-
  Checkout mit eigenem .git; er bestimmt seine Wurzel ohnehin aus dem eigenen
  Pfad, es genuegt also, deploy/ dorthin zu kopieren.
- Falscher Manifest-Pfad: der Test schrieb nach storage/app/deploy/
  deployment.json, gelesen wird storage/app/deployment.json (lib/release.sh:19)
  — eine Ebene darueber. Der Test haette die ausgelieferte Version nie gesetzt
  und etwas anderes gemessen, als er behauptet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 14:38:59 +02:00
nexxo 40e42a88fe Umsetzungsplan: Release-Decke in fuenf Schritten
tests / pest (push) Waiting to run Details
tests / assets (push) Waiting to run Details
tests / release (push) Blocked by required conditions Details
Fuenf Tasks, jeder mit eigenem Testzyklus: Versions-Arithmetik mit Decke,
Agent klemmt Ziel und Zaehler, Kanal schreibt atomar und reicht durch, Beleg
dass das Wartungsfenster von selbst folgt, und der Griff in der Konsole.

Task 4 hat bewusst KEINEN Produktivcode. Wenn der Test ohne Aenderung an
AutoUpdate.php gruen wird, ist genau das der Befund — und wenn nicht, ist die
Antwort nicht eine Decken-Sonderregel in der Automatik, sondern ein Fehler in
state(). Der Plan sagt das ausdruecklich, weil ein zweiter Weg in eine
Auslieferung genau das ist, wovor AutoUpdate.php im Kopfkommentar warnt.

Bei der Selbstpruefung gegen die Spec fielen drei Fehler auf:

- Ein Apostroph in einem einfach zitierten Testnamen — Syntaxfehler.
- `Operator::factory()->create()->givePermissionTo(...)` gibt es hier nicht;
  Berechtigungen haengen ueber Rollen am operator-Guard.
- Eine Spec-Anforderung ohne Task: der Zustand „Decke unter dem
  Ausgelieferten". Er ist ueber das Auswahlfeld nicht erreichbar, ueber die
  Kommandozeile schon, und er darf nicht als „aktuell" durchgehen. Jetzt mit
  eigenem `ceiling_passed`, eigenem Satz und zwei Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 14:34:41 +02:00
nexxo 29f13275f8 Das Register behauptete etwas Falsches: der Plan-Wechsel IST gebaut
"Ein Plan-Wechsel wird nirgends angewendet" stimmt seit laengerem nicht mehr.
ApplyPlanChange faehrt die plan-change-Pipeline, aufgerufen vom OrderObserver
bei einer Aufstufung und von clupilot:apply-due-plan-changes bei einer
Abstufung zum Laufzeitende. settleCustomDomain() hat sehr wohl einen Aufrufer
(ApplyPlanChange:256), und sieben Testdateien mit 43 Pruefungen decken den Weg.

Aufgeschrieben, weil der Kopfkommentar dieser Datei genau das ausschliesst:
"Ein Punkt verschwindet, wenn die Arbeit im selben Commit fertig wird, der ihn
streicht — und damit kann die Liste nicht behaupten, etwas sei offen, das es
laengst nicht mehr ist." Genau das ist passiert. Wer eine Liste fuehrt, deren
einziger Zweck Ehrlichkeit ist, muss sie mit der Arbeit streichen, nicht
danach.

Gepruefte Restliste: neun Punkte. Zwei davon (zweiter Sicherungsort, Office
Pro) haengen an Infrastruktur, die es noch nicht gibt; einer (Hostnamen-
Abnahme) an der echten Anlage; einer (Support-Mail) an einem SMTP-Konto, das
der Betreiber anlegen muss.
2026-08-04 14:29:21 +02:00
nexxo cddba14993 Entwurf: Release-Decke — gezielt vorwaerts festnageln aus der Konsole
tests / pest (push) Waiting to run Details
tests / assets (push) Waiting to run Details
tests / release (push) Blocked by required conditions Details
Bei zehn Servern laesst sich eine Auslieferung heute nicht staffeln: entweder
alle nehmen die neueste Version oder niemand. Der einzige Griff, der eine
bestimmte setzt, ist `RELEASE=vX.Y.Z bash deploy/update.sh` auf der
Kommandozeile — zehnmal.

Zwei Funde haben den Entwurf geformt:

- Zurueckrollen ist kein fehlender Knopf, sondern eine bewusst verbotene
  Handlung (update.sh:222). Die Fehlermeldung dort verweist auf einen
  Datenbank-Schnappschuss vor dem Update — den nimmt niemand, in update.sh
  steht kein einziger Dump. Echtes Zurueckrollen ist deshalb eine eigene
  Baustelle, und ihr fehlendes Stueck ist der Schnappschuss, nicht der Knopf.
- `clupilot:auto-update` haette einen einmaligen Sprung beim naechsten
  Wartungsfenster sofort wieder auf die neueste Version gehoben. Festnageln
  muss also eine stehende Obergrenze sein, sonst haelt es nicht.

Der Ansatz ist dadurch klein: der Agent uebergibt in Zeile 669 ohnehin schon
`RELEASE="$TARGET_RELEASE"`. Wird diese eine Variable auf die Decke geklemmt,
folgt alles andere — `behind` faellt aus derselben Rechnung, und Knopf wie
Automatik lesen beide `UpdateChannel::state()`. Kein zweiter Weg in eine
Auslieferung.

Entscheidend beim Fehlerverhalten: die Decke faellt zu, nicht auf. Eine
unlesbare oder ins Leere zeigende Decke darf nicht auf „neueste Version"
zurueckfallen — das installierte genau das, wovon weggenagelt wurde. Dass ein
Tag verschwindet, ist dabei kein Randfall: der Release-Prozess loescht falsche
Tags und ueberspringt die Nummer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 14:25:21 +02:00
nexxo 6de7bf9db8 Version 1.8.2 — die Zugangs-Plakette bricht nicht mehr mitten im Wort
tests / pest (push) Waiting to run Details
tests / assets (push) Waiting to run Details
tests / release (push) Blocked by required conditions Details
Am Telefon brach "Mitarbeiter-Zugang" in der Plakette um und machte aus ihr
einen doppelt hohen Kasten mit einem Bindestrich am Zeilenende. whitespace-nowrap.

Das ist die kleinste der Beschwerden an dieser Zeile, aber die einzige mit einer
eindeutigen Ursache. Die uebrige Mobilansicht — jede Angabe auf einer eigenen
Zeile, Statusplakette und Knoepfe oben rechts weit weg von den Daten — ist eine
Gestaltungsfrage und keine Klasse.
2026-08-04 14:17:09 +02:00
nexxo d9280dae1e Version 1.8.1 — die Update-Knoepfe sind am Telefon eine Leiste, keine Streuung
tests / pest (push) Waiting to run Details
tests / assets (push) Waiting to run Details
tests / release (push) Blocked by required conditions Details
flex-wrap allein liess die zwei Knoepfe in ihrer natuerlichen Breite unter dem
Text stehen: zwei ungleich breite Kaesten, linksbuendig, die aussahen wie
verrutscht statt wie eine Gruppe.

Am Telefon jetzt volle Breite und gestapelt — eine erkennbare Handlungsleiste.
Ab sm stehen sie wie bisher nebeneinander, dort ist Platz.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 14:10:06 +02:00
nexxo e8b51f3c70 Der Waechter nimmt die Sperre erst beim Eingriff, nicht beim Nachsehen
tests / pest (push) Waiting to run Details
tests / assets (push) Waiting to run Details
tests / release (push) Blocked by required conditions Details
Der Update-Agent meldete dauerhaft "kommt nicht an die Arbeit", obwohl nichts
kaputt war. Wächter und Agent haben beide einen minütlichen Zeitgeber und nahmen
beide `.agent.lock` in ihren ersten Zeilen — bevor sie wussten, ob sie überhaupt
etwas tun würden. Der Wächter tut an fast jedem Tag nichts: seine vier Prüfungen
sind allesamt lesend. Er hielt die Sperre trotzdem, jede Minute; der Agent kam
leer aus und schrieb einen Übersprung-Vermerk. Weil der Agent der ist, der an die
Konsole berichtet, stand dort eine Dauerstörung, während beide Dienste genau das
taten, was sie sollten.

Der Wächter sieht jetzt ungesperrt nach und nimmt die Sperre erst, wenn wirklich
etwas zu richten ist — im gesunden Fall fasst er sie nie an. Wo er eingreift,
wird die Lage nach dem Nehmen der Sperre noch einmal geprüft: zwischen dem
ungesperrten Blick und der Sperre kann ein Update fertig geworden sein, und
`--force-recreate` auf gesunde Container reißt jede offene Verbindung ab.

Versetzte Zeitgeber wären nur seltener gewesen, nicht weg — zwei Takte derselben
Länge wandern gegeneinander, und systemd zieht sie über AccuracySec aktiv auf
gemeinsame Weckpunkte zusammen. Eine zweite Sperre wäre schlimmer als der Fehler
gewesen: dann liefe der Wächter mitten in ein Update hinein.

Dazu zwei Dinge, die derselbe Vorfall aufgedeckt hat:

- Vierzehn Aufrufe nach draußen standen im Wächter ohne Frist, alle unter der
  Sperre. Der Agent hat `timeout -k` am 4. August gelernt, der Wächter nie — ein
  `docker compose exec`, das auf den Daemon wartet, hätte die Sperre unbegrenzt
  gehalten.
- Ein übersprungener Lauf sah im Journal aus wie ein erfolgreicher: der Agent
  beendet sich sauber, systemd meldet Starting → Deactivated. Das hat die
  Fehlersuche zweimal in die falsche Richtung geschickt. Er sagt es jetzt, mit
  Halter und Zähler in der Zeile.

WatchdogLockContentionTest lässt beide echten Skripte gegeneinander laufen statt
die Sperrenlogik nachzurechnen — und prüft beide Richtungen: der Agent kommt an
die Arbeit, während der Wächter nur nachsieht, und der Wächter hält still,
während ein Update die Sperre hält.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 14:08:44 +02:00
nexxo 993b529584 Version 1.8.0 — gekuendigte Maschinen werden endlich abgebaut
tests / pest (push) Has been cancelled Details
tests / assets (push) Has been cancelled Details
tests / release (push) Has been cancelled Details
Bis heute lief die Maschine eines gekuendigten Kunden fuer immer weiter und
band einen Platz auf dem Host, den niemand mehr verkaufen konnte. Vierzehn
Tage nach dem Laufzeitende wird sie jetzt heruntergefahren, archiviert und
geloescht — und der Platz ist wieder verkaufbar.

Die Regel, die ueber allem steht: niemals loeschen, bevor das Archiv
NACHWEISLICH existiert. Ein vzdump kann mit einer Auftragskennung enden und
trotzdem nichts hinterlassen. Der Abbau sieht deshalb nach, ob wirklich eine
Datei daliegt — und ob sie von DIESEM Lauf stammt und nicht von der
naechtlichen Sicherung, der der letzte Tag fehlt.

Dasselbe gilt fuer jede andere Stufe: Herunterfahren, Sichern und Loeschen
liefern bei Proxmox alle drei nur eine Auftragskennung, keine Zusage. Auf jede
wird gewartet und jede wird nachgeprueft.

Dazu aus Teil A (v1.7.1): beim Kuendigen wird gefragt, ob der Kunde seine
Daten will, und er wird gewarnt, bevor ihn seine eigene Cloud aussperrt.

NACH DEM UPDATE: nichts zu tun. Der Abbau laeuft taeglich um 05:30 und fasst
hoechstens fuenf Instanzen je Lauf an, damit er die bezahlte Bereitstellung
nicht aushungert. Was haengt, steht rot ueber der Instanzliste.
2026-08-04 12:40:39 +02:00
nexxo 19a770e1eb Kuendigung B1, Fix-Welle: der Platz wird frei, und das Loeschen ist vollzogen
K1 — abgebaute Instanzen zaehlten weiter als belegend. `scopeOccupyingHost`
filtert jetzt `torn_down_at`: eine abgebaute Instanz behaelt `ended`, `vmid`
und `disk_gb` als Nachweis, `status != 'failed'` war fuer sie also wahr. Der
Knoten wurde physisch leer und die Buchhaltung blieb voll — die naechste
bezahlte Bestellung derselben Groesse waere geparkt und verworfen worden.

K2 — `deleteVm()` gibt die UPID zurueck, der Abbau wartet den qmdestroy-Auftrag
ab und schreibt `torn_down_at` erst danach. Ein 200 heisst nur, dass Proxmox
den Auftrag angenommen hat; scheitert die Zerstoerung danach, stand bisher eine
laufende Maschine mit einem Datensatz da, der "abgebaut" sagte — und niemand
sah sie je wieder an. Die Attrappe bildet die Asynchronitaet ab
(`destroyedVmids`, `destroyExitStatus`, `destroyHangs`). Die Fristen des
Auftrags wandern mit: Sichern 900 s, Zerstoeren 300 s, Summe unveraendert 1800 s
unter der Uhr des Arbeiters.

K3 — vor `shutdownVm()` steht derselbe `vmStatus()`-Riegel wie im
Nachbarschritt. Eine wegen offener Zahlung gesperrte Cloud und jeder
Wiederholungslauf nach einem Teilfehlschlag treffen einen gestoppten Gast; der
Wurf haette die Instanz unheilbar gemacht und den echten Grund am Datensatz
ueberschrieben. Die Attrappe weist eine Bitte gegen eine stehende Maschine
jetzt ab, und ein Test faehrt erstmals einen zweiten Lauf nach einem
Fehlschlag durch.

W2 — der rote Kasten "Abbau haengt" filtert `status = 'ended'`. Eine
wiederbelebte Instanz waere sonst fuer immer darin stehengeblieben.

Neun Pruefungen im Mahnwesen lassen ihre Cloud jetzt laufen, bevor sie gesperrt
wird — die geschaerfte Attrappe legt offen, dass `SuspendInstance` denselben
fehlenden Riegel hat (Folgepunkt im Bericht).

Zu jedem der vier Punkte eine Zusicherung, die ohne den Fix rot ist; die
Rotproben stehen im Bericht.
2026-08-04 12:39:06 +02:00
nexxo df73e558e9 Kuendigung B1, Tasks 4+5: der Zeitplan-Griff und die Sichtbarkeit
Gekuendigte Kundenmaschinen liefen bisher fuer immer weiter. Task 3 hat den
Abbau gebaut; hier kommen der Griff, der ihn faehrt, und der Ort, an dem man
sieht, was passiert ist.

Die Wartezeit-Frage, entschieden: ein Auftrag je Instanz auf der
provisioning-Warteschlange. Das ist keine Abwaegung — nur queue-provisioning
steht im Netz-Namensraum des vpn-hub, der scheduler-Container nicht. Ein
Befehl, der selbst mit Proxmox spraeche, haette gar keine Route zu einem Host.

Die Fristen stehen ausdruecklich ineinander: die Aktion bekommt 600+1200 =
1800 s, der Auftrag hat $timeout 2100 s, retry_after der Verbindung ist
2400 s. Nur die unterste Uhr hinterlaesst einen lesbaren Grund am Datensatz;
die mittlere toetet den Arbeiterprozess stumm, die oberste startet einen
ZWEITEN Abbau gegen eine Maschine mitten im vzdump. $tries=1, weil ein
sofortiger zweiter Versuch am Herunterfahren einer gesperrten VM scheitern und
den richtigen Grund ueberschreiben wuerde. Die Staffelung ist als Pruefung
festgenagelt. Der Preis — zwanzig statt sechzig Minuten fuers Sichern — steht
im Kopfkommentar ausgeschrieben.

Dazu zwei Entscheidungen, nach denen niemand gefragt hat: eine Obergrenze je
Lauf, weil ueber dieselbe serielle Warteschlange bezahlte Bestellungen laufen;
und eine Reihenfolge, die einen Dauerfall die uebrigen nicht aushungern laesst.

Zeitplan taeglich um 05:30 — der Abbau hat keinen Moment, auf den es ankommt,
aber er darf nicht ins naechtliche vzdump-Fenster um 02:00 fallen.

Sichtbarkeit: zwei Kaesten in der Konsole. „Abbau haengt" (rot, ganz oben) —
eine Instanz mit gefuelltem teardown_error steht unbegrenzt und belegt weiter
einen Platz. Und „Archiviert und abgebaut" (unter der Liste) mit archive_volid
im Klartext. Beide sortieren absteigend und beide haben ein Ende: der Fehler
raeumt sich beim naechsten erfolgreichen Lauf selbst ab, das Archiv faellt
nach zwoelf Monaten heraus. Der Folgepunkt vom Export-Kasten also nicht noch
einmal.

Suite 2929 gruen. 25 neue Pruefungen, vier Mutationsproben rot gesehen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 12:00:37 +02:00
nexxo 51ec6ebed2 Kuendigung B1, Task 3 — Fix-Runde: der Nachtrag braucht ein Lebenszeichen
Vier Befunde aus der Pruefung.

Der schwerste: der Nachtrags-Zweig konnte eine LAUFENDE Kundenmaschine als
abgebaut verbuchen. vmExists() ist ->get(...)->successful() ohne ->throw(), und
Proxmox antwortet auf die Konfiguration einer nicht vorhandenen VM mit 500 —
demselben Code wie ein hakender Knoten. "Gibt es nicht" und "ich konnte nicht
fragen" sahen damit gleich aus, und der Zustand entsteht im Regelbetrieb: ein
Lauf sichert, scheitert am Loeschen, und der naechste findet archive_volid
gesetzt und einen Knoten, der nichts beantwortet. Die Ablagenpruefung steht
deshalb jetzt VOR dem Zweig: nodeStorage() ruft ->throw(), laeuft sie durch, hat
der Knoten geantwortet, und erst dann ist ein "nein" aus vmExists() ein Befund
statt einer Vermutung.

Drei Ausnahmen lagen ausserhalb des try und haetten im Zeitplan die uebrigen
Instanzen mitgerissen: das Anlegen der Sperre (im Betrieb Redis), das Vermerken
des Grundes im catch, und die Freigabe im finally — die sogar am Erfolgsfall
vorbei. Alle drei abgesichert.

Die Sperrfrist war mit backupWaitSeconds + 600 knapper als der laengste Lauf
(Herunterfahren UND Sichern) und konnte kurz vor dem Loeschen auslaufen. Jetzt
shutdownWaitSeconds + backupWaitSeconds + Puffer — und sie hat endlich eine
eigene Pruefung.

Ein einzelnes Archiv ohne Zeitpunkt bleibt gueltig, mehrere nicht: sie lassen
sich nicht ordnen, und vermerkt wuerde womoeglich die Sicherung von vorgestern.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 11:36:18 +02:00
nexxo 104ac46d45 Version 1.7.3 — ein Tor, eine Liste
tests / pest (push) Has been cancelled Details
tests / assets (push) Has been cancelled Details
tests / release (push) Has been cancelled Details
Bei verborgener Website kam der Betreiber auf admin. und stand auf www., app.
und status. vor der Baustellenseite seines eigenen Portals — obwohl er im VPN
war und seine Adresse in der Konsole freigegeben hatte.

Zwei Tore hatten zwei Vorstellungen davon, wer "wir" sind:

  RestrictConsoleNetwork  VPN-Subnetz + die Freigabeliste aus der Konsole
  PublicSiteGate          nur admin_access.trusted_ranges

Wer seine Bueroadresse eintrug, oeffnete damit nur das eine Tor. Das andere
kannte die Liste nicht und liess ihn stehen — was wie ein kaputtes VPN aussah
und keines war.

Jetzt fragt das Seiten-Tor dieselbe Stelle wie das Konsolentor. Eine Liste, ein
Begriff davon, wer hereindarf: wer die Konsole sehen darf, darf die versteckte
Seite auch sehen. Das ist dieselbe Person.

2901 Tests gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 11:17:05 +02:00
nexxo 24a0f64128 Kuendigung B1, Task 3: archivieren, nachsehen, und erst dann abbauen
Gekuendigte Kundenmaschinen wurden nie abgebaut. EndInstanceService nimmt der
Instanz am Laufzeitende die Adresse weg und laesst die Maschine ausdruecklich
stehen; danach kam nichts mehr, und sie belegte fuer immer einen Platz auf dem
Host. ArchiveAndTearDown ist der Schritt danach: vierzehn Tage nach dem Ende
wird die Maschine archiviert und geloescht.

Die eine Regel: niemals loeschen, bevor das Archiv nachweislich existiert. Ein
vzdump kann mit einer Auftragskennung enden und trotzdem nichts hinterlassen —
volle Ablage, abgebrochener Lauf, ein Fehler im Gast. Deshalb steht zwischen
Sichern und Loeschen eine echte Nachschau auf der Ablage (backupsFor), und der
juengste Eintrag muss nach dem Beginn dieses Laufes entstanden sein: eine
naechtliche Sicherung von gestern ist kein Archiv, dem der letzte Tag fehlen
darf.

Beide Proxmox-Aufrufe liefern nur eine Kennung, keine Zusage, also wird auf den
Auftrag gewartet und danach nachgesehen — beim Herunterfahren, ob der Gast
wirklich steht, beim Sichern, ob die Datei liegt. Unmittelbar vor dem Loeschen
wird der Datensatz neu gelesen: eine Instanz, die in der Zwischenzeit
wiederbelebt wurde, wird nicht geloescht. Keine Ausnahme entkommt — ein
Fehlschlag ist false und ein Grund in teardown_error, weil dieser Ablauf spaeter
im Zeitplan ueber viele Instanzen laeuft.

Die Ablage kommt aus einer Einstellung (provisioning.proxmox.archive_storage,
Vorgabe local wie bei den naechtlichen Sicherungen) UND wird gegen die Ablagen
des Knotens geprueft. Fehlt sie oder nimmt sie keine Sicherungen auf, bricht der
Abbau ab, bevor eine Kundenmaschine dafuer heruntergefahren wurde.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 11:15:52 +02:00