feat: short clusev commands in versions panel, lang strings, MOTD
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>feat/v1-foundation
parent
437a22592b
commit
8d2b7087a9
|
|
@ -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' ""
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
|
|
|
|||
|
|
@ -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://<Server-IP> als Rückfallweg erreichbar.',
|
||||
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
|
|
|
|||
|
|
@ -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://<server-IP> as a fallback.',
|
||||
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ WebSocket: aktivieren (für Realtime: /app/* und /apps/*)</pre>
|
|||
<pre class="{{ $pre }}">TRUSTED_PROXY_CIDR=<Proxy-IP>/32
|
||||
|
||||
# danach einmal anwenden:
|
||||
sudo ./update.sh</pre>
|
||||
sudo clusev update</pre>
|
||||
|
||||
<p class="{{ $p }}"><span class="text-ink">4. Firewall:</span> sichere den HTTP-Port (80) des Clusev-Servers so ab, dass er <span class="text-ink">nur vom Proxy</span> erreichbar ist.</p>
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ WebSocket: enable (for realtime: /app/* and /apps/*)</pre>
|
|||
<pre class="{{ $pre }}">TRUSTED_PROXY_CIDR=<proxy-ip>/32
|
||||
|
||||
# then apply once:
|
||||
sudo ./update.sh</pre>
|
||||
sudo clusev update</pre>
|
||||
|
||||
<p class="{{ $p }}"><span class="text-ink">4. Firewall:</span> secure the Clusev server's HTTP port (80) so it is reachable <span class="text-ink">only from the proxy</span>.</p>
|
||||
|
||||
|
|
|
|||
|
|
@ -159,9 +159,7 @@
|
|||
<x-panel :title="__('versions.update_title')" :padded="false">
|
||||
<div class="space-y-3 px-4 py-4 sm:px-5">
|
||||
<p class="font-mono text-[11px] leading-relaxed text-ink-3">{{ __('versions.update_hint') }}</p>
|
||||
<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">docker compose -f docker-compose.prod.yml pull
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
artisan migrate --force</pre>
|
||||
<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>
|
||||
</div>
|
||||
</x-panel>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
<?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',
|
||||
'resources/views/livewire/help/content/de/domain-tls.blade.php',
|
||||
'resources/views/livewire/help/content/en/domain-tls.blade.php',
|
||||
'docker/motd/00-clusev',
|
||||
];
|
||||
|
||||
/** Operator-facing files that must use the short `sudo clusev update`, never `sudo ./update.sh`. */
|
||||
private const UPDATE_COMMAND_SURFACES = [
|
||||
'resources/views/livewire/help/content/de/domain-tls.blade.php',
|
||||
'resources/views/livewire/help/content/en/domain-tls.blade.php',
|
||||
'resources/views/livewire/help/content/de/updates.blade.php',
|
||||
'resources/views/livewire/help/content/en/updates.blade.php',
|
||||
'lang/de/system.php',
|
||||
'lang/en/system.php',
|
||||
'lang/de/versions.php',
|
||||
'lang/en/versions.php',
|
||||
];
|
||||
|
||||
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->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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue