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>
Aus dem Gesamt-Review, und der erste Befund ist der, der still zugeschlagen
haette.
`terminal` lebt im Netz-Namensraum von `queue-provisioning`, und ein Prozess
bleibt in dem Namensraum, in dem er gestartet ist. `update.sh` startet den Hub
neu — danach lauscht die Bruecke in einem, den es nicht mehr gibt. Nichts meldet
dabei einen Fehler: `docker compose ps` sagt weiter "healthy", weil die
Lebendpruefung ueber Loopback INNERHALB des verwaisten Namensraums laeuft. Nach
aussen antwortet nginx mit 502, und der Betreiber liest "Keine Verbindung —
laeuft der Terminal-Dienst?", waehrend der Dienst behauptet, es gehe ihm gut.
Genau dieselbe Falle, die zwei Bloecke tiefer schon fuer vpn-dns/vpn-gateway
behandelt ist; die Bruecke fehlte in der Behandlung.
Nachgemessen statt geglaubt: Hub neu gestartet -> Docker sagt "healthy", curl aus
dem Namensraum bekommt gar keine Antwort. Nach `restart terminal`: 200.
Und ein zweiter Ausrollfehler daneben: gebaut wurde nur `app`. `docker compose
up -d` baut nur Images, die es noch GAR NICHT gibt — beim ersten Ausrollen faellt
das nicht auf, danach nie wieder. Eine Aenderung an docker/terminal/ saehe
ausgeliefert aus, und es liefe das alte Image.
Ausserdem:
- Die Meldung zu 4502 zaehlte zwei Ursachen auf, der Code deckt fuenf. Die
Bruecke schickt 4502 fuer JEDE gescheiterte Anmeldung, auch fuer einen
abgewiesenen Schluessel — und das ist der wahrscheinlichste Fall, wenn ein Host
neu aufgesetzt wurde. "antwortet nicht" war dort schlicht falsch: die Maschine
hat geantwortet und abgelehnt. Titel und Text legen sich nicht mehr fest.
- R19: der Kommentar an der Kopfzeile der Spalte nannte "Berechtigung,
Betriebsbereitschaft" als Grund, warum der Knopf nicht ueberall steht.
Letzteres entscheidet seit dem Entsperren nichts mehr, und zwanzig Zeilen
tiefer begruendete der Kommentar am Knopf ausfuehrlich das Gegenteil.
- Der Kommentar am Retry-Knopf erklaerte die Reihenfolge von .hidden gegen
.inline-flex fuer zu unsicher, waehrend die Buehne dreissig Zeilen hoeher genau
darauf baut. Tailwind gibt .hidden als letzte Display-Klasse aus; der wahre
Grund fuer den Wrapper ist, dass die Klassen des Knopfes aus einem geteilten
Bauteil kommen.
- REDIS_URL stand fest auf Datenbank 1, waehrend PHP REDIS_CACHE_DB liest. Wer
die anfasst, legt auf der einen Seite ab, wo die andere nicht sucht.
2507 Tests gruen, compose config und bash -n sauber.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
- 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>
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>