33 KiB
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 leaktdocker-compose.prod.yml; Kurzformen vorhanden. - Ändern
install.sh— Wrapper rendern + nach/usr/local/bin/clusevinstallieren (Phase 9 → „MOTD + CLI"). - Ändern
app/Livewire/Help/Index.php—#[Url]auf$topic;commandsinTOPICS+ 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— Markerupdates/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:
#!/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:
#!/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:
# ── [9/9] MOTD (themed, shown at host login) ─────────────────────────
phase 9/9 "MOTD installieren"
with:
# ── [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
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:
use Livewire\Attributes\Url;
Add these test methods (after test_security_topic_explains_the_2fa_access_paths):
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:
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]+commandstopic to the component
In app/Livewire/Help/Index.php:
Change the imports (lines 5–6):
use Livewire\Attributes\Layout;
use Livewire\Attributes\Url;
use Livewire\Component;
Change the TOPICS constant (insert commands after updates):
private const TOPICS = [
'overview', 'domain-tls', 'security', 'updates', 'commands',
'servers', 'sessions', 'email', 'audit', 'recovery',
];
Add #[Url] to the property (line ~22):
#[Url]
public string $topic = 'overview';
Add the commands label to the $labels array in render() (after the updates entry):
'updates' => __('help.topic_updates'),
'commands' => __('help.topic_commands'),
- Step 4: Add the
topic_commandslang key (DE + EN)
In lang/de/help.php, after the topic_updates line:
'topic_commands' => 'Befehle / CLI',
In lang/en/help.php, after the topic_updates line:
'topic_commands' => 'Commands / CLI',
- Step 5: Create the DE „Befehle" partial
Create resources/views/livewire/help/content/de/commands.blade.php:
@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 <befehl></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 <...></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 <...></span></p>
</div>
</div>
- Step 6: Create the EN „Commands" partial
Create resources/views/livewire/help/content/en/commands.blade.php:
@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 <command></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 <...></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 <...></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 21–22) and the following paragraph (line 23):
<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 21–22) and the following paragraph (line 23):
<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:
<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:
<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
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
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 162–164):
<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_hintlang strings
In lang/de/versions.php (line 47):
'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):
'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):
'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):
'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):
'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):
'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:
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
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):
'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):
## [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
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 showssudo clusev update, not the long command.- On the host shell run
clusev help,clusev ps,clusev version(read-only — NOTreset-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 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.ymlstays the real filename, referenced internally in the wrapper + MOTDstack_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.