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

19 KiB
Raw Blame History

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

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

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

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

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:

@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:

use App\Livewire\Help;

Inside the EnsureSecurityOnboarded group, after the /versions route:

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:

<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:

@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:

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
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:

    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

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:

    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

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:

    /** @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

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
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):

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 verifysudo ./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.