Commit Graph

939 Commits (ef1774078728eb524c44798edb9d575c8625f79b)

Author SHA1 Message Date
nexxo ef17740787 Version 1.11.0 — die oeffentlichen Seiten sind im Tunnel erreichbar
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 19:07:18 +02:00
nexxo a3cbaaef61 Abschluss-Review: fuenf Befunde, ein Fix-Durchgang
Kritisch: ein doppelt genannter Name — APP_HOST auch in SITE_HOST, ein Name
zweimal in SITE_HOST, oder einer davon gleich dem Konsolennamen — liess
vpn-entrypoint.sh zwei identische Site-Bloecke schreiben. Caddy lehnt das nicht
bloss ab, es startet dann ueberhaupt nicht ("ambiguous site definition"), der
Container laeuft in eine Neustartschleife, und mit ihm ist die Konsole aus dem
Tunnel verschwunden. Also genau der Ausfall, den dieser Zweig verhindern soll,
erreicht durch einen Tippfehler in der .env. Belegt mit caddy validate in beide
Richtungen.

Dazu: clupilot:publish-tunnel-names wurde von nichts aufgerufen. Die
Gateway-Haelfte liest die .env bei jedem up -d neu, die Resolver-Haelfte wurde
einmal von Hand geschrieben und nie wieder — eine neue Installation, ein
zusaetzlicher STATUS_HOST oder ein neu angelegtes dns-hosts-Volume haetten sie
still veralten lassen. Sie laeuft jetzt im Deploy mit.

Und drei Kleinigkeiten: die Spec beschrieb den Gesundheits-Port noch nach der
alten Annahme, der Resolver uebernahm ungeprueft, was in der .env steht (auf der
Produktivmaschine steht dort noch ein Markdown-Link), und die Testattrappe
raeumte weniger auf als die echte Umsetzung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 19:07:05 +02:00
nexxo 620539a512 bind-hosts nimmt keinen Wert mehr an, der kein Hostname sein kann 2026-08-04 19:07:05 +02:00
nexxo 5967c56d16 Fix-Runde 1: der Update-Agent stirbt nicht mehr an einem gestoppten Gateway, und der Gesundheits-Port luegt nicht mehr
Kritisch: sync_vpn_certificate() rief docker compose exec als blanke
Zuweisung unter set -Eeuo pipefail auf — schlaegt das fehl (kein
laufender vpn-gateway), beendet set -e den ganzen Agenten, still,
nachdem write_alive schon lief. Beide Zuweisungen bekommen jetzt
`|| stamp=''`, mit Begruendung im Kommentar.

Wichtig: der Gesundheits-Port antwortete immer mit 204, auch wenn das
Startskript fuer den Konsolennamen kein Zertifikat fand und den
Gateway ohne jede Seite auf 443 rendert. VPN_READY wurde dann wahr,
und ausgegebene Client-Konfigurationen nannten einen Resolver, der die
Verbindung ablehnt. vpn-entrypoint.sh prueft jetzt, ob die
Konsolen-Seite wirklich gerendert wurde, und antwortet sonst mit 503 —
wget --spider (gegen das echte caddy:2-alpine-Image verifiziert, nicht
angenommen) behandelt das als Fehlschlag, vpn_ready bleibt false, und
die bestehende Warnung in update.sh greift wieder: sie ist nicht
verschwunden, sondern hierher gewandert. Zwei Tests in
VpnGatewayConfigTest decken beide Richtungen ab; der alte Test mit der
jetzt falschen Annahme "unabhaengig von jedem Zertifikat" wich dem
Test fuer den Fall mit Zertifikat.

Kleinigkeit: ein Satz im Kommentar von sync_vpn_certificate() haelt
fest, dass die Zertifikatsliste einmalig beim Start geschrieben wird
und ein nachtraeglich ausgestelltes Zertifikat erst den naechsten
Neustart des Gateways sieht.
2026-08-04 19:07:05 +02:00
nexxo 4dbbed72e2 Plan: zwei Fehler aus meinem eigenen Text, beide mit Ausfallfolge
Erstens: die neue Zuweisung stamp="$(docker compose exec …)" ist blank und laeuft
unter set -Eeuo pipefail. Auf jedem Wirt ohne laufenden Gateway beendet sie den
ganzen Update-Agenten — nach write_alive, der Takt sieht also gesund aus,
waehrend der Update-Knopf still nicht mehr funktioniert. Der Docblock derselben
Funktion warnt vor genau dieser Falle, nur fuer sed. Jetzt "|| stamp=''".

