From 8d2b7087a9ab3ba81e46819145bbd63c1b0edcd5 Mon Sep 17 00:00:00 2001 From: boban Date: Fri, 19 Jun 2026 23:09:35 +0200 Subject: [PATCH] feat: short clusev commands in versions panel, lang strings, MOTD Co-Authored-By: Claude Sonnet 4.6 --- docker/motd/00-clusev | 8 +- lang/de/settings.php | 2 +- lang/de/system.php | 4 +- lang/de/versions.php | 4 +- lang/en/settings.php | 2 +- lang/en/system.php | 4 +- lang/en/versions.php | 4 +- .../help/content/de/domain-tls.blade.php | 2 +- .../help/content/en/domain-tls.blade.php | 2 +- .../views/livewire/versions/index.blade.php | 4 +- tests/Feature/CommandShortcutsTest.php | 77 +++++++++++++++++++ tests/Feature/ForgotPasswordSmtpAwareTest.php | 6 +- 12 files changed, 97 insertions(+), 22 deletions(-) create mode 100644 tests/Feature/CommandShortcutsTest.php diff --git a/docker/motd/00-clusev b/docker/motd/00-clusev index 8208108..07cffb0 100644 --- a/docker/motd/00-clusev +++ b/docker/motd/00-clusev @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Clusev themed MOTD — printed at host login (Debian/Ubuntu run-parts MOTD). # Installed by install.sh, which substitutes __CLUSEV_URL__, __CLUSEV_VERSION__ and -# __CLUSEV_COMPOSE__ (absolute path to docker-compose.prod.yml). Signal-orange brand, +# __CLUSEV_COMPOSE__ (absolute path to the production compose file). Signal-orange brand, # live stack status, no emoji (status = colour + dot). Idempotent: install.sh only ever # overwrites this single 00-clusev file, never other MOTD parts. Never fails a login # (every dynamic lookup degrades to "unbekannt"). @@ -40,8 +40,8 @@ printf ' %s%s▀▀▀ ▀▀▀ ▀▀▀ ▀▀▀ ▀▀▀ ▀ %s %sFlee printf ' %s──────────────────────────────────────────────────────────%s\n' "$D" "$R" printf ' %sDashboard%s %s%s%s\n' "$D" "$R" "$O" "$URL" "$R" printf ' %sStack%s %s\n' "$D" "$R" "$(stack_status)" -printf ' %sLogin%s %sStandard-Passwort: clusev — beim ersten Login aendern · Reset: clusev:reset-admin%s\n' "$D" "$R" "$D" "$R" -printf ' %sVerwalten%s %sdocker compose -f docker-compose.prod.yml ps | logs -f | restart%s\n' "$D" "$R" "$D" "$R" -printf ' %sUpdate%s %ssudo ./update.sh (in %s)%s\n' "$D" "$R" "$D" "$(dirname "$COMPOSE")" "$R" +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" printf ' %s──────────────────────────────────────────────────────────%s\n' "$D" "$R" printf '%s\n' "" diff --git a/lang/de/settings.php b/lang/de/settings.php index 503d1a2..a54c7fa 100644 --- a/lang/de/settings.php +++ b/lang/de/settings.php @@ -41,7 +41,7 @@ return [ 'twofa_remove_totp' => 'Authenticator entfernen', // Account recovery (last-resort) note — admin-only, post-login - 'recovery_note' => 'Konto-Wiederherstellung (letzter Ausweg): per SSH auf den Host einloggen und `clusev:reset-admin` ausführen.', + 'recovery_note' => 'Konto-Wiederherstellung (letzter Ausweg): per SSH auf den Host einloggen und `clusev reset-admin` ausführen.', // Notifications 'profile_saved' => 'Profil gespeichert.', diff --git a/lang/de/system.php b/lang/de/system.php index 5d80672..16be80e 100644 --- a/lang/de/system.php +++ b/lang/de/system.php @@ -31,7 +31,7 @@ return [ 'domain_invalid' => 'Bitte eine gültige Domain eingeben (z. B. panel.example.com) — ohne http://, Port oder Pfad.', 'access_address' => 'Zugriffsadresse', 'access_bare_ip' => 'HTTP (Bare-IP)', - 'ssh_reset_hint' => 'Komplett ausgesperrt? Per SSH auf den Host einloggen und `clusev:reset-admin` ausführen, um den Zugang zurückzusetzen.', + 'ssh_reset_hint' => 'Komplett ausgesperrt? Per SSH auf den Host einloggen und `clusev reset-admin` ausführen, um den Zugang zurückzusetzen.', // Change-domain confirmation modal 'change_domain_heading' => 'Domain ändern', @@ -45,7 +45,7 @@ return [ 'restart_body' => 'Die Änderung ist gespeichert. Starte den Stack neu, damit sie übernommen wird.', 'restart_now' => 'Jetzt neu starten', 'restart_running' => 'Neustart wird ausgeführt …', - 'restart_write_failed' => 'Neustart konnte nicht angestoßen werden (Sentinel nicht schreibbar). Bitte einmalig per SSH „sudo ./update.sh" aktualisieren.', + 'restart_write_failed' => 'Neustart konnte nicht angestoßen werden (Sentinel nicht schreibbar). Bitte einmalig per SSH „sudo clusev update" aktualisieren.', 'restart_watcher_hint' => 'Falls nach ~30 s nichts passiert, ist der Host-Watcher evtl. nicht installiert — siehe Doku (docker/restart-sentinel).', 'restart_lockout_hint' => 'Nach dem Neustart eventuell neu anmelden. Falls die neue Domain (DNS/Zertifikat) noch nicht erreichbar ist, bleibt das Panel über http:// als Rückfallweg erreichbar.', diff --git a/lang/de/versions.php b/lang/de/versions.php index 7020fd7..b43ee77 100644 --- a/lang/de/versions.php +++ b/lang/de/versions.php @@ -44,7 +44,7 @@ return [ // Update panel 'update_title' => 'Aktualisierung', - 'update_hint' => 'Updates kommen über getaggte Releases im Deploy — neues Image ziehen und Migrationen anwenden:', + 'update_hint' => 'Updates kommen über getaggte Releases. Auf dem Host genügt ein Befehl — er baut das Image neu und wendet Migrationen an:', // Dashboard-triggered update (one-click; a host watcher runs update.sh) 'update_now' => 'Jetzt aktualisieren', @@ -52,7 +52,7 @@ return [ 'update_running' => 'Update läuft — Stack wird neu gebaut. Bitte gleich neu laden.', 'update_not_available' => 'Kein Update verfügbar — die installierte Version ist bereits aktuell.', 'update_throttled' => 'Zu viele Update-Anfragen — bitte in :seconds Sekunden erneut versuchen.', - 'update_write_failed' => 'Update konnte nicht angestoßen werden (Sentinel nicht schreibbar). Bitte einmalig per SSH „sudo ./update.sh" aktualisieren.', + 'update_write_failed' => 'Update konnte nicht angestoßen werden (Sentinel nicht schreibbar). Bitte einmalig per SSH „sudo clusev update" aktualisieren.', 'update_done' => 'Update abgeschlossen — jetzt auf v:version.', 'update_stalled' => 'Update dauert ungewöhnlich lange — Logs prüfen (journalctl -u clusev-update.service) oder Seite neu laden.', diff --git a/lang/en/settings.php b/lang/en/settings.php index f46e2ce..871eb4d 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -41,7 +41,7 @@ return [ 'twofa_remove_totp' => 'Remove authenticator', // Account recovery (last-resort) note — admin-only, post-login - 'recovery_note' => 'Account recovery (last resort): sign in to the host over SSH and run `clusev:reset-admin`.', + 'recovery_note' => 'Account recovery (last resort): sign in to the host over SSH and run `clusev reset-admin`.', // Notifications 'profile_saved' => 'Profile saved.', diff --git a/lang/en/system.php b/lang/en/system.php index a2712e4..d7ff095 100644 --- a/lang/en/system.php +++ b/lang/en/system.php @@ -31,7 +31,7 @@ return [ 'domain_invalid' => 'Please enter a valid domain (e.g. panel.example.com) — without http://, port or path.', 'access_address' => 'Access address', 'access_bare_ip' => 'HTTP (bare IP)', - 'ssh_reset_hint' => 'Fully locked out? SSH into the host and run `clusev:reset-admin` to reset dashboard access.', + 'ssh_reset_hint' => 'Fully locked out? SSH into the host and run `clusev reset-admin` to reset dashboard access.', // Change-domain confirmation modal 'change_domain_heading' => 'Change domain', @@ -45,7 +45,7 @@ return [ 'restart_body' => 'The change is saved. Restart the stack to apply it.', 'restart_now' => 'Restart now', 'restart_running' => 'Restart is running …', - 'restart_write_failed' => 'Could not start the restart (sentinel not writable). Please update once via SSH with "sudo ./update.sh".', + 'restart_write_failed' => 'Could not start the restart (sentinel not writable). Please update once via SSH with "sudo clusev update".', 'restart_watcher_hint' => 'If nothing happens after ~30 s, the host watcher may not be installed — see the docs (docker/restart-sentinel).', 'restart_lockout_hint' => 'You may need to sign in again after the restart. If the new domain (DNS/certificate) is not reachable yet, the panel stays available at http:// as a fallback.', diff --git a/lang/en/versions.php b/lang/en/versions.php index ae36ba3..654a61c 100644 --- a/lang/en/versions.php +++ b/lang/en/versions.php @@ -44,7 +44,7 @@ return [ // Update panel 'update_title' => 'Update', - 'update_hint' => 'Updates arrive via tagged releases in the deploy — pull the new image and apply migrations:', + 'update_hint' => 'Updates arrive via tagged releases. On the host a single command rebuilds the image and applies migrations:', // Dashboard-triggered update (one-click; a host watcher runs update.sh) 'update_now' => 'Update now', @@ -52,7 +52,7 @@ return [ 'update_running' => 'Update in progress — the stack is rebuilding. Reload shortly.', 'update_not_available' => 'No update available — the installed version is already current.', 'update_throttled' => 'Too many update requests — try again in :seconds seconds.', - 'update_write_failed' => 'Could not start the update (sentinel not writable). Please update once via SSH with "sudo ./update.sh".', + 'update_write_failed' => 'Could not start the update (sentinel not writable). Please update once via SSH with "sudo clusev update".', 'update_done' => 'Update complete — now on v:version.', 'update_stalled' => 'Update is taking unusually long — check the logs (journalctl -u clusev-update.service) or reload.', diff --git a/resources/views/livewire/help/content/de/domain-tls.blade.php b/resources/views/livewire/help/content/de/domain-tls.blade.php index e1daea4..69747d4 100644 --- a/resources/views/livewire/help/content/de/domain-tls.blade.php +++ b/resources/views/livewire/help/content/de/domain-tls.blade.php @@ -43,7 +43,7 @@ WebSocket: aktivieren (für Realtime: /app/* und /apps/*)
TRUSTED_PROXY_CIDR=<Proxy-IP>/32
 
 # danach einmal anwenden:
