222 Commits (8b631acb72a21cab3d90c1e4c2f7cc32f99f3df8)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
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> |
|
|
|
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> |
|
|
|
022279a93c |
Version 1.7.1 — der Zweig mit der Sperren-Freigabe kommt nach main
tests / pest (push) Has been cancelled
Details
tests / assets (push) Has been cancelled
Details
tests / release (push) Has been cancelled
Details
v1.7.0 war getaggt, aber nie zusammengefuehrt. Der Update-Agent folgt Tags, die Arbeit war also live — main kannte sie nicht, und main trug VERSION=1.6.6, also UNTER dem existierenden Tag. Ein naechstes Release aus main haette 1.6.7 geheissen und waere nie ausgeliefert worden. Drei Konflikte, alle so aufgeloest, dass beide Seiten ueberleben: - VERSION auf 1.7.1, damit die Reihenfolge wieder steigt. - deploy/install-agent.sh: main brachte install-server-terminal, der Zweig release-update-lock. Zwei unabhaengige case-Arme an derselben Stelle — beide bleiben, vier Schritte und vier sudoers-Zeilen. - deploy/update.sh: nur ein Kommentar zu HOST_STEP_NEEDS=3. Der timeout -k 10 aus 1.6.5 ist dabei erhalten geblieben. Ohne ihn haengt der Agent unbegrenzt an docker compose exec, und genau das war der Ausfall, der diese ganze Kette ausgeloest hat. Dazu ein Merge-Artefakt: RbacMoveTest zaehlte 22 Berechtigungen. Beide Seiten haben je eine ergaenzt — server.terminal und deployment.unblock — also sind es 23. 2868 Tests gruen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
37e1914246 |
Merge branch 'claude/nice-moser-521659'
# Conflicts: # VERSION # deploy/install-agent.sh # deploy/update.sh |
|
|
|
4debc79883 |
Eine hängende Sperre ist jetzt ein Knopf, kein SSH-Zugang
Am 4. August 2026 hielt ein einziger Aufruf — `timeout 45 docker compose exec` ohne `--kill-after` — die Sperre stundenlang, und jeder folgende Lauf wurde übersprungen. Behoben ist die Ursache in v1.6.5; was fehlte, war der Griff für das nächste Mal. Er lag per SSH auf dem Wirt. Die Konsole kann ihn nicht selbst führen: sie ist www-data in einem Behälter, der Sperrenhalter ist ein Prozess auf dem WIRT. Also bittet sie den Agenten, und der ruft den root-eigenen Helfer — dasselbe Muster wie `apply-proxy-hosts`, mit eigener sudoers-Zeile. Die Bitte liegt in einer EIGENEN Datei, und das ist keine Geschmacksfrage: der Agent nimmt die Sperre in seinen ersten Zeilen, lange bevor er den Postkasten ansieht. Ein blockierter Lauf steigt vorher aus — eine Entsperr-Bitte im Postkasten erreichte ihn also genau dann nie, wenn sie gebraucht wird. Dazu nimmt der Postkasten eine Bitte zur Zeit an, und die dreißig Minuten, in denen dort eine wartende Update-Anfrage liegt, sind die, in denen jemand entsperren will. Der Dienstbenutzer sagt „gib die Sperre frei", nicht „töte 1234": welcher Prozess das ist, sucht der Helfer selbst. Dürfte der Anrufer die Nummer liefern, wäre die Freigabe das Recht, jeden beliebigen Prozess als root zu beenden. Beendet werden alle, die die Sperrdatei OFFEN halten — nicht nur der, der das flock genommen hat. Ein flock hängt an der offenen Dateibeschreibung, und die wird vererbt: stirbt der Agent, sein hängendes `docker compose exec` aber nicht, bleibt die Sperre gehalten. Genau das war der Vorfall. Verschont bleiben PID 1 und der Anrufer samt Vorfahren — im Betrieb hält der blockierte Agent die Datei selbst offen und ist zugleich der, der anruft. Erst SIGTERM, fünf Sekunden, dann SIGKILL. Eine Frist ohne Nachdruck ist keine Frist. Bestätigt wird im Modal (R23), das VORHER nennt, was es beendet — PID, Laufzeit und Kommandozeile aus dem Lebenszeichen. Eigene Berechtigung `deployment.unblock`, nicht `site.manage`: wer aktualisieren darf, darf damit nicht automatisch in einen laufenden Vorgang hineingreifen. Der Helfer-Vertrag steigt auf 3, und update.sh verlangt ihn. Ohne das wäre der Knopf auf einem Wirt, der install-agent.sh seither nicht mehr gefahren hat, ein Knopf, der still nichts tut — und die Konsole sagt es jetzt, statt den Betreiber dorthin zurückzuschicken, wo er ohne sie schon war. Geprüft wird ausgeführt, nicht begutachtet: der Helfer läuft im Test gegen eine echte Sperrdatei mit echten Prozessen daran. Das hat gleich einen Fehler gefunden — der Helfer erbt den Deskriptor und hielt seine eigenen Kommandosubstitutionen für Halter, sodass die Runde nie zum Ende gekommen wäre. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
9fa45e869f |
Ein Terminal auf den CluPilot-Server selbst
Eigene Faehigkeit, eigener Schluessel, eigene Tuer. Die Bruecke bleibt unveraendert: sie kennt keine Hosts, nur Tickets. Den Weg zum Wirt traegt die root-eigene Haelfte des Updaters — sie holt sich die oeffentliche Haelfte selbst, prueft sie hart und schreibt die Optionsliste, die das Dienstkonto nicht bestimmen darf. Kein sudo-Weg dorthin: einen Root-Schluessel eintraegt ein Mensch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
6a5609cfc7 |
Ein einzelner übersprungener Lauf ist kein Ausfall
Die Konsole meldete „kommt seit HH:MM nicht an die Arbeit", sobald der Agent EINMAL an der Sperre vorbeilief. Gemessen wurde das bei einem Überholen von neun Sekunden: Zeitgeber und Wächter laufen beide minütlich, einer nimmt die Sperre, der andere geht weg. Das ist Betrieb, kein Fehler — und der Besitzer hat daraufhin eine Stunde lang eine gesunde Anlage auseinandergenommen. Der Kopfkommentar an der Stelle kannte den Unterschied längst („ließe sich von einem gesunden Überholen zweier Läufe nicht unterscheiden"). Die Dauer wurde mitgeführt, nur gegen nichts verglichen. Der Agent zählt die Serie jetzt mit (`skips` im Lebenszeichen), die Konsole macht ab zwei Läufen eine Meldung daraus. Gezählt wird in LÄUFEN, nicht in Minuten: wie oft der Zeitgeber wirklich auslöst, steht in der systemd-Unit auf dem Wirt, die diese Anwendung nicht sehen kann. Die Schwelle gilt für den ganzen Zustand, nicht nur für den Satz. Nur die Meldung zu unterdrücken hätte den Fehlalarm gegen einen stilleren getauscht: `blocked_since` blendet auch „N Aktualisierungen zurück" und die Zielversion aus, und die wären beim ersten übersprungenen Lauf verschwunden, ohne dass irgendwo stünde warum. Geprüft wird der Zähler am echten Skript, nicht an einer Nachbildung seiner Logik: der Zweig liegt vor allem Teuren, also läuft der Agent im Test gegen eine gehaltene Sperre und steigt aus, bevor `git fetch` oder `docker compose` in die Nähe kommen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
cfbe339df0 |
Die Seitenleiste fragt zuerst, ob etwas auf dich wartet
Achtundzwanzig Eintraege in sieben Gruppen, und „System" war der Platz fuer
alles, was sonst nirgends hinpasste: Mail-Einrichtung neben einem
Rechtsdokument, neben den persoenlichen Kontoeinstellungen, neben der
Mitarbeiterverwaltung — und ganz unten die Seite, die sagt, was liegt. Der
Betreiber hat es so beschrieben: „offene Punkte ist der letzte Punkt, dann
Rolle drueber und Einstellungen wieder drueber".
Zwei Regeln ordnen es jetzt.
Was man einmal einrichtet, verlaesst die Leiste: neun Seiten liegen als
Kacheln hinter EINEM Eintrag (admin.setup), gruppiert nach dem, was sie
einrichten. Keine dieser Seiten wurde angefasst — sie behalten Route,
Berechtigung und Inhalt, es aendert sich nur der Weg dorthin. Damit ist der
Umbau rueckholbar.
Und die drei Seiten, auf denen etwas WARTET, stehen ganz oben, mit einer Zahl
daneben: das ist die Frage, mit der man eine Konsole oeffnet. Stoerungen sind
aus „Betrieb" nach oben gezogen und Zahlungsprobleme aus „Geld" — umgezogen,
nicht verdoppelt. Bei null faellt die Plakette weg, der Eintrag bleibt: eine
Seite, die verschwindet, sobald nichts offen ist, ist genau dann nicht
erreichbar, wenn man nachsehen will, ob wirklich nichts offen ist.
Zwanzig Eintraege statt achtundzwanzig, jede Seite genau einmal.
Zwei Dinge, die beim Verschieben kaputtgegangen waeren:
* Die neun verschobenen Seiten standen nicht mehr in console(). Damit war auf
ihnen KEIN Eintrag markiert (Codex R15, P2) und currentLabel() lieferte
null — die Brotkrume haette dort nur noch „Konsole" gesagt. Zwei Stellen,
eine Wurzel: die Kachelliste ist nach Navigation::setup() gewandert, wo
beide sie lesen, und isCurrent() haelt die Tuer markiert, solange man
dahinter steht.
* Die Versionszeile im Fuss stand als toter Text da, waehrend die Seite mit
dem Aktualisierungsknopf in die Einrichtung gezogen war. Gemeldet vom
Betreiber: „man sieht es nicht, ohne genau hinzuklicken." Sie ist jetzt der
Weg dorthin — und sagt in der Akzentfarbe, wenn etwas wartet. Wartet
nichts, bleibt sie grau: eine Zeile, die immer ruft, ruft nie.
Die drei Zahlen liegen fuer eine Minute im Zwischenspeicher. Diese Leiste
rendert auf JEDER Konsolenseite; ohne das waeren es vier Abfragen je
Seitenaufruf — eine Abgabe, die man erst sucht, wenn die Konsole zaeh ist.
Achtzehn Zusicherungen, darunter die, auf die es ankommt: keine der
achtundzwanzig Seiten von vorher ist verlorengegangen. Die Liste steht im Test
ausgeschrieben und nicht aus der Repository-Geschichte gelesen — ein Test, der
sich seine Erwartung aus demselben Repository holt, das er prueft, prueft
nichts.
Entwurf: docs/superpowers/specs/2026-08-04-konsolen-seitenleiste-design.md
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
|
d04a76b6ee |
Ein Rückweg aus der abgelaufenen Betreiber-Einladung
tests / pest (push) Has been cancelled
Details
tests / assets (push) Has been cancelled
Details
tests / release (push) Has been cancelled
Details
Läuft der 72-Stunden-Link ab, gab es keinen Weg zurück ausser der Shell — und die setzt dabei ungefragt auf Owner. Settings::resendInvitation() zieht für ein noch nie benutztes Konto (last_login_at ist null) ein neues Token und verschickt dieselbe Einladungsmail; der Broker macht das alte Token dabei von selbst ungültig. inviteStaff() läuft jetzt in einer Transaktion, damit eine Störung zwischen Kontoanlage und Mailversand keine für immer blockierte Adresse mehr hinterlässt. Dazu der Nachtrag am Testnachweis: die reflektierende Prüfung auf ein verstecktes Passwort steigt jetzt in Arrays ab statt sie zu überspringen, und prüft zusätzlich das gerenderte HTML statt nur die Komponenteneigen- schaften. Drei Kleinigkeiten: ein falscher Kommentarhinweis auf eine angeblich fehlende Übersetzungsdatei korrigiert, eine fehlende Zusicherung gegen einen rohen Statusschlüssel ergänzt, und ein toter throttle-Wert aus config/auth.php entfernt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
7edd88b71c |
Ein Zugang zur Konsole, dessen Passwort niemand kennt
Bisher erzeugte Settings::inviteStaff() beim Einladen eines Betreiber-Mitarbeiters ein Passwort und zeigte es dem Inhaber einmal auf dem Bildschirm, damit er es "sicher weitergibt" -- fuer einen Zugang zur Konsole, die die ganze Flotte verwaltet. Der Kommentar dort nannte sich selbst eine Attrappe. Jetzt geht eine Einladung ohne Passwort hinaus: der Eingeladene bekommt eine Mail mit einem Link und vergibt sein Passwort selbst, ueber App\Livewire\Auth\OperatorSetPassword. Niemand -- auch der Inhaber nicht -- kennt je ein fremdes Passwort. Dafuer brauchte es einen eigenen Weg, den es fuer Betreiber noch nicht gab (R21: Konsole und Portal teilen keine Identitaet). Ein eigener Broker mit eigener Tabelle (operator_password_reset_tokens, config/auth.php), eine eigene Route in der Gast-Gruppe der Konsole (admin.invitation), und Operator::sendPasswordResetNotification() wirft jetzt, statt Laravels Vorgabe zu nutzen, die auf die Portalseite verlinkt haette. Der Einladungslink gilt 72 Stunden -- lang genug fuer ein Wochenende, kurz genug, dass ein altes Postfach nicht auf Dauer einen Schluessel zur Konsole haelt. Zwei-Faktor bleibt unberuehrt: die neue Seite meldet niemanden automatisch an, der Eingeladene durchlaeuft danach die normale Anmeldung samt ihrer bestehenden Zwei-Faktor-Pruefung. Die Route liegt hinter denselben Netz- und Hostwaechtern wie der Rest der Konsole (RestrictAdminHost, RestrictConsoleNetwork), ohne Sonderfall. OperatorInvitationMail reiht sich in den Versandtakt ein (MailLane::LOCKED, wie ResetPasswordMail und VerifyEmailMail -- ein Mensch wartet gerade) und ist damit in der Vorschau- und Versandtakt-Uebersicht der Konsole sichtbar, statt lautlos in die gedrosselte Spur zu fallen. Zwei Mutationsproben gegen die tragende Zusicherung durchgefuehrt (Bericht: .superpowers/sdd/betreiber-einladung-report.md): die erste (durch die Vorgaengersitzung) hielt fest, dass drei Zusicherungen fallen wuerden, waere das Konto sofort anmeldbar; die zweite -- das Passwort testweise wieder auf die Seite gebracht -- faellt exakt an der dafuer gebauten reflektierenden Pruefung ueber alle oeffentlichen Eigenschaften der Seite. Suite: 2771 passed (9683 assertions). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
d1755f5921 |
Der Update-Agent sagt, dass er lebt — auch wenn er nicht arbeiten kann
Gemeldet: „seit 1.5.1 muss ich nach jedem Update install-agent.sh fahren,
sonst kommt kein Update mehr". Die Konsole meldete „Der Update-Dienst auf dem
Server laeuft nicht" und schickte genau dorthin.
Er lief. Das Journal zeigt zwei Tage lang lueckenlos 57-59 Laeufe je Stunde.
Aber ueber zweiundachtzig Minuten hinweg startete und endete jeder Lauf in
DERSELBEN Sekunde, waehrend ein arbeitender Lauf zwei braucht: sie stiegen
alle sofort wieder aus, an der Sperre eines anderen Vorgangs — `flock -n 9 ||
exit 0`. Lautlos. Kein Journal-Eintrag (systemd sieht einen sauberen Lauf),
keine Zeile in der Statusdatei, nichts in der Konsole.
Und die Statusdatei war die einzige Lebendmeldung, die es gab. Sie wird erst
nach rund 190 Zeilen geschrieben — nach dem `git fetch` und nach einem
`docker compose exec`, beide ohne Zeitgrenze und beide unter der Sperre. Ein
Lauf, der davor aussteigt, hinterlaesst nichts, und nach zwanzig Minuten
schliesst die Konsole daraus, der Dienst sei tot. Sie schloss falsch, und die
Handlungsanweisung dazu aendert an einer gehaltenen Sperre nichts.
Drei Aenderungen:
* Ein Lebenszeichen (agent-alive.json) als ERSTES bei jedem Lauf, vor allem,
was blockieren kann. Zwei Zustaende: `running` heisst "ich habe die Sperre
und arbeite", `blocked` heisst "ich bin ausgestiegen" — mit `since` (seit
wann ununterbrochen) und `held_by` (wer, per fuser und ps).
* Zeitgrenzen: 45 Sekunden um den docker-exec, 120 um den git fetch. Ohne
sie wartet ein Abruf gegen eine tote Verbindung, bis das Betriebssystem
ihn nach vielen Minuten aufgibt — und haelt dabei die Sperre.
* Die Konsole liest das Lebenszeichen statt der Statusdatei. Der Unterschied
ist der Punkt: Status heisst "zuletzt ERFOLGREICH nachgesehen",
Lebenszeichen heisst "zuletzt ueberhaupt gelaufen". Ein blockierter Agent
gilt als lebendig, aber seine Zahlen zaehlen nicht mehr als aktuell —
`behind` und `target_release` fallen auf "unbekannt", statt eine Stunde
alte Auskunft als frisch auszugeben.
Statt "laeuft nicht" steht dort jetzt "laeuft, kommt aber seit HH:MM nicht an
die Arbeit", mit dem Prozess darunter.
Ein Agent von VOR dieser Aenderung schreibt die Datei nicht — fuer den gilt
weiter die alte Regel. Ihn dafuer fuer tot zu erklaeren waere derselbe Fehler
mit umgekehrtem Vorzeichen; ein Test haelt das fest.
Widerlegt und damit ausgeschlossen: Besitzrechte (Wirt, .env und Behaelter
fuehren alle 1001), das Ausführbar-Bit (100755 im Repo), systemds
Startdrosselung (seit v1.1.0 abgeschaltet), ein zwischengespeicherter Zustand
im Panel (es liest bei jedem Aufruf frisch) und eine Luecke im Timer (keine).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
|
aaf8697c3d |
Unter jeder Spur steht, was in ihr faehrt
„Ruhig / mail-ruhig" war dieselbe Auskunft zweimal, die zweite in Kleinschrift. Der Warteschlangenname stand da, weil ich ihn fuer nuetzlich hielt — er ist es, aber in docker-compose.yml und in MailLane, also dort, wo jemand ihn wirklich braucht. Auf der Seite, auf der ein Betreiber entscheidet, was wie eilig ist, sagt er nichts. An seiner Stelle steht jetzt, was in der Spur faehrt und warum sie so faehrt: wer auf die direkte wartet (jemand vor einem Bildschirm), was die wichtige traegt (Wartung, Abschaltung, Wiederanlauf), und warum die ruhige warten darf (eine Rechnung um zwei nachts ist dieselbe Rechnung). Nachgesehen, bevor der Name verschwand: die Bereitschaftsseite nennt die Spuren nirgends beim Warteschlangennamen — es geht nichts verloren. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
714f19d4d4 |
Der Versandtakt nennt die Mails beim Namen
Die Zuordnung zeigte class_basename(): „DormantAccountWarningMail" stand
dort, wo der Betreiber „Konto ohne Paket wird geloescht" sucht. Ein
Klassenname ist eine Auskunft ueber den Bauplan, keine ueber die Mail.
Die Namen liegen in lang/{de,en}/mail_pace.php und NICHT in MailCatalogue,
obwohl der sehr aehnlich klingt: der Katalog zaehlt Mail*arten* — vier
Mahnstufen, vier Eintraege, vier Absenderwege —, diese Seite verteilt
Mail*klassen* auf Spuren, und alle vier Mahnstufen sind EINE Klasse, die als
eine Zeile eine Spur bekommt. Zwei Listen, weil es zwei verschiedene Dinge
sind. Fehlt doch ein Name, steht der Klassenname da wie bisher statt einer
leeren Zeile — und ein Test haelt fest, dass keiner fehlt, in beiden Sprachen.
Der Warteschlangenname (mail-direkt und Geschwister) stand in der Wertspalte
zwischen Beschriftung und Eingabefeldern und las sich wie ein weiterer Wert.
Er steht jetzt als Unterzeile bei der Spur. Weg gehoert er nicht: es ist die
Zeichenkette, die der Arbeiter in docker-compose.yml abhoert.
Suite gruen, Codex ohne Befund. Der Namenstest faellt gegen die alte Fassung
— nachgemessen, nicht angenommen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
|
4023a0939c |
Der Altbestand des Registers wird geradegezogen
Was der doppelte Zuhoerer geschrieben hat, steht noch da: zweiundzwanzig Zeilen fuer zehn Vorgaenge. Das Register beantwortet damit genau die eine Frage falsch, fuer die es gebaut wurde — hat der Kunde das bekommen, und wie oft. Die Wanderung liest den Fehler rueckwaerts. Der Zuhoerer verdoppelte jeden Versand exakt, also bleibt von jeder Gruppe gleicher Zeilen (Empfaenger, Betreff, Mailklasse, Zeitpunkt) die aeltere Haelfte stehen. Aus vier werden zwei — das waren zwei echte Anmeldungen in derselben Sekunde, beide verdoppelt —, aus zwei wird eine, Einzelnes bleibt unberuehrt. Ungerade Gruppen kann dieser Fehler nicht erzeugt haben; taucht doch eine auf, wird aufgerundet, damit im Zweifel eine Zeile zu viel stehen bleibt. down() ist leer, mit Begruendung: geloeschte Zeilen kommen nicht zurueck, und eine erfundene Zeile waere schlimmer als die Luecke. Auf dieser Installation gefahren: 22 Zeilen vorher, 11 danach. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
ea54387a5e |
Eine verschickte Mail hinterlaesst wieder eine Zeile, nicht zwei
App\Listeners\RecordSentMail hoert auf MessageSent, und Laravel meldet die Zuhoerer in app/Listeners von selbst an. Die zusaetzliche Anmeldung von Hand im AppServiceProvider wirkte deshalb nicht als Bekraeftigung, sondern als zweiter Zuhoerer: jeder Versand wurde zweimal verbucht. An einer einzelnen Mail faellt das niemandem auf — der Trockenlauf des Versandtakts schickte fuenfzig und fand hundert Zeilen. Alle zweiundzwanzig echten Zeilen im Register sind entsprechend Doppelungen; das sind Daten, ueber die der Betreiber entscheidet, und sie bleiben hier unangetastet. Die Handanmeldung ist weg, ihre Begruendung steht an ihrer Stelle. Ein Test in MailRegisterTest nagelt eine Mail auf genau eine Zeile fest, egal wie der Zuhoerer angemeldet ist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
1886bf2076 |
Sicht auf die Spuren, und ein Schalter, der die Drossel abstellt
Wenn die Drossel je klemmt, muss der Ausweg ein Klick sein und kein Deployment. Eigene Seite (admin/mail-pace) statt eines fünften Abschnitts auf admin/mail: die dortige Seite ist bereits "alles in einer Wurst", und diese hier beobachtet laufenden Betrieb statt etwas einzurichten — deshalb auch in der Navigation unter "Betrieb", nicht unter "System". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
f40b762413 |
Zwei Empfaengerkreise, und eine Wegwahl, die auch das Konto verschiebt
Fix-Welle nach dem Gesamt-Review, Punkte 4 und 5. Beide sitzen in denselben
Dateien, deshalb ein Commit.
DIE HOST-MELDUNG WAR WORTWOERTLICH DIE KUNDENMAIL (Punkt 4).
notifyHostManagers() verschickte dieselbe SecurityBlockMail. Deren Text ist fuer
den Kunden geschrieben ("bei Ihrer Cloud", "…koennen Sie die Sperre auf der
Sicherheitsuebersicht aufheben"), der Knopf zeigte auf portal.security — wo ein
Betreiber kein Konto hat und Host-Sperren nie erscheinen —, welcher Host
betroffen ist stand nirgends, und die Anrede blieb leer, weil $block->instance
null ist.
Die Mail kann jetzt beide Faelle. Unterscheidungsmerkmal in der Ansicht ist
$hostName: gesetzt heisst Host-Sperre, null heisst Instanz-Sperre. Ein zweites
Flag daneben waere eine zweite Wahrheit.
- Betreff nennt den Host.
- Eigener Textblock (mail_heading_host, mail_preheader_host, mail_intro_host mit
"fehlgeschlagene SSH-Anmeldungen am Host :host"), eigene Tabellenzeile
field_host, eigener Hinweis mail_note_host.
- Der Knopf zeigt auf admin.hosts.show, also auf die Host-Detailseite der
Konsole, wo die Host-Sperren mit ihrem Aufheben-Knopf stehen.
- Keine Anrede im Host-Fall — sie war vorher leer, und der Empfaengerkreis ist
ohnehin mehrere Personen.
- Der Fehlalarm-Satz ist ein anderer und sagt, was ein Betreiber wissen will:
bestehende Verbindungen bleiben unberuehrt, die Regel steht unter
"established,related accept".
R19: die zwei Kommentare, die fuer Host-Sperren Unwahres behaupteten, sind
richtiggestellt — der Kopfkommentar von SecurityBlockMail und der
MailCatalogue-Eintrag. Beide beschreiben jetzt zwei Empfaengerkreise. Ein
Kommentar mit falscher Begruendung haelt den Naechsten vom Nachsehen ab.
DIE WEGWAHL VERSCHOB DEN ABSENDER, NICHT DAS KONTO (Punkt 5).
SecurityBlockMail nagelte mailer('cp_system') fest und holte das Postfach ueber
die Wegwahl. MailboxTransport loeste weiter ueber den ZWECK auf und
authentifizierte mit dessen Zugangsdaten. Legte der Betreiber security-block auf
info@ — der ausdrueckliche Anlass fuer Abschnitt 3a der Spec —, ging die Mail
mit From: info@ durch die SMTP-Sitzung von no-reply@. Viele Server weisen das ab
("553 5.7.1 Sender address rejected: not owned by user"), und dann verschwindet
ausgerechnet die Mail, die er gerade umgelegt hat. Betraf ALLE Mailarten.
Die Wegwahl bestimmt jetzt auch den Mailer. Die zweitbeste Loesung — die Auswahl
in der Konsole auf Postfaecher zu begrenzen, deren Konto benutzt wird — war
nicht noetig.
- config/mail.php erzeugt AUS MailCatalogue::all() je Mailart einen Mailer
cp_mail_<mailart> mit transport, purpose und mail_key. Erzeugt statt
aufgelistet, weil der Katalog ausdruecklich die EINE Liste ist; eine zweite
hier waere die, die beim siebzehnten Mail vergessen wird. Genauso statisch wie
die fuenf Zweck-Mailer: ein reines Array, keine Datenbank, auch unter
config:cache unveraendert.
- MailboxTransport nimmt den mail_key entgegen und loest damit ueber DIESELBE
MailRoute::purposeOrMailbox() auf, die auch der Umschlag fragt. Ohne
Schluessel entscheidet wie bisher allein der Zweck — jeder Mailer, der keinen
hat, verhaelt sich unveraendert.
- Jede Mailart mit Schluessel benennt ihren Mailer ueber
MailCatalogue::mailer(). Dessen Rueckfall auf cp_<zweck> fuer einen
unbekannten Schluessel ist nicht Zierde: DunningNoticeMail baut seinen
Schluessel aus der Mahnstufe, und ohne den Rueckfall waere eine Stufe
ausserhalb 0-3 ein "Mailer [...] is not defined" und damit ein Totalausfall
statt eines falschen Absenders.
- Betrifft alle Mailarten: 13 Mailables, die CloudReady-Notification und die
cloud-ready-Vorschau. ContactRequestMail bleibt auf cp_support — es hat keinen
Schluessel im Katalog, weil es keine eigene Mailart ist.
MailSenderOwnershipTest ist mitgezogen und dabei STRENGER geworden. Die alte
Fassung verglich "From-Zweck == Mailer-Zweck"; jetzt prueft sie, dass der
Katalog dem Zweck zustimmt, den der Umschlag verlangt. Katalog-Drift war bis
hierher nur als Warnung im Docblock von MailCatalogue festgehalten und durch
nichts erzwungen. Ein Literal, das selbst kein Schluessel ist, wird als PRAEFIX
behandelt (der Mahnlauf) — dann muessen alle Schluessel dahinter zustimmen, und
es muss mindestens einen geben.
MailPreviewTest fragte den Zweck bisher, indem es 'cp_' vom Mailer-Namen
abschnitt. Das war eine Zusicherung ueber das Namensschema, nicht ueber die
Zusage; sie liest den Zweck jetzt aus der Konfiguration des Mailers selbst.
tests/Feature/Mail/SenderAddressTest.php gehoert inhaltlich zur parallel
laufenden Sitzung. Vier Zusicherungen darin pruefen genau den Mailer-Namen, den
dieser Commit aendert, also sind sie mitgezogen — die Datei war dabei sauber,
es steckt keine fremde unfertige Arbeit in diesem Commit.
Committet mit ausdruecklicher Dateiangabe am Zeilenende, weil eine parallele
Sitzung an derselben Ablage arbeitet und der Index fremde Arbeit enthalten kann.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
|
51a97019a8 |
Sperren sehen und aufheben: Portalseite fuer den Inhaber, Abschnitte in der Konsole
Aufgabe 6 des Fruehwarnsystems. Der Inhaber sieht im Portal die Sperren SEINER Instanzen und hebt sie dort auf; der Betreiber sieht in der Konsole alle, an der Kunden- und an der Host-Detailseite, und auf der Uebersicht steht ein Hinweis, solange irgendwo eine Sperre aktiv ist. Aufgehoben wird ueber ein Bestaetigungs-Modal (R23), und das Modal mutiert nichts: es wirft ein Ereignis, das die Seite auffaengt und an ihre eigene Methode weiterreicht. Die Berechtigungspruefung bleibt damit an der einen Stelle, an der sie schon stand — noetig, weil ein Modal ohne die Middleware der Seite erreichbar ist (R20). Dazu die Berechtigung `instances.manage`, nach dem Muster der bestehenden `instances.restart`-Migration; Abrechnung und Read-only bleiben unberuehrt. ACHTUNG, was hier sonst noch drinsteckt und NICHT zu dieser Aufgabe gehoert: rund 150 Zeilen zum Versandtakt — das Merkmal `RidesALane`, vierzehn Mailables und `MailLaneRoutingTest`. Die stammen aus einer PARALLEL laufenden Sitzung an einem anderen Feature. Wie das hineingeriet: der Implementierer dieser Aufgabe brach vor dem Commit ab und liess seine fertige Arbeit ungespeichert im Baum. Ich habe sie dateigenau mit `git add <dateien>` vorgemerkt, um nichts Fremdes mitzunehmen — und dabei uebersehen, dass `git add` nur HINZUFUEGT: die andere Sitzung hatte ihre Arbeit bereits vorgemerkt, und `git commit` schreibt den ganzen Index, nicht nur das zuletzt Hinzugefuegte. Richtig waere `git commit -- <dateien>` gewesen. Nichts ist verloren, und die volle Suite ist auf diesem Stand gruen (2571). Aber diese Botschaft soll nicht behaupten, sie beschreibe alles, was hier steht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
5dcfee9957 |
Wegwahl je Mailart: welche Mail aus welchem Postfach geht
MailCatalogue haelt die eine Liste aller sechzehn Mailarten (Schluessel, Beschriftung, Vorgabe-Zweck), aus MailPreviews herausgezogen, damit es nur noch eine Stelle gibt, die beim naechsten Mailtyp vergessen werden kann. MailRoute sitzt darueber: ein Eintrag ist eine Ausnahme fuer GENAU diese eine Mailart, keine zweite Zuordnungsebene — ohne Eintrag oder bei abgeschaltetem Zielpostfach faellt sie unveraendert auf den Zweck zurueck, den MailboxResolver schon kennt. SendsFromMailbox bekommt dafuer einen optionalen $mailKey; alle bestehenden Aufrufer (inklusive ContactRequestMail, das mailboxAddresses selbst zusammensetzt) bleiben bei null und damit beim alten Verhalten. Jede Mailklasse und die CloudReady-Benachrichtigung nennen jetzt ihren Katalog-Schluessel. Die Konsole bekommt eine vierte Karte unter der Zweck-Zuordnung: eine Zeile je Mailart, ein <select> mit den aktiven Postfaechern und "wie der Zweck (...)" als Vorgabe. Der wichtigste Test schickt eine Mail ohne jeden Eintrag und prueft, dass sie exakt beim bisherigen Postfach landet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
5ae2dc41fc |
Ein Waechter, der aufraeumt — und die vier offenen Punkte aus dem Betrieb
DER WAECHTER. Ein abgebrochenes Update liess den Stapel halb unten liegen, den Tunnel weg und die Seite eine Stunde lang mit 500 antworten — bis jemand von Hand nachsah. Das wartet nie wieder auf einen Menschen. deploy/watchdog.sh laeuft jede Minute als systemd-Dienst AUF DEM WIRT, nicht in einem Container: ein Waechter im Container braeuchte den Docker-Socket hineingereicht (Root auf dem Wirt fuer jeden, der je hineinkommt) und waere genau dann tot, wenn man ihn braucht. Er nimmt dieselbe Sperre wie der Update-Agent und fasst waehrend eines Updates nichts an. Er kennt vier Fehlerbilder, alle vier heute wirklich passiert, und fuer jedes genau einen Griff: fehlende Dienste starten; Container, die einander nicht mehr finden, NEU ERZEUGEN (ein Neustart hilft da nicht); wg0 hochziehen; einen seit ueber einer halben Stunde haengenden Wartungsmodus beenden. Was er nicht kennt, protokolliert er und laesst es in Ruhe. `maintenance-hold` ist die Handbremse. Beides nachgemessen: Dienst gestoppt -> wieder da; wg0 abgeraeumt -> wieder oben. DREI LUECKEN, DIE DERSELBE VORFALL AUFGEDECKT HAT: 1. `phase()` machte `mkdir -p` ohne Fangnetz. Gehoerte storage/ nach einem frueheren Fehltritt root, starb das Update mit `set -e` an seiner ERSTEN Zeile, ohne eine einzige Ausgabe. Von aussen sah das aus wie "haengengeblieben"; in Wahrheit war es nach einer Millisekunde vorbei. 2. Ein zweiter Aufruf meldete "Already up to date" und tat NICHTS — der Checkout stand ja schon auf dem Ziel. Der Commit ist die falsche Frage; jetzt wird der Zustand gefragt: laeuft jeder Dienst, ist der Wartungsmodus aus. 3. Nach einer Netz-Umstellung reicht `up -d` nicht: nur neu gestartete Container haengen weiter am alten Netz, alle laufen, und trotzdem loest kein Name mehr auf. Jetzt `--force-recreate`. DIE VIER PUNKTE AUS DEM BETRIEB: - Provisioning zeigte 15/16 in der Liste und "16 von 16" in der Karte daneben, bei Status "Fertig" und 100 %. Die Liste rechnete `current_step + 1` — "dieser Schritt laeuft gerade" —, und bei einem fertigen Lauf laeuft keiner mehr. - Mahngebuehren standen in CENT im Feld. Wer eine Gebuehr eintraegt, denkt in Euro und tippt "5"; daraus wurden fuenf Cent, ohne Widerspruch. Jetzt Euro im Feld, Cent in der Datenbank, gerundet statt abgeschnitten ((int)(19.99*100) ist 1998). Und Fristen und Geld stehen in zwei eigenen Bloecken mit eigener Ueberschrift, die Einheit im Feld statt in der Beschriftung. - Die Bueroadresse stand in der Konsole neben dem Management-Netz mit dem Vermerk "nicht entfernbar" — sie kommt aber aus TRUSTED_RANGES in der .env und ist sehr wohl aenderbar. Beim naechsten Umzug des Anschlusses waere das ein Aussperren gewesen. Strukturell sind nur zwei Eintraege; alles andere steht jetzt als das da, was es ist, mit dem Weg heraus. - Ein Host-Zugang hiess weiter "pve-fns-1", waehrend der Host laengst "fsn-01" hiess. Die Zeile zeigt jetzt den Namen des Hosts, nicht den einmal gespeicherten — damit traegt sie jede kuenftige Umbenennung von selbst. Und die Frage "wofuer brauche ich Uptime Kuma": es ist benutzt — RegisterMonitoring legt fuer jede Kunden-Instanz eine Ueberwachung an, SyncMonitoringStatus holt den Stand, und ein Ausfall steht auf der Uebersicht. Ohne Kunden-Instanzen gibt es nichts zu sehen, was wie "unbenutzt" aussieht. Das steht jetzt in den Einstellungen, statt dort nur "API-Token und wo die Bruecke erreichbar ist". 2522 Tests gruen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
e3c781f067 |
Der Tunnel bekommt einen eigenen Container
Bisher wohnte wg0 im Container des Provisionierungs-Arbeiters, also im Abbild `clupilot-app`. Das wird bei fast jeder Freigabe neu gebaut — und ein neu gebauter Container bekommt eine neue Adresse im Compose-Netz, womit die Weiterleitung fuer UDP 51820 neu geschrieben wird und JEDE bestehende WireGuard-Sitzung abreisst. Der Tunnel hing damit am Veroeffentlichungstakt der Anwendung, und zusaetzlich daran, dass ein PHP-Prozess nicht abstuerzt. Jetzt gehoert der Netz-Namensraum einem eigenen Container `vpn-hub` mit eigenem, winzigem Abbild (Alpine plus wireguard-tools), das sich fast nie aendert. Provisionierungs-Arbeiter, Terminal-Bruecke, interner DNS und internes Gateway steigen dort ein, statt einer von ihnen den Namensraum zu besitzen. NACHGEMESSEN, nicht angenommen: App-Abbild neu gebaut, Arbeiter, Bruecke und Gateway per --force-recreate neu erzeugt — der Hub blieb Container 92e928cf53b0, wg0 und beide Zugaenge unangetastet, und nginx erreichte die Bruecke weiter (HTTP 426). Genau der Vorgang, der bisher jedes Mal alles abgerissen hat. Nachgezogen: - nginx spricht die Bruecke unter `vpn-hub:8082` an — dem Namen des Namensraum-Eigentuemers; ein Mitbewohner hat keinen eigenen DNS-Eintrag. - update.sh baut vpn-hub mit und haengt Nachbar-Neustarts und den conntrack-Griff an die Frage, ob der Hub WIRKLICH neu gebaut wurde. - update-agent.sh startet den Arbeiter nicht mehr neu, sondern signalisiert ihm. Diese Stelle laeuft unbeaufsichtigt hinter dem Knopf „Dienste neu starten" — wer den drueckt, rechnet nicht damit, sich selbst auszusperren. - Vier Meldungen in der Konsole rieten dem Betreiber, genau den Befehl von Hand auszufuehren, der ihm den Tunnel abreisst. Auch die sind korrigiert. - rescue-tunnel.sh und das Runbook zeigen auf den neuen Besitzer. Eine Kleinigkeit unterwegs, die ich falsch angekuendigt hatte: `[[ … ]] && x=true` bricht unter `set -e` NICHT ab — bash nimmt die linke Seite einer &&-Liste ausdruecklich aus. Nachgeprueft; die if-Form bleibt trotzdem, aus Lesbarkeit, und der Kommentar sagt jetzt den wahren Grund. 2509 Tests gruen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
92ebd5d545 |
Terminal: das Fenster sagt jetzt selbst, ob die Bruecke laeuft
Der Betreiber oeffnete ein Terminal und las "Keine Verbindung — laeuft der
Terminal-Dienst, und steht der Tunnel?". Das war keine Diagnose, das war eine
Rueckfrage an den, der gerade unterwegs ist und nicht nachsehen kann.
Der Grund ist eine Eigenart des Protokolls: scheitert ein WebSocket schon am
Handschlag, bekommt die Seite laut Norm KEINEN HTTP-Status — `event.code` ist
1006, sonst nichts. Das ist Absicht (sonst waere ein Socket ein Portscanner) und
macht ausgerechnet die Unterscheidung unmoeglich, auf die es hier ankommt: laeuft
die Bruecke nicht, oder ist die Leitung weg? Beides sah gleich aus.
Eine gewoehnliche Anfrage an dieselbe Stelle darf den Status sehr wohl sehen.
Scheitert der Socket, ohne dass je ein Byte kam, fragt das Fenster deshalb einmal
nach und liest die Antwort:
502/503/504 nginx erreicht die Bruecke nicht -> "Der Terminal-Dienst laeuft
nicht", mit dem Befehl, der ihn zurueckholt, und dem Hinweis auf
den geteilten Netz-Namensraum
404 oeffentlicher Hostname -> "Auf diesem Namen gibt es kein Terminal"
sonst die Bruecke lebt, die Sitzung ist an etwas anderem gescheitert;
"Keine Verbindung" bleibt stehen
gar nichts die Anfrage kam nicht einmal los -> die Leitung ist wirklich weg
Alle drei Faelle nachgemessen, nicht angenommen: Bruecke laeuft -> 426,
oeffentlicher Name -> 404, `docker compose stop terminal` -> 502. Und danach mit
gestoppter Bruecke im Browser angesehen, im selben Zustand, in dem der Betreiber
gerade steht.
Dazu ein Test, der jeden Schluessel abdeckt, den terminal.js an showStage()
uebergeben kann — ein fehlender schriebe "undefined" ins Fenster, und zwar
ausgerechnet in dem Moment, in dem etwas kaputt ist.
2507 Tests gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
|
f64a564c40 |
Terminal: keine Fehlerseite mehr, ein Knopf auf jeder Zeile, eine Buehne statt roter Zeilen
Drei Dinge, die beim ersten Hinsehen im Betrieb auffielen.
1. Der Knopf war weg. Er verschwand, wenn dem Host die Tunneladresse oder der
Fingerabdruck fehlte — mit der Begruendung, ein Knopf, der verlaesslich in
eine Ausnahme laeuft, sei schlechter als gar keiner. Das stimmte, solange die
Seite dahinter mit Laravels Fehlerseite aufging. Jetzt steht er auf jeder
Zeile: ein fehlender Knopf sah aus wie "hier gibt es kein Terminal" statt
"hier noch nicht, und zwar deshalb".
2. Die Seite ging mit einem Stacktrace auf. `TerminalTicket::issue()` warf,
niemand fing es, und wer den Knopf drueckte, bekam Klassenname, Dateipfad,
Zeilennummer und Quelltextauszug in einem Fenster des eigenen Produkts.
`blocker()` beantwortet die Frage jetzt VOR dem Ausstellen und gibt ein
Merkwort zurueck, keinen Satz — die Formulierung gehoert in die
Sprachdateien. `mount()` wirft nicht mehr, mit Fangzaun fuer das, womit
niemand gerechnet hat.
3. Der Abbruch war die einzige ungestaltete Stelle im Produkt: eine rote
ANSI-Zeile mitten in der eigenen Ausgabe. Der Vorspann und der Schirm waren
Geschwister, von denen abwechselnd eines `hidden` trug — das trug genau
einmal, beim Aufbau, und fuer alles danach fehlte die Rueckfahrkarte. Die
Buehne liegt jetzt UEBER dem Terminal und kann dreimal auftreten: beim
Verbinden, beim Ende, beim Abbruch. Die Sitzung darunter bleibt stehen.
Welcher Text, entscheidet der Schliesscode der Bruecke (4401 Ticket, 4502
kein SSH); dazu ein Knopf, der neu laedt, weil ein Ticket dreissig Sekunden
gilt und genau einmal.
Beim Hinsehen gefunden, nicht beim Testen:
- Die Schriftgrafik war unlesbar. Die Figlet-Zeichnung setzt darauf, dass der
Unterstrich einer Zeile den Strich der naechsten beruehrt; in IBM Plex Mono
sitzt er tiefer. Eng verschmierte das Wort, weit zerfiel es. Vollbloecke
fuellen ihre Zelle und stapeln in jeder Schrift.
- Dunkelrot auf Fast-Schwarz hatte kaum Kontrast. Die Wortmarke bleibt jetzt
immer in der Akzentfarbe — sie ist keine Statuslampe, was los ist, sagt die
Zeile darunter.
- Auf dem Schirm stand ":host antwortet nicht". Der Name war an die Erklaerung
uebergeben, an die Ueberschrift nicht. Ein Test mit
`toContain(__('...title'))` haette das nie gefunden — er verglich ":host" mit
":host". Der neue prueft das Ergebnis.
Nachgewiesen: Knopf oeffnet ein NEUES Tab (die Liste bleibt stehen), Ticket
ausgestellt, Socket verbunden, Bruecke kommt nicht auf den Host, schliesst 4502,
Buehne kommt mit "pve-fsn-1 antwortet nicht" und Knopf zurueck, Knopf laedt
wirklich neu und holt ein frisches 64-Zeichen-Ticket. Der Fingerabdruck dafuer
war geliehen und ist wieder entfernt.
2507 Tests gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
|
4e52c35f24 |
Terminal Fix-Runde 1: Ticket-Zusicherung im Test, ehrliche Kommentare, ein Ereignis eine Meldung
- Der Vorspann-Test prüft jetzt data-terminal-splash und die Ticketform (64 Hexzeichen) selbst, statt sich auf ein aria-label zu verlassen, das nur wegen des Tests existiert. - Neuer Test: Berechtigung vor Nachschlagen — eine erfundene UUID meldet 403, nie 404. - Der Knopf-Sichtbarkeitstest sichert jetzt auf die konkrete Terminal-Route zu statt auf das nackte Wort "Terminal" irgendwo auf der Seite. - Drei Kommentare (terminal.js, bare.blade.php, vite.config.js) behaupteten, die Seite liefe ohne Livewire/Chart.js — sie ist aber eine Vollseiten- Livewire-Komponente und zieht app.js über <x-shell.head> ohnehin mit. Kommentare korrigiert: eigener Einstiegspunkt, damit der Terminalcode nicht in app.js landet, nicht weil die Seite ohne Livewire liefe. - terminal.js: ein fehlgeschlagener Socket feuert error UND danach close; onclose schweigt jetzt, wenn nie ein Byte ankam, statt "Verbindung beendet" hinter "Verbindung nicht möglich" zu schreiben. - terminal.js: Textrahmen landen jetzt als String im Terminal statt als leeres Uint8Array. - data-host wird jetzt gelesen und steht in den Verbindungsmeldungen. - wire:ignore auf dem Terminalschirm, bevor die Komponente ihre erste Aktion bekommt und xterms DOM beim nächsten Render löscht. - hosts.blade.php/host-detail.blade.php: der Terminal-Knopf trägt sein href jetzt selbst (x-ui.button :href), statt in einem <a> zu stecken — interaktiver Inhalt in einem Link war ungültiges HTML. Suite: 2502 bestanden (vorher 2501 + ein neuer Test). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
941950584c |
Terminal: eigenes Fenster mit Vorspann, Knopf in Liste und Detailseite
Aufgabe 2: alles, was der Betreiber sieht, noch ohne Container dahinter. Der Knopf bleibt fuer einen Host ohne Tunneladresse oder Fingerabdruck absichtlich unsichtbar, statt in eine unbehandelte RuntimeException aus Aufgabe 1 zu fuehren. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
714b044ff1 |
Fix-Runde 1: Ticket ueber rohes Redis (GETDEL, reines JSON) statt Cache-Fassade
Cache::put() serialisierte den JSON-Inhalt zusaetzlich mit PHP serialize()
(kein 'serializer' konfiguriert), und Cache::pull() war get()+forget() in
zwei Runden statt atomar. issue()/redeem() sprechen jetzt direkt ueber
Redis::connection('cache') (setex/getdel), der volle Schluessel inkl.
REDIS_PREFIX steht im Kopfkommentar fuer Aufgabe 3. issue() weist ausserdem
Hosts ohne wg_ip oder ohne ssh_host_key zurueck, statt die Pruefung an einen
noch nicht existierenden Container zu delegieren.
|
|
|
|
830af24b6c | Terminal: das Ticket, einmalig und dreissig Sekunden gueltig | |
|
|
d8aee6ec2f | Zwei Attrappen: ein Banner ohne Hintergrund und Rechte, die ihren Schluessel statt ihrer Beschreibung zeigten | |
|
|
06d87f2111 | Offene Punkte: was fehlt und was folgt nebeneinander, statt auf halber Breite zu enden | |
|
|
d7bb0f2e63 | Offene Punkte: nach Dringlichkeit gruppiert, in der Formensprache der Bereitschaftsseite | |
|
|
07c51474d3 | Update drehte sich im Kreis: es ersetzte sich selbst mitten im Lauf; dazu eine Seite fuer offene Punkte | |
|
|
2c30d1fb86 | Host anlegen: die Seite beschrieb das Rettungssystem und startete die Pipeline | |
|
|
c0f83ab957 | Seitenleiste neu geordnet: Betrieb trug zwoelf Eintraege aus drei Themen | |
|
|
1207182662 |
Ein angehefteter Host umging die Reservierung
Heftet ein Betreiber eine geparkte Bestellung an einen Host, prüfte der Anheft-Zweig nur die Kapazität — nicht, ob die Maschine jemand anderem reserviert ist. Die frisch gebaute Reservierung ließ sich damit über einen ganz normalen Weg durch die Oberfläche aushebeln, denn das Auswahlfeld auf der Kapazitätsseite bot weiterhin alle aktiven Hosts an. Der Schritt stellt jetzt dieselbe Bedingung wie die freie Platzierung — unreserviert oder dem Kunden dieser Bestellung reserviert — über Host::scopeUnreserved(), statt sie ein zweites Mal zu formulieren. Ein Pin auf eine fremde Maschine wird nicht umgeleitet, sondern wartet (awaiting_pinned_host): zu korrigieren ist die Wahl, nicht die Maschine. Und die Ursache eine Ebene höher: das Auswahlfeld stellt fremd reservierte Hosts gar nicht mehr zur Wahl, pin() lehnt sie zusätzlich ab, und ein Pin, der seit dem Anheften reserviert wurde, sagt das in der Zeile — ein Auswahlfeld, das dann kommentarlos wieder "Automatisch" zeigte, verschwiege, warum die Bestellung weiter wartet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
b95cae1669 |
Test für den Normalfall der Statusanzeige: "Im Verkauf"
Drei der vier Zustände waren belegt, der häufigste nicht. Eine reine assertSee() wäre nicht falsifizierbar gewesen: die Spaltenüberschrift "plans.live_version" übersetzt zufällig auf denselben Satz wie die Plakette "plans.on_sale" und stünde selbst dann auf der Seite, wenn keine Zeile die Plakette zeigte — deshalb über die Häufigkeit geprüft (Überschrift kommt genau einmal vor, mehr als das beweist eine echte Zeile). Mit einer Mutation (sellable-Prüfung stumpf auf false) probeweise als rot bestätigt, dann zurückgesetzt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
6314bb60fb |
Schalter je Paket, ehrliche Statusanzeige, Reste der alten Leiter
Die Umschaltmigration verglich beim Erkennen eines bereits umgeschalteten Bestands nur Kontingent und Platte. Bei Intern waren beide schon vorher richtig (5/20 GB), also hielt sie das Paket für erledigt und ließ RAM, Kerne und Plätze auf ihren alten Werten stehen (1024 MB/1 Kern/5 statt 4096 MB/2/3). Die Prüfung vergleicht jetzt alle neun Vorgaben; eine zweite, kleine Migration hebt einen Bestand nach, auf dem die erste schon lief, und tut nichts auf einer Neuinstallation. Die Statusanzeige unterschied bisher nicht zwischen "kein Angebot" und "läuft, aber nicht im Laden" — ein internes Paket zeigte "Nichts verfügbar" in der Liste und "Im Verkauf" auf der Versionsseite darunter. Vier Zustände statt zwei, mit fester Reihenfolge: der Notausschalter (sales_enabled) schlägt die Konsolen-Kennzeichnung (internal). Ein zweiter Schalter je Paketfamilie nimmt sie aus dem Preisblatt, ohne sie unverkäuflich zu machen — nach dem Vorbild des vorhandenen Verkaufsschalters, ohne Bestätigungsmodal, weil reversibel. Enterprise wechselt von sales_enabled=false (weder käuflich noch verschenkbar) auf internal=true, wie das Testpaket. Dazu die liegengebliebenen Zahlen der alten Leiter in Produktattrappe, Mail-Vorschau, Fabrik-Vorgaben und Seedern, sowie eine Testzusicherung, die auf eine wandernde ID statt auf den berechneten Wert hätte treffen können. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
f961205512 |
Die neue Leiter: Start 39, Team 79, Business 139
Zugeschnitten auf die Maschine, die dasteht. 388 GB vergebbar geteilt durch 40 GB Platte sind neun Startkunden statt drei — Speicher und RAM gehen damit gleichzeitig aus, wo vorher sechs RAM-Plaetze verfielen. 351 Euro Umsatz je Server statt 147. Umgeschaltet wird durch Beenden und Nachfolgen, nicht durch Aendern: eine veroeffentlichte Version ist unveraenderlich, und laufende Vertraege tragen ihren eigenen Schnappschuss. Als Migration und nicht als Befehl, weil der Katalog von einer Migration gesetzt wird — sonst blieben Neuinstallation und Testsuite auf einer Leiter, die wir nicht mehr verkaufen. Enterprise verlaesst den Verkauf (2000 GB finden auf 388 GB keinen Host), das Testpaket heisst Intern und wird intern — es steht ausserdem zum ersten Mal im Katalog einer frischen Installation, statt von Hand angelegt werden zu muessen. Dreissig Testdateien nennen die neuen Zahlen. Drei Faelle waren keine Zahlen: Vertraege auf Enterprise entstehen nur noch als Bestandsvertraege (Helfer asGrandfathered() in tests/Pest.php), die Preisblatt-Stufe "Premium" haengt am Paket, das den Laden verlassen hat, und ein Katalogleser, der die einzige Preiszeile einer Familie suchte, findet seit der Handreichung zwei. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
ce7d0426cb |
Fix-Runde: preferredDatacenter() blind fuer Reservierung, Loeschen hob sie auf
preferredDatacenter() waehlte das Rechenzentrum ueber Host::availableGb() ohne ->unreserved() - ein Rechenzentrum mit einem grossen, aber komplett reservierten Host sah geraeumiger aus als eines mit echtem allgemeinem Bestand. Der Checkout haette die Bestellung dorthin gelegt, placeableIn() haette dort niemanden gefunden, und sie waere geparkt, obwohl anderswo Platz war. Jetzt ->unreserved(), derselbe Bestand wie largestPlaceableGb(). Die Migration nutzte nullOnDelete() und tat damit das Gegenteil der eigenen Vorgabe: die Reservierung sollte einen Kundenaustritt nicht stillschweigend ueberleben, loeste sich mit nullOnDelete() aber genau so auf, sobald der Kunde verschwindet. restrictOnDelete() macht das Loeschen eines Kunden mit eigener Maschine zum Fehler, bis ein Operator die Reservierung von Hand gelöst hat - Migration und Modellkommentar sagen jetzt dasselbe. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
4ad1828f27 |
Ein reservierter Host gehoert seinem Kunden
placeableIn() nahm jeden aktiven Host im Rechenzentrum, und hasRoomFor() sowie largestPlaceableGb() zaehlten eine exklusiv verkaufte Maschine obendrein mit — der Shop versprach damit Platz, der bereits vergeben war. Ohne diese Markierung ist 'eigener Server' ein Satz im Angebot und nirgends eine Tatsache. hosts.reserved_for_customer_id (nullable, ueberlebt den Kunden) markiert die Maschine; Host::placeableIn() und HostCapacity zaehlen sie nur noch fuer den eigenen Mieter oder gar nicht mehr zum allgemeinen Bestand. Auf der Host-Detailseite kann ein Operator reservieren und wieder loesen — Loesen laeuft ueber ein eigenes Bestaetigungsmodal (R23), das selbst nichts aendert, sondern nur an HostDetail::releaseReservation() zurueckmeldet. Host-Liste und Kapazitaetsseite weisen eine reservierte Maschine als solche aus, statt sie kommentarlos verschwinden zu lassen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
c3c05ff9f8 |
Interne Pakete verschwinden aus dem Laden und bleiben in der Konsole
sales_enabled waere der falsche Hebel gewesen: es schaltet auch das Verschenken ab. Das Testpaket ist fuer Abnahmelaeufe da und muss verschenkbar bleiben, also zwei Felder fuer zwei Fragen. Der Checkout lehnt einen internen Schluessel ausdruecklich ab, ueber denselben Fang wie einen unbekannten — eine URL ist keine Liste, und die Antwort darf keinen Unterschied verraten. GrantPlan las bislang dieselbe sellable()-Liste wie Preisblatt und Warenkorb, sowohl fuer sein Dropdown als auch fuer die Validierung des Formularfelds — ein interner Schluessel waere dort ebenso abgelehnt worden wie im Checkout, und das Verschenken haette sein einziges Tor verloren. PlanCatalogue bekommt deshalb grantable() als zweiten Leser derselben Abfrage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
11ba7ee34f | Hostdetailseite: die Adresse steht neben den IPs und ist ein Klick zur Proxmox-Oberflaeche | |
|
|
c815aee0b5 |
Fix-Runde 1: Vorabpruefung vor dem Unique-Index, zwei Praezisierungen, ein fehlender Test
Migration: die Reihenfolge in up() stellte den unwiderruflichsten Schritt
(app_settings loeschen, dns_name-Spalte weg) VOR den einzigen Schritt, der an
vorhandenen Daten scheitern kann (unique('name') - hosts.name trug noch nie
einen eindeutigen Index). Schlug der auf MariaDB fehl, war der Schaden nicht
mehr rueckgaengig zu machen: DDL committet dort implizit, die Migration gilt
aber mangels Eintrag in der Migrationstabelle als nicht gelaufen, und ein
zweiter up()-Versuch stirbt an der bereits fehlenden dns_name-Spalte. Jetzt
steht eine reine Vorabpruefung ganz am Anfang, die simuliert, was die
Uebertragung schreiben wuerde, und mit einer RuntimeException abbricht, bevor
irgendetwas angefasst ist; das Loeschen der app_settings-Zeilen steht jetzt
hinter dem Unique-Index, nicht davor. Gegen echtes MariaDB geprueft,
einschliesslich eines Laufs mit zwei absichtlich kollidierenden Hostnamen.
HostStepsTest: Titel und ein Kommentar praezisiert - der Schritt vergibt den
Namen nicht mehr, er veroeffentlicht ihn nur noch.
HostNamingTest: ungenutzten Import entfernt (Pint), Rueckfall-Test fuer
HostName::label() bei einem Code ohne gueltige Zeichen ergaenzt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
|
d62a2c8ff8 |
Hostnamen vergibt CluPilot: ein Name statt zweier, und der Zaehler ueberlebt das Loeschen
HostName::claim() haengt den Namen jetzt an die Rechenzentrums-Zeile
(next_host_number), nicht an MAX(hosts.name)+1: der Zaehler ueberlebt so
das Loeschen des zuletzt angelegten Hosts. hosts.dns_name faellt weg -
name ist ab jetzt der einzige Name, den Konsole, DNS, /etc/hosts und
Proxmox-Node teilen. RegisterHostDns veroeffentlicht nur noch, was
StartHostOnboarding beim Anlegen vergeben hat, statt selbst zu
nummerieren; PrepareBaseSystem baut den FQDN ueber HostName::fqdn()
statt ueber die nirgends konfigurierte clupilot.net.
Zwei Testdateien ausserhalb der Aufgabenliste (DatacenterTest,
HostTakeoverPageTest) setzten ->set('name', ...) auf HostCreate, das
Feld jetzt aber nicht mehr hat - im vollen Testlauf nachgezogen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
|
55b749c129 |
Release v1.3.94 — die Kurven zeigen wieder, wie ausgelastet der Host ist
tests / pest (push) Failing after 9m19s
Details
tests / assets (push) Successful in 21s
Details
tests / release (push) Has been skipped
Details
Drei Punkte des Besitzers, und der dritte hat den eigentlichen Fehler
freigelegt.
Die Verlaufslinien sahen komisch aus, weil sie auf ihr EIGENES Minimum und
Maximum skalierten. Ein Host mit 0,2 % CPU, der zwischen 0,1 und 0,3 schwankt,
zeichnete damit einen Seismographen über die volle Höhe — und widersprach flach
der Zahl direkt daneben. Dieselbe Regel stand im Chart.js-Entwurf schon
ausformuliert ("eine automatisch skalierte Achse lässt 3 % wie eine Wand
aussehen") und ging beim Umstieg auf die Kacheln verloren.
x-ui.spark bekommt deshalb `min`/`max`. Ohne Angabe bleibt alles wie bisher —
das ist, was jeder bestehende Aufrufer übergibt. Die Prozent-Kacheln geben
0 und 100 mit, die Netz-Kacheln nur die 0, weil MiB/s keine natürliche
Obergrenze haben. Ein untätiger Host zeichnet jetzt vier ruhige Linien statt
zweier wogender und zweier flacher — vorher wirkte CPU (0,2 %) belebter als RAM
(3,6 %), also genau verkehrt herum.
Dazu: Werte außerhalb der Grenzen werden geklemmt statt aus dem Kasten
gezeichnet, die Fläche ist ein Verlauf statt einer harten Kante, und die
Linien sind mit 120×40 statt 80×32 lesbar.
Ich hatte max=100 zwischendurch selbst verworfen, weil es "zu tot" aussah — auf
einem Bild, auf dem die Füllung wegen eines CSS-Fehlers gar nicht gezeichnet
wurde. Ein Vergleich mit einem kaputten Bild. Der Fehler: `.spark path
{ fill: none }` schlägt als CSS-Regel das Präsentationsattribut fill="url(#…)".
`fill: none` gehört an die Linie, nicht an jeden Pfad.
Die Ausstattung ist wieder einzeilig. Sechs Felder, sechs Spalten — und die
Bau-Kennung steht nur noch im Titel: als eigene Zeile zwang sie die ganze Tafel
in eine zweite Reihe, für eine Zeichenkette, die fast niemand liest.
Und die Wartezeit: die Seite stößt beim Öffnen eine Sammlung an, wenn noch
keine Messwerte da sind, statt bis zum nächsten minütlichen Lauf leer zu
bleiben. Ein leerer Kasten liest sich als "kaputt", nicht als "gleich". Der Job
ist ShouldBeUnique, zwei geöffnete Seiten reihen also keine zwei ein.
Codex-Befund dazu (P2): das galt auch für Hosts, die der Sammler ohnehin
überspringt — ein Host mitten in der Übernahme hat keinen Token, und seine
Seite hätte die ganze Flotte abgeklappert und die eigenen Kacheln trotzdem leer
gelassen. Wer gesammelt wird, steht jetzt einmal am Modell (Host::collectable),
gelesen von Job und Seite. Zwei Fassungen dieser Frage waren genau der Grund.
Geprüft: 2307 Tests grün, Pint sauber, Codex ohne Befund. Der Test für den
Maßstab prüft jetzt auch die echten Aufrufstellen und nicht nur das Bauteil —
gegengeprobt durch Entfernen von max=100, dann fällt er. Im Browser beide
Fälle angesehen: ruhiger Host vier flache Linien, ausgelasteter Host lesbare
Form, null Konsolenfehler.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
|
dc35e5310f |
Release v1.3.93 — die Seite konnte den Host gar nicht fragen
tests / pest (push) Failing after 9m23s
Details
tests / assets (push) Successful in 28s
Details
tests / release (push) Has been skipped
Details
Auf echter Hardware blieb jede Messkachel leer, während der Host sichtbar online war und vor zwei Minuten geantwortet hatte. Kein Zufall und kein Aussetzer: ein Entwurfsfehler. Nur der queue-provisioning-Container hängt im WireGuard-Netz — NET_ADMIN, /dev/net/tun, das wireguard-Volume. Der app-Container, der die Konsole rendert, hat überhaupt keine Route zu 10.66.0.x. Ich hatte den Proxmox-Aufruf in render() gelegt, also in den einen Container, der die Management-Adresse nicht erreichen kann. Ein Aufruf von dort konnte nie etwas anderes sein als eine Zeitüberschreitung. PingHosts schreibt dieselbe Regel seit Langem in seinen Kopf: "runs on the provisioning queue, which is where the Proxmox credentials are usable." Ich habe sie gelesen und nicht angewendet. Verschlimmert hat es mein eigenes catch (Throwable): der Grund wurde verschluckt, und die Kachel sagte "keine Messwerte" — ununterscheidbar davon, dass der Host schweigt. Auf dem Testhost fiel nichts auf, weil TEST-NET ohnehin nie antwortet. Jetzt zwei Hälften: - HostLoadSeries::collect() holt und legt ab, aus Jobs\CollectHostLoad auf der provisioning-Warteschlange, minütlich — der Takt, in dem Proxmox einen frischen Messwert schreibt. Ein Fehlschlag wird protokolliert, mit Host, Node und Grund. - HostLoadSeries::forHost() liest nur aus dem Zwischenspeicher und öffnet nie eine Verbindung. Ein Test hält das mit Http::assertNothingSent() fest. Der Eintrag lebt fünf Minuten bei minütlichem Sammeln: länger als der Takt, damit ein ausgefallener Lauf keine Seite leert, die eine Sekunde vorher in Ordnung war — und kurz genug, dass ein stehengebliebener Sammler die Zahlen mitnimmt, statt eine alte Stunde als aktuell stehenzulassen. Der Sammler ist ShouldBeUnique (Codex-Befund, P1). Die provisioning-Warteschlange ist DIESELBE, auf der Kunden-Bereitstellung läuft; ein stiller Host kostet den vollen HTTP-Zeitablauf, und ohne diese Sperre stauten sich minütlich neue Läufe hinter dem alten und verzögerten bezahlte Arbeit. Dasselbe Mittel, das CollectInstanceTraffic nebenan schon benutzt. Dazu: der Zustands-Punkt war mit 62 px so groß wie der Speicher-Ring nebenan. Eine gefüllte Scheibe wiegt optisch weit mehr als ein dünner Ring und erschlug die Kachel — jetzt 32 px. Und ein Test, der aus Versehen recht behielt: die Kachel-Prüfung verglich mit "50", was auch die 500 GB in der Instanzenliste darunter trifft. Sie prüft jetzt Zahlen, die sonst nirgends auf der Seite vorkommen. Noch offen, nicht hier angefasst: VmTemplateCheck fragt die Proxmox-API ebenfalls aus dem app-Container heraus, von der Bereitschaftsseite aus. Selber Fehler, Bestand, eigener Punkt. Geprüft: 2299 Tests grün, Pint sauber, Codex ohne Befund. Im Browser mit eingespielten Messwerten: sechs gefüllte Kacheln, Zustands-Scheibe in Proportion. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
589489a9fc |
Release v1.3.92 — sechs Kacheln statt dreier ungleicher Kästen
tests / pest (push) Failing after 10m15s
Details
tests / assets (push) Successful in 28s
Details
tests / release (push) Has been skipped
Details
Nach der Vorlage des Besitzers. Zustand, Speicher und die gemeinsame Kurve standen nebeneinander: zwei davon halb leer, die dritte überfüllt, und die gemeinsame Kurve brauchte eine Legende, um zu sagen, welche Linie welche ist. Eine Reihe je Kachel löst alle drei Beschwerden auf einmal. Die Beschriftung der Kachel benennt die Reihe, also braucht es keine Legende mehr. Die Höhen sind durch das Raster gleich statt zufällig. Und der leere Platz ist mit Messwerten gefüllt, die es ohnehin schon gab: Proxmox' Aufzeichnung liefert Netzdurchsatz in beide Richtungen mit, ungefragt. Sechs Kacheln: CPU-Auslastung, RAM-Auslastung, Speicher (als Ring), Eingehend, Ausgehend, Zustand. Gebaut mit x-ui.metric, x-ui.spark und x-ui.ring — die gab es alle schon, und der Kopfkommentar von x-ui.metric sagt selbst "exactly as the approved template draws it". Nichts daneben neu gebaut. Die öffentliche IP steht jetzt auf der Seite. Sie stand vorher NUR klein unter der Überschrift — die Adresse, unter der der Host wirklich erreichbar ist, war in der Detailseite nirgends ein Feld. Sie führt jetzt die Ausstattungs-Tafel an, und die Reserve-Eingabe ist mit dorthin gezogen: die Kacheln zeigen, was gemessen wurde, die Tafel, was eingestellt ist. Ein Eingabefeld zwischen Messwerten sähe aus, als ließe sich eine Messung ändern. Alle vier Verlaufslinien tragen denselben Ton. Die Regel steht im Bauteil selbst — "muted where the figure is observed, accent where it can be acted on" —, und hier ist keine Zahl anzufassen. Vier verschiedene Töne nebeneinander behaupten einen Unterschied, den es nicht gibt. Zwei Funde aus der Prüfung -------------------------- - x-ui.spark warf fehlende Messwerte per array_filter heraus und verband die Nachbarn. Zwei Fehler auf einmal: die Linie behauptete eine Messung, die es nicht gab, und alles danach rutschte nach links — eine Stunde mit zwei Lücken zeichnete sich als achtundfünfzig Minuten. Die x-Lage kommt jetzt aus dem Platz in der URSPRÜNGLICHEN Reihe, und zusammenhängende Messwerte werden als eigene Züge gezeichnet. Eine saubere Reihe ergibt genau einen Zug und dasselbe Bild wie vorher, was alle bisherigen Aufrufer liefern. - Der Zwischenspeicher überlebt einen Deploy. Ein Eintrag aus v1.3.91 kennt netin/netout nicht, und die Host-Seite wäre 55 Sekunden lang an einem fehlenden Schlüssel gestorben — genau in der Minute, in der jemand nachsieht, ob das Update durch ist. Der Schlüssel heißt jetzt host-load:v2:<id> und wandert mit der Form mit. Und einer, den kein Prüfer gemeldet hat: beim Zerlegen in Züge stand im Flächenpfad ein `L` unmittelbar vor einem `M`. Gültig gelesen, nicht gezeichnet — die Füllung verschwand still. Aufgefallen ist es beim Ansehen der Seite, nicht durch eine Meldung; jetzt prüft ein Test, dass jeder Flächenpfad mit M anfängt, mit Z endet und keinen Befehl direkt hinter einem anderen trägt. x-ui.chart behält seinen update-on-Weg, obwohl diese Seite ihn nicht mehr benutzt: er ist eine geprüfte Fähigkeit des gemeinsamen Bauteils, und der darunterliegende Fix (Instanz aus dem reaktiven Alpine-Objekt) gilt für jeden Chart. Geprüft: 2291 Tests grün, Pint sauber, Codex ohne Befund. Im Browser mit eingespielten Messwerten: sechs Kacheln, Lücke als echte Aussparung in Linie UND Fläche, Leerzustand zeigt "—" statt einer Null, null Konsolenfehler über einen vollen Poll-Zyklus. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
665a0c4e08 |
Release v1.3.91 — die Host-Seite zeigt Last statt Ausstattung
tests / pest (push) Failing after 9m29s
Details
tests / assets (push) Successful in 20s
Details
tests / release (push) Has been skipped
Details
Die Karte "Rechenleistung" zeigte keine Leistung. 12 Kerne und 63 GB sind die
Ausstattung des Blechs und ändern sich nie — sie standen aber im selben
Kartenraster wie "Zustand" und "Speicher", die beide leben. Wer die Seite
öffnete, um zu sehen, wie es dem Host geht, las dort eine Zahl, die das nie
sagen konnte.
An ihrer Stelle steht jetzt die Last: CPU und RAM als Stundenkurve, beide in
Prozent auf EINER Achse, dazu die aktuellen Werte als beschriftete Zahlen.
Die Geschichte kommt aus Proxmox' eigener Aufzeichnung
(/nodes/{node}/rrddata), nicht aus einem eigenen Sampler. Ein Sampler hieße
neue Tabelle, minütlicher Job, Aufräum-Job und ~1440 Zeilen je Host und Tag —
um weniger genau nachzubauen, was ohnehin auf der Platte liegt. Die RRD ist ab
der ersten Sekunde gefüllt, auch für die Stunde vor dem ersten Hinsehen, und
kann nicht von dem abweichen, was Proxmox' eigene Oberfläche zeigt.
Eine Lücke bleibt eine Lücke: ein Punkt ohne Messwert wird null, nie 0, und
spanGaps steht auf false. Dieselbe Regel wie in instance_metrics. Antwortet der
Host gar nicht, sagt die Tafel das in einem Satz, statt eine ruhige Stunde zu
zeichnen — auf dem Testhost live bestätigt.
Der Fehler, den das ans Licht gebracht hat
------------------------------------------
x-ui.chart steht überall unter wire:ignore, sonst zerstört Livewire das Canvas.
Ein Poll erreicht den Chart also nie. Dafür bekam das Bauteil ein optionales
update-on: es hört auf ein Fenster-Ereignis und tauscht die Daten IM
bestehenden Chart.js-Objekt.
Das lief nicht. Die Zahlen neben der Kurve wanderten, die Kurve nicht, und
chart.update() starb still im Legenden-Plugin:
TypeError: Cannot set properties of undefined (setting 'fullSize')
Grund: die Chart.js-Instanz lag als Eigenschaft im Alpine-Objekt und wurde
damit reaktiv umhüllt. Chart.js' Plugin-Innenleben überlebt das Proxy nicht.
Gemessen statt geschlossen: dieselbe Instanz wirft über das Proxy und läuft
über Alpine.raw(). Sie liegt jetzt in der Closure.
Aufgefallen ist es nie, weil bis zum ersten Live-Chart kein einziger Chart in
diesem Repo je update() gerufen hat — konstruieren und Erstzeichnen gehen durch
die Hülle noch. tests/Feature/ChartLiveUpdateTest.php hält die Regel fest,
damit der nächste Live-Chart nicht denselben Nachmittag kostet.
Der Rest
--------
- Version lesbar: "Proxmox VE 9.2.6" statt pve-manager/9.2.6/7f8d…, mitten in
der Bau-Kennung abgeschnitten. Die Kennung steht klein darunter. Eine
unerwartete Form wird unverändert durchgereicht statt verschluckt.
- Vier Kleinkarten (Mgmt-IP, Node, Version, Instanzen) sind eine
Ausstattungs-Tafel geworden. Die Instanzen-Anzahl steht in der Überschrift
der Liste, die sie ohnehin zeigt.
- Der Übernahme-Fortschritt klappt zu, sobald sie durch ist. Fünfzehn
abgehakte Schritte sind auf einem laufenden Host kein Dauerinhalt —
aufklappbar über <details>, ohne JavaScript.
- PlanVersion::requiredTemplateVmids() ersetzt die dritte Kopie derselben
Fensterlogik.
- BuildVmTemplate sagt nicht mehr "this takes 10–20 minutes". Das war eine
Schätzung vor dem ersten Lauf; gemessen waren es unter zwei. Eine Konsole,
die falsch vorhersagt, erzieht dazu, sie zu ignorieren.
Geprüft: 2281 Tests grün, Pint sauber, Codex ohne Befund. Die Farbwahl gegen
den Validator gerechnet (ΔE 28,3 protan / 39,2 normal; der Akzent liegt unter
3:1 gegen die Fläche, deshalb tragen beide Reihen sichtbare Beschriftung). Im
Browser: null Konsolenfehler über einen vollen Poll-Zyklus, und die Kurve
wandert auf dem echten Poll ohne Neuladen — mit eingespielten Messwerten
belegt, weil die Testhosts in TEST-NET liegen und nie antworten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
|
b9fb401823 |
Release v1.3.85 — Handshake-Prüfung hing an einem Programm, das es nicht gibt
tests / pest (push) Failing after 8m54s
Details
tests / assets (push) Successful in 22s
Details
tests / release (push) Has been skipped
Details
Die Übernahme brach in "WireGuard einrichten" ab: fünf Wiederholungen, "WireGuard handshake not up yet". Geprüft wurde mit `ping -c1 -W2 <hub-ip>`. Das fragte drei Dinge auf einmal und nannte nur eines: ob der Handshake steht, ob ICMP durchkommt — und ob es `ping` auf der Maschine überhaupt GIBT. Auf Hetzners `Debian-trixie-latest-amd64-base` gibt es das nicht. `iputils-ping` ist im Image nicht dabei, und PrepareBaseSystem installiert `curl gnupg ifupdown2 chrony`. Der Schritt scheiterte damit über einem Tunnel, der stehen konnte. Auf dem alten Proxmox-Image war ping dabei, deshalb lief es dort durch — dieselbe Pipeline, anderes Grundsystem. Gefragt wird jetzt WireGuard selbst: `date +%s; wg show wg0 latest-handshakes`. Die Uhr des HOSTS kommt in derselben Antwort mit, weil `latest-handshakes` eine absolute Zeit ausgibt und ein Vergleich gegen UNSERE Uhr eine Zeitverschiebung zwischen zwei Maschinen als Tunnelzustand läse. `date` ist in coreutils und überall da. Codex, zwei Runden: - P1: `> 0` hiesse "hat jemals". WireGuard behält den Zeitstempel unbegrenzt, also meldete ein Wiederholungslauf über einem toten Tunnel "steht", und die Schritte danach wählten die Tunneladresse für SSH. Jetzt muss der Handshake frisch sein (180 s) und vom KONFIGURIERTEN Hub kommen — ein fremder Peer auf wg0 ist kein Beweis dafür, dass wir erreichbar sind. - P1: Der neue Host-Versatz (.100) liess die Vergabe in einem Subnetz kleiner als /26 "erschöpft" melden, obwohl unten alles frei war. Der Versatz ist eine Bevorzugung, keine Bedingung: zweiter Durchgang von vorn. Ausserdem, wie gewünscht: Hosts bekommen ihre Tunneladresse ab .100 (CLUPILOT_WG_HOST_OFFSET), Personen zählen weiter von unten. Fortlaufend vergeben landete der erste Host zwischen zwei Notebooks, und wer eine Adresse in einem Protokoll las, konnte nicht sagen, ob dahinter ein Mensch oder eine Maschine steht. 2243 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
2f15d1e7a4 |
Release v1.3.84 — VPN-Zugänge nach Personen und Hosts getrennt
tests / pest (push) Failing after 9m6s
Details
tests / assets (push) Successful in 20s
Details
tests / release (push) Has been skipped
Details
Personen und Hosts standen gemischt untereinander, nach Verbindungszustand sortiert — ein Host-Zugang konnte also zwischen zwei Mitarbeitern stehen. Beide sind Peers im selben Netz und haben dieselben Messwerte, aber es sind zwei verschiedene Fragen: "wer von uns ist im Netz" und "welche Maschinen hängen dran". Wer die eine stellt, liest die Antworten der anderen als Rauschen. Jetzt zwei Gruppen mit Überschrift und Anzahl. Eine Gruppe ohne Einträge wird gar nicht erst gezeichnet — auf einer frischen Installation stünde sonst "Hosts" über einer Lücke, bevor je einer angelegt wurde. Getrennt wird nach der geladenen host-Beziehung, nicht nach `kind`: die Plakette in der Zeile fragt dasselbe, und wer einen "Host-Zugang" liest, soll ihn auch unter den Hosts finden. Ein adoptierter Peer (kind=system) mit host_id gehört zu den Hosts, obwohl seine Art etwas anderes sagt. Die Zeile selbst ist nach resources/views/components/admin/vpn-peer-row.blade.php gewandert. Sie zweimal hinzuschreiben hätte geheissen, sie ab dem nächsten Knopf an zwei Stellen zu pflegen — und die zweite fällt erst auf, wenn jemand einen Host-Zugang sucht und ihn anders aussehen sieht als seinen eigenen. Die Plakette bleibt trotz der Überschrift daneben: eine Zeile wandert beim Suchen aus ihrer Überschrift heraus, und dann steht sie ohne sie da. Codex: keine Befunde. 2237 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
ac81bc5b98 |
Release v1.3.82 — Übernahme-Befehlszeile neu ausstellen
tests / pest (push) Failing after 8m50s
Details
tests / assets (push) Successful in 23s
Details
tests / release (push) Has been skipped
Details
Die Zeile gab es genau einmal: auf der Seite direkt nach dem Anlegen. Wer sie dort nicht kopierte — oder wem sie wegbrach, wie beim WireGuard-Fehler bis 1.3.80 — hatte danach einen Host in der Liste, für den es keinen Weg zu einer Befehlszeile mehr gab. Ein zweites Anlegen scheitert an der eindeutigen IP, also blieb nur Entfernen und von vorn. HostTakeoverCommand behauptet in seinem Kopfkommentar, die Zeile werde "an zwei Stellen gezeigt (beim Anlegen und beim Neuausstellen eines Codes)". Das Zweite gab es nie — ein Kommentar, der eine Absicht beschreibt und wie eine Beschreibung des Gebauten klingt. Neu: ReissueTakeover als Modal auf der Host-Detailseite. Bestätigen zuerst (R23), denn ein bisher ausgegebener Code wird dabei wertlos; danach steht die ganze Zeile mit Kopieren-Knopf darin, nicht nur der Code. Codex, zwei Runden, beide Male dieselbe Sorte Fehler von mir — die Ansicht versteckt den Knopf, die Methode prüft nichts: - P1: `issue()` erzwingt jetzt selbst, welcher Zustand zulässig ist. Ein Modal, das offen blieb, während der Host aktiv wurde, rief die Methode trotzdem; eine Livewire-Methode ist ohnehin von aussen aufrufbar. - P2: `issue()` prüft die Tunnel-Einstellungen erneut. Sonst wird ein gültiger Code entwertet und dafür eine Zeile ausgegeben, der die WireGuard-Angaben fehlen — genau die kaputte Zeile, vor der die Warnung daneben steht. - P1 der zweiten Runde: `disabled` gehörte nicht in die Liste der zulässigen Zustände. Es sieht aus wie "noch nicht fertig" und ist das Gegenteil — toggleMaintenance() schaltet einen LAUFENDEN Host so still. Der Knopf hätte eine Produktionsmaschine zur Neuinstallation angeboten. Die Bedingung steht deshalb einmal am Bauteil (ReissueTakeover::eligible) und wird von Ansicht und Methode gefragt: zwei Fassungen liefen auseinander, sobald jemand eine ändert. 2231 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |