feat: help tab-URL + Commands/CLI topic + short command copy

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
feat/v1-foundation
boban 2026-06-19 23:00:34 +02:00
parent 580c72a59d
commit 437a22592b
10 changed files with 158 additions and 13 deletions

View File

@ -3,6 +3,7 @@
namespace App\Livewire\Help;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Url;
use Livewire\Component;
/**
@ -15,12 +16,16 @@ class Index extends Component
{
/** Help topics in display order. */
private const TOPICS = [
'overview', 'domain-tls', 'security', 'updates',
'overview', 'domain-tls', 'security', 'updates', 'commands',
'servers', 'sessions', 'email', 'audit', 'recovery',
];
#[Url]
public string $topic = 'overview';
// mount() handles the test/parameter-injection path (Livewire::test($class, ['topic' => …])).
// Browser deep links populate $topic via the #[Url] attribute above and bypass mount on
// hydration. Both paths are validated by the render() clamp below.
public function mount(?string $topic = null): void
{
if ($topic !== null) {
@ -41,6 +46,7 @@ class Index extends Component
'domain-tls' => __('help.topic_domain_tls'),
'security' => __('help.topic_security'),
'updates' => __('help.topic_updates'),
'commands' => __('help.topic_commands'),
'servers' => __('help.topic_servers'),
'sessions' => __('help.topic_sessions'),
'email' => __('help.topic_email'),

View File

@ -10,6 +10,7 @@ return [
'topic_domain_tls' => 'Domain, TLS & Reverse-Proxy',
'topic_security' => 'Sicherheit & 2FA',
'topic_updates' => 'Updates & Versionen',
'topic_commands' => 'Befehle / CLI',
'topic_servers' => 'Server & SSH',
'topic_sessions' => 'Sitzungen & Benutzer',
'topic_email' => 'E-Mail (SMTP)',

View File

@ -10,6 +10,7 @@ return [
'topic_domain_tls' => 'Domain, TLS & reverse proxy',
'topic_security' => 'Security & 2FA',
'topic_updates' => 'Updates & versions',
'topic_commands' => 'Commands / CLI',
'topic_servers' => 'Servers & SSH',
'topic_sessions' => 'Sessions & users',
'topic_email' => 'Email (SMTP)',

View File

@ -0,0 +1,56 @@
@php
$h = 'font-display text-base font-semibold text-ink';
$p = 'text-sm leading-relaxed text-ink-2';
$code = 'rounded bg-inset px-1.5 py-0.5 font-mono text-[12px] text-accent-text';
$cmd = 'block w-full overflow-x-auto rounded-md border border-line bg-void px-3 py-2 font-mono text-[12px] text-accent-text';
$real = 'mt-1 font-mono text-[11px] leading-relaxed text-ink-3';
@endphp
<div class="space-y-3">
<h3 class="{{ $h }}">Befehle / CLI</h3>
<p class="{{ $p }}">Auf dem Host richtet der Installer den Befehl <code class="{{ $code }}">clusev</code> ein. Er kapselt die langen Docker-Kommandos du musst dir weder Compose-Datei noch Container-Namen merken. Per SSH einloggen und von überall <code class="{{ $code }}">clusev &lt;befehl&gt;</code> tippen. <code class="{{ $code }}">clusev help</code> zeigt die Übersicht.</p>
</div>
<div class="space-y-4">
<div class="space-y-1">
<pre class="{{ $cmd }}">sudo clusev update</pre>
<p class="{{ $p }}">Holt die neueste Version, baut das Image neu (inkl. CSS/JS) und wendet Datenbank-Migrationen an. Braucht root (<code class="{{ $code }}">sudo</code>). Secrets und die konfigurierte Domain bleiben erhalten; das Panel ist ein bis zwei Minuten kurz nicht erreichbar.</p>
<p class="{{ $real }}"> führt <span class="text-ink-2">update.sh</span> aus (git pull + Image-Rebuild + migrate)</p>
</div>
<div class="space-y-1">
<pre class="{{ $cmd }}">clusev reset-admin</pre>
<p class="{{ $p }}">Setzt den Admin-Zugang zurück: entfernt den zweiten Faktor (2FA), sodass beim nächsten Login ein neues Passwort gesetzt werden kann. Letzter Ausweg, wenn Passwort und 2FA verloren sind (siehe <span class="text-ink">Konto-Wiederherstellung</span>).</p>
<p class="{{ $real }}"> <span class="text-ink-2">docker compose exec app php artisan clusev:reset-admin</span></p>
</div>
<div class="space-y-1">
<pre class="{{ $cmd }}">clusev restart</pre>
<p class="{{ $p }}">Startet den Stack neu (alle Container) und wendet Konfigurationsänderungen an, ohne neu zu bauen.</p>
<p class="{{ $real }}"> <span class="text-ink-2">docker compose up -d</span></p>
</div>
<div class="space-y-1">
<pre class="{{ $cmd }}">clusev logs</pre>
<p class="{{ $p }}">Folgt den Live-Logs des Stacks (z. B. zur Fehlersuche). Mit einem Dienstnamen dahinter etwa <code class="{{ $code }}">clusev logs app</code> nur dessen Logs. Strg-C beendet.</p>
<p class="{{ $real }}"> <span class="text-ink-2">docker compose logs -f</span></p>
</div>
<div class="space-y-1">
<pre class="{{ $cmd }}">clusev ps</pre>
<p class="{{ $p }}">Zeigt, welche Dienste laufen (Alias: <code class="{{ $code }}">clusev status</code>). Schneller Gesundheits-Check des Stacks.</p>
<p class="{{ $real }}"> <span class="text-ink-2">docker compose ps</span></p>
</div>
<div class="space-y-1">
<pre class="{{ $cmd }}">clusev migrate</pre>
<p class="{{ $p }}">Wendet ausstehende Datenbank-Migrationen an. Normalerweise erledigt das <code class="{{ $code }}">clusev update</code> automatisch dieser Befehl ist für den seltenen manuellen Fall.</p>
<p class="{{ $real }}"> <span class="text-ink-2">docker compose exec app php artisan migrate --force</span></p>
</div>
<div class="space-y-1">
<pre class="{{ $cmd }}">clusev artisan &lt;...&gt;</pre>
<p class="{{ $p }}">Für Fortgeschrittene: reicht ein beliebiges artisan-Kommando in den app-Container durch, etwa <code class="{{ $code }}">clusev artisan about</code>.</p>
<p class="{{ $real }}"> <span class="text-ink-2">docker compose exec app php artisan &lt;...&gt;</span></p>
</div>
</div>

View File

@ -17,8 +17,7 @@
<div class="space-y-3">
<h3 class="{{ $h }}">Komplett ausgesperrt (letzter Ausweg)</h3>
<p class="{{ $p }}">Passwort und 2FA verloren, kein SMTP? Vom Host wiederherstellen per SSH auf den Server einloggen und im Projektverzeichnis ausführen:</p>
<pre class="{{ $pre }}">cd clusev
docker compose -f docker-compose.prod.yml exec app php artisan clusev:reset-admin</pre>
<p class="{{ $p }}"><code class="{{ $code }}">clusev:reset-admin</code> entfernt den zweiten Faktor, sodass du beim nächsten Login ein neues Passwort setzen kannst.</p>
<p class="{{ $p }}">Passwort und 2FA verloren, kein SMTP? Vom Host wiederherstellen per SSH auf den Server einloggen und ausführen:</p>
<pre class="{{ $pre }}">clusev reset-admin</pre>
<p class="{{ $p }}"><code class="{{ $code }}">clusev reset-admin</code> entfernt den zweiten Faktor, sodass du beim nächsten Login ein neues Passwort setzen kannst. (Dahinter läuft <code class="{{ $code }}">docker compose exec app php artisan clusev:reset-admin</code> siehe „Befehle / CLI".)</p>
</div>

View File

@ -10,7 +10,7 @@
<p class="{{ $p }}">Unter <span class="text-ink">Version &amp; Releases</span> prüft das Panel beim Öffnen automatisch, ob eine neuere Version im Kanal verfügbar ist (Vergleich mit dem neuesten getaggten Release). Ist eines verfügbar, erscheint der Knopf <span class="text-ink">„Jetzt aktualisieren"</span>.</p>
<ul class="ml-4 list-disc space-y-1.5">
<li class="{{ $li }}"><span class="text-ink">Per Knopf:</span> „Jetzt aktualisieren" anstoßen. Das Panel ist während des Neubaus ein bis zwei Minuten kurz nicht erreichbar; danach springt die Anzeige selbst auf „Aktuell".</li>
<li class="{{ $li }}"><span class="text-ink">Per SSH:</span> auf dem Host <code class="{{ $code }}">cd clusev</code> und <code class="{{ $code }}">sudo ./update.sh</code> ausführen.</li>
<li class="{{ $li }}"><span class="text-ink">Per SSH:</span> auf dem Host <code class="{{ $code }}">sudo clusev update</code> ausführen (von überall). Mehr unter „Befehle / CLI".</li>
</ul>
<p class="{{ $p }}">Beim Update wird das Image neu gebaut (inkl. <code class="{{ $code }}">npm run build</code> für CSS/JS) und es werden <span class="text-ink">Datenbank-Migrationen</span> angewendet. Stil-Änderungen und Schema-Updates greifen also automatisch. Secrets und die konfigurierte Domain bleiben erhalten.</p>
</div>

View File

@ -0,0 +1,56 @@
@php
$h = 'font-display text-base font-semibold text-ink';
$p = 'text-sm leading-relaxed text-ink-2';
$code = 'rounded bg-inset px-1.5 py-0.5 font-mono text-[12px] text-accent-text';
$cmd = 'block w-full overflow-x-auto rounded-md border border-line bg-void px-3 py-2 font-mono text-[12px] text-accent-text';
$real = 'mt-1 font-mono text-[11px] leading-relaxed text-ink-3';
@endphp
<div class="space-y-3">
<h3 class="{{ $h }}">Commands / CLI</h3>
<p class="{{ $p }}">The installer sets up the <code class="{{ $code }}">clusev</code> command on the host. It wraps the long Docker invocations you never need to remember the compose file or container names. SSH in and run <code class="{{ $code }}">clusev &lt;command&gt;</code> from anywhere. <code class="{{ $code }}">clusev help</code> prints the overview.</p>
</div>
<div class="space-y-4">
<div class="space-y-1">
<pre class="{{ $cmd }}">sudo clusev update</pre>
<p class="{{ $p }}">Pulls the latest release, rebuilds the image (including CSS/JS) and applies database migrations. Requires root (<code class="{{ $code }}">sudo</code>). Secrets and the configured domain are preserved; the panel is briefly unreachable for a minute or two.</p>
<p class="{{ $real }}"> runs <span class="text-ink-2">update.sh</span> (git pull + image rebuild + migrate)</p>
</div>
<div class="space-y-1">
<pre class="{{ $cmd }}">clusev reset-admin</pre>
<p class="{{ $p }}">Resets dashboard access: clears the second factor (2FA) so you can set a new password on the next login. Last resort when password and 2FA are both lost (see <span class="text-ink">Account recovery</span>).</p>
<p class="{{ $real }}"> <span class="text-ink-2">docker compose exec app php artisan clusev:reset-admin</span></p>
</div>
<div class="space-y-1">
<pre class="{{ $cmd }}">clusev restart</pre>
<p class="{{ $p }}">Restarts the stack (all containers) and applies configuration changes without rebuilding.</p>
<p class="{{ $real }}"> <span class="text-ink-2">docker compose up -d</span></p>
</div>
<div class="space-y-1">
<pre class="{{ $cmd }}">clusev logs</pre>
<p class="{{ $p }}">Follows the stack's live logs (handy for troubleshooting). Add a service name e.g. <code class="{{ $code }}">clusev logs app</code> for just that one. Ctrl-C stops.</p>
<p class="{{ $real }}"> <span class="text-ink-2">docker compose logs -f</span></p>
</div>
<div class="space-y-1">
<pre class="{{ $cmd }}">clusev ps</pre>
<p class="{{ $p }}">Shows which services are running (alias: <code class="{{ $code }}">clusev status</code>). A quick stack health check.</p>
<p class="{{ $real }}"> <span class="text-ink-2">docker compose ps</span></p>
</div>
<div class="space-y-1">
<pre class="{{ $cmd }}">clusev migrate</pre>
<p class="{{ $p }}">Applies pending database migrations. Normally <code class="{{ $code }}">clusev update</code> does this for you this is for the rare manual case.</p>
<p class="{{ $real }}"> <span class="text-ink-2">docker compose exec app php artisan migrate --force</span></p>
</div>
<div class="space-y-1">
<pre class="{{ $cmd }}">clusev artisan &lt;...&gt;</pre>
<p class="{{ $p }}">For advanced use: passes any artisan command through to the app container, e.g. <code class="{{ $code }}">clusev artisan about</code>.</p>
<p class="{{ $real }}"> <span class="text-ink-2">docker compose exec app php artisan &lt;...&gt;</span></p>
</div>
</div>

View File

@ -17,8 +17,7 @@
<div class="space-y-3">
<h3 class="{{ $h }}">Completely locked out (last resort)</h3>
<p class="{{ $p }}">Lost password and 2FA, no SMTP? Recover from the host SSH into the server and run in the project directory:</p>
<pre class="{{ $pre }}">cd clusev
docker compose -f docker-compose.prod.yml exec app php artisan clusev:reset-admin</pre>
<p class="{{ $p }}"><code class="{{ $code }}">clusev:reset-admin</code> clears the second factor so you can set a new password on the next login.</p>
<p class="{{ $p }}">Lost password and 2FA, no SMTP? Recover from the host SSH into the server and run:</p>
<pre class="{{ $pre }}">clusev reset-admin</pre>
<p class="{{ $p }}"><code class="{{ $code }}">clusev reset-admin</code> clears the second factor so you can set a new password on the next login. (Under the hood it runs <code class="{{ $code }}">docker compose exec app php artisan clusev:reset-admin</code> see "Commands / CLI".)</p>
</div>

View File

@ -10,7 +10,7 @@
<p class="{{ $p }}">Under <span class="text-ink">Version &amp; Releases</span> the panel checks on open whether a newer release is available in the channel (compared against the newest tagged release). When one is available, the <span class="text-ink">"Update now"</span> button appears.</p>
<ul class="ml-4 list-disc space-y-1.5">
<li class="{{ $li }}"><span class="text-ink">From the button:</span> trigger "Update now". The panel is briefly unreachable for a minute or two during the rebuild; afterwards the card flips to "Current" on its own.</li>
<li class="{{ $li }}"><span class="text-ink">Over SSH:</span> on the host run <code class="{{ $code }}">cd clusev</code> and <code class="{{ $code }}">sudo ./update.sh</code>.</li>
<li class="{{ $li }}"><span class="text-ink">Over SSH:</span> on the host run <code class="{{ $code }}">sudo clusev update</code> (from anywhere). More under "Commands / CLI".</li>
</ul>
<p class="{{ $p }}">An update rebuilds the image (including <code class="{{ $code }}">npm run build</code> for CSS/JS) and applies <span class="text-ink">database migrations</span>. So style changes and schema updates take effect automatically. Secrets and the configured domain are preserved.</p>
</div>

View File

@ -5,6 +5,7 @@ namespace Tests\Feature;
use App\Livewire\Help\Index;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Attributes\Url;
use Livewire\Livewire;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
@ -57,6 +58,31 @@ class HelpPageTest extends TestCase
->assertSee('TOTP');
}
public function test_topic_property_is_url_bound(): void
{
$attrs = (new \ReflectionProperty(Index::class, 'topic'))->getAttributes(Url::class);
$this->assertNotEmpty($attrs, 'Help topic must be #[Url]-bound for deep links / reload');
}
public function test_recovery_shows_the_short_host_command_not_the_long_compose_one(): void
{
$this->actAsAdmin();
Livewire::test(Index::class)
->set('topic', 'recovery')
->assertSee('clusev reset-admin')
->assertDontSee('docker-compose.prod.yml');
}
public function test_commands_topic_lists_the_cli(): void
{
$this->actAsAdmin();
Livewire::test(Index::class)
->set('topic', 'commands')
->assertSee('clusev reset-admin')
->assertSee('sudo clusev update')
->assertDontSee('docker-compose.prod.yml');
}
#[DataProvider('topicProvider')]
public function test_each_topic_renders_its_own_partial(string $topic, string $marker): void
{
@ -67,12 +93,13 @@ class HelpPageTest extends TestCase
public static function topicProvider(): array
{
return [
['updates', 'update.sh'],
['updates', 'clusev update'],
['servers', 'SSH'],
['sessions', 'Sitzung'],
['email', 'SMTP'],
['audit', 'Audit'],
['recovery', 'clusev:reset-admin'],
['recovery', 'clusev reset-admin'],
['commands', 'clusev ps'],
];
}
}