clusev/docs/superpowers/plans/2026-06-19-clusev-cli-and-c...

741 lines
33 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.

# `clusev` Host-CLI + kurze Befehle + Tab-URL — 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:** Operatoren rufen kurze Host-Befehle auf (`sudo clusev update`, `clusev reset-admin`, …) statt langer `docker compose -f docker-compose.prod.yml …`; alle user-sichtbaren Stellen zeigen die Kurzform, die Hilfe erklärt sie ausführlich, und der Help-Tab landet (wie Settings) in der URL.
**Architecture:** Ein vom Installer generiertes Host-Skript `/usr/local/bin/clusev` kapselt den Prod-Stack (Install-Verzeichnis eingebacken). UI/Hilfe/Lang-Strings/MOTD zeigen nur noch `clusev <sub>``docker-compose.prod.yml` verschwindet aus jeder Anzeige. Der Help-Komponente bekommt `#[Url]` auf `$topic` und ein neues Thema „Befehle / CLI".
**Tech Stack:** Bash (Wrapper + Skript-Test), Laravel 13 / Livewire 3 (`#[Url]`), Blade-Hilfe-Partials (DE/EN), PHPUnit (Feature-Tests), `install.sh` (Template-Rendering wie beim MOTD).
**Conventions (Projekt):** UI-Strings DE+EN (`__('group.key')`, R9/R16); keine Volt; Tests laufen im Container — bevorzugt isoliert gegen den Live-Dev-Stack:
`docker compose exec -T app php artisan test --filter=<X>` (oder mit eigenem `VIEW_COMPILED_PATH`, siehe `[[clusev-test-view-cache-race]]`). Vor Browser-Verify `view:clear` (siehe `[[clusev-dev-view-cache-gotcha]]`).
---
## File Structure
- **Neu** `docker/clusev/clusev` — Bash-Wrapper-Template (Platzhalter `__CLUSEV_DIR__`). Eine Verantwortung: kurze Unterbefehle → Prod-Stack-Kommandos.
- **Neu** `tests/scripts/test-clusev-cli.sh` — Host-Bash-Test für den Wrapper (Syntax, help, unknown, version, install-Verdrahtung). Kein Docker nötig.
- **Neu** `resources/views/livewire/help/content/de/commands.blade.php` + `…/en/commands.blade.php` — Hilfe-Thema „Befehle / CLI".
- **Neu** `tests/Feature/CommandShortcutsTest.php` — Regressions-Guard: keine Anzeige-Fläche leakt `docker-compose.prod.yml`; Kurzformen vorhanden.
- **Ändern** `install.sh` — Wrapper rendern + nach `/usr/local/bin/clusev` installieren (Phase 9 → „MOTD + CLI").
- **Ändern** `app/Livewire/Help/Index.php``#[Url]` auf `$topic`; `commands` in `TOPICS` + Label.
- **Ändern** `lang/de/help.php` + `lang/en/help.php``topic_commands`.
- **Ändern** `resources/views/livewire/help/content/{de,en}/recovery.blade.php``<pre>``clusev reset-admin`.
- **Ändern** `resources/views/livewire/help/content/{de,en}/updates.blade.php``sudo ./update.sh``sudo clusev update`.
- **Ändern** `resources/views/livewire/versions/index.blade.php` — 3-Zeilen-`<pre>` → `sudo clusev update`.
- **Ändern** `lang/de/versions.php` + `lang/en/versions.php``update_hint`.
- **Ändern** `lang/de/settings.php` + `lang/en/settings.php``recovery_note` (`clusev:reset-admin` → `clusev reset-admin`).
- **Ändern** `lang/de/system.php` + `lang/en/system.php``ssh_reset_hint` (dito).
- **Ändern** `docker/motd/00-clusev` — Verwalten/Update/Reset-Zeilen → Kurzform.
- **Ändern** `tests/Feature/HelpPageTest.php` — Marker `updates`/`recovery`, `commands`-Thema, `#[Url]`-Test, „kein docker-compose.prod.yml".
- **Ändern** `config/clusev.php`, `CHANGELOG.md` — Release.
---
## Task 1: `clusev` Host-CLI-Wrapper
**Files:**
- Create: `docker/clusev/clusev`
- Create: `tests/scripts/test-clusev-cli.sh`
- Modify: `install.sh` (Phase 9-Block, ~Zeilen 315342)
- [ ] **Step 1: Write the failing test (host bash test)**
Create `tests/scripts/test-clusev-cli.sh`:
```bash
#!/usr/bin/env bash
# Verifies the clusev host CLI template renders and behaves. No docker needed — only the
# help / version / unknown-command paths are exercised. Run from the repo root:
# bash tests/scripts/test-clusev-cli.sh
set -euo pipefail
cd "$(dirname "$0")/../.."
REPO="$(pwd)"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
fail() { printf 'FAIL: %s\n' "$1" >&2; exit 1; }
# Render the template the way install.sh does.
sed "s|__CLUSEV_DIR__|${REPO}|g" docker/clusev/clusev > "${TMP}/clusev"
chmod +x "${TMP}/clusev"
CLI="${TMP}/clusev"
# 1. Valid bash syntax.
bash -n "$CLI" || fail "syntax error in rendered clusev"
# 2. help lists the key subcommands.
out="$("$CLI" help)"
for needle in "clusev update" "clusev reset-admin" "clusev logs" "clusev ps" "clusev migrate"; do
printf '%s' "$out" | grep -qF "$needle" || fail "help missing: $needle"
done
# 3. bare invocation and --help also show usage.
"$CLI" | grep -qF "clusev reset-admin" || fail "bare invocation did not show usage"
"$CLI" --help | grep -qF "clusev reset-admin" || fail "--help did not show usage"
# 4. unknown command exits 64 and warns on stderr.
set +e
"$CLI" definitely-not-a-command >/dev/null 2>"${TMP}/err"; rc=$?
set -e
[ "$rc" -eq 64 ] || fail "unknown command exit = $rc (want 64)"
grep -qF "Unbekannter Befehl" "${TMP}/err" || fail "unknown command did not warn"
# 5. version reads config/clusev.php (no docker).
want="$(grep -oE "'version' => '[^']+'" config/clusev.php | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)"
got="$("$CLI" version)"
[ "$got" = "$want" ] || fail "version = '$got' (want '$want')"
# 6. install.sh wires the CLI into PATH.
grep -qF "/usr/local/bin/clusev" install.sh || fail "install.sh does not install /usr/local/bin/clusev"
# 7. usage text must NOT leak the long compose filename.
printf '%s' "$out" | grep -qF "docker-compose.prod.yml" && fail "usage leaks docker-compose.prod.yml"
printf 'ok — clusev CLI checks passed\n'
```
- [ ] **Step 2: Run it to confirm it fails**
Run: `bash tests/scripts/test-clusev-cli.sh`
Expected: FAIL — `sed: can't read docker/clusev/clusev: No such file or directory` (template missing).
- [ ] **Step 3: Create the wrapper template**
Create `docker/clusev/clusev`:
```bash
#!/usr/bin/env bash
# Clusev host CLI — short wrappers around the production stack.
# Generated by install.sh (it substitutes __CLUSEV_DIR__ with the install directory) and
# installed to /usr/local/bin/clusev. Operators run `clusev <command>` from anywhere instead
# of long `docker compose -f docker-compose.prod.yml ...` invocations. The compose filename is
# referenced internally only — it is never printed to the operator.
set -euo pipefail
CLUSEV_DIR="__CLUSEV_DIR__"
COMPOSE_FILE="${CLUSEV_DIR}/docker-compose.prod.yml"
compose() { docker compose -f "$COMPOSE_FILE" "$@"; }
usage() {
cat <<'EOF'
clusev — Fleet-Control Verwaltung (Host)
sudo clusev update Update holen, Image neu bauen, Migrationen anwenden (root)
clusev reset-admin Admin-Zugang zuruecksetzen (2FA entfernen, Passwort neu setzen)
clusev restart Stack neu starten
clusev logs [dienst] Logs folgen (Strg-C beendet)
clusev ps Dienst-Status (Alias: status)
clusev migrate Datenbank-Migrationen anwenden
clusev artisan <...> beliebiges artisan-Kommando im app-Container
clusev version installierte Version anzeigen
clusev help diese Uebersicht
Was im Hintergrund laeuft:
update -> update.sh (git pull + Image-Rebuild + migrate)
reset-admin -> docker compose exec app php artisan clusev:reset-admin
migrate -> docker compose exec app php artisan migrate --force
EOF
}
cmd="${1:-help}"; shift 2>/dev/null || true
case "$cmd" in
update) exec "${CLUSEV_DIR}/update.sh" "$@" ;;
reset-admin) compose exec app php artisan clusev:reset-admin "$@" ;;
restart) compose up -d "$@" ;;
logs) compose logs -f "$@" ;;
ps|status) compose ps "$@" ;;
migrate) compose exec app php artisan migrate --force "$@" ;;
artisan) compose exec app php artisan "$@" ;;
version) grep -oE "'version' => '[^']+'" "${CLUSEV_DIR}/config/clusev.php" \
| grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1 || true ;;
help|-h|--help) usage ;;
*) printf 'Unbekannter Befehl: %s\n\n' "$cmd" >&2; usage; exit 64 ;;
esac
```
- [ ] **Step 4: Run the test to confirm it passes**
Run: `bash tests/scripts/test-clusev-cli.sh`
Expected: PASS — `ok — clusev CLI checks passed`.
- [ ] **Step 5: Wire the wrapper into install.sh**
In `install.sh`, change the phase 9 header (line ~316) and add a render+install block **before** the `motd_render`/MOTD block. Replace:
```bash
# ── [9/9] MOTD (themed, shown at host login) ─────────────────────────
phase 9/9 "MOTD installieren"
```
with:
```bash
# ── [9/9] Host-CLI + MOTD ────────────────────────────────────────────
phase 9/9 "Host-CLI + MOTD installieren"
# clusev host command: render the template with the install dir baked in, drop it in PATH.
# Best-effort — a read-only /usr/local/bin must never fail the installer.
if sed "s|__CLUSEV_DIR__|$(pwd)|g" docker/clusev/clusev > /usr/local/bin/clusev 2>/dev/null \
&& chmod 0755 /usr/local/bin/clusev 2>/dev/null; then
info "Host-Befehl installiert: clusev (z. B. 'clusev ps', 'sudo clusev update')"
else
info "Host-Befehl uebersprungen (/usr/local/bin nicht beschreibbar)"
fi
```
(Leave the rest of the MOTD block unchanged.)
- [ ] **Step 6: Verify install.sh still parses + test still green**
Run: `bash -n install.sh && bash tests/scripts/test-clusev-cli.sh`
Expected: no syntax error; `ok — clusev CLI checks passed`.
- [ ] **Step 7: Commit**
```bash
git add docker/clusev/clusev tests/scripts/test-clusev-cli.sh install.sh
git commit -m "feat: clusev host CLI wrapper for short stack commands"
```
---
## Task 2: Hilfe-Seite — Tab-URL + Thema „Befehle"
**Files:**
- Modify: `app/Livewire/Help/Index.php`
- Modify: `lang/de/help.php`, `lang/en/help.php`
- Create: `resources/views/livewire/help/content/de/commands.blade.php`, `…/en/commands.blade.php`
- Modify: `resources/views/livewire/help/content/de/recovery.blade.php`, `…/en/recovery.blade.php`
- Modify: `resources/views/livewire/help/content/de/updates.blade.php`, `…/en/updates.blade.php`
- Test: `tests/Feature/HelpPageTest.php`
- [ ] **Step 1: Update HelpPageTest — url binding, new topic, short commands**
In `tests/Feature/HelpPageTest.php`, add the `Url` import and three checks, and update the data provider. Add at the top with the other `use` lines:
```php
use Livewire\Attributes\Url;
```
Add these test methods (after `test_security_topic_explains_the_2fa_access_paths`):
```php
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');
}
```
Replace the `topicProvider` body:
```php
public static function topicProvider(): array
{
return [
['updates', 'clusev update'],
['servers', 'SSH'],
['sessions', 'Sitzung'],
['email', 'SMTP'],
['audit', 'Audit'],
['recovery', 'clusev reset-admin'],
['commands', 'clusev ps'],
];
}
```
- [ ] **Step 2: Run the tests to confirm they fail**
Run: `docker compose exec -T app php artisan test --filter=HelpPageTest`
Expected: FAIL — `test_topic_property_is_url_bound` (no `#[Url]`), `commands` topic renders overview (no marker), recovery still shows `docker-compose.prod.yml`.
- [ ] **Step 3: Add `#[Url]` + `commands` topic to the component**
In `app/Livewire/Help/Index.php`:
Change the imports (lines 56):
```php
use Livewire\Attributes\Layout;
use Livewire\Attributes\Url;
use Livewire\Component;
```
Change the `TOPICS` constant (insert `commands` after `updates`):
```php
private const TOPICS = [
'overview', 'domain-tls', 'security', 'updates', 'commands',
'servers', 'sessions', 'email', 'audit', 'recovery',
];
```
Add `#[Url]` to the property (line ~22):
```php
#[Url]
public string $topic = 'overview';
```
Add the `commands` label to the `$labels` array in `render()` (after the `updates` entry):
```php
'updates' => __('help.topic_updates'),
'commands' => __('help.topic_commands'),
```
- [ ] **Step 4: Add the `topic_commands` lang key (DE + EN)**
In `lang/de/help.php`, after the `topic_updates` line:
```php
'topic_commands' => 'Befehle / CLI',
```
In `lang/en/help.php`, after the `topic_updates` line:
```php
'topic_commands' => 'Commands / CLI',
```
- [ ] **Step 5: Create the DE „Befehle" partial**
Create `resources/views/livewire/help/content/de/commands.blade.php`:
```blade
@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">
<code class="{{ $cmd }}">sudo clusev update</code>
<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">
<code class="{{ $cmd }}">clusev reset-admin</code>
<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">
<code class="{{ $cmd }}">clusev restart</code>
<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">
<code class="{{ $cmd }}">clusev logs</code>
<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">
<code class="{{ $cmd }}">clusev ps</code>
<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">
<code class="{{ $cmd }}">clusev migrate</code>
<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">
<code class="{{ $cmd }}">clusev artisan &lt;...&gt;</code>
<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>
```
- [ ] **Step 6: Create the EN „Commands" partial**
Create `resources/views/livewire/help/content/en/commands.blade.php`:
```blade
@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">
<code class="{{ $cmd }}">sudo clusev update</code>
<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">
<code class="{{ $cmd }}">clusev reset-admin</code>
<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">
<code class="{{ $cmd }}">clusev restart</code>
<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">
<code class="{{ $cmd }}">clusev logs</code>
<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">
<code class="{{ $cmd }}">clusev ps</code>
<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">
<code class="{{ $cmd }}">clusev migrate</code>
<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">
<code class="{{ $cmd }}">clusev artisan &lt;...&gt;</code>
<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>
```
- [ ] **Step 7: Shorten the recovery `<pre>` (DE + EN)**
In `resources/views/livewire/help/content/de/recovery.blade.php`, replace the `<pre>` block (lines 2122) and the following paragraph (line 23):
```blade
<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>
```
In `resources/views/livewire/help/content/en/recovery.blade.php`, replace the `<pre>` block (lines 2122) and the following paragraph (line 23):
```blade
<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>
```
- [ ] **Step 8: Switch the updates partial to `clusev update` (DE + EN)**
In `resources/views/livewire/help/content/de/updates.blade.php`, replace line 13:
```blade
<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>
```
In `resources/views/livewire/help/content/en/updates.blade.php`, replace line 13:
```blade
<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>
```
- [ ] **Step 9: Run the Help tests to confirm they pass**
Run: `docker compose exec -T app php artisan test --filter=HelpPageTest`
Expected: PASS — all topics render, url-binding present, recovery/commands show short forms, no `docker-compose.prod.yml`.
- [ ] **Step 10: Commit**
```bash
git add app/Livewire/Help/Index.php lang/de/help.php lang/en/help.php \
resources/views/livewire/help/content/de/commands.blade.php \
resources/views/livewire/help/content/en/commands.blade.php \
resources/views/livewire/help/content/de/recovery.blade.php \
resources/views/livewire/help/content/en/recovery.blade.php \
resources/views/livewire/help/content/de/updates.blade.php \
resources/views/livewire/help/content/en/updates.blade.php \
tests/Feature/HelpPageTest.php
git commit -m "feat: help tab-URL + Commands/CLI topic + short command copy"
```
---
## Task 3: Übrige Anzeige-Flächen + Regressions-Guard
**Files:**
- Modify: `resources/views/livewire/versions/index.blade.php` (lines 162164)
- Modify: `lang/de/versions.php`, `lang/en/versions.php` (`update_hint`)
- Modify: `lang/de/settings.php`, `lang/en/settings.php` (`recovery_note`)
- Modify: `lang/de/system.php`, `lang/en/system.php` (`ssh_reset_hint`)
- Modify: `docker/motd/00-clusev` (lines 4345)
- Test: `tests/Feature/CommandShortcutsTest.php`
- [ ] **Step 1: Write the failing regression test**
Create `tests/Feature/CommandShortcutsTest.php`:
```php
<?php
namespace Tests\Feature;
use Tests\TestCase;
class CommandShortcutsTest extends TestCase
{
/** Files shown to the operator — none may print the long compose filename. */
private const DISPLAY_SURFACES = [
'resources/views/livewire/versions/index.blade.php',
'resources/views/livewire/help/content/de/recovery.blade.php',
'resources/views/livewire/help/content/en/recovery.blade.php',
'resources/views/livewire/help/content/de/updates.blade.php',
'resources/views/livewire/help/content/en/updates.blade.php',
'resources/views/livewire/help/content/de/commands.blade.php',
'resources/views/livewire/help/content/en/commands.blade.php',
'docker/motd/00-clusev',
];
public function test_no_display_surface_leaks_the_long_compose_filename(): void
{
foreach (self::DISPLAY_SURFACES as $rel) {
$contents = file_get_contents(base_path($rel));
$this->assertStringNotContainsString('docker-compose.prod.yml', $contents, "{$rel} still shows docker-compose.prod.yml");
}
}
public function test_versions_panel_shows_the_short_update_command(): void
{
$blade = file_get_contents(base_path('resources/views/livewire/versions/index.blade.php'));
$this->assertStringContainsString('sudo clusev update', $blade);
$this->assertStringNotContainsString('docker compose -f', $blade);
}
public function test_reset_hints_use_the_short_host_command(): void
{
foreach (['de', 'en'] as $locale) {
$settings = require base_path("lang/{$locale}/settings.php");
$system = require base_path("lang/{$locale}/system.php");
$this->assertStringContainsString('clusev reset-admin', $settings['recovery_note']);
$this->assertStringContainsString('clusev reset-admin', $system['ssh_reset_hint']);
}
}
public function test_motd_uses_short_commands(): void
{
$motd = file_get_contents(base_path('docker/motd/00-clusev'));
$this->assertStringContainsString('clusev ps | logs | restart', $motd);
$this->assertStringContainsString('sudo clusev update', $motd);
$this->assertStringContainsString('clusev reset-admin', $motd);
}
}
```
- [ ] **Step 2: Run it to confirm it fails**
Run: `docker compose exec -T app php artisan test --filter=CommandShortcutsTest`
Expected: FAIL — versions blade + MOTD still carry `docker-compose.prod.yml`; lang strings still say `clusev:reset-admin`.
- [ ] **Step 3: Shorten the versions update panel**
In `resources/views/livewire/versions/index.blade.php`, replace the `<pre>` block (lines 162164):
```blade
<pre class="overflow-x-auto rounded-md border border-line bg-void px-3 py-2.5 font-mono text-[11px] leading-relaxed text-ink-2">sudo clusev update</pre>
```
- [ ] **Step 4: Reword the `update_hint` lang strings**
In `lang/de/versions.php` (line 47):
```php
'update_hint' => 'Updates kommen über getaggte Releases. Auf dem Host genügt ein Befehl — er baut das Image neu und wendet Migrationen an:',
```
In `lang/en/versions.php` (line 47):
```php
'update_hint' => 'Updates arrive via tagged releases. On the host a single command rebuilds the image and applies migrations:',
```
- [ ] **Step 5: Short host command in the reset hints**
In `lang/de/settings.php` (line 44):
```php
'recovery_note' => 'Konto-Wiederherstellung (letzter Ausweg): per SSH auf den Host einloggen und `clusev reset-admin` ausführen.',
```
In `lang/en/settings.php` (line 44):
```php
'recovery_note' => 'Account recovery (last resort): sign in to the host over SSH and run `clusev reset-admin`.',
```
In `lang/de/system.php` (line 34):
```php
'ssh_reset_hint' => 'Komplett ausgesperrt? Per SSH auf den Host einloggen und `clusev reset-admin` ausführen, um den Zugang zurückzusetzen.',
```
In `lang/en/system.php` (line 34):
```php
'ssh_reset_hint' => 'Fully locked out? SSH into the host and run `clusev reset-admin` to reset dashboard access.',
```
- [ ] **Step 6: Short commands in the MOTD**
In `docker/motd/00-clusev`, replace lines 4345:
```bash
printf ' %sLogin%s %sStandard-Passwort: clusev — beim ersten Login aendern · Reset: clusev reset-admin%s\n' "$D" "$R" "$D" "$R"
printf ' %sVerwalten%s %sclusev ps | logs | restart%s\n' "$D" "$R" "$D" "$R"
printf ' %sUpdate%s %ssudo clusev update%s\n' "$D" "$R" "$D" "$R"
```
(`stack_status()` keeps using `$COMPOSE` internally — that is script logic, not displayed text, and uses the `__CLUSEV_COMPOSE__` placeholder, not the literal filename.)
- [ ] **Step 7: Run the regression test to confirm it passes**
Run: `docker compose exec -T app php artisan test --filter=CommandShortcutsTest`
Expected: PASS — all four assertions green.
- [ ] **Step 8: Commit**
```bash
git add resources/views/livewire/versions/index.blade.php \
lang/de/versions.php lang/en/versions.php \
lang/de/settings.php lang/en/settings.php \
lang/de/system.php lang/en/system.php \
docker/motd/00-clusev tests/Feature/CommandShortcutsTest.php
git commit -m "feat: short clusev commands in versions panel, lang strings, MOTD"
```
---
## Task 4: Integration, Verifizierung & Release
**Files:**
- Modify: `config/clusev.php` (version bump)
- Modify: `CHANGELOG.md`
- [ ] **Step 1: Full test suite green**
Run: `docker compose exec -T app php artisan test`
Expected: PASS — no regressions (note `[[clusev-test-view-cache-race]]`: if a Blade-touch flake appears, re-run the failing file with an isolated `VIEW_COMPILED_PATH`).
- [ ] **Step 2: Wrapper host test green**
Run: `bash tests/scripts/test-clusev-cli.sh`
Expected: `ok — clusev CLI checks passed`.
- [ ] **Step 3: Build assets (prod parity)**
Run: `docker compose exec -T app npm run build`
Expected: Vite build succeeds, no errors.
- [ ] **Step 4: Bump version**
In `config/clusev.php`, set the `'version'` to the next patch (current is `0.9.24``0.9.25`):
```php
'version' => '0.9.25',
```
- [ ] **Step 5: Update CHANGELOG**
Add a top entry to `CHANGELOG.md` (match the existing format/heading style already in the file):
```markdown
## [0.9.25] — 2026-06-19
### Added
- Host CLI `clusev` (installed to `/usr/local/bin`): short wrappers for `update`, `reset-admin`, `restart`, `logs`, `ps`, `migrate`, `artisan`, `version`. Operators no longer type the long `docker compose -f docker-compose.prod.yml …` invocations.
- Help topic „Befehle / CLI" (DE/EN) documenting every `clusev` command and what it runs underneath.
### Changed
- Help tab now reflects in the URL (`#[Url]` on the topic) — reload / bookmark / share keep the active tab, matching Settings.
- All operator-facing surfaces (version update panel, help recovery/updates, settings/system reset hints, MOTD) now show the short `clusev …` commands; the `docker-compose.prod.yml` filename no longer appears in the UI.
```
- [ ] **Step 6: Commit the release**
```bash
git add config/clusev.php CHANGELOG.md
git commit -m "chore: release 0.9.25 — clusev host CLI + short commands + help tab-URL"
```
- [ ] **Step 7: R12 browser verify on the live domain**
Deploy to the VM, then load the panel in headless Chrome (per `[[clusev-prod-browser-verify]]`):
- `/help` → HTTP 200; click „Befehle / CLI" → the new topic renders; reload `…/help?topic=commands` → still on the topic; switch DE/EN; zero console/network errors; no leaked `@`/`{{ }}`/`group.key`.
- `/versions` (Version & Releases) → the update panel shows `sudo clusev update`, not the long command.
- On the host shell run `clusev help`, `clusev ps`, `clusev version` (read-only — NOT `reset-admin`/`update`); confirm sane output.
Expected: every check passes; throwaway admin (if created) removed afterwards.
- [ ] **Step 8: Codex review (R15)**
Run `/codex:review` over the branch diff. Fix anything it flags as an error or security issue; re-run until clean.
- [ ] **Step 9: Tag, push, deploy**
Tag `v0.9.25`, push branch + tag to the Gitea remote (token read only at push time from `/home/nexxo/.env.gitea`, never echoed/committed; sanitize push output `sed -E 's#https://[^@]*@#https://<redacted>@#g; s/[0-9a-f]{40,}/<redacted>/g'`), then deploy to the VM and re-confirm `clusev` is on PATH (`which clusev`).
---
## Self-Review
**1. Spec coverage:**
- Teil A (clusev wrapper) → Task 1 (template, test, install.sh). ✓
- Teil B (Help `#[Url]`) → Task 2 Step 3 + url-binding test. ✓
- Teil C (alle Anzeige-Flächen) → versions+lang+motd Task 3; help recovery/updates Task 2. ✓
- Teil D (Thema „Befehle" + ausführliche Erklärung + echtes Kommando) → Task 2 Steps 46. ✓
- Teil E (Dateistruktur) → File Structure section + per-task file lists. ✓
- Teil F (Test/Verify) → bash test (Task 1), HelpPageTest (Task 2), CommandShortcutsTest (Task 3), suite+build+R12+Codex (Task 4). ✓
- Teil G (kein Rename, keine internen Skripte) → respected; only display surfaces changed; `docker-compose.prod.yml` stays the real filename, referenced internally in the wrapper + MOTD `stack_status`. ✓
**2. Placeholder scan:** No TBD/TODO; every code step shows full content. ✓
**3. Type/name consistency:** Topic key `commands` used identically in `TOPICS`, `$labels`, lang key `topic_commands`, partial filenames `commands.blade.php`, and test markers. Short command spelled `clusev reset-admin` / `sudo clusev update` consistently across wrapper usage, help, lang, MOTD, tests. The wrapper's internal `COMPOSE_FILE` is the only place the long filename appears (not a display surface). ✓
**Note on the regression guard:** `CommandShortcutsTest` deliberately excludes `docker/clusev/clusev` (the wrapper *must* reference the compose file internally) — only operator-visible files are scanned.