diff --git a/app/Http/Middleware/PublicSiteGate.php b/app/Http/Middleware/PublicSiteGate.php index e76e22a..b869224 100644 --- a/app/Http/Middleware/PublicSiteGate.php +++ b/app/Http/Middleware/PublicSiteGate.php @@ -57,6 +57,10 @@ class PublicSiteGate return $next($request); } + if ($this->isDownload($request)) { + return $next($request); + } + if ($this->fromManagementNetwork($request) || $this->isActiveOperator()) { return $next($request); } @@ -68,6 +72,30 @@ class PublicSiteGate ]); } + /** + * Die Datei-Domain bleibt erreichbar, auch solange die Website verborgen ist. + * + * Sonst bricht jede Host-Übernahme, und zwar unsichtbar: ein Server im + * Rettungssystem holt das Archiv mit `curl … | tar xz`, bekäme hier die + * 503-Platzhalterseite und schöbe sie in `tar`. Der Betreiber sähe einen + * Entpackfehler auf einer Maschine, an die er nur noch über die + * Anbieterkonsole kommt — und nichts davon deutete auf einen Schalter in + * der Konsole hin. Er ist per Definition weder in einem verwalteten Netz + * noch angemeldet: das ist ja gerade der Zustand, den er verlassen will. + * + * Über den ROUTENNAMEN, nicht über den Pfad: die Route existiert nur, wenn + * die Domain gebunden hat. Der Docblock oben warnt zu Recht davor, per + * Hostname auszunehmen — dort ginge es aber um das gesamte Portal, und ein + * gefälschter Host-Header hätte es aufgedeckt. Hier stehen zwei Routen + * dahinter: die Rechtsdokumente, die ohnehin öffentlich sind, und das + * Archiv, das ohne gültigen Einmal-Code mit 404 antwortet. Wer den Header + * fälscht, gewinnt damit nichts, was er nicht auch nach dem Start hätte. + */ + private function isDownload(Request $request): bool + { + return in_array($request->route()?->getName(), ['bootstrap.archive', 'files.public'], true); + } + private function fromManagementNetwork(Request $request): bool { $ranges = (array) config('admin_access.trusted_ranges', []); diff --git a/lang/de/hosts.php b/lang/de/hosts.php index 51d1be5..0347e97 100644 --- a/lang/de/hosts.php +++ b/lang/de/hosts.php @@ -135,45 +135,64 @@ return [ 'complete_host_onboarding' => 'Onboarding abschließen', ], 'takeover' => [ + 'eyebrow' => 'Host-Übernahme', 'how_title' => 'So läuft eine Host-Übernahme ab', - 'how_sub' => 'Sechs Schritte, etwa 20 bis 40 Minuten. Die ersten beiden passieren beim Anbieter — bevor hier etwas angelegt wird.', + 'how_sub' => 'Sechs Schritte, etwa 20 bis 40 Minuten. Die ersten beiden passieren beim Anbieter.', + 'rail_title' => 'Ablauf', 'badge_before' => 'vorher', - 'badge_now' => 'jetzt', - 'badge_later' => 'danach', + + // Kurz — sie stehen in der Schiene nebeneinander, nicht als Absatz. + 's1_label' => 'Server bestellen', + 's2_label' => 'Rettungssystem', + 's3_label' => 'Host anlegen', + 's4_label' => 'Zeile kopieren', + 's5_label' => 'Einfügen', + 's6_label' => 'Zusehen', 's1_title' => 'Server bestellen', - 's1_body' => 'Eine dedizierte Maschine. Keine Cloud-Instanz: ohne Hardware-Virtualisierung (/dev/kvm) startet später kein einziger Gast, und das Skript weist die Maschine gleich zu Beginn ab. Bei Hetzner CPX/CX ist das immer so, bei netcup je nach Produkt.', + 's1_body' => 'Dediziert, nicht Cloud. Ohne /dev/kvm startet kein Gast, und das Skript weist die Maschine ab.', 's2_title' => 'Rettungssystem starten', - 's2_body' => 'Im Kundenbereich des Anbieters das Rettungssystem einschalten UND den Server neu starten. Einschalten allein genügt nicht — er muss wirklich darin hochgefahren sein, sonst weigert sich das Skript. Es überschreibt Platten und prüft deshalb zuerst, ob es das darf.', + 's2_body' => 'Einschalten UND neu starten. Läuft der Server noch im alten System, weigert sich das Skript — es überschreibt Platten.', - 's3_title' => 'Host hier anlegen', - 's3_body' => 'Name, Rechenzentrum, öffentliche IP und root-Kennwort eintragen und speichern. Erst danach entsteht der Einmal-Code, und ab dann läuft seine Frist von 24 Stunden.', + 's3_title' => 'Host anlegen', + 's3_body' => 'Die Angaben unten. Danach entsteht der Einmal-Code, und seine Frist von 24 Stunden beginnt.', - 's4_title' => 'Die Befehlszeile kopieren', - 's4_body' => 'Sie erscheint direkt nach dem Speichern und wird genau einmal gezeigt.', + 's4_title' => 'Befehlszeile kopieren', + 's4_body' => 'Sie erscheint nach dem Speichern und wird genau einmal gezeigt.', 's5_title' => 'Im Rettungssystem einfügen', - 's5_body' => 'Per SSH als root auf den Server, Zeile einfügen, Eingabetaste. Es wird nichts abgetippt und nichts ausgefüllt: die Zeile trägt alles, was der Server vor dem Tunnel braucht.', - 's5_hint' => 'Die Zeile holt das Installationsskript von :url, packt es nach /opt/clupilot aus und startet es. Mehr lädt der Server nicht nach.', + 's5_body' => 'Per SSH als root, Zeile einfügen, Eingabetaste. Nichts abtippen, nichts ausfüllen.', + 's5_hint' => 'Holt das Skript von :url und startet es. Mehr lädt der Server nicht nach.', 's6_title' => 'Zusehen', - 's6_body' => 'Der Fortschritt läuft auf der Host-Seite mit, Abschnitt für Abschnitt. Bis der Tunnel steht, meldet der Server nichts — das ist so gewollt und dauert über den ersten Neustart hinweg. Danach kommt alles Vorherige auf einmal nach, mit den Zeiten von damals.', + 's6_body' => 'Der Fortschritt läuft auf der Host-Seite mit. Bis der Tunnel steht, meldet der Server nichts — danach kommt alles auf einmal nach.', + + 'requirements_title' => 'Beim Anbieter, vorher', + 'requirements_sub' => 'Beides muss erledigt sein, bevor unten gespeichert wird — der Code läuft ab dem Speichern.', + + 'form_title' => 'Der Host', + 'form_sub' => 'Vier Angaben. Den Rest holt sich der Server selbst.', + + 'command_title' => 'Die Befehlszeile', + 'command_sub' => 'Einfügen im Rettungssystem, als root. Sie trägt alles, was der Server vor dem Tunnel braucht.', + + 'after_title' => 'Danach', 'missing_title' => 'Der Tunnel ist noch nicht eingerichtet', - 'missing_body' => 'Es fehlt: :settings. Ohne diese Werte entsteht eine Befehlszeile, die sauber aussieht, läuft — und in einem Tunnel endet, der nie einen Handshake hat. Das fällt erst auf der Maschine auf. Trage sie unter Einstellungen ein, bevor du einen Host anlegst.', + 'missing_body' => 'Es fehlt: :settings. Ohne diese Werte entsteht eine Zeile, die läuft und in einem Tunnel endet, der nie einen Handshake hat — und das fällt erst auf der Maschine auf.', 'title' => ':name ist angelegt', 'subtitle' => 'Noch drei Schritte, dann macht der Server den Rest allein.', 'once_title' => 'Diese Zeile gibt es nur jetzt', - 'once_body' => 'In der Datenbank steht nur der Hash des Codes, der WireGuard-Schlüssel steht dort gar nicht. Wer die Seite verlässt oder neu lädt, bekommt sie nicht wieder, sondern legt einen neuen Code an — der alte ist damit wertlos.', + 'once_body' => 'In der Datenbank steht nur der Hash des Codes, der WireGuard-Schlüssel gar nicht. Neu laden bringt sie nicht zurück, sondern legt einen neuen Code an — der alte ist dann wertlos.', 'watch' => 'Fortschritt ansehen', 'copy' => 'Kopieren', 'copied' => 'Kopiert', - 'footnote' => 'Der Code gilt 24 Stunden und lässt sich genau einmal einlösen. Bleibt die Übernahme stehen, wird die Maschine neu aufgesetzt und nicht nachgebessert — was wo nachzusehen ist, steht im Runbook unter docs/runbooks/host-bootstrap.md.', + 'footnote' => 'Der Code gilt 24 Stunden und lässt sich einmal einlösen. Bleibt die Übernahme stehen, wird die Maschine neu aufgesetzt statt nachgebessert — siehe docs/runbooks/host-bootstrap.md.', ], ]; diff --git a/lang/en/hosts.php b/lang/en/hosts.php index 099f196..66eedb9 100644 --- a/lang/en/hosts.php +++ b/lang/en/hosts.php @@ -135,45 +135,63 @@ return [ 'complete_host_onboarding' => 'Complete onboarding', ], 'takeover' => [ + 'eyebrow' => 'Host takeover', 'how_title' => 'How a host takeover works', - 'how_sub' => 'Six steps, roughly 20 to 40 minutes. The first two happen at the provider — before anything is created here.', + 'how_sub' => 'Six steps, roughly 20 to 40 minutes. The first two happen at the provider.', + 'rail_title' => 'Procedure', 'badge_before' => 'first', - 'badge_now' => 'now', - 'badge_later' => 'then', + + 's1_label' => 'Order server', + 's2_label' => 'Rescue system', + 's3_label' => 'Add host', + 's4_label' => 'Copy line', + 's5_label' => 'Paste', + 's6_label' => 'Watch', 's1_title' => 'Order the server', - 's1_body' => 'A dedicated machine. Not a cloud instance: without hardware virtualisation (/dev/kvm) no guest will ever start, and the script turns the machine away at the very beginning. Hetzner CPX/CX never has it; netcup depends on the product.', + 's1_body' => 'Dedicated, not cloud. Without /dev/kvm no guest will start, and the script turns the machine away.', 's2_title' => 'Boot the rescue system', - 's2_body' => 'Enable the rescue system in the provider\'s panel AND restart the server. Enabling alone is not enough — it has to actually be running in it, or the script refuses. It overwrites disks, so it checks first whether it may.', + 's2_body' => 'Enable it AND restart. If the server is still in the old system the script refuses — it overwrites disks.', - 's3_title' => 'Add the host here', - 's3_body' => 'Name, datacenter, public IP and root password, then save. The one-time code is created at that point, and its 24-hour window starts running.', + 's3_title' => 'Add the host', + 's3_body' => 'The fields below. The one-time code is created after that, and its 24-hour window starts.', 's4_title' => 'Copy the command line', - 's4_body' => 'It appears right after saving and is shown exactly once.', + 's4_body' => 'It appears after saving and is shown exactly once.', 's5_title' => 'Paste it into the rescue system', - 's5_body' => 'SSH in as root, paste the line, press enter. Nothing is typed out and nothing is filled in: the line carries everything the server needs before the tunnel exists.', - 's5_hint' => 'The line fetches the installer from :url, unpacks it to /opt/clupilot and starts it. The server downloads nothing else.', + 's5_body' => 'SSH in as root, paste, press enter. Nothing typed out, nothing filled in.', + 's5_hint' => 'Fetches the installer from :url and starts it. The server downloads nothing else.', 's6_title' => 'Watch', - 's6_body' => 'Progress appears on the host page, section by section. Until the tunnel is up the server reports nothing — that is intended, and it lasts across the first reboot. Everything before it then arrives at once, carrying the times it actually happened.', + 's6_body' => 'Progress appears on the host page. Until the tunnel is up the server reports nothing — then everything arrives at once.', + + 'requirements_title' => 'At the provider, first', + 'requirements_sub' => 'Both have to be done before you save below — the code starts expiring the moment you do.', + + 'form_title' => 'The host', + 'form_sub' => 'Four fields. The server works out the rest itself.', + + 'command_title' => 'The command line', + 'command_sub' => 'Paste into the rescue system as root. It carries everything the server needs before the tunnel.', + + 'after_title' => 'Then', 'missing_title' => 'The tunnel is not set up yet', - 'missing_body' => 'Missing: :settings. Without these, the command line looks clean, runs, and ends in a tunnel that never handshakes — which only shows up on the machine. Fill them in under Settings before adding a host.', + 'missing_body' => 'Missing: :settings. Without these the line runs and ends in a tunnel that never handshakes — and that only shows up on the machine.', 'title' => ':name is created', 'subtitle' => 'Three steps left, then the server does the rest on its own.', 'once_title' => 'This line exists only now', - 'once_body' => 'The database holds only the hash of the code, and not the WireGuard key at all. Leaving or reloading this page does not bring it back — it mints a new code, and the old one becomes worthless.', + 'once_body' => 'The database holds only the hash of the code, and not the WireGuard key at all. Reloading does not bring it back — it mints a new code, and the old one becomes worthless.', 'watch' => 'Watch progress', 'copy' => 'Copy', 'copied' => 'Copied', - 'footnote' => 'The code is valid for 24 hours and can be redeemed exactly once. If the takeover stops, the machine is reinstalled rather than repaired — where to look is in the runbook at docs/runbooks/host-bootstrap.md.', + 'footnote' => 'The code is valid for 24 hours and can be redeemed once. If the takeover stops, the machine is reinstalled rather than repaired — see docs/runbooks/host-bootstrap.md.', ], ]; diff --git a/resources/views/components/admin/takeover-guide.blade.php b/resources/views/components/admin/takeover-guide.blade.php deleted file mode 100644 index 331a165..0000000 --- a/resources/views/components/admin/takeover-guide.blade.php +++ /dev/null @@ -1,109 +0,0 @@ -@props([ - // Die fertige Befehlszeile, sobald der Host angelegt ist — sonst null. - 'command' => null, - 'archiveUrl' => '', - 'hostUuid' => null, -]) - -{{-- Die Anleitung für eine Host-Übernahme, an EINER Stelle geschrieben. - - Sie steht bewusst dort, wo jemand sie braucht, bevor er anfängt: die - Schritte 1 und 2 passieren beim Anbieter, und wer sie erst nach dem Anlegen - liest, hat den Einmal-Code schon in der Zwischenablage und dessen Frist - schon laufen. Die erste Fassung dieser Seite zeigte die Anleitung erst - hinterher — das war genau dieser Fehler. - - Deshalb sind IMMER alle sechs Schritte zu sehen, auch die erledigten und - die kommenden. Eine Anleitung, die nur den aktuellen Schritt zeigt, - beantwortet die Frage „wie lange dauert das noch" nicht. --}} - -@php - $done = $command !== null; - // Schritt 3 ist das Formular auf dieser Seite; ist die Zeile da, sind 1–3 - // vorbei und 4 ist dran. - $current = $done ? 4 : 3; - - $steps = [ - 1 => ['title' => __('hosts.takeover.s1_title'), 'body' => __('hosts.takeover.s1_body')], - 2 => ['title' => __('hosts.takeover.s2_title'), 'body' => __('hosts.takeover.s2_body')], - 3 => ['title' => __('hosts.takeover.s3_title'), 'body' => __('hosts.takeover.s3_body')], - 4 => ['title' => __('hosts.takeover.s4_title'), 'body' => __('hosts.takeover.s4_body')], - 5 => ['title' => __('hosts.takeover.s5_title'), 'body' => __('hosts.takeover.s5_body')], - 6 => ['title' => __('hosts.takeover.s6_title'), 'body' => __('hosts.takeover.s6_body')], - ]; -@endphp - -
merge(['class' => 'rounded-lg border border-line bg-surface shadow-xs']) }}> -
-