-sudo ./update.sh
+sudo clusev update

4. Firewall: sichere den HTTP-Port (80) des Clusev-Servers so ab, dass er nur vom Proxy erreichbar ist.

diff --git a/resources/views/livewire/help/content/en/domain-tls.blade.php b/resources/views/livewire/help/content/en/domain-tls.blade.php index fef45f7..b7b6aec 100644 --- a/resources/views/livewire/help/content/en/domain-tls.blade.php +++ b/resources/views/livewire/help/content/en/domain-tls.blade.php @@ -43,7 +43,7 @@ WebSocket: enable (for realtime: /app/* and /apps/*)
TRUSTED_PROXY_CIDR=<proxy-ip>/32
 
 # then apply once:
-sudo ./update.sh
+sudo clusev update

4. Firewall: secure the Clusev server's HTTP port (80) so it is reachable only from the proxy.

diff --git a/resources/views/livewire/versions/index.blade.php b/resources/views/livewire/versions/index.blade.php index 1e3fd74..772b808 100644 --- a/resources/views/livewire/versions/index.blade.php +++ b/resources/views/livewire/versions/index.blade.php @@ -159,9 +159,7 @@

{{ __('versions.update_hint') }}

-
docker compose -f docker-compose.prod.yml pull
-docker compose -f docker-compose.prod.yml up -d
-artisan migrate --force
+
sudo clusev update
diff --git a/tests/Feature/CommandShortcutsTest.php b/tests/Feature/CommandShortcutsTest.php new file mode 100644 index 0000000..798531d --- /dev/null +++ b/tests/Feature/CommandShortcutsTest.php @@ -0,0 +1,77 @@ +assertNotFalse($contents, "Could not read {$rel}"); + $this->assertStringNotContainsString('docker-compose.prod.yml', $contents, "{$rel} still shows docker-compose.prod.yml"); + } + } + + public function test_operator_update_instructions_use_the_short_command(): void + { + foreach (self::UPDATE_COMMAND_SURFACES as $rel) { + $contents = file_get_contents(base_path($rel)); + $this->assertNotFalse($contents, "Could not read {$rel}"); + $this->assertStringNotContainsString('sudo ./update.sh', $contents, "{$rel} still shows the long update command"); + } + } + + 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); + } +} diff --git a/tests/Feature/ForgotPasswordSmtpAwareTest.php b/tests/Feature/ForgotPasswordSmtpAwareTest.php index 0be5b6b..9a0326f 100644 --- a/tests/Feature/ForgotPasswordSmtpAwareTest.php +++ b/tests/Feature/ForgotPasswordSmtpAwareTest.php @@ -136,12 +136,12 @@ class ForgotPasswordSmtpAwareTest extends TestCase config(['mail.default' => 'log']); Livewire::test(ForgotPassword::class) - ->assertDontSee('clusev:reset-admin'); + ->assertDontSee('clusev reset-admin'); config(['mail.default' => 'smtp']); Livewire::test(ForgotPassword::class) - ->assertDontSee('clusev:reset-admin'); + ->assertDontSee('clusev reset-admin'); } public function test_settings_security_shows_the_recovery_note_with_the_command(): void @@ -150,6 +150,6 @@ class ForgotPasswordSmtpAwareTest extends TestCase Livewire::actingAs($user)->test(Security::class) ->assertSee(__('settings.recovery_note')) - ->assertSee('clusev:reset-admin'); + ->assertSee('clusev reset-admin'); } }