clusev/docs/superpowers/plans/2026-06-19-help-page.md

460 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# Hilfe-Seite Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Eine bilinguale In-Panel-Hilfe-Seite unter `/help` mit linker Themen-Navigation, die alle Einstellungen erklärt — inkl. generischer Reverse-Proxy-Anleitung und dem 2FA-Zugangspfad-Hinweis.
**Architecture:** Full-Page-Livewire-Komponente `App\Livewire\Help\Index` (App-Layout, hinter Auth + Onboarding). Kurze Strings in `lang/{de,en}/help.php`; lange Texte als Blade-Partials je Sprache unter `resources/views/livewire/help/content/{de,en}/<topic>.blade.php`, nach `app()->getLocale()` eingebunden (Fallback `de`). Kein State außer `$topic`.
**Tech Stack:** Laravel 13, Livewire 3 (class-based), Tailwind v4 `@theme`-Tokens, bestehende Blade-Komponenten (`x-nav-item`, `x-panel`, `x-icon`).
**Konventionen (hart):** Route-Pfad englisch (R13); sichtbare Texte DE+EN, kein Emoji (R9/R16); nur `@theme`-Token-Utilities, keine Inline-Styles (R4/R3); Container-Tooling (R8); Browser-Verify (R12) + Codex (R15) vor „fertig".
Spec: `docs/superpowers/specs/2026-06-19-help-page-design.md` (Abschnitt 4 = verbindliche Inhalte).
---
## Task 1: Gerüst + Thema „overview" + Tests (TDD)
**Files:**
- Create: `app/Livewire/Help/Index.php`
- Create: `resources/views/livewire/help/index.blade.php`
- Create: `resources/views/livewire/help/content/de/overview.blade.php`
- Create: `resources/views/livewire/help/content/en/overview.blade.php`
- Create: `lang/de/help.php`, `lang/en/help.php`
- Modify: `routes/web.php` (Route + Import)
- Modify: `resources/views/components/sidebar.blade.php` (Nav-Eintrag nach Zeile 36)
- Modify: `lang/de/shell.php`, `lang/en/shell.php` (`nav_help`)
- Modify: `resources/views/components/command-palette.blade.php` (chords + `$nav`)
- Modify: `resources/js/app.js` (`CMDK_GO` + `h`)
- Test: `tests/Feature/HelpPageTest.php`
- [ ] **Step 1: Failing test**
`tests/Feature/HelpPageTest.php`:
```php
<?php
namespace Tests\Feature;
use App\Livewire\Help\Index;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Tests\TestCase;
class HelpPageTest extends TestCase
{
use RefreshDatabase;
private function actAsAdmin(): void
{
$this->actingAs(User::factory()->create(['must_change_password' => false]));
}
public function test_help_route_loads_for_an_authenticated_admin(): void
{
$this->actAsAdmin();
$this->get('/help')->assertOk();
}
public function test_default_topic_is_overview(): void
{
$this->actAsAdmin();
Livewire::test(Index::class)
->assertSet('topic', 'overview')
->assertSee(__('help.topic_overview'));
}
public function test_unknown_topic_falls_back_to_overview(): void
{
$this->actAsAdmin();
Livewire::test(Index::class, ['topic' => 'bogus'])->assertSet('topic', 'overview');
}
}
```
- [ ] **Step 2: Run — expect FAIL**
Run: `docker compose exec -T -u app app php artisan test --filter=HelpPageTest`
Expected: FAIL (class `App\Livewire\Help\Index` not found).
- [ ] **Step 3: Component**
`app/Livewire/Help/Index.php`:
```php
<?php
namespace App\Livewire\Help;
use Livewire\Attributes\Layout;
use Livewire\Component;
/**
* In-panel help/documentation. Pure display: a left topic nav (like Settings) and a per-locale
* content partial. No persistence, no external calls. Topic keys map to content partials at
* resources/views/livewire/help/content/{de,en}/<topic>.blade.php.
*/
#[Layout('layouts.app')]
class Index extends Component
{
/** Help topics in display order. Keys are also the content-partial filenames. */
private const TOPICS = [
'overview', 'domain-tls', 'security', 'updates',
'servers', 'sessions', 'email', 'audit', 'recovery',
];
public string $topic = 'overview';
public function mount(?string $topic = null): void
{
if ($topic !== null && in_array($topic, self::TOPICS, true)) {
$this->topic = $topic;
}
}
public function render()
{
$labels = [
'overview' => __('help.topic_overview'),
'domain-tls' => __('help.topic_domain_tls'),
'security' => __('help.topic_security'),
'updates' => __('help.topic_updates'),
'servers' => __('help.topic_servers'),
'sessions' => __('help.topic_sessions'),
'email' => __('help.topic_email'),
'audit' => __('help.topic_audit'),
'recovery' => __('help.topic_recovery'),
];
$topics = array_map(fn (string $key): array => ['key' => $key, 'label' => $labels[$key]], self::TOPICS);
return view('livewire.help.index', ['topics' => $topics])->title(__('help.title'));
}
}
```
- [ ] **Step 4: Chrome lang files**
`lang/de/help.php`:
```php
<?php
return [
'title' => 'Hilfe — Clusev',
'eyebrow' => 'Hilfe',
'heading' => 'Hilfe & Dokumentation',
'subtitle' => 'Einstellungen und Abläufe erklärt.',
'topic_overview' => 'Überblick',
'topic_domain_tls' => 'Domain, TLS & Reverse-Proxy',
'topic_security' => 'Sicherheit & 2FA',
'topic_updates' => 'Updates & Versionen',
'topic_servers' => 'Server & SSH',
'topic_sessions' => 'Sitzungen & Benutzer',
'topic_email' => 'E-Mail (SMTP)',
'topic_audit' => 'Audit-Log',
'topic_recovery' => 'Konto-Wiederherstellung',
];
```
`lang/en/help.php` (same keys, English values):
```php
<?php
return [
'title' => 'Help — Clusev',
'eyebrow' => 'Help',
'heading' => 'Help & documentation',
'subtitle' => 'Settings and procedures explained.',
'topic_overview' => 'Overview',
'topic_domain_tls' => 'Domain, TLS & reverse proxy',
'topic_security' => 'Security & 2FA',
'topic_updates' => 'Updates & versions',
'topic_servers' => 'Servers & SSH',
'topic_sessions' => 'Sessions & users',
'topic_email' => 'Email (SMTP)',
'topic_audit' => 'Audit log',
'topic_recovery' => 'Account recovery',
];
```
- [ ] **Step 5: Index view**
`resources/views/livewire/help/index.blade.php`:
```blade
@php
$locale = in_array(app()->getLocale(), ['de', 'en'], true) ? app()->getLocale() : 'de';
$partial = 'livewire.help.content.'.$locale.'.'.$topic;
if (! view()->exists($partial)) {
$partial = 'livewire.help.content.de.'.$topic;
}
@endphp
<div class="space-y-5">
<div>
<p class="font-mono text-[11px] uppercase tracking-[0.2em] text-accent-text">{{ __('help.eyebrow') }}</p>
<h2 class="mt-0.5 font-display text-xl font-semibold text-ink sm:text-2xl">{{ __('help.heading') }}</h2>
<p class="mt-1 font-mono text-xs text-ink-3">{{ __('help.subtitle') }}</p>
</div>
<div class="grid gap-5 lg:grid-cols-[15rem_1fr]">
{{-- Topic nav --}}
<nav class="flex flex-row flex-wrap gap-1 lg:flex-col">
@foreach ($topics as $t)
<button type="button" wire:click="$set('topic', '{{ $t['key'] }}')" @class([
'rounded-md px-3 py-2 text-left font-mono text-[12px] transition-colors',
'bg-accent/10 text-accent-text shadow-[inset_2px_0_0_var(--color-accent)]' => $topic === $t['key'],
'text-ink-2 hover:bg-raised hover:text-ink' => $topic !== $t['key'],
])>{{ $t['label'] }}</button>
@endforeach
</nav>
{{-- Content --}}
<article class="min-w-0 space-y-4 rounded-xl border border-line bg-surface p-5 shadow-panel">
@include($partial)
</article>
</div>
</div>
```
- [ ] **Step 6: overview partials (de + en)**
`resources/views/livewire/help/content/de/overview.blade.php` — short, real prose. Cover: was Clusev ist (Fleet-Control über SSH); Erst-Login `admin@clusev.local` / `clusev` (sofort ändern, Zwangswechsel); Bare-IP vs. Domain; wo man was findet (Server, Dienste, Dateien, Audit, Einstellungen, System, Version, Hilfe). Use `<h3 class="font-display text-base font-semibold text-ink">`, `<p class="text-sm leading-relaxed text-ink-2">`, `<code class="font-mono text-accent-text">`. NO emoji, NO inline styles.
`resources/views/livewire/help/content/en/overview.blade.php` — same content, English.
- [ ] **Step 7: Route + import**
`routes/web.php`: add import near the other `use App\Livewire\...;` lines:
```php
use App\Livewire\Help;
```
Inside the `EnsureSecurityOnboarded` group, after the `/versions` route:
```php
Route::get('/help', Help\Index::class)->name('help');
```
- [ ] **Step 8: Sidebar nav**
`resources/views/components/sidebar.blade.php` — after the `/versions` `x-nav-item` (line 36), add:
```blade
<x-nav-item icon="help-circle" href="/help" :active="request()->is('help*')">{{ __('shell.nav_help') }}</x-nav-item>
```
If `x-icon` has no `help-circle` case, add it to `resources/views/components/icon.blade.php` with the Lucide path:
```blade
@case('help-circle')
<circle cx="12" cy="12" r="10" /><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" /><path d="M12 17h.01" />
@break
```
(Match the existing `@case`/`@break` structure + the component's stroke attributes.)
- [ ] **Step 9: shell lang + cmdk**
`lang/de/shell.php`: add `'nav_help' => 'Hilfe',`. `lang/en/shell.php`: add `'nav_help' => 'Help',`.
`resources/views/components/command-palette.blade.php`:
- In `$chords` (after `['g v', __('shell.nav_versions')],`): add `['g h', __('shell.nav_help')],`
- In `$nav` (after the versions entry): add `['label' => __('shell.nav_help'), 'href' => '/help', 'hint' => 'g h'],`
`resources/js/app.js` line ~256 `CMDK_GO`: add `h: '/help'` to the map:
```js
const CMDK_GO = { d: '/', s: '/servers', i: '/services', f: '/files', l: '/audit', e: '/settings', y: '/system', v: '/versions', h: '/help' };
```
- [ ] **Step 10: Run — expect PASS**
Run: `docker compose exec -T -u app app php artisan test --filter=HelpPageTest`
Expected: PASS (3 tests).
- [ ] **Step 11: Pint + commit**
```bash
docker compose exec -T -u app app ./vendor/bin/pint app/Livewire/Help/Index.php tests/Feature/HelpPageTest.php
git add app/Livewire/Help routes/web.php resources/views/livewire/help resources/views/components/sidebar.blade.php resources/views/components/command-palette.blade.php resources/views/components/icon.blade.php resources/js/app.js lang/de/help.php lang/en/help.php lang/de/shell.php lang/en/shell.php tests/Feature/HelpPageTest.php
git commit -m "feat(help): in-panel help page scaffold + overview topic"
```
---
## Task 2: Thema „domain-tls" (Reverse-Proxy-Anleitung)
**Files:**
- Create: `resources/views/livewire/help/content/de/domain-tls.blade.php`
- Create: `resources/views/livewire/help/content/en/domain-tls.blade.php`
- Test: `tests/Feature/HelpPageTest.php` (add a test)
- [ ] **Step 1: Failing test** — add to `HelpPageTest`:
```php
public function test_domain_tls_topic_has_the_reverse_proxy_guide(): void
{
$this->actAsAdmin();
Livewire::test(Index::class)
->set('topic', 'domain-tls')
->assertSee('X-Forwarded-Proto')
->assertSee('TRUSTED_PROXY_CIDR')
->assertDontSee('Zoraxy'); // generic — no product names
}
```
- [ ] **Step 2: Run — expect FAIL** (`view [livewire.help.content.de.domain-tls] not found`).
Run: `docker compose exec -T -u app app php artisan test --filter=HelpPageTest`
- [ ] **Step 3: Write the partials (de + en)**
Content per spec §4a — three modes (Bare-IP / eingebautes TLS / externer Proxy) then the **generic step-by-step** proxy guide. MUST contain the literal tokens `X-Forwarded-Proto` and `TRUSTED_PROXY_CIDR`; MUST NOT contain `Zoraxy` (use „Proxy Manager"). Steps verbatim:
1. Im Panel: System → Domain → TLS „Externer Reverse-Proxy" → speichern → Stack neu starten.
2. Im Proxy Manager: Domain = `panel.<domain>`; Ziel = `http://<Clusev-IP>:80` (HTTP); Host-Header + `X-Forwarded-Proto: https` weiterreichen; WebSocket aktivieren (`/app/*`, `/apps/*`).
3. `.env`: `TRUSTED_PROXY_CIDR` = Proxy-Adresse → Secure-Cookie + echte IP im Audit; danach `sudo ./update.sh`.
4. Firewall: HTTP-Port nur vom Proxy erreichbar.
5. In diesem Modus stellt Clusev kein Zertifikat aus.
Use `<h3>`, `<p>`, `<ol class="...">`/`<ul>`, and a `<pre class="overflow-x-auto rounded-md border border-line bg-inset p-3 font-mono text-[11px] text-ink-2">` for the proxy fields/CLI. @theme tokens only.
- [ ] **Step 4: Run — expect PASS.** `docker compose exec -T -u app app php artisan test --filter=HelpPageTest`
- [ ] **Step 5: Commit**
```bash
git add resources/views/livewire/help/content tests/Feature/HelpPageTest.php
git commit -m "feat(help): domain-tls topic with the reverse-proxy setup guide"
```
---
## Task 3: Thema „security" (2FA-Zugangspfade + Extensions)
**Files:**
- Create: `resources/views/livewire/help/content/de/security.blade.php`
- Create: `resources/views/livewire/help/content/en/security.blade.php`
- Test: `tests/Feature/HelpPageTest.php`
- [ ] **Step 1: Failing test** — add:
```php
public function test_security_topic_explains_the_2fa_access_paths(): void
{
$this->actAsAdmin();
Livewire::test(Index::class)
->set('topic', 'security')
->assertSee('Backup-Code')
->assertSee('TOTP');
}
```
- [ ] **Step 2: Run — expect FAIL.**
- [ ] **Step 3: Write the partials (de + en)** — content per spec §4b + §4c:
- TOTP / Security-Keys (YubiKey) / Backup-Codes erklären.
- **Security-Key nur über die HTTPS-Domain** (WebAuthn braucht sicheren Kontext + Domain als rpId); über **Bare-IP/HTTP** kein Security-Key → **Backup-Code** nötig.
- **TOTP funktioniert überall** (auch Bare-IP/HTTP), braucht keinen Backup-Code für den IP-Pfad.
- Empfehlung: reiner Security-Key-Nutzer → Backup-Codes aufbewahren.
- Passwort-Manager-Extensions (Bitwarden/Kaspersky) können die Registrierung abfangen → in der Extension Passkey-Übernahme deaktivieren oder ohne Extension registrieren (privates Fenster).
Must contain literal `Backup-Code` and `TOTP`.
- [ ] **Step 4: Run — expect PASS.**
- [ ] **Step 5: Commit**
```bash
git add resources/views/livewire/help/content tests/Feature/HelpPageTest.php
git commit -m "feat(help): security topic — 2FA access paths + extension note"
```
---
## Task 4: Restliche Themen (updates, servers, sessions, email, audit, recovery)
**Files:**
- Create: `resources/views/livewire/help/content/{de,en}/{updates,servers,sessions,email,audit,recovery}.blade.php` (12 Partials)
- Test: `tests/Feature/HelpPageTest.php`
- [ ] **Step 1: Failing test** — assert every topic renders without falling back (a marker string per topic). Add:
```php
/** @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'],
];
}
```
- [ ] **Step 2: Run — expect FAIL** (missing partials).
- [ ] **Step 3: Write the 12 partials (de + en)** — each short + accurate:
- `updates`: Auto-Check, „Jetzt aktualisieren"-Knopf, CLI `sudo ./update.sh`, Build+Migrate beim Update. (marker `update.sh`)
- `servers`: Server hinzufügen, SSH-Credential-Vault, Hardening, SSH-Key-Provisioning. (marker `SSH`)
- `sessions`: aktive Sitzungen sehen/widerrufen, weitere Admins, Audit-Attribution. (marker `Sitzung`/`Session` — DE uses `Sitzung`)
- `email`: SMTP konfigurieren (Settings → E-Mail) + Testmail. (marker `SMTP`)
- `audit`: was protokolliert wird + Retention. (marker `Audit`)
- `recovery`: Forgot-Password (E-Mail-Link / 2FA-Proof), Bare-IP-Recovery, `clusev:reset-admin`. (marker `clusev:reset-admin`)
Same markup conventions (h3/p/ul/pre), @theme tokens, no emoji. EN partials mirror DE (the marker strings are language-neutral tokens, so the EN test passes too).
- [ ] **Step 4: Run — expect PASS** (full HelpPageTest green).
- [ ] **Step 5: Run full suite + Pint**
```bash
docker compose exec -T -u app app ./vendor/bin/pint
docker compose exec -T -u app app php artisan test
```
Expected: all green.
- [ ] **Step 6: Commit**
```bash
git add resources/views/livewire/help/content tests/Feature/HelpPageTest.php
git commit -m "feat(help): remaining topics (updates, servers, sessions, email, audit, recovery)"
```
---
## Task 5: Browser-Verify (R12), Codex (R15), Release
**Files:** Modify `config/clusev.php` (version), `CHANGELOG.md`.
- [ ] **Step 1: R12 browser-verify on the domain** — temp admin (create + delete after, do not touch the real account), headless Chrome via the puppeteer docker image against `https://panel.test.bave.dev/help`:
- HTTP 200, no console errors, no leaked `{{ }}`/`help.`/`shell.` tokens.
- Click two topic-nav buttons (`domain-tls`, `security`) → content swaps.
- Toggle DE/EN → content language changes (the partial locale switches).
- Check at 375 / 768 / 1280 widths.
- [ ] **Step 2: Codex review** — run `/codex:review` over the diff; fix until no errors / no security issues.
- [ ] **Step 3: Version + CHANGELOG** — bump `config/clusev.php` version; add a `### Hinzugefügt` entry: „In-Panel-Hilfe-Seite (`/help`) mit Themen-Navigation; erklärt alle Einstellungen, die Reverse-Proxy-Einrichtung und die 2FA-Zugangspfade (Security-Key nur über HTTPS-Domain, Backup-Code über Bare-IP, TOTP überall). Zoraxy-Produktname aus dem TLS-Hinweis entfernt."
- [ ] **Step 4: Commit, tag, push** (token from `/home/nexxo/.env.gitea`, output sanitized):
```bash
git add config/clusev.php CHANGELOG.md
git commit -m "chore(release): vX.Y.Z — in-panel help page"
git tag -a vX.Y.Z -m "Clusev vX.Y.Z — Hilfe-Seite"
TOKEN="$(grep -E '^GIT_ACCESS_TOKEN=' /home/nexxo/.env.gitea | cut -d= -f2- | tr -d '\r\n')"
git push "https://boban:${TOKEN}@git.bave.dev/boban/clusev.git" feat/v1-foundation --follow-tags 2>&1 | sed -E 's#https://[^@]*@#https://<redacted>@#g; s/[0-9a-f]{40,}/<redacted>/g'
```
- [ ] **Step 5: Deploy to VM + final verify**`sudo ./update.sh` on the VM; confirm `/help` renders over the domain (R12 spot-check).
---
## Self-Review (Plan vs. Spec)
- **Spec §1 Platzierung** → Task 1 (route, sidebar, cmdk, layout). ✓
- **Spec §2 Aufbau** → Task 1 (component `$topic`, index view, topic nav). ✓
- **Spec §3 zweisprachig** → Task 1 (lang chrome + per-locale partials + fallback). ✓
- **Spec §4a Proxy-Anleitung** → Task 2 (must-have tokens, no product name). ✓
- **Spec §4b 2FA-Pfade** → Task 3 (Backup-Code/TOTP markers). ✓
- **Spec §4c Extensions** → Task 3. ✓
- **Spec §5 Logik klein** → Task 1 component (pure display). ✓
- **Spec §6 Dateistruktur** → all 18 partials across Tasks 14. ✓
- **Spec §7 Test/Verify** → Tasks 14 (Livewire tests) + Task 5 (R12/Codex/Release). ✓
- **Spec §8 YAGNI** → no search/markdown engine; static partials only. ✓
Type/name consistency: topic keys identical in `TOPICS`, labels map, partial filenames, and tests (`overview`, `domain-tls`, `security`, `updates`, `servers`, `sessions`, `email`, `audit`, `recovery`). `$set('topic', …)` matches the public `$topic` prop.