Put the instructions where somebody reads them before they start

The previous commit shipped the guide AFTER the host was created. Its first two
steps happen at the provider — order the machine, boot the rescue system — and
whoever reads them there has the one-time code already in the clipboard with its
24-hour window running. The instructions were correct and in the wrong place,
which is its own kind of wrong.

Six steps now, not three, and the two provider ones are named rather than
assumed. They are marked "first" so the list does not read as something you can
start at the top of and work down: step 2 is half an hour of waiting, and doing
it inside a running code is exactly the mistake the page should prevent.

All six are visible the whole time, including the done ones and the ones still
to come. A guide that shows only the current step cannot answer "how much is
left", which is the question somebody has while a server they are paying for
sits in a rescue system.

It is on the hosts list too, collapsed. Somebody standing there may not have
ordered the machine yet — and that is step 1. Requiring them to click "add host"
to find out what the procedure is puts the answer behind the action it describes.

One component, two places, so the archive address in the instructions is the same
string the command line will carry. Two copies of that would drift, and the
difference would surface on a server that has already been paid for.

The step-4 command box moved into the guide rather than sitting beside it, so
after creating a host the whole procedure stays on screen. Somebody stuck at
step 5 should not be looking at a page that has become a single box.

1991 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feature/host-bootstrap
nexxo 2026-07-30 20:51:47 +02:00
parent 903ebdd2b2
commit 2194ab8079
8 changed files with 270 additions and 97 deletions

View File

@ -53,6 +53,10 @@ class Hosts extends Component
'datacenters' => Datacenter::query()->orderBy('name')->get(),
'statuses' => ['pending', 'onboarding', 'active', 'error', 'disabled'],
'total' => Host::query()->count(),
// Für die aufklappbare Anleitung: dieselbe Adresse, die später in
// der Befehlszeile steht, damit hier nichts anderes behauptet wird
// als dort getan wird.
'archiveUrl' => \App\Support\HostTakeoverCommand::archiveUrl(),
]);
}
}

View File

