Compare commits
3 Commits
fc9cc20610
...
fced87d23a
| Author | SHA1 | Date |
|---|---|---|
|
|
fced87d23a | |
|
|
cfc4985f89 | |
|
|
398028a57d |
|
|
@ -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', []);
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
<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>
|
||||
|
|
@ -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
|
||||
|
||||
<nav {{ $attributes->merge(['class' => '-mx-4 shrink-0 overflow-x-auto px-4 lg:mx-0 lg:w-[232px] lg:overflow-visible lg:px-0 lg:sticky lg:top-8']) }}
|
||||
aria-label="{{ __('hosts.takeover.rail_title') }}">
|
||||
<p class="lbl hidden lg:block">{{ __('hosts.takeover.rail_title') }}</p>
|
||||
|
||||
<ol class="mt-2 flex gap-1.5 lg:flex-col lg:gap-0.5">
|
||||
@foreach ($steps as $number => $label)
|
||||
@php
|
||||
$isCurrent = $number === $current;
|
||||
$isPast = $number < $current;
|
||||
@endphp
|
||||
<li class="shrink-0">
|
||||
<div @class([
|
||||
'flex items-center gap-2.5 rounded px-2.5 py-2 text-sm whitespace-nowrap lg:whitespace-normal',
|
||||
'bg-surface-2 font-semibold text-ink' => $isCurrent,
|
||||
'text-muted' => ! $isCurrent,
|
||||
])>
|
||||
{{-- Die Ziffer trägt den Zustand. Ein Häkchen daneben wäre
|
||||
ein zweites Zeichen für dieselbe Aussage, und die
|
||||
Nummer kann man abzählen. --}}
|
||||
<span @class([
|
||||
'flex size-5 shrink-0 items-center justify-center rounded-full border text-[11px] font-bold tabular-nums',
|
||||
'border-transparent bg-success text-on-accent' => $isPast,
|
||||
'border-accent text-accent-text' => $isCurrent,
|
||||
'border-line text-muted' => ! $isPast && ! $isCurrent,
|
||||
])>{{ $number }}</span>
|
||||
<span class="min-w-0">{{ $label }}</span>
|
||||
</div>
|
||||
</li>
|
||||
@endforeach
|
||||
</ol>
|
||||
|
||||
<p class="mt-4 hidden max-w-[232px] text-xs leading-relaxed text-muted lg:block">
|
||||
{{ __('hosts.takeover.how_sub') }}
|
||||
</p>
|
||||
</nav>
|
||||
|
|
@ -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
|
||||
|
||||
<ol {{ $attributes->merge(['class' => 'divide-y divide-line']) }}>
|
||||
@foreach ($steps as $number => [$title, $body, $atProvider])
|
||||
<li class="grid gap-x-6 gap-y-1 px-6 py-3.5 sm:grid-cols-[minmax(0,220px)_minmax(0,1fr)] sm:items-baseline">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<span class="flex size-5 shrink-0 items-center justify-center rounded-full border border-line text-[11px] font-bold tabular-nums text-muted">{{ $number }}</span>
|
||||
<span class="text-sm font-medium text-ink">{{ $title }}</span>
|
||||
@if ($atProvider)
|
||||
<x-ui.badge status="warning">{{ __('hosts.takeover.badge_before') }}</x-ui.badge>
|
||||
@endif
|
||||
</div>
|
||||
<p class="text-sm leading-relaxed text-muted">
|
||||
{{ $body }}
|
||||
@if ($number === 5 && $archiveUrl)
|
||||
<span class="block pt-0.5 text-xs">{{ __('hosts.takeover.s5_hint', ['url' => $archiveUrl]) }}</span>
|
||||
@endif
|
||||
</p>
|
||||
</li>
|
||||
@endforeach
|
||||
</ol>
|
||||
|
|
@ -1,72 +1,153 @@
|
|||
<div class="mx-auto max-w-2xl space-y-5">
|
||||
@if ($command === null)
|
||||
<div class="animate-rise">
|
||||
<a href="{{ route('admin.hosts') }}" wire:navigate class="inline-flex items-center gap-1.5 text-xs font-medium text-muted hover:text-body">
|
||||
<x-ui.icon name="arrow-left" class="size-4" />{{ __('hosts.back') }}
|
||||
</a>
|
||||
<h1 class="mt-2 text-2xl font-bold tracking-tight text-ink">{{ __('hosts.create_title') }}</h1>
|
||||
<p class="mt-1 text-sm text-muted">{{ __('hosts.create_sub') }}</p>
|
||||
<div class="space-y-6">
|
||||
{{-- 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. --}}
|
||||
<header class="animate-rise">
|
||||
<p class="lbl">{{ __('hosts.takeover.eyebrow') }}</p>
|
||||
<h1 class="mt-[7px] text-[23px] font-bold leading-[1.12] tracking-[-0.03em] text-ink min-[901px]:text-[30px]">
|
||||
@if ($command === null)
|
||||
{{ __('hosts.create_title') }}
|
||||
@else
|
||||
{{ __('hosts.takeover.title', ['name' => $createdName]) }}
|
||||
@endif
|
||||
</h1>
|
||||
<p class="mt-2 max-w-[76ch] text-sm leading-relaxed text-muted">
|
||||
{{ $command === null ? __('hosts.create_sub') : __('hosts.takeover.subtitle') }}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="flex flex-col gap-6 lg:flex-row lg:items-start lg:gap-8">
|
||||
{{-- Der Ablauf als Schiene, nicht als Wand. Sie beantwortet „wo stehe
|
||||
ich" auf einen Blick und überlässt dem Hauptteil die Arbeit. --}}
|
||||
<x-admin.takeover-rail :current="$command === null ? 3 : 4"
|
||||
class="animate-rise [animation-delay:40ms]" />
|
||||
|
||||
<div class="min-w-0 flex-1 space-y-8 lg:max-w-[820px]">
|
||||
@if ($command === null)
|
||||
@if ($missingSettings)
|
||||
<x-ui.alert variant="warning" class="animate-rise [animation-delay:60ms]">
|
||||
<p class="font-medium">{{ __('hosts.takeover.missing_title') }}</p>
|
||||
<p class="mt-1">{{ __('hosts.takeover.missing_body', ['settings' => implode(', ', $missingSettings)]) }}</p>
|
||||
</x-ui.alert>
|
||||
@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. --}}
|
||||
<section class="animate-rise [animation-delay:60ms]">
|
||||
<h2 class="text-md font-bold tracking-[-0.01em] text-ink">{{ __('hosts.takeover.requirements_title') }}</h2>
|
||||
<p class="mt-1 max-w-[70ch] text-sm leading-relaxed text-muted">{{ __('hosts.takeover.requirements_sub') }}</p>
|
||||
|
||||
<x-ui.panel class="mt-4">
|
||||
<x-ui.row :label="__('hosts.takeover.s1_title')">
|
||||
<p class="text-sm leading-relaxed text-muted">{{ __('hosts.takeover.s1_body') }}</p>
|
||||
</x-ui.row>
|
||||
<x-ui.row :label="__('hosts.takeover.s2_title')">
|
||||
<p class="text-sm leading-relaxed text-muted">{{ __('hosts.takeover.s2_body') }}</p>
|
||||
</x-ui.row>
|
||||
</x-ui.panel>
|
||||
</section>
|
||||
|
||||
<form wire:submit="save" class="animate-rise [animation-delay:80ms]">
|
||||
<h2 class="text-md font-bold tracking-[-0.01em] text-ink">{{ __('hosts.takeover.form_title') }}</h2>
|
||||
<p class="mt-1 max-w-[70ch] text-sm leading-relaxed text-muted">{{ __('hosts.takeover.form_sub') }}</p>
|
||||
|
||||
<x-ui.panel class="mt-4">
|
||||
<x-ui.row :label="__('hosts.field.name')" :hint="__('hosts.field.name_hint')" for="name">
|
||||
<x-ui.input name="name" wire:model="name" autofocus />
|
||||
</x-ui.row>
|
||||
|
||||
<x-ui.row :label="__('hosts.field.datacenter')" for="datacenter">
|
||||
<select id="datacenter" wire:model="datacenter"
|
||||
class="block w-full rounded border border-line bg-surface px-3 py-2 text-sm text-ink transition">
|
||||
{{-- The building too, where it is known: two
|
||||
entries both reading "Falkenstein (fsn)" are
|
||||
not a choice anybody can make, and placing a
|
||||
second machine for redundancy is exactly when
|
||||
it matters which hall it lands in. --}}
|
||||
@foreach ($datacenters as $dc)
|
||||
<option value="{{ $dc->code }}">{{ $dc->name }} ({{ $dc->code }})@if ($dc->facility) · {{ $dc->facility }}@endif</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('datacenter') <p class="mt-1 text-xs text-danger">{{ $message }}</p> @enderror
|
||||
</x-ui.row>
|
||||
|
||||
<x-ui.row :label="__('hosts.field.public_ip')" for="public_ip">
|
||||
<x-ui.input name="public_ip" wire:model="public_ip" inputmode="numeric" placeholder="203.0.113.10" />
|
||||
</x-ui.row>
|
||||
|
||||
<x-ui.row :label="__('hosts.field.root_password')" :hint="__('hosts.field.root_password_hint')" for="root_password">
|
||||
<x-ui.input name="root_password" wire:model="root_password" type="password" autocomplete="off" />
|
||||
</x-ui.row>
|
||||
</x-ui.panel>
|
||||
|
||||
<div class="mt-4 flex items-center justify-end gap-3">
|
||||
<a href="{{ route('admin.hosts') }}" wire:navigate>
|
||||
<x-ui.button variant="secondary" type="button">{{ __('hosts.cancel') }}</x-ui.button>
|
||||
</a>
|
||||
<x-ui.button variant="primary" type="submit" wire:loading.attr="disabled">
|
||||
<span wire:loading.remove wire:target="save">{{ __('hosts.save') }}</span>
|
||||
<span wire:loading wire:target="save">{{ __('hosts.save') }}…</span>
|
||||
</x-ui.button>
|
||||
</div>
|
||||
</form>
|
||||
@else
|
||||
{{-- Ab hier ist die Befehlszeile die Hauptsache. Sie bekommt die
|
||||
volle Breite und steht ganz oben; alles andere ordnet sich
|
||||
unter. --}}
|
||||
<section class="animate-rise [animation-delay:60ms]">
|
||||
<h2 class="text-md font-bold tracking-[-0.01em] text-ink">{{ __('hosts.takeover.command_title') }}</h2>
|
||||
<p class="mt-1 max-w-[70ch] text-sm leading-relaxed text-muted">{{ __('hosts.takeover.command_sub') }}</p>
|
||||
|
||||
<div x-data="{ copied: false }" class="mt-4 overflow-hidden rounded-lg border border-accent-border bg-surface shadow-xs">
|
||||
<div class="flex items-center justify-between gap-3 border-b border-line bg-accent-subtle px-4 py-2.5">
|
||||
<p class="min-w-0 text-xs font-medium text-accent-text">{{ __('hosts.takeover.once_title') }}</p>
|
||||
<button type="button"
|
||||
class="inline-flex shrink-0 items-center gap-1.5 rounded border border-line bg-surface px-2.5 py-1 text-xs font-medium text-body transition hover:text-ink"
|
||||
x-on:click="navigator.clipboard.writeText($refs.command.textContent.trim()); 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>
|
||||
|
||||
{{-- 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-64 overflow-y-auto px-4 py-3.5 font-mono text-xs leading-relaxed text-ink break-all whitespace-pre-wrap"
|
||||
x-ref="command">{{ $command }}</pre>
|
||||
</div>
|
||||
|
||||
<p class="mt-2 text-xs leading-relaxed text-muted">{{ __('hosts.takeover.once_body') }}</p>
|
||||
</section>
|
||||
|
||||
<section class="animate-rise [animation-delay:80ms]">
|
||||
<h2 class="text-md font-bold tracking-[-0.01em] text-ink">{{ __('hosts.takeover.after_title') }}</h2>
|
||||
|
||||
<x-ui.panel class="mt-4">
|
||||
<x-ui.row :label="__('hosts.takeover.s5_title')">
|
||||
<p class="text-sm leading-relaxed text-muted">{{ __('hosts.takeover.s5_body') }}</p>
|
||||
<p class="mt-1 text-xs leading-relaxed text-muted">{{ __('hosts.takeover.s5_hint', ['url' => $archiveUrl]) }}</p>
|
||||
</x-ui.row>
|
||||
<x-ui.row :label="__('hosts.takeover.s6_title')">
|
||||
<p class="text-sm leading-relaxed text-muted">{{ __('hosts.takeover.s6_body') }}</p>
|
||||
<div class="mt-3">
|
||||
<a href="{{ route('admin.hosts.show', ['host' => $createdUuid]) }}" 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>
|
||||
</x-ui.row>
|
||||
</x-ui.panel>
|
||||
</section>
|
||||
@endif
|
||||
|
||||
<p class="max-w-[76ch] text-xs leading-relaxed text-muted animate-rise [animation-delay:100ms]">
|
||||
{{ __('hosts.takeover.footnote') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@if ($missingSettings)
|
||||
<x-ui.alert variant="warning" class="animate-rise">
|
||||
<p class="font-medium">{{ __('hosts.takeover.missing_title') }}</p>
|
||||
<p class="mt-1">{{ __('hosts.takeover.missing_body', ['settings' => implode(', ', $missingSettings)]) }}</p>
|
||||
</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 />
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label for="datacenter" class="block text-sm font-medium text-body">{{ __('hosts.field.datacenter') }}</label>
|
||||
<select id="datacenter" wire:model="datacenter"
|
||||
class="block w-full rounded border border-line bg-surface px-3 py-2 text-sm text-ink transition">
|
||||
{{-- The building too, where it is known: two entries both
|
||||
reading "Falkenstein (fsn)" are not a choice anybody can
|
||||
make, and placing a second machine for redundancy is
|
||||
exactly when it matters which hall it lands in. --}}
|
||||
@foreach ($datacenters as $dc)
|
||||
<option value="{{ $dc->code }}">{{ $dc->name }} ({{ $dc->code }})@if ($dc->facility) · {{ $dc->facility }}@endif</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('datacenter') <p class="text-xs text-danger">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<x-ui.input name="public_ip" wire:model="public_ip" :label="__('hosts.field.public_ip')" inputmode="numeric" placeholder="203.0.113.10" />
|
||||
|
||||
<x-ui.input name="root_password" wire:model="root_password" type="password"
|
||||
:label="__('hosts.field.root_password')" :hint="__('hosts.field.root_password_hint')" autocomplete="off" />
|
||||
|
||||
<div class="flex items-center justify-end gap-3 pt-1">
|
||||
<a href="{{ route('admin.hosts') }}" wire:navigate>
|
||||
<x-ui.button variant="secondary" type="button">{{ __('hosts.cancel') }}</x-ui.button>
|
||||
</a>
|
||||
<x-ui.button variant="primary" type="submit" wire:loading.attr="disabled">
|
||||
<span wire:loading.remove wire:target="save">{{ __('hosts.save') }}</span>
|
||||
<span wire:loading wire:target="save">{{ __('hosts.save') }}…</span>
|
||||
</x-ui.button>
|
||||
</div>
|
||||
</form>
|
||||
@else
|
||||
{{-- 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>
|
||||
</div>
|
||||
|
||||
<x-ui.alert variant="warning" class="animate-rise">
|
||||
<p class="font-medium">{{ __('hosts.takeover.once_title') }}</p>
|
||||
<p class="mt-1">{{ __('hosts.takeover.once_body') }}</p>
|
||||
</x-ui.alert>
|
||||
|
||||
<x-admin.takeover-guide :command="$command" :archive-url="$archiveUrl" :host-uuid="$createdUuid" class="animate-rise" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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. --}}
|
||||
<details class="group rounded-lg border border-line bg-surface shadow-xs animate-rise [animation-delay:20ms]">
|
||||
<details class="group overflow-hidden 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 class="flex items-center gap-2.5 text-sm font-medium text-body">
|
||||
<x-ui.icon name="life-buoy" class="size-4" />
|
||||
{{ __('hosts.takeover.how_title') }}
|
||||
<span class="hidden text-xs font-normal text-muted sm:inline">· {{ __('hosts.takeover.how_sub') }}</span>
|
||||
</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" />
|
||||
<x-admin.takeover-steps :archive-url="$archiveUrl" class="border-t border-line" />
|
||||
</details>
|
||||
|
||||
{{-- Filter bar — scales to many hosts (search + datacenter + status). --}}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue