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 Beiwagen nach dem Vorbild von kuma-bridge: Python, ein Zweck, kein
Laravel. Er steht im Netz-Namensraum von queue-provisioning, weil dort wg0
lebt und nur von dort ein Host ueberhaupt erreichbar ist.
Drei Punkte, die das Review von Aufgabe 2 offen gelassen hat, sind hier
entschieden:
- Das Ticket reist als Sec-WebSocket-Protocol, nicht in der Adresszeile.
Die Adresse eines Upgrade-Antrags schreibt jeder Reverse Proxy mit, und
auf der Strecke stehen zwei, von denen nur einer aus diesem Repo
konfiguriert wird.
- /terminal/ws bleibt an der Wurzel, unabhaengig von AdminArea::prefix():
nginx kann die Konfiguration der Anwendung nicht lesen, der Ort muss ein
fester Text sein. In beiden Dateien steht die Begruendung.
- Die Bruecke sendet ausschliesslich Binaerrahmen.
Abweichungen vom Plan, jeweils gemessen statt vermutet:
- Der Schluessel in Redis traegt REDIS_PREFIX (clupilot-database-), nicht
CACHE_PREFIX: TerminalTicket schreibt ueber Redis::connection('cache'),
und phpredis stellt die Praefix-Option dieser Verbindung voran.
- Port 8082 statt 8081: im selben Namensraum lauscht der VPN-Gateway schon
auf VPN_HEALTH_PORT, und der zweite Zuhoerer auf einem Port stirbt.
- SSH ueber paramiko.Transport statt SSHClient. SSHClient prueft gegen
known_hosts, die dieser Container nicht hat und aus einem Fingerabdruck
auch nicht bilden kann; RejectPolicy verbaende nie, AutoAddPolicy
meldete sich mit einem Root-Schluessel an, bevor irgendetwas geprueft
ist. Der Transport erlaubt die richtige Reihenfolge: Handschlag,
Fingerabdruck, dann erst Anmeldung.
- Der Fingerabdruck wird gebildet wie in PhpseclibRemoteShell, nicht wie
bei OpenSSH: gehasht wird "<algorithmus> <base64-blob>", nicht der Blob.
- proxy_pass ueber eine Variable mit resolver, damit nginx nicht beim
Start scheitert, wenn die Bruecke gerade nicht laeuft.
- /terminal/ws antwortet auf einem oeffentlichen Hostnamen mit 404,
dieselbe Regel wie /admin eine Zeile darueber.
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>