@ -135,28 +135,41 @@ return [
'complete_host_onboarding' => 'Onboarding abschließen',
],
'takeover' => [
'before_title' => 'Vorher: Rettungssystem starten',
'before_body' => 'Bestelle den Server beim Anbieter und starte sein Rettungssystem, bevor du hier anlegst. Direkt nach dem Anlegen zeigt diese Seite eine Befehlszeile, die im Rettungssystem eingefügt wird — sie wird genau einmal gezeigt.',
'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.',
'badge_before' => 'vorher',
'badge_now' => 'jetzt',
'badge_later' => 'danach',
'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.',
'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.',
'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.',
's4_title' => 'Die Befehlszeile kopieren',
's4_body' => 'Sie erscheint direkt 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.',
'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.',
'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.',
'title' => ':name ist angelegt',
'subtitle' => 'Drei Schritte, danach macht der Server den Rest allein.',
'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.',
'step1_title' => 'Rettungssystem starten',
'step1_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.',
'step2_title' => 'Diese Zeile im Rettungssystem einfügen',
'step2_body' => 'Per SSH als root auf den Server, Zeile einfügen, Eingabetaste. Es wird nichts abgetippt und nichts ausgefüllt: sie trägt alles, was der Server vor dem Tunnel braucht.',
'step2_hint' => 'Die Zeile holt das Installationsskript von :url, packt es nach /opt/clupilot aus und startet es. Mehr lädt der Server nicht nach.',
'step3_title' => 'Zusehen',
'step3_body' => 'Der Fortschritt läuft in der Konsole 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.',
'watch' => 'Fortschritt ansehen',
'copy' => 'Kopieren',
'copied' => 'Kopiert',

View File

@ -135,28 +135,41 @@ return [
'complete_host_onboarding' => 'Complete onboarding',
],
'takeover' => [
'before_title' => 'First: boot the rescue system',
'before_body' => 'Order the server at the provider and boot its rescue system before you add it here. Right after you add it, this page shows a command line to paste into the rescue system — shown exactly once.',
'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.',
'badge_before' => 'first',
'badge_now' => 'now',
'badge_later' => 'then',
'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.',
'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.',
'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.',
's4_title' => 'Copy the command line',
's4_body' => 'It appears right 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.',
'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.',
'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.',
'title' => ':name is created',
'subtitle' => 'Three steps, then the server does the rest on its own.',
'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.',
'step1_title' => 'Boot the rescue system',
'step1_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.',
'step2_title' => 'Paste this line into the rescue system',
'step2_body' => 'SSH in as root, paste the line, press enter. Nothing is typed out and nothing is filled in: it carries everything the server needs before the tunnel exists.',
'step2_hint' => 'The line fetches the installer from :url, unpacks it to /opt/clupilot and starts it. The server downloads nothing else.',
'step3_title' => 'Watch',
'step3_body' => 'Progress appears in the console, 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.',
'watch' => 'Watch progress',
'copy' => 'Copy',
'copied' => 'Copied',

View File

@ -0,0 +1,109 @@
@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 13
// 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
<div {{ $attributes->merge(['class' => 'rounded-lg border border-line bg-surface shadow-xs']) }}>
<div class="border-b border-line px-6 py-4">
<h2 class="text-sm font-semibold text-ink">{{ __('hosts.takeover.how_title') }}</h2>
<p class="mt-1 text-sm text-muted">{{ __('hosts.takeover.how_sub') }}</p>
</div>
<ol class="divide-y divide-line">
@foreach ($steps as $number => $step)
@php
$isCurrent = $number === $current;
$isPast = $number < $current;
@endphp
<li class="flex gap-3 px-6 py-4 {{ $isCurrent ? 'bg-info-bg' : '' }}">
{{-- 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. --}}
<span class="mt-0.5 flex size-6 shrink-0 items-center justify-center rounded-full border text-xs font-bold
{{ $isPast ? 'border-success-border bg-success-bg text-success' : ($isCurrent ? 'border-info-border bg-surface text-info' : 'border-line text-muted') }}">
{{ $number }}
</span>
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<h3 class="text-sm font-semibold {{ $isCurrent ? 'text-ink' : 'text-body' }}">{{ $step['title'] }}</h3>
@if ($number <= 2 && ! $done)
<x-ui.badge status="warning">{{ __('hosts.takeover.badge_before') }}</x-ui.badge>
@elseif ($isCurrent)
<x-ui.badge status="info">{{ __('hosts.takeover.badge_now') }}</x-ui.badge>
@endif
</div>
<p class="mt-1 text-sm text-muted">{{ $step['body'] }}</p>
@if ($number === 4 && $done)
<div x-data="{ copied: false }" class="mt-3">
<div class="relative">
{{-- Umbrechen statt waagerecht rollen: aus einem
Kasten mit Rollbalken markiert jemand die
Hälfte und merkt es erst auf der Maschine. --}}
<pre class="max-h-56 overflow-y-auto rounded border border-line bg-canvas p-3 pr-24 font-mono text-xs leading-relaxed text-ink break-all whitespace-pre-wrap"
x-ref="command">{{ $command }}</pre>
<button type="button"
class="absolute right-2 top-2 inline-flex items-center gap-1 rounded border border-line bg-surface px-2 py-1 text-xs font-medium text-body hover:text-ink"
x-on:click="navigator.clipboard.writeText($refs.command.textContent); copied = true; setTimeout(() => copied = false, 2000)">
<x-ui.icon name="copy" class="size-4" x-show="!copied" />
<x-ui.icon name="check" class="size-4" x-show="copied" x-cloak />
<span x-text="copied ? @js(__('hosts.takeover.copied')) : @js(__('hosts.takeover.copy'))">{{ __('hosts.takeover.copy') }}</span>
</button>
</div>
</div>
@endif
@if ($number === 5)
<p class="mt-2 text-xs text-muted">{{ __('hosts.takeover.s5_hint', ['url' => $archiveUrl]) }}</p>
@endif
@if ($number === 6 && $hostUuid)
<div class="mt-3">
<a href="{{ route('admin.hosts.show', ['host' => $hostUuid]) }}" wire:navigate>
<x-ui.button variant="primary" size="sm">
<x-slot:icon><x-ui.icon name="activity" class="size-4" /></x-slot:icon>
{{ __('hosts.takeover.watch') }}
</x-ui.button>
</a>
</div>
@endif
</div>
</li>
@endforeach
</ol>
<div class="border-t border-line px-6 py-3">
<p class="text-xs text-muted">{{ __('hosts.takeover.footnote') }}</p>
</div>
</div>

View File

@ -8,15 +8,6 @@
<p class="mt-1 text-sm text-muted">{{ __('hosts.create_sub') }}</p>
</div>
{{-- Was der Betreiber VORHER wissen muss, steht vor dem Formular und
nicht danach: das Rettungssystem zu starten dauert beim Anbieter ein
paar Minuten. Wer das erst hinterher liest, hat den Code schon in
der Zwischenablage und wartet. --}}
<x-ui.alert variant="info" class="animate-rise">
<p class="font-medium">{{ __('hosts.takeover.before_title') }}</p>
<p class="mt-1">{{ __('hosts.takeover.before_body') }}</p>
</x-ui.alert>
@if ($missingSettings)
<x-ui.alert variant="warning" class="animate-rise">
<p class="font-medium">{{ __('hosts.takeover.missing_title') }}</p>
@ -24,6 +15,12 @@
</x-ui.alert>
@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. --}}
<x-admin.takeover-guide :archive-url="$archiveUrl" class="animate-rise" />
<form wire:submit="save" class="space-y-5 rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise">
<x-ui.input name="name" wire:model="name" :label="__('hosts.field.name')" :hint="__('hosts.field.name_hint')" autofocus />
@ -58,9 +55,8 @@
</div>
</form>
@else
{{-- Der Host steht. Ab hier ist diese Seite eine Anleitung und kein
Formular mehr und sie ist die EINZIGE Stelle, an der die
Befehlszeile je zu sehen ist. --}}
{{-- Der Host steht. Dieselbe Anleitung, jetzt mit der Befehlszeile an
Schritt 4 und ohne Formular, weil es nichts mehr auszufüllen gibt. --}}
<div class="animate-rise">
<h1 class="text-2xl font-bold tracking-tight text-ink">{{ __('hosts.takeover.title', ['name' => $createdName]) }}</h1>
<p class="mt-1 text-sm text-muted">{{ __('hosts.takeover.subtitle') }}</p>
@ -71,58 +67,6 @@
<p class="mt-1">{{ __('hosts.takeover.once_body') }}</p>
</x-ui.alert>
<ol class="space-y-4 animate-rise">
<li class="rounded-lg border border-line bg-surface p-5 shadow-xs">
<div class="flex items-baseline gap-2">
<span class="text-xs font-bold text-muted">1</span>
<h2 class="text-sm font-semibold text-ink">{{ __('hosts.takeover.step1_title') }}</h2>
</div>
<p class="mt-2 text-sm text-muted">{{ __('hosts.takeover.step1_body') }}</p>
</li>
<li class="rounded-lg border border-line bg-surface p-5 shadow-xs">
<div class="flex items-baseline gap-2">
<span class="text-xs font-bold text-muted">2</span>
<h2 class="text-sm font-semibold text-ink">{{ __('hosts.takeover.step2_title') }}</h2>
</div>
<p class="mt-2 text-sm text-muted">{{ __('hosts.takeover.step2_body') }}</p>
<div x-data="{ copied: false }" class="mt-3">
<div class="relative">
{{-- Umbrechen statt waagerecht rollen: aus einem Kasten
mit Rollbalken markiert jemand die Hälfte und merkt
es erst auf der Maschine. --}}
<pre class="max-h-56 overflow-y-auto rounded border border-line bg-canvas p-3 pr-24 font-mono text-xs leading-relaxed text-ink break-all whitespace-pre-wrap"
x-ref="command">{{ $command }}</pre>
<button type="button"
class="absolute right-2 top-2 inline-flex items-center gap-1 rounded border border-line bg-surface px-2 py-1 text-xs font-medium text-body hover:text-ink"
x-on:click="navigator.clipboard.writeText($refs.command.textContent); copied = true; setTimeout(() => copied = false, 2000)">
<x-ui.icon name="copy" class="size-4" x-show="!copied" />
<x-ui.icon name="check" class="size-4" x-show="copied" x-cloak />
<span x-text="copied ? @js(__('hosts.takeover.copied')) : @js(__('hosts.takeover.copy'))">{{ __('hosts.takeover.copy') }}</span>
</button>
</div>
<p class="mt-2 text-xs text-muted">{{ __('hosts.takeover.step2_hint', ['url' => $archiveUrl]) }}</p>
</div>
</li>
<li class="rounded-lg border border-line bg-surface p-5 shadow-xs">
<div class="flex items-baseline gap-2">
<span class="text-xs font-bold text-muted">3</span>
<h2 class="text-sm font-semibold text-ink">{{ __('hosts.takeover.step3_title') }}</h2>
</div>
<p class="mt-2 text-sm text-muted">{{ __('hosts.takeover.step3_body') }}</p>
<div class="mt-3">
<a href="{{ route('admin.hosts.show', ['host' => $createdUuid]) }}" wire:navigate>
<x-ui.button variant="primary">
<x-slot:icon><x-ui.icon name="activity" class="size-4" /></x-slot:icon>
{{ __('hosts.takeover.watch') }}
</x-ui.button>
</a>
</div>
</li>
</ol>
<p class="text-xs text-muted animate-rise">{{ __('hosts.takeover.footnote') }}</p>
<x-admin.takeover-guide :command="$command" :archive-url="$archiveUrl" :host-uuid="$createdUuid" class="animate-rise" />
@endif
</div>

