tests / release (push) Blocked by required conditionsDetails
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.
tests / release (push) Blocked by required conditionsDetails
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>
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.
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>
Auf dem Telefon erschien das Vollbild-Fenster "Aktualisierung laeuft" immer
wieder, obwohl nichts lief; nach einem Neuladen war es weg.
Die Ursache stand im catch des Waechters:
} catch {
this.wasRunning = true; // ohne jede Bedingung
Jede fehlgeschlagene Anfrage schaltete damit das Fenster ein — auch eine, die
mit einer Aktualisierung nichts zu tun hatte. Am Telefon passierte das
staendig: visibilitychange prueft sofort bei jeder Rueckkehr in den
Vordergrund, das Funkmodul ist dann noch nicht wach, die Anfrage scheitert.
Nach einem Neuladen war es weg, weil die Seite dann den echten Serverwert
mitbrachte — was es wie einen Geist aussehen liess statt wie den Fehler, der es
war.
Jetzt gilt eine gescheiterte Anfrage erst dann als Deployment, wenn vorher
bekannt war, dass eines laeuft (wasRunning oder serverConfirmed). Waehrend
eines echten Deployments ist das erfuellt — der Server meldet running, bevor
die Behaelter heruntergehen —, also bleibt das Fenster dort stehen wie bisher.
2879 Tests gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Zwei Fehler, die nur auf schmalen Schirmen auffielen.
Die Seitenleiste trug ihre Anfangsposition NUR in der Alpine-Bindung, nicht in
den statischen Klassen. Bis Alpine startete, stand sie damit auf translate-x-0,
also offen — und wurde danach hinausgeschoben. Zusammen mit transition-transform
war das keine unsichtbare Korrektur, sondern eine sichtbare Fahrt: bei JEDEM
Seitenwechsel klappte sie kurz auf und wieder zu.
Jetzt steht -translate-x-full statisch in der Klassenliste; das erste Bild zeigt
sie geschlossen, es gibt nichts mehr zu korrigieren. Geoeffnet wird ueber
!translate-x-0 — mit Ausrufezeichen, weil bei zwei gleich starken
Tailwind-Klassen die Reihenfolge im Stylesheet entscheidet und nicht die im
Element.
Und die zwei Update-Knoepfe liefen ueber den Rand: zusammen breiter als ein
Telefon, und shrink-0 verbot ihnen zusaetzlich, schmaler zu werden. Jetzt
flex-wrap, shrink-0 erst ab sm.
148 Layout-Tests gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Zwei Dinge am Update-Agenten, beide am 4. August 2026 auf dem Live-Server
gemessen.
Der Fehlalarm: die Konsole meldete „kommt seit HH:MM nicht an die Arbeit"
schon beim ERSTEN übersprungenen Lauf — bei einem Überholen von neun
Sekunden zwischen Zeitgeber und Wächter. Der Agent zählt die Serie jetzt
mit, gemeldet wird ab zwei Läufen.
Der Griff: eine hängende Sperre lässt sich aus der Konsole lösen, statt
per SSH auf dem Wirt. Bestätigung im Modal, die vorher nennt, welchen
Prozess sie beendet; eigene Berechtigung; SIGTERM vor SIGKILL.
Nach dem Update EINMALIG auf dem Server nachziehen — der root-eigene
Helfer kennt den neuen Schritt sonst nicht, und der Knopf kann dann
nichts tun (die Konsole sagt es, aber sie kann es nicht selbst richten):
sudo bash /opt/clupilot/deploy/install-agent.sh
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Update-Agent kam seit 08:02 nicht mehr an die Arbeit. Gemessen auf dem
Live-Server: ein Kind mit "timeout 45" stand nach VIER MINUTEN noch.
timeout schickt nach der Frist ein SIGTERM. `docker compose exec` stirbt daran
nicht, wenn es auf den Docker-Daemon wartet — und danach wartet timeout selbst
unbegrenzt weiter. Der Aufruf haelt dabei die Sperre, also wurde JEDER folgende
Lauf uebersprungen. Stundenlang.
Beide Fristen im Agenten bekommen jetzt `-k 10`: zehn Sekunden nach dem SIGTERM
folgt ein SIGKILL, den niemand ignorieren kann. Aus einem unbegrenzten Haenger
wird eine Verzoegerung von 55 Sekunden.
Der Kommentar an der Stelle kannte die Gefahr bereits ("timeout, weil dieser
Aufruf die Sperre haelt") — die Massnahme reichte nur nicht. Die Begruendung
steht jetzt mit dem Messwert dabei.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
abfa489 lag auf main, aber in keinem Release: das Gate ist der v*-Tag, nicht
der Commit. Ohne diese Zeile bleibt die Korrektur auf dem Server unsichtbar.
Enthalten ist die Einladen-Zeile der Benutzerseite. Sie war unten ausgerichtet,
also schoben Fehlermeldung und Rollen-Hinweis ihre Eingabefelder nach oben —
drei Felder auf drei Höhen, und die Zeile sprang, sobald ein Fehler erschien.
Jetzt oben ausgerichtet, und alles was wachsen kann steht unter der Zeile.
Dazu: der Satz zu "Nur Lesen" stand immer da, auch wenn "Mitglied" gewählt
war — eine Erklärung zu etwas, das gar nicht passiert. Er erscheint nur noch
zur wirklich gewählten Rolle.
2791 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Das groesste Stueck: aus der Sitzverwaltung im Userpanel wird echte
Verwaltung. Bis heute erreichte KEINE Aktion dort je die Nextcloud des
Kunden — Einladen legte keinen Benutzer an, Aussetzen sperrte niemanden aus,
Entziehen loeschte eine Zeile bei uns, waehrend der Mitarbeiter weiterarbeitete.
resend() trug seit jeher den Kommentar "Invite delivery is mocked for now."
Die tragende Entscheidung: niemand kennt ein fremdes Passwort. Nextcloud
erzeugt es selbst und mailt dem Mitarbeiter einen Link, an dem er sein eigenes
setzt. Weder der Inhaber noch der Betreiber noch CluPilot bekommen es je zu
sehen.
Dafuer musste die Kunden-Nextcloud erst ueberhaupt Mail verschicken lernen —
das konnte sie nie. Weder die Bereitstellung noch die Vorlage setzten je einen
Versand; keine Freigabe-Benachrichtigung, kein "Passwort vergessen", nichts,
stumm und ohne Fehlermeldung. Neuer Bereitstellungsschritt, eigene
Absenderdomain, und clupilot:configure-instance-mail fuer den Bestand.
Entziehen loescht ab jetzt nichts mehr: der Zugang wird gesperrt, die Arbeit
bleibt dort, wo das Team sie braucht. Wer wiederkommt, wird ueber dieselbe
Platzpruefung wieder aufgenommen.
Dazu aus dem Betrieb: das Aufheben einer Sicherheitssperre sagt jetzt die
Wahrheit, wenn die Firewall nicht mitkam, und der Abgleich holt es nach.
Und der Versandtakt der zweiten Sitzung: drei Mailspuren mit Prioritaet, ein
Register, das jeden Versand einmal zaehlt, und ein Arbeiter, der die Spuren
auch wirklich abholt.
NACH DEM UPDATE ZU TUN, sonst bleibt die Mitarbeiterverwaltung wirkungslos:
in der Konsole steht eine neue, leere und abgeschaltete Postfachzeile
`instance-relay`. Sie gehoert ausgefuellt, danach einmal
`php artisan clupilot:configure-instance-mail --dry-run` und dann ohne.
Solange sie leer ist, sagt das Panel das ehrlich und verspricht keine Mail.
Die alte Leiter wurde entworfen, bevor jemand ausgerechnet hatte, was ein Server
traegt. Ein Host mit 456 GB und 15 % Reserve vergibt 388 GB — das sind auf
100-GB-Paketen drei Kunden fuer 147 EUR, waehrend 64 GB RAM und zwoelf Kerne
acht bis sechzehn getragen haetten. Es wurde eine Rechenmaschine fuer ein
Speicherprodukt gekauft, und sechs RAM-Plaetze verfielen.
Neu: Start 30 GB fuer 39 EUR, Team 85 fuer 79, Business 175 fuer 139 — jeweils
auf 40, 100 und 200 GB Platte. Neun Startkunden je Server statt drei, 351 EUR
statt 147 bei unveraenderter Miete, und der Punkt, an dem Speicher und RAM
gleichzeitig ausgehen, ist derselbe.
Der Kopfraum waechst mit: max(10 GB, 12 %). Eine 175-GB-Nextcloud mit fuenfzig
Nutzern hat eine groessere Datenbank, mehr Dateiversionen und mehr Papierkorb
als eine mit drei Leuten, und eine volle Platte legt nicht den Upload still,
sondern die Instanz.
Der Zusatzblock geht auf 20 GB fuer 15 EUR und ist bei drei gedeckelt. Bei
12 EUR waere er mit 0,60 EUR/GB billiger gewesen als jeder Aufstieg (0,73 und
0,67) — dann stapeln Kunden, statt umzusteigen, und belegen den knappsten
Rohstoff zum niedrigsten Preis. Ein Block bringt zwei Gigabyte eigenen Kopfraum
mit, damit die Regel auch fuer ein gestapeltes Paket gilt.
Drei Reparaturen mussten der Preisaenderung vorausgehen: die Packungsgroesse
stand in der Konfiguration statt auf der Buchung (der Zuschnitt haette jeden
gekauften 100-GB-Block auf 20 schrumpfen lassen), der Deckel war nirgends
geprueft, und das Aufstocken fragte den Host nicht nach Platz.
Enterprise verlaesst den Laden und wird angefragt: 500 GB finden auf 388 GB
keinen Platz, und der Kunde erfuehre das nach der Zahlung. Damit "eigener
Server" mehr ist als ein Wort, koennen Hosts jetzt einem Kunden reserviert
werden — placeableIn() nahm bisher jeden aktiven Host, und die
Kapazitaetszahlen zaehlten eine exklusiv verkaufte Maschine mit.
Das Testpaket heisst Intern, verschwindet aus dem Verkauf und bleibt
verschenkbar. Dafuer trennen zwei Schalter jetzt zwei Fragen: internal heisst
"nicht im Laden, aber ueber die Konsole vergebbar", sales_enabled = false
heisst "ganz aus".
Codex fand drei Fehler, die keine Durchsicht vorher hatte: der Deckel war nicht
nebenlaeufigkeitssicher, ein angehefteter Host umging die Reservierung, und
dieselbe max(1, ...)-Falle hatte im Bestaetigungsfenster ueberlebt.
2466 Tests gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
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>
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>
Der zweite Bau auf echter Hardware brach ab mit "qemu-guest-agent ist NICHT im
Abbild". Es war im Abbild. Die Prüfung suchte /usr/bin/qemu-ga, Debian legt den
Gastagenten nach /usr/sbin/qemu-ga.
Dass das Paket eingebaut war, stand im selben Protokoll: customise_cloud_image
ruft --install und danach --run-command 'systemctl enable qemu-guest-agent'. Ein
fehlgeschlagenes --run-command lässt virt-customize abbrechen, und systemctl
kann keine Unit aktivieren, die es nicht gibt. Der Lauf ging durch — also war
das Paket da.
Das ist zum zweiten Mal derselbe Fehler in dieser Datei: eine Eigenschaft über
einen NAMEN behauptet statt über die Sache. Erst die Partitionsnummer statt der
Lage auf der Platte (v1.3.89), jetzt ein Pfad statt der Verpackung. Ein Pfad ist
eine Vermutung darüber, wie ein Paket seine Dateien verteilt.
Gefragt wird jetzt /var/lib/dpkg/info/<paket>.list — die Aussage des Pakets über
sich selbst, und genau die Frage, die hier gestellt gehört: hat apt es still
übergangen? Ein einziges Verzeichnis-Listing für alle vier Pakete, weil jeder
virt-ls-Aufruf eine eigene Appliance startet.
Geprüft werden damit qemu-guest-agent (Falle 3), cloud-guest-utils für growpart
(Falle 4), docker-compose-plugin und docker-ce. Der Vergleich ist grep -qx: ein
vorhandenes docker-ce-cli.list darf nicht als docker-ce durchgehen —
durchgespielt, zusammen mit einem vollständigen und einem lückenhaften Listing.
Falle 1 bleibt eine Inhaltsprüfung, weil sie eine ist: user: www-data steht in
einer Datei, die wir selbst hineinkopieren, nicht in einem Paket.
Und die Meldung nennt jetzt die fehlenden Pakete beim Namen statt nur die Falle.
Bestätigt aus demselben Lauf: Falle 2 besteht seit v1.3.89 ("Wurzel auf
/dev/sda1 und nichts dahinter"), und der Umbau selbst — Docker, Compose,
Gastagent — lief in 38 Sekunden durch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der erste echte Vorlagenbau brach ab mit:
Die Root-Partition (/dev/sda1) ist nicht die letzte auf der Platte
(/dev/sda15).
Das Abbild war in Ordnung, die Prüfung nicht. Sie verglich Partitions-NAMEN und
hielt für die letzte, was `sort -V` nach hinten sortiert. Debians Cloud-Abbild
legt die BIOS-Boot- und die EFI-Partition aber an den ANFANG der Platte und
nummeriert sie als 14 und 15; die Wurzel ist Nummer 1 und liegt physisch
dahinter. Gemessen: sda15 beginnt bei 4 MB, sda1 bei 128 MB.
Das ist kein Zufall dieses Abbilds, sondern genau die Eigenschaft, die Falle 2
verlangt — nur so kann growpart die Wurzel über den freien Rest ausdehnen. Die
Prüfung verweigerte also ein Abbild, weil es ihre Bedingung erfüllte.
Jetzt wird nach dem ANFANG auf der Platte gefragt statt nach der Nummer, über
`guestfish part-list`, das Offsets in Bytes liefert. "Nichts liegt dahinter" ist
eine Aussage über die Platte, nicht über die Benennung.
Die Auswertung steht in zwei eigenen Funktionen (last_partition_number,
partition_start), damit sie sich ohne Proxmox und ohne libguestfs gegen
aufgezeichnete part-list-Ausgaben durchspielen lässt — gemacht, in beide
Richtungen: Debians Anordnung besteht, eine Platte mit etwas hinter der Wurzel
fällt durch. Genau diese Gegenprobe fehlte, weshalb der Fehler bis auf echte
Hardware durchkam.
Und die Fehlermeldung trägt jetzt die Byte-Offsets beider Partitionen mit.
Scheitert das je wieder, steht die Platte im Protokoll statt einer Vermutung.
Nebenbei bestätigt: der VPN-Aussetzer vor dem Bau hat zweimal
"could not ask Proxmox about the VM templates" gemeldet und wiederholt, statt
einen Neubau auszulösen — der Codex-P1-Fix aus v1.3.88 hat am ersten Tag
verhindert, dass ein Netzausfall qm destroy --purge auslöst.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Die Übernahme des ersten PVE-9-Hosts blieb in CreateAutomationToken stehen:
`pveum role modify` lehnt die GANZE Privilegienliste ab, sobald ein einziger
Name darin unbekannt ist — "invalid privilege 'VM.Monitor'". Die Liste stammt
aus der PVE-8-Zeit.
Ersatzlos gestrichen, nicht ersetzt: VM.Monitor gab Zugriff auf den
QEMU-Monitor, und den benutzt CluPilot nirgends. Geprüft, nicht vermutet — die
einzigen Treffer auf "monitor" im Provisioning betreffen Uptime Kuma. Auf PVE 8
fehlt damit ein Recht, das dort ohnehin niemand gebraucht hat; eine Liste passt
weiterhin auf beide Fassungen.
Dazu ein Test, der die angeforderten Namen gegen die gültigen hält — abgelesen
aus der Administrator-Rolle eines echten PVE 9. Die Liste ist die Verabredung
zwischen CluPilot und jedem Host, den es je übernimmt; sie hier zu prüfen ist
billiger als der Abbruch auf einer Maschine, die schon halb eingerichtet ist.
2245 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
„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>
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>
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>
Unter jedem Schritt stand `dashboard.step_state.done` statt eines Wortes —
im Adminbereich UND im Kundenportal, wo derselbe Stepper jemanden durch die
Einrichtung seiner Cloud begleitet.
Die Schlüssel gab es in keiner der beiden Sprachdateien. Die Paritätsprüfung
aus R16 konnte das nicht finden: sie vergleicht Deutsch mit Englisch, und die
beiden waren sich einig. Der Aufruf ist zusätzlich zusammengesetzt
(`__('dashboard.step_state.'.$state)`), also findet ihn auch keine Suche nach
festen Zeichenketten. Der neue Test rendert deshalb das Bauteil und prüft alle
vier Zustände in beiden Sprachen.
Dazu ein Test, der heute Nacht hochgegangen ist: PortalInvoicesTest verbot die
Zeichenkette '01.08.2026' — das Datum aus einer alten Attrappe. Heute ist der
1. August 2026, und die echten Rechnungen des Tests tragen das
Ausstellungsdatum von heute. Eine Zusicherung, die ein Datum verbietet,
verbietet auch den Tag, an dem es echt wird. Was die Attrappe ausmachte, war
ihr Satz, und den prüft der Nachbartest über 'Nächste Abbuchung' — stabil,
weil er nirgends sonst entsteht.
2235 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
The DNS check now requires a real zones array before it says anything about the
account. A 200 carrying an HTML page — a portal, a filter, a proxy — was being
read as "no zones at all", which is what the live server was seeing while the
token had read and write and the zone existed with fifteen records.
An intercepted response is now reported as one, with its status and the first
characters of what actually came back.
2053 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The DNS check no longer reports an unclassified HTTP failure as "this account
holds no zones". Anything other than a successful zone list is now named as
what it is, with its status code, and says explicitly that it tells you nothing
about the zones.
2050 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>