diff --git a/docs/superpowers/plans/2026-06-19-clusev-cli-and-command-shortcuts.md b/docs/superpowers/plans/2026-06-19-clusev-cli-and-command-shortcuts.md new file mode 100644 index 0000000..690204b --- /dev/null +++ b/docs/superpowers/plans/2026-06-19-clusev-cli-and-command-shortcuts.md @@ -0,0 +1,740 @@ +# `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 ` — `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=` (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` — `
` → `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-`
` → `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 315–342)
+
+- [ ] **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 ` 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 5–6):
+
+```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
+
+
+

Befehle / CLI

+

Auf dem Host richtet der Installer den Befehl clusev ein. Er kapselt die langen Docker-Kommandos — du musst dir weder Compose-Datei noch Container-Namen merken. Per SSH einloggen und von überall clusev <befehl> tippen. clusev help zeigt die Übersicht.

+
+ +
+
+ sudo clusev update +

Holt die neueste Version, baut das Image neu (inkl. CSS/JS) und wendet Datenbank-Migrationen an. Braucht root (sudo). Secrets und die konfigurierte Domain bleiben erhalten; das Panel ist ein bis zwei Minuten kurz nicht erreichbar.

+

→ führt update.sh aus (git pull + Image-Rebuild + migrate)

+
+ +
+ clusev reset-admin +

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 Konto-Wiederherstellung).

+

docker compose exec app php artisan clusev:reset-admin

+
+ +
+ clusev restart +

Startet den Stack neu (alle Container) und wendet Konfigurationsänderungen an, ohne neu zu bauen.

+

docker compose up -d

+
+ +
+ clusev logs +

Folgt den Live-Logs des Stacks (z. B. zur Fehlersuche). Mit einem Dienstnamen dahinter — etwa clusev logs app — nur dessen Logs. Strg-C beendet.

+

docker compose logs -f

+
+ +
+ clusev ps +

Zeigt, welche Dienste laufen (Alias: clusev status). Schneller Gesundheits-Check des Stacks.

+

docker compose ps

+
+ +
+ clusev migrate +

Wendet ausstehende Datenbank-Migrationen an. Normalerweise erledigt das clusev update automatisch — dieser Befehl ist für den seltenen manuellen Fall.

+

docker compose exec app php artisan migrate --force

+
+ +
+ clusev artisan <...> +

Für Fortgeschrittene: reicht ein beliebiges artisan-Kommando in den app-Container durch, etwa clusev artisan about.

+

docker compose exec app php artisan <...>

+
+
+``` + +- [ ] **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 + +
+

Commands / CLI

+

The installer sets up the clusev 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 clusev <command> from anywhere. clusev help prints the overview.

+
+ +
+
+ sudo clusev update +

Pulls the latest release, rebuilds the image (including CSS/JS) and applies database migrations. Requires root (sudo). Secrets and the configured domain are preserved; the panel is briefly unreachable for a minute or two.

+

→ runs update.sh (git pull + image rebuild + migrate)

+
+ +
+ clusev reset-admin +

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 Account recovery).

+

docker compose exec app php artisan clusev:reset-admin

+
+ +
+ clusev restart +

Restarts the stack (all containers) and applies configuration changes without rebuilding.

+

docker compose up -d

+
+ +
+ clusev logs +

Follows the stack's live logs (handy for troubleshooting). Add a service name — e.g. clusev logs app — for just that one. Ctrl-C stops.

+

docker compose logs -f

+
+ +
+ clusev ps +

Shows which services are running (alias: clusev status). A quick stack health check.

+

docker compose ps

+
+ +
+ clusev migrate +

Applies pending database migrations. Normally clusev update does this for you — this is for the rare manual case.

+

docker compose exec app php artisan migrate --force

+
+ +
+ clusev artisan <...> +

For advanced use: passes any artisan command through to the app container, e.g. clusev artisan about.

+

docker compose exec app php artisan <...>

+
+
+``` + +- [ ] **Step 7: Shorten the recovery `
` (DE + EN)**
+
+In `resources/views/livewire/help/content/de/recovery.blade.php`, replace the `
` block (lines 21–22) and the following paragraph (line 23):
+
+```blade
+    
clusev reset-admin
+

clusev reset-admin entfernt den zweiten Faktor, sodass du beim nächsten Login ein neues Passwort setzen kannst. (Dahinter läuft docker compose exec app php artisan clusev:reset-admin — siehe „Befehle / CLI".)

+``` + +In `resources/views/livewire/help/content/en/recovery.blade.php`, replace the `
` block (lines 21–22) and the following paragraph (line 23):
+
+```blade
+    
clusev reset-admin
+

clusev reset-admin clears the second factor so you can set a new password on the next login. (Under the hood it runs docker compose exec app php artisan clusev:reset-admin — see "Commands / CLI".)

+``` + +- [ ] **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 +
  • Per SSH: auf dem Host sudo clusev update ausführen (von überall). Mehr unter „Befehle / CLI".
  • +``` + +In `resources/views/livewire/help/content/en/updates.blade.php`, replace line 13: + +```blade +
  • Over SSH: on the host run sudo clusev update (from anywhere). More under "Commands / CLI".
  • +``` + +- [ ] **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 162–164) +- 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 43–45) +- Test: `tests/Feature/CommandShortcutsTest.php` + +- [ ] **Step 1: Write the failing regression test** + +Create `tests/Feature/CommandShortcutsTest.php`: + +```php +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 `
    ` block (lines 162–164):
    +
    +```blade
    +                    
    sudo clusev update
    +``` + +- [ ] **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 43–45: + +```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://@#g; s/[0-9a-f]{40,}//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 4–6. ✓ +- 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.