diff --git a/lang/de/hosts.php b/lang/de/hosts.php index 770a723..06d5ce0 100644 --- a/lang/de/hosts.php +++ b/lang/de/hosts.php @@ -167,6 +167,16 @@ return [ 'title' => 'Keine Sitzung auf :host', 'note' => 'Die Brücke kam nicht bis zur Kommandozeile: die Maschine antwortet nicht, ihr Fingerabdruck weicht vom hinterlegten ab, oder sie hat den Flottenschlüssel abgewiesen — Letzteres ist das Wahrscheinlichste, wenn der Host neu aufgesetzt wurde. Was davon, steht im Protokoll des Terminal-Dienstes; dieses Fenster erfährt es absichtlich nicht.', ], + // Die beiden hier stehen für Fälle, die der Browser von sich aus gar + // nicht auseinanderhalten kann — siehe resources/js/terminal.js. + 'service_down' => [ + 'title' => 'Der Terminal-Dienst läuft nicht', + 'note' => 'Die Konsole steht, die Brücke dahinter nicht: nginx meldet für diese Stelle 502. Auf dem Server hilft „docker compose up -d terminal". Sie teilt sich den Netz-Namensraum des Provisioning-Containers und muss nach jedem Neustart des Tunnels selbst mit neu gestartet werden — sonst lauscht sie in einem Namensraum, den es nicht mehr gibt, und meldet dabei weiter „healthy".', + ], + 'not_here' => [ + 'title' => 'Auf diesem Namen gibt es kein Terminal', + 'note' => 'Die Brücke ist Werkzeug der Konsole und antwortet auf öffentlichen Hostnamen mit 404 — dieselbe Regel wie für /admin. Öffne die Konsole unter ihrem eigenen Namen.', + ], 'retry' => 'Neu verbinden', ], diff --git a/lang/en/hosts.php b/lang/en/hosts.php index 6ccd0fb..bdc66d6 100644 --- a/lang/en/hosts.php +++ b/lang/en/hosts.php @@ -167,6 +167,16 @@ return [ 'title' => 'No session on :host', 'note' => 'The bridge never reached a command line: the machine is not answering, its fingerprint differs from the one on file, or it refused the fleet key — the last is the likeliest once a host has been rebuilt. Which of them is in the terminal service log; this window is deliberately not told.', ], + // These two cover cases the browser cannot tell apart on its own — + // see resources/js/terminal.js. + 'service_down' => [ + 'title' => 'The terminal service is not running', + 'note' => 'The console is up, the bridge behind it is not: nginx answers 502 for this path. On the server, "docker compose up -d terminal" fixes it. The bridge shares the provisioning container\'s network namespace and has to be restarted along with the tunnel — otherwise it listens inside a namespace that no longer exists while still reporting "healthy".', + ], + 'not_here' => [ + 'title' => 'No terminal on this hostname', + 'note' => 'The bridge is a console tool and answers 404 on public hostnames — the same rule as /admin. Open the console under its own name.', + ], 'retry' => 'Reconnect', ], diff --git a/resources/js/terminal.js b/resources/js/terminal.js index e8fa941..9a24999 100644 --- a/resources/js/terminal.js +++ b/resources/js/terminal.js @@ -112,8 +112,33 @@ if (root && root.dataset.ticket) { socket.onclose = (event) => { if (event.code === 4401) return showStage('rejected', true) if (event.code === 4502) return showStage('unreachable', true) + if (received) return showStage('ended', true) - showStage(received ? 'ended' : 'lost', true) + // Nie eine Sitzung gehabt — und genau hier wusste das Fenster bisher + // nichts und riet. + // + // Scheitert ein WebSocket schon am Handschlag, bekommt die Seite laut + // Norm KEINEN HTTP-Status: `event.code` ist 1006, sonst nichts. Das ist + // Absicht (sonst wäre ein Socket ein Portscanner), macht aber genau die + // Auskunft unmöglich, die ein Betreiber hier braucht — läuft die Brücke + // nicht, oder ist die Leitung weg? Beides sah gleich aus. + // + // Eine gewöhnliche Anfrage an dieselbe Stelle darf den Status sehen. + // nginx antwortet dort mit 502, solange die Brücke nicht erreichbar ist + // (`resolver` + Variable, docker/nginx/default.conf), mit 404 auf einem + // öffentlichen Namen, und sonst mit dem, was die Brücke selbst sagt — + // ein Upgrade-Fehler, aber eben von ihr. Das reicht zum Unterscheiden. + showStage('lost', true) + + fetch('/terminal/ws', { cache: 'no-store' }) + .then((probe) => { + if ([502, 503, 504].includes(probe.status)) showStage('service_down', true) + else if (probe.status === 404) showStage('not_here', false) + }) + .catch(() => { + // Die Anfrage kam nicht einmal los: dann ist wirklich die + // Leitung weg, und „Keine Verbindung" von oben stimmt. + }) } // Kein `onerror`: ein Fehler zieht immer ein `close` nach sich, und dort oben diff --git a/resources/views/livewire/admin/host-terminal.blade.php b/resources/views/livewire/admin/host-terminal.blade.php index 816e7f6..aa759dc 100644 --- a/resources/views/livewire/admin/host-terminal.blade.php +++ b/resources/views/livewire/admin/host-terminal.blade.php @@ -43,6 +43,14 @@ 'title' => __('hosts.terminal.stage.unreachable.title', ['host' => $host->name]), 'note' => __('hosts.terminal.stage.unreachable.note', ['host' => $host->name]), ], + 'service_down' => [ + 'title' => __('hosts.terminal.stage.service_down.title'), + 'note' => __('hosts.terminal.stage.service_down.note'), + ], + 'not_here' => [ + 'title' => __('hosts.terminal.stage.not_here.title'), + 'note' => __('hosts.terminal.stage.not_here.note'), + ], ]; @endphp
toContain($host->name) ->and($messages['ended']['note'])->toContain($host->name) ->and($messages['lost']['note'])->toContain($host->name); + + // Jeder Schlüssel, den terminal.js an `showStage()` übergeben kann, muss + // hier liegen — sonst stünde im Fenster „undefined", und zwar ausgerechnet + // in dem Moment, in dem etwas kaputt ist. + expect(array_keys($messages))->toEqualCanonicalizing([ + 'ended', 'lost', 'rejected', 'unreachable', 'service_down', 'not_here', + ]); }); it('bietet den Terminal-Knopf nur dem, der ihn drücken darf', function () {