{{ __('hosts.takeover.how_title') }}

-

{{ __('hosts.takeover.how_sub') }}

-
- -
    - @foreach ($steps as $number => $step) - @php - $isCurrent = $number === $current; - $isPast = $number < $current; - @endphp -
  1. - {{-- Die Nummer trägt den Zustand, nicht ein Icon daneben: eine - Ziffer, die man mit dem Blick abzählen kann, ist an dieser - Stelle mehr wert als ein Häkchen. --}} - - {{ $number }} - - -
    -
    -

    {{ $step['title'] }}

    - @if ($number <= 2 && ! $done) - {{ __('hosts.takeover.badge_before') }} - @elseif ($isCurrent) - {{ __('hosts.takeover.badge_now') }} - @endif -
    -

    {{ $step['body'] }}

    - - @if ($number === 4 && $done) -
    -
    - {{-- Umbrechen statt waagerecht rollen: aus einem - Kasten mit Rollbalken markiert jemand die - Hälfte und merkt es erst auf der Maschine. --}} -
    {{ $command }}
    - -
    -
    - @endif - - @if ($number === 5) -

    {{ __('hosts.takeover.s5_hint', ['url' => $archiveUrl]) }}

    - @endif - - @if ($number === 6 && $hostUuid) - - @endif -
    -
  2. - @endforeach -
- -
-

{{ __('hosts.takeover.footnote') }}

-
-
diff --git a/resources/views/components/admin/takeover-rail.blade.php b/resources/views/components/admin/takeover-rail.blade.php new file mode 100644 index 0000000..e79546d --- /dev/null +++ b/resources/views/components/admin/takeover-rail.blade.php @@ -0,0 +1,67 @@ +@props([ + // Der Schritt, an dem der Betreiber gerade steht: 3 solange das Formular + // offen ist, 4 sobald die Befehlszeile da ist. + 'current' => 3, +]) + +{{-- Der Ablauf als schmale Schiene, nicht als Textwand. + + Die erste Fassung war sechs Absätze übereinander, oberhalb des Formulars. + Sie beantwortete alles und zeigte nichts: der eigentliche Auftrag dieser + Seite — vier Felder ausfüllen — verschwand unter der Anleitung dazu. + + Sechs Wortpaare untereinander beantworten die Frage, die man hier wirklich + hat („wo stehe ich, wie viel kommt noch"), auf einen Blick. Was ein Schritt + inhaltlich bedeutet, steht in der Zeile, in der er dran ist — nicht sechsmal + im Voraus. + + Dieselbe Form wie die Seitennavigation der Einstellungsseite: 232 px, ab + `lg` klebend, darunter waagerecht scrollbar. Ein zweites Muster für + dasselbe Problem wäre eine zweite Sprache. --}} + +@php + $steps = [ + 1 => __('hosts.takeover.s1_label'), + 2 => __('hosts.takeover.s2_label'), + 3 => __('hosts.takeover.s3_label'), + 4 => __('hosts.takeover.s4_label'), + 5 => __('hosts.takeover.s5_label'), + 6 => __('hosts.takeover.s6_label'), + ]; +@endphp + + diff --git a/resources/views/components/admin/takeover-steps.blade.php b/resources/views/components/admin/takeover-steps.blade.php new file mode 100644 index 0000000..d6f9239 --- /dev/null +++ b/resources/views/components/admin/takeover-steps.blade.php @@ -0,0 +1,41 @@ +@props(['archiveUrl' => '']) + +{{-- Dieselben sechs Schritte, für die Stelle, an der noch nichts angelegt ist: + die Host-Liste. Dort ist es eine Auskunft und kein Ablauf — also Zeilen mit + je einem Satz, nicht die Schiene mit Zustand. + + Eine Zeile je Schritt, nach dem Muster von `x-ui.row`: links wozu, rechts + was. Die zwei Schritte beim Anbieter tragen eine Plakette, sonst liest sich + die Liste, als könne man oben anfangen und sich durcharbeiten — und Schritt + 2 ist eine halbe Stunde Wartezeit. --}} + +@php + $steps = [ + 1 => [__('hosts.takeover.s1_title'), __('hosts.takeover.s1_body'), true], + 2 => [__('hosts.takeover.s2_title'), __('hosts.takeover.s2_body'), true], + 3 => [__('hosts.takeover.s3_title'), __('hosts.takeover.s3_body'), false], + 4 => [__('hosts.takeover.s4_title'), __('hosts.takeover.s4_body'), false], + 5 => [__('hosts.takeover.s5_title'), __('hosts.takeover.s5_body'), false], + 6 => [__('hosts.takeover.s6_title'), __('hosts.takeover.s6_body'), false], + ]; +@endphp + +
    merge(['class' => 'divide-y divide-line']) }}> + @foreach ($steps as $number => [$title, $body, $atProvider]) +
  1. +
    + {{ $number }} + {{ $title }} + @if ($atProvider) + {{ __('hosts.takeover.badge_before') }} + @endif +
    +

    + {{ $body }} + @if ($number === 5 && $archiveUrl) + {{ __('hosts.takeover.s5_hint', ['url' => $archiveUrl]) }} + @endif +

    +
  2. + @endforeach +