View File

@ -9,6 +9,21 @@
</a>
</div>
{{-- Die Anleitung gehört hierher, nicht nur auf die Anlegen-Seite: ihre
ersten zwei Schritte passieren beim Anbieter, und wer erst auf „Host
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. --}}
<details class="group rounded-lg border border-line bg-surface shadow-xs animate-rise [animation-delay:20ms]">
<summary class="flex cursor-pointer list-none items-center justify-between gap-3 px-6 py-3">
<span class="flex items-center gap-2 text-sm font-medium text-body">
<x-ui.icon name="life-buoy" class="size-4" />{{ __('hosts.takeover.how_title') }}
</span>
<x-ui.icon name="chevron-down" class="size-4 shrink-0 text-muted transition group-open:rotate-180" />
</summary>
<x-admin.takeover-guide :archive-url="$archiveUrl" class="rounded-none border-0 border-t border-line shadow-none" />
</details>
{{-- Filter bar scales to many hosts (search + datacenter + status). --}}
<div class="flex flex-wrap items-center gap-2 animate-rise [animation-delay:40ms]">
<div class="relative min-w-48 flex-1">

View File

@ -0,0 +1,73 @@
<?php
use App\Livewire\Admin\HostCreate;
use App\Livewire\Admin\Hosts;
use App\Models\Operator;
use Livewire\Livewire;
beforeEach(function () {
fakeServices();
config()->set('admin_access.app_host', 'clupilot.cloud');
});
function guidePage(string $component): \Livewire\Features\SupportTesting\Testable
{
return Livewire::actingAs(Operator::factory()->role('Owner')->create(), 'operator')->test($component);
}
/**
* Der eigentliche Fund, der diese Datei nötig gemacht hat.
*
* Die erste Fassung zeigte die Anleitung ERST NACH dem Anlegen. Ihre Schritte 1
* und 2 passieren aber beim Anbieter Server bestellen, Rettungssystem starten
* , und wer sie dort zum ersten Mal liest, hat den Einmal-Code schon in der
* 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'));
});
/**
* Und schon auf der Liste, nicht erst hinter „Host hinzufügen": wer dort steht,
* hat den Server womöglich noch gar nicht bestellt und genau das ist Schritt 1.
*/
it('offers the same procedure on the hosts list', function () {
guidePage(Hosts::class)
->assertSee(__('hosts.takeover.how_title'))
->assertSee(__('hosts.takeover.s1_title'))
->assertSee(__('hosts.takeover.s2_title'));
});
/**
* Die zwei Schritte beim Anbieter sind als solche gekennzeichnet. Ohne die
* Markierung liest sich die Liste, als könne man oben anfangen und sich
* durcharbeiten und Schritt 2 ist dann eine halbe Stunde Wartezeit mitten
* in einem laufenden Code.
*/
it('marks the two provider steps as things to do first', function () {
guidePage(HostCreate::class)->assertSee(__('hosts.takeover.badge_before'));
});
/**
* Die Anleitung nennt die Adresse, von der geladen wird, und es muss dieselbe
* sein, die später in der Zeile steht. Zwei Fassungen davon liefen auseinander,
* 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('clupilot.cloud/bootstrap.tar.gz');
guidePage(Hosts::class)->assertSee('clupilot.cloud/bootstrap.tar.gz');
});
/**
* Vor dem Anlegen gibt es keine Befehlszeile sonst stünde dort eine, die zu
* keinem Host gehört.
*/
it('shows no command until a host exists', function () {
guidePage(HostCreate::class)->assertDontSee('--wg-private');
});

View File

@ -122,14 +122,16 @@ it('warns before creating a host when the tunnel settings are missing', function
});
/**
* Die drei Schritte sind die Anleitung. Fehlt einer, fehlt genau der, den
* jemand nicht von selbst weiß meistens das Rettungssystem.
* Nach dem Anlegen bleibt dieselbe Anleitung stehen, nur mit der Zeile darin.
* Ein Betreiber, der bei Schritt 5 nicht weiterweiß, soll nicht auf einer Seite
* landen, die nur noch aus einem Kasten besteht.
*/
it('spells out the three steps, rescue system first', function () {
it('keeps the whole procedure visible after the host exists', function () {
createHostAs()
->assertSee(__('hosts.takeover.step1_title'))
->assertSee(__('hosts.takeover.step2_title'))
->assertSee(__('hosts.takeover.step3_title'));
->assertSee(__('hosts.takeover.s1_title'))
->assertSee(__('hosts.takeover.s5_title'))
->assertSee(__('hosts.takeover.s6_title'))
->assertSee('--wg-private', escape: false);
});
it('refuses to create a host without the permission', function () {