Zweitens: der Gesundheits-Port meldete Bereitschaft, sobald der Gateway laeuft.
Seit dieses Skript Namen ohne Zertifikat auslaesst, kann er laufen und auf 443
gar nichts bedienen — VPN_READY stuende auf true und ausgegebene
Client-Konfigurationen nennten einen Resolver, der auf eine ablehnende Adresse
zeigt. Er antwortet jetzt nur mit 204, wenn der Konsolenblock wirklich gerendert
wurde. Damit erscheint auch die Betreiber-Warnung wieder, ueber die vorhandene
Pruefung in update.sh.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 19:07:05 +02:00
nexxo 58e0943d5b Der Gateway bekommt seine Namen aus der .env und sucht die Zertifikate selbst
Compose mountet das Startskript aus Task 2 statt einer festen Caddyfile und
gibt ihm VPN_TUNNEL_HOSTS mit; die alte vpn.Caddyfile entfaellt. update.sh
haengt das vpn-Profil nur noch an VPN_INTERNAL_HOST, nicht mehr an
VPN_CERT_PATH/VPN_KEY_PATH, weil der Gateway jeden Namen einzeln prueft statt
mit leeren tls-Pfaden abzustuerzen. update-agent.sh ueberwacht zur
Zertifikatserneuerung die Liste, die das Startskript schreibt, mit Rueckfall
auf den alten Einzelpfad fuer einen Gateway von vor dieser Fassung.
2026-08-04 19:07:05 +02:00
nexxo 3e47e02882 Fix-Runde 1: „nimmt keinen Namen auf" bekommt seine positive Zusage
Der Test bestand bislang auch dann, wenn das Skript ueberhaupt nichts
rendert — die einzige Behauptung war eine Verneinung. Jetzt prueft er
zusaetzlich, dass admin.clupilot.test wirklich im Ergebnis steht.
2026-08-04 19:07:05 +02:00
nexxo 1f96dd2ed2 Plan: ein Test ohne positive Zusage prueft nichts
Der Test "nimmt keinen Namen auf, den niemand konfiguriert hat" behauptete nur
die Abwesenheit von files. — er waere auch dann gruen gewesen, wenn das Skript
ueberhaupt nichts gerendert haette. Die anderen Tests mit not->toContain haben
ihre positive Haelfte, dieser hatte sie nicht.

Dazu die Testanzahl im Schritt nachgezogen: durch die Vorabkorrektur an der
Zertifikatsliste sind es sieben, nicht sechs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 19:07:05 +02:00
nexxo 78768cb219 Der Tunnel-Gateway laesst Namen ohne Zertifikat aus, statt nicht zu starten 2026-08-04 19:07:05 +02:00
nexxo 22dd6df61f Die eigenen Namen loesen jetzt auch im Tunnel auf 2026-08-04 19:07:05 +02:00
nexxo a317145468 Plan: die Erneuerungswache muss alle Zertifikate abdecken, nicht eines
Vorabcheck vor dem ersten Dispatch. sync_vpn_certificate() in update-agent.sh
ueberwacht GENAU EINEN Pfad aus VPN_CERT_PATH und startet den Gateway neu, wenn
er sich aendert — weil Caddys tls-Direktive die Datei nur beim Start liest. Mit
mehreren Namen haette eine Erneuerung von www. keinen Neustart ausgeloest, und
der Tunnel liefe mit einem abgelaufenen Zertifikat weiter. Genau der Ausfall,
den die Funktion laut ihrem eigenen Kommentar verhindern soll, nur eine Ebene
groesser.

Das Startskript schreibt jetzt mit, welche Zertifikate es geladen hat; die Wache
liest diese Liste und faellt auf den alten Pfad zurueck, solange ein Gateway von
vor dieser Fassung laeuft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 19:07:05 +02:00
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