diff --git a/resources/views/livewire/admin/host-create.blade.php b/resources/views/livewire/admin/host-create.blade.php index 996eb3f..6f75346 100644 --- a/resources/views/livewire/admin/host-create.blade.php +++ b/resources/views/livewire/admin/host-create.blade.php @@ -1,72 +1,153 @@ -
- @if ($command === null) -
- - {{ __('hosts.back') }} - -

{{ __('hosts.create_title') }}

-

{{ __('hosts.create_sub') }}

+
+ {{-- Kopf wie auf der Einstellungsseite: Augenbraue, Titel, ein Satz + darunter. Die Seite hatte vorher einen Zurück-Link an dieser Stelle und + darunter eine sechsteilige Anleitung — der Auftrag der Seite, vier + Felder auszufüllen, ging darin unter. --}} +
+

{{ __('hosts.takeover.eyebrow') }}

+

+ @if ($command === null) + {{ __('hosts.create_title') }} + @else + {{ __('hosts.takeover.title', ['name' => $createdName]) }} + @endif +

+

+ {{ $command === null ? __('hosts.create_sub') : __('hosts.takeover.subtitle') }} +

+
+ +
+ {{-- Der Ablauf als Schiene, nicht als Wand. Sie beantwortet „wo stehe + ich" auf einen Blick und überlässt dem Hauptteil die Arbeit. --}} + + +
+ @if ($command === null) + @if ($missingSettings) + +

{{ __('hosts.takeover.missing_title') }}

+

{{ __('hosts.takeover.missing_body', ['settings' => implode(', ', $missingSettings)]) }}

+
+ @endif + + {{-- Die zwei Schritte beim Anbieter. Sie stehen VOR dem + Formular, weil sie vorher erledigt sein müssen: der + Einmal-Code beginnt mit dem Speichern abzulaufen. --}} +
+

{{ __('hosts.takeover.requirements_title') }}

+

{{ __('hosts.takeover.requirements_sub') }}

+ + + +

{{ __('hosts.takeover.s1_body') }}

+
+ +

{{ __('hosts.takeover.s2_body') }}

+
+
+
+ +
+

{{ __('hosts.takeover.form_title') }}

+

{{ __('hosts.takeover.form_sub') }}

+ + + + + + + + + @error('datacenter')

{{ $message }}

@enderror +
+ + + + + + + + +
+ +
+ + {{ __('hosts.cancel') }} + + + {{ __('hosts.save') }} + {{ __('hosts.save') }}… + +
+
+ @else + {{-- Ab hier ist die Befehlszeile die Hauptsache. Sie bekommt die + volle Breite und steht ganz oben; alles andere ordnet sich + unter. --}} +
+

{{ __('hosts.takeover.command_title') }}

+

{{ __('hosts.takeover.command_sub') }}

+ +
+
+

{{ __('hosts.takeover.once_title') }}

+ +
+ + {{-- Umbrechen statt waagerecht rollen: aus einem Kasten + mit Rollbalken markiert jemand die Hälfte und merkt + es erst auf der Maschine. --}} +
{{ $command }}
+
+ +

{{ __('hosts.takeover.once_body') }}

+
+ +
+

{{ __('hosts.takeover.after_title') }}

+ + + +

{{ __('hosts.takeover.s5_body') }}

+

{{ __('hosts.takeover.s5_hint', ['url' => $archiveUrl]) }}

+
+ +

{{ __('hosts.takeover.s6_body') }}

+ +
+
+
+ @endif + +

+ {{ __('hosts.takeover.footnote') }} +

- - @if ($missingSettings) - -

{{ __('hosts.takeover.missing_title') }}

-

{{ __('hosts.takeover.missing_body', ['settings' => implode(', ', $missingSettings)]) }}

-
- @endif - - {{-- Die Anleitung steht VOR dem Formular, weil ihre ersten beiden - Schritte beim Anbieter passieren und vor dem Anlegen erledigt sein - müssen. Wer sie erst danach liest, hat den Einmal-Code schon und - dessen Frist läuft. --}} - - -
- - -
- - - @error('datacenter')

{{ $message }}

@enderror -
- - - - - -
- - {{ __('hosts.cancel') }} - - - {{ __('hosts.save') }} - {{ __('hosts.save') }}… - -
- - @else - {{-- Der Host steht. Dieselbe Anleitung, jetzt mit der Befehlszeile an - Schritt 4 — und ohne Formular, weil es nichts mehr auszufüllen gibt. --}} -
-

{{ __('hosts.takeover.title', ['name' => $createdName]) }}

-

{{ __('hosts.takeover.subtitle') }}

-
- - -

{{ __('hosts.takeover.once_title') }}

-

{{ __('hosts.takeover.once_body') }}

-
- - - @endif +
diff --git a/resources/views/livewire/admin/hosts.blade.php b/resources/views/livewire/admin/hosts.blade.php index aeafed3..1e8d938 100644 --- a/resources/views/livewire/admin/hosts.blade.php +++ b/resources/views/livewire/admin/hosts.blade.php @@ -14,14 +14,16 @@ hinzufügen" klickt, hat den Server womöglich noch gar nicht bestellt. Zusammengeklappt, damit sie die Liste nicht verdrängt — aber auffindbar, ohne dass man vorher etwas anlegen muss. --}} -
+
- - {{ __('hosts.takeover.how_title') }} + + + {{ __('hosts.takeover.how_title') }} + - +
{{-- Filter bar — scales to many hosts (search + datacenter + status). --}} diff --git a/tests/Feature/Admin/HostTakeoverGuideTest.php b/tests/Feature/Admin/HostTakeoverGuideTest.php index 9f42ca8..0275d3b 100644 --- a/tests/Feature/Admin/HostTakeoverGuideTest.php +++ b/tests/Feature/Admin/HostTakeoverGuideTest.php @@ -25,13 +25,19 @@ function guidePage(string $component): \Livewire\Features\SupportTesting\Testabl * Zwischenablage und dessen 24-Stunden-Frist läuft bereits. */ it('explains the whole procedure before anything is created', function () { - guidePage(HostCreate::class) - ->assertSee(__('hosts.takeover.s1_title')) - ->assertSee(__('hosts.takeover.s2_title')) - ->assertSee(__('hosts.takeover.s3_title')) - ->assertSee(__('hosts.takeover.s4_title')) - ->assertSee(__('hosts.takeover.s5_title')) - ->assertSee(__('hosts.takeover.s6_title')); + $page = guidePage(HostCreate::class); + + // Alle sechs, als Schiene — kurze Beschriftungen, damit die Seite eine + // Seite bleibt und keine Textwand wird. Das war der zweite Fund: sechs + // Absätze übereinander beantworteten alles und zeigten nichts. + foreach (range(1, 6) as $step) { + $page->assertSee(__("hosts.takeover.s{$step}_label")); + } + + // Die zwei beim Anbieter ausführlich, weil sie VOR dem Speichern erledigt + // sein müssen und nur hier erklärt werden. + $page->assertSee(__('hosts.takeover.s1_body')) + ->assertSee(__('hosts.takeover.s2_body')); }); /** @@ -61,7 +67,10 @@ it('marks the two provider steps as things to do first', function () { * und der Unterschied fiele auf einem Server auf, der schon bestellt ist. */ it('names the same archive address the command will use', function () { - guidePage(HostCreate::class)->assertSee('files.clupilot.test/bootstrap.tar.gz'); + // Auf der Liste, wo die Anleitung Auskunft gibt. Auf der Anlegen-Seite + // steht die Adresse erst NACH dem Speichern: dort ist sie Teil der + // Befehlszeile und keine Angabe für sich, und vorher wäre sie eine + // Einzelheit mehr auf einer Seite, die vier Felder ausgefüllt haben will. guidePage(Hosts::class)->assertSee('files.clupilot.test/bootstrap.tar.gz'); }); diff --git a/tests/Feature/Host/FilesHostTest.php b/tests/Feature/Host/FilesHostTest.php index 818dbd8..824853a 100644 --- a/tests/Feature/Host/FilesHostTest.php +++ b/tests/Feature/Host/FilesHostTest.php @@ -118,3 +118,32 @@ it('falls back to the portal host until the files host exists', function () { expect(HostTakeoverCommand::archiveUrl()) ->toBe('https://app.clupilot.com/bootstrap.tar.gz'); }); + +/** + * Der Fund, der diesen Test nötig gemacht hat. + * + * `PublicSiteGate` hängt an der ganzen `web`-Gruppe. Solange die Website + * verborgen ist, bekäme ein Server im Rettungssystem die 503-Platzhalterseite + * und schöbe sie in `tar xz` — der Betreiber sähe einen Entpackfehler auf einer + * Maschine, an die er nur noch über die Anbieterkonsole kommt. + */ +it('serves downloads even while the site is hidden', function () { + App\Support\Settings::set('site.public', false); + + // Von einer fremden Adresse, denn genau die hat ein Server im + // Rettungssystem: 127.0.0.1 steht in TRUSTED_RANGES und käme ohnehin durch, + // der Test bewiese dann nichts. + $vonAussen = ['REMOTE_ADDR' => '203.0.113.77']; + + $this->withServerVariables($vonAussen) + ->get('http://files.clupilot.test/bootstrap.tar.gz?code='.issuedCode())->assertOk(); + + file_put_contents($this->publicDir.'/tom-2026-01.pdf', '%PDF-1.4'); + $this->withServerVariables($vonAussen) + ->get('http://files.clupilot.test/tom-2026-01.pdf')->assertOk(); + unlink($this->publicDir.'/tom-2026-01.pdf'); + + // Und die Website selbst bleibt für dieselbe Adresse verborgen — sonst + // hätte die Ausnahme mehr aufgemacht als sie sollte. + $this->withServerVariables($vonAussen)->get('http://localhost/')->assertStatus(503); +});