feat(help): remaining topics (updates, servers, sessions, email, audit, recovery)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat/v1-foundation
boban 2026-06-19 22:20:58 +02:00
parent d60f97413f
commit d1fb93ffe3
13 changed files with 225 additions and 0 deletions

View File

@ -0,0 +1,15 @@
@php
$h = 'font-display text-base font-semibold text-ink';
$p = 'text-sm leading-relaxed text-ink-2';
$li = 'text-sm leading-relaxed text-ink-2';
@endphp
<div class="space-y-3">
<h3 class="{{ $h }}">Audit-Log</h3>
<p class="{{ $p }}">Das <span class="text-ink">Audit</span>-Log protokolliert sicherheits- und konfigurationsrelevante Aktionen wer, wann, was, von welcher IP. So bleibt jede Änderung nachvollziehbar und einem Benutzer zugeordnet.</p>
<ul class="ml-4 list-disc space-y-1.5">
<li class="{{ $li }}">Erfasst u. a.: Logins, 2FA-Änderungen, Domain-/TLS-Änderungen, Update-Anstöße, Server-Hardening und SSH-Aktionen.</li>
<li class="{{ $li }}"><span class="text-ink">Aufbewahrung:</span> die Vorhaltedauer lässt sich konfigurieren; ältere Einträge werden danach automatisch bereinigt.</li>
</ul>
<p class="{{ $p }}">Hinweis: Hinter einem externen Proxy zeigt das Audit-Log nur dann die echte Besucher-IP, wenn <code class="font-mono text-accent-text">TRUSTED_PROXY_CIDR</code> gesetzt ist (siehe „Domain, TLS &amp; Reverse-Proxy").</p>
</div>

View File

@ -0,0 +1,15 @@
@php
$h = 'font-display text-base font-semibold text-ink';
$p = 'text-sm leading-relaxed text-ink-2';
$li = 'text-sm leading-relaxed text-ink-2';
@endphp
<div class="space-y-3">
<h3 class="{{ $h }}">E-Mail (SMTP)</h3>
<p class="{{ $p }}">Unter <span class="text-ink">Einstellungen E-Mail</span> hinterlegst du einen <span class="text-ink">SMTP</span>-Server. Damit kann das Panel E-Mails versenden vor allem den <span class="text-ink">Passwort-Reset-Link</span> über die „Passwort vergessen"-Seite.</p>
<ul class="ml-4 list-disc space-y-1.5">
<li class="{{ $li }}">Host, Port, Verschlüsselung, Benutzer/Passwort und die Absenderadresse eintragen.</li>
<li class="{{ $li }}">Mit <span class="text-ink">„Testmail senden"</span> prüfst du die Konfiguration sofort.</li>
</ul>
<p class="{{ $p }}">Ohne SMTP funktioniert die Wiederherstellung weiterhin über den 2FA-Nachweis (E-Mail + TOTP oder Backup-Code) oder über die Host-Konsole siehe „Konto-Wiederherstellung".</p>
</div>

View File

@ -0,0 +1,24 @@
@php
$h = 'font-display text-base font-semibold text-ink';
$p = 'text-sm leading-relaxed text-ink-2';
$li = 'text-sm leading-relaxed text-ink-2';
$code = 'rounded bg-inset px-1.5 py-0.5 font-mono text-[12px] text-accent-text';
$pre = 'overflow-x-auto rounded-md border border-line bg-inset p-3 font-mono text-[11px] leading-relaxed text-ink-2';
@endphp
<div class="space-y-3">
<h3 class="{{ $h }}">Konto-Wiederherstellung</h3>
<p class="{{ $p }}">Mehrere Wege, falls du nicht mehr reinkommst vom bequemsten zum letzten Ausweg:</p>
<ul class="ml-4 list-disc space-y-1.5">
<li class="{{ $li }}"><span class="text-ink">Passwort vergessen:</span> die „Passwort vergessen"-Seite bietet einen <span class="text-ink">E-Mail-Reset-Link</span> (wenn SMTP konfiguriert ist, gültig 15 Min) oder einen <span class="text-ink">2FA-Nachweis-Reset</span> (E-Mail + TOTP oder Backup-Code → neues Passwort).</li>
<li class="{{ $li }}"><span class="text-ink">Bare-IP-Zugang:</span> <code class="{{ $code }}">http://&lt;Server-IP&gt;</code> bleibt immer erreichbar falls die Domain (DNS/Zertifikat/Proxy) gerade nicht geht. Beachte: über diesen Weg kein Security-Key, nur TOTP oder Backup-Code (siehe „Sicherheit &amp; 2FA").</li>
</ul>
</div>
<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>
</div>

View File

@ -0,0 +1,17 @@
@php
$h = 'font-display text-base font-semibold text-ink';
$p = 'text-sm leading-relaxed text-ink-2';
$li = 'text-sm leading-relaxed text-ink-2';
$code = 'rounded bg-inset px-1.5 py-0.5 font-mono text-[12px] text-accent-text';
@endphp
<div class="space-y-3">
<h3 class="{{ $h }}">Server &amp; SSH</h3>
<p class="{{ $p }}">Clusev verwaltet die Flotte <span class="text-ink">agentenlos über SSH</span> auf den Zielservern wird nichts installiert. Du fügst Server mit Host/IP, Port und Zugangsdaten hinzu; im Dashboard und auf der Server-Detailseite siehst du Metriken, Dienste, Dateien und mehr.</p>
<ul class="ml-4 list-disc space-y-1.5">
<li class="{{ $li }}"><span class="text-ink">Zugangsdaten-Tresor:</span> SSH-Passwörter/Schlüssel werden verschlüsselt gespeichert (App-Key), nie im Klartext.</li>
<li class="{{ $li }}"><span class="text-ink">Hardening:</span> auf der Server-Detailseite UFW/firewalld-Regeln, fail2ban-Status, und ein Ein-Klick-Ablauf „SSH-Schlüssel erzeugen und Passwort-Login sicher abschalten".</li>
<li class="{{ $li }}"><span class="text-ink">SSH-Key-Provisioning:</span> erzeugt ein Schlüsselpaar, hinterlegt den öffentlichen Schlüssel auf dem Zielserver und stellt die Verbindung danach auf Key-Auth um.</li>
</ul>
<p class="{{ $p }}">Jede Aktion über <code class="{{ $code }}">SSH</code> wird im Audit-Log dem ausführenden Benutzer zugeordnet.</p>
</div>

View File

@ -0,0 +1,16 @@
@php
$h = 'font-display text-base font-semibold text-ink';
$p = 'text-sm leading-relaxed text-ink-2';
$li = 'text-sm leading-relaxed text-ink-2';
@endphp
<div class="space-y-3">
<h3 class="{{ $h }}">Sitzungen</h3>
<p class="{{ $p }}">Unter <span class="text-ink">Einstellungen Sitzungen</span> siehst du alle aktiven Sitzungen deines Kontos (Gerät, Browser, IP, letzte Aktivität). Du kannst eine einzelne Sitzung beenden oder <span class="text-ink">alle anderen außer der aktuellen</span> abmelden.</p>
<p class="{{ $p }}">Ein Passwortwechsel macht jede andere Sitzung automatisch ungültig die aktive Sitzung bleibt bestehen.</p>
</div>
<div class="space-y-3">
<h3 class="{{ $h }}">Mehrere Administratoren</h3>
<p class="{{ $p }}">Unter <span class="text-ink">Einstellungen Benutzer</span> legst du weitere Admin-Konten an. Jede Aktion wird im Audit-Log dem jeweiligen Benutzer zugeordnet, und du kannst aktive Sitzungen pro Gerät, pro Benutzer oder global widerrufen.</p>
</div>

View File

@ -0,0 +1,16 @@
@php
$h = 'font-display text-base font-semibold text-ink';
$p = 'text-sm leading-relaxed text-ink-2';
$li = 'text-sm leading-relaxed text-ink-2';
$code = 'rounded bg-inset px-1.5 py-0.5 font-mono text-[12px] text-accent-text';
@endphp
<div class="space-y-3">
<h3 class="{{ $h }}">Updates</h3>
<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>
</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,15 @@
@php
$h = 'font-display text-base font-semibold text-ink';
$p = 'text-sm leading-relaxed text-ink-2';
$li = 'text-sm leading-relaxed text-ink-2';
@endphp
<div class="space-y-3">
<h3 class="{{ $h }}">Audit log</h3>
<p class="{{ $p }}">The <span class="text-ink">Audit</span> log records security- and configuration-relevant actions who, when, what, from which IP. Every change stays traceable and attributed to a user.</p>
<ul class="ml-4 list-disc space-y-1.5">
<li class="{{ $li }}">Captures, among others: logins, 2FA changes, domain/TLS changes, update triggers, server hardening and SSH actions.</li>
<li class="{{ $li }}"><span class="text-ink">Retention:</span> the keep duration is configurable; older entries are pruned automatically afterwards.</li>
</ul>
<p class="{{ $p }}">Note: behind an external proxy the audit log shows the real visitor IP only when <code class="font-mono text-accent-text">TRUSTED_PROXY_CIDR</code> is set (see "Domain, TLS &amp; reverse proxy").</p>
</div>

View File

@ -0,0 +1,15 @@
@php
$h = 'font-display text-base font-semibold text-ink';
$p = 'text-sm leading-relaxed text-ink-2';
$li = 'text-sm leading-relaxed text-ink-2';
@endphp
<div class="space-y-3">
<h3 class="{{ $h }}">Email (SMTP)</h3>
<p class="{{ $p }}">Under <span class="text-ink">Settings Email</span> you configure an <span class="text-ink">SMTP</span> server. This lets the panel send e-mail primarily the <span class="text-ink">password-reset link</span> from the "Forgot password" screen.</p>
<ul class="ml-4 list-disc space-y-1.5">
<li class="{{ $li }}">Enter host, port, encryption, username/password and the sender address.</li>
<li class="{{ $li }}">Use <span class="text-ink">"Send test email"</span> to verify the configuration right away.</li>
</ul>
<p class="{{ $p }}">Without SMTP, recovery still works via the 2FA proof (e-mail + TOTP or a backup code) or via the host console see "Account recovery".</p>
</div>

View File

@ -0,0 +1,24 @@
@php
$h = 'font-display text-base font-semibold text-ink';
$p = 'text-sm leading-relaxed text-ink-2';
$li = 'text-sm leading-relaxed text-ink-2';
$code = 'rounded bg-inset px-1.5 py-0.5 font-mono text-[12px] text-accent-text';
$pre = 'overflow-x-auto rounded-md border border-line bg-inset p-3 font-mono text-[11px] leading-relaxed text-ink-2';
@endphp
<div class="space-y-3">
<h3 class="{{ $h }}">Account recovery</h3>
<p class="{{ $p }}">Several ways back in if you are locked out from most convenient to last resort:</p>
<ul class="ml-4 list-disc space-y-1.5">
<li class="{{ $li }}"><span class="text-ink">Forgot password:</span> the "Forgot password" screen offers an <span class="text-ink">e-mail reset link</span> (when SMTP is configured, valid 15 min) or a <span class="text-ink">2FA-proof reset</span> (e-mail + TOTP or a backup code new password).</li>
<li class="{{ $li }}"><span class="text-ink">Bare-IP access:</span> <code class="{{ $code }}">http://&lt;server-ip&gt;</code> always stays reachable in case the domain (DNS/cert/proxy) is currently down. Note: no security key over this path, only TOTP or a backup code (see "Security &amp; 2FA").</li>
</ul>
</div>
<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>
</div>

View File

@ -0,0 +1,17 @@
@php
$h = 'font-display text-base font-semibold text-ink';
$p = 'text-sm leading-relaxed text-ink-2';
$li = 'text-sm leading-relaxed text-ink-2';
$code = 'rounded bg-inset px-1.5 py-0.5 font-mono text-[12px] text-accent-text';
@endphp
<div class="space-y-3">
<h3 class="{{ $h }}">Servers &amp; SSH</h3>
<p class="{{ $p }}">Clusev manages the fleet <span class="text-ink">agentless over SSH</span> nothing is installed on the target servers. You add servers with host/IP, port and credentials; the dashboard and the server detail page show metrics, services, files and more.</p>
<ul class="ml-4 list-disc space-y-1.5">
<li class="{{ $li }}"><span class="text-ink">Credential vault:</span> SSH passwords/keys are stored encrypted (app key), never in plaintext.</li>
<li class="{{ $li }}"><span class="text-ink">Hardening:</span> on the server detail page UFW/firewalld rules, fail2ban status, and a one-click "generate an SSH key and safely disable password login" flow.</li>
<li class="{{ $li }}"><span class="text-ink">SSH key provisioning:</span> generates a key pair, installs the public key on the target server and switches the connection to key auth afterwards.</li>
</ul>
<p class="{{ $p }}">Every action over <code class="{{ $code }}">SSH</code> is attributed to the acting user in the audit log.</p>
</div>

View File

@ -0,0 +1,15 @@
@php
$h = 'font-display text-base font-semibold text-ink';
$p = 'text-sm leading-relaxed text-ink-2';
@endphp
<div class="space-y-3">
<h3 class="{{ $h }}">Sessions (Sitzungen)</h3>
<p class="{{ $p }}">Under <span class="text-ink">Settings Sessions</span> you see all active sessions of your account (device, browser, IP, last activity). You can end a single session or sign out <span class="text-ink">all others except the current one</span>.</p>
<p class="{{ $p }}">A password change automatically invalidates every other session the active session stays.</p>
</div>
<div class="space-y-3">
<h3 class="{{ $h }}">Multiple administrators</h3>
<p class="{{ $p }}">Under <span class="text-ink">Settings Users</span> you add further admin accounts. Every action is attributed to the respective user in the audit log, and you can revoke active sessions per device, per user, or globally.</p>
</div>

View File

@ -0,0 +1,16 @@
@php
$h = 'font-display text-base font-semibold text-ink';
$p = 'text-sm leading-relaxed text-ink-2';
$li = 'text-sm leading-relaxed text-ink-2';
$code = 'rounded bg-inset px-1.5 py-0.5 font-mono text-[12px] text-accent-text';
@endphp
<div class="space-y-3">
<h3 class="{{ $h }}">Updates</h3>
<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>
</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

@ -6,6 +6,7 @@ use App\Livewire\Help\Index;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
class HelpPageTest extends TestCase
@ -55,4 +56,23 @@ class HelpPageTest extends TestCase
->assertSee('Backup-Code')
->assertSee('TOTP');
}
#[DataProvider('topicProvider')]
public function test_each_topic_renders_its_own_partial(string $topic, string $marker): void
{
$this->actAsAdmin();
Livewire::test(Index::class)->set('topic', $topic)->assertSee($marker);
}
public static function topicProvider(): array
{
return [
['updates', 'update.sh'],
['servers', 'SSH'],
['sessions', 'Sitzung'],
['email', 'SMTP'],
['audit', 'Audit'],
['recovery', 'clusev:reset-admin'],
];
}
}