diff --git a/app/Exceptions/DockerNotInstalled.php b/app/Exceptions/DockerNotInstalled.php new file mode 100644 index 0000000..457d4a3 --- /dev/null +++ b/app/Exceptions/DockerNotInstalled.php @@ -0,0 +1,13 @@ +available($server)) { - $this->notInstalled = true; - } else { - $this->containers = $docker->containers($server); - $this->connected = true; - } + // ONE SSH round-trip: containers() derives the "not installed" state from the docker ps + // call itself (a missing binary throws DockerNotInstalled), so no separate available() probe. + $this->containers = $docker->containers($server); + $this->connected = true; + } catch (DockerNotInstalled) { + $this->notInstalled = true; } catch (Throwable $e) { $this->connected = false; $this->error = mb_scrub(mb_strcut($e->getMessage(), 0, 300), 'UTF-8'); diff --git a/app/Livewire/Servers/ServerDocker.php b/app/Livewire/Servers/ServerDocker.php index 9530e3b..720fff4 100644 --- a/app/Livewire/Servers/ServerDocker.php +++ b/app/Livewire/Servers/ServerDocker.php @@ -2,6 +2,7 @@ namespace App\Livewire\Servers; +use App\Exceptions\DockerNotInstalled; use App\Models\AuditEvent; use App\Models\Server; use App\Services\DockerService; @@ -43,12 +44,12 @@ class ServerDocker extends Component // hydration, so check the relation directly here. if ($this->server->credential()->exists()) { try { - if (! $docker->available($this->server)) { - $this->notInstalled = true; - } else { - $this->containers = $docker->containers($this->server); - $this->connected = true; - } + // ONE SSH round-trip: a missing docker binary throws DockerNotInstalled (derived from + // the docker ps call), so no separate available() probe is needed. + $this->containers = $docker->containers($this->server); + $this->connected = true; + } catch (DockerNotInstalled) { + $this->notInstalled = true; } catch (Throwable $e) { $this->connected = false; $this->error = mb_scrub(mb_strcut($e->getMessage(), 0, 300), 'UTF-8'); diff --git a/app/Services/DockerService.php b/app/Services/DockerService.php index 7bcbee0..94eac85 100644 --- a/app/Services/DockerService.php +++ b/app/Services/DockerService.php @@ -2,6 +2,7 @@ namespace App\Services; +use App\Exceptions\DockerNotInstalled; use App\Models\Server; use InvalidArgumentException; use RuntimeException; @@ -46,9 +47,16 @@ class DockerService $res = $this->fleet->runPrivileged($server, self::PATH_PREFIX."docker ps -a --format '{$fmt}'"); if (! $res['ok']) { + $err = $this->firstLine($res['output']); + // Distinguish "docker binary absent" (a native server, no runtime) from a real fault + // right here, so callers get the honest "not installed" state from THIS single `docker + // ps` call instead of paying a second SSH round-trip on a separate available() probe. + if (preg_match('/docker:\s.*not found/i', $err)) { + throw new DockerNotInstalled($err); + } // Surface the REAL reason (daemon down / permission / rootless socket) rather than a // misleading empty list — the component shows this instead of "no containers". - throw new RuntimeException($this->firstLine($res['output']) ?: 'docker ps failed'); + throw new RuntimeException($err ?: 'docker ps failed'); } $out = []; diff --git a/lang/de/docker.php b/lang/de/docker.php index 62cb89e..b7236b9 100644 --- a/lang/de/docker.php +++ b/lang/de/docker.php @@ -8,9 +8,7 @@ return [ 'disconnected' => 'Getrennt', 'host_subtitle' => 'Container auf dem Clusev-Host. Container einzelner Server findest du auf deren Detailseite im Reiter „Docker".', - 'target_label' => 'Ziel', 'host_target' => 'Clusev-Host', - 'server_target' => 'Server', 'host_unconfigured_title' => 'Host-SSH nicht eingerichtet', 'host_unconfigured_hint' => 'Richte den Host-SSH-Zugang unter Terminal (Kachel „Clusev-Host“) ein, um die Container dieses Hosts zu sehen.', diff --git a/lang/de/terminal.php b/lang/de/terminal.php index 3b8bf52..bd3aeea 100644 --- a/lang/de/terminal.php +++ b/lang/de/terminal.php @@ -3,8 +3,6 @@ return [ 'title' => 'Terminal — Clusev', 'eyebrow' => 'Host', - 'heading' => 'Terminal', - 'subtitle' => 'Interaktive Shell — pro Server per SSH, oder die Clusev-Umgebung selbst.', 'host_heading' => 'Host-Terminal', 'host_page_subtitle' => 'Echte SSH-Shell auf der Maschine, auf der Clusev läuft. Server-Shells findest du auf der jeweiligen Server-Detailseite im Reiter „Terminal".', 'connect' => 'Verbinden', @@ -12,18 +10,9 @@ return [ 'no_permission_title' => 'Kein Zugriff', 'no_permission_hint' => 'Für das Terminal dieses Servers fehlt dir die Berechtigung.', - 'targets_title' => 'Ziele', - 'targets_subtitle' => 'Wähle eine Sitzung', 'host_label' => 'Clusev-Host', 'host_not_configured' => 'Nicht eingerichtet', 'host_configure' => 'Host-Login einrichten', - 'servers_heading' => 'Server', - 'search_placeholder' => 'Server suchen…', - 'search_clear' => 'Suche zurücksetzen', - 'no_match' => 'Keine Treffer.', - 'no_servers' => 'Noch keine Server angelegt.', - 'no_target' => 'Kein Ziel gewählt', - 'pick_target' => 'Ziel links wählen, um eine Sitzung zu starten.', 'status_idle' => 'Bereit', 'status_connecting' => 'Verbinde…', diff --git a/lang/en/docker.php b/lang/en/docker.php index 59e9e31..008a905 100644 --- a/lang/en/docker.php +++ b/lang/en/docker.php @@ -8,9 +8,7 @@ return [ 'disconnected' => 'Disconnected', 'host_subtitle' => 'Containers on the Clusev host. A specific server’s containers live on its detail page under the “Docker” tab.', - 'target_label' => 'Target', 'host_target' => 'Clusev host', - 'server_target' => 'Server', 'host_unconfigured_title' => 'Host SSH not configured', 'host_unconfigured_hint' => 'Set up the host SSH login under Terminal (the “Clusev host” tile) to see this host’s containers.', diff --git a/lang/en/terminal.php b/lang/en/terminal.php index a20daeb..714e118 100644 --- a/lang/en/terminal.php +++ b/lang/en/terminal.php @@ -3,8 +3,6 @@ return [ 'title' => 'Terminal — Clusev', 'eyebrow' => 'Host', - 'heading' => 'Terminal', - 'subtitle' => 'Interactive shell — per server over SSH, or the Clusev environment itself.', 'host_heading' => 'Host terminal', 'host_page_subtitle' => 'A real SSH shell on the machine Clusev runs on. A server’s shell lives on its detail page under the “Terminal” tab.', 'connect' => 'Connect', @@ -12,18 +10,9 @@ return [ 'no_permission_title' => 'No access', 'no_permission_hint' => 'You don’t have permission to open this server’s terminal.', - 'targets_title' => 'Targets', - 'targets_subtitle' => 'Pick a session', 'host_label' => 'Clusev host', 'host_not_configured' => 'Not configured', 'host_configure' => 'Set up host login', - 'servers_heading' => 'Servers', - 'search_placeholder' => 'Search servers…', - 'search_clear' => 'Clear search', - 'no_match' => 'No matches.', - 'no_servers' => 'No servers added yet.', - 'no_target' => 'No target selected', - 'pick_target' => 'Pick a target on the left to start a session.', 'status_idle' => 'Ready', 'status_connecting' => 'Connecting…', diff --git a/resources/views/components/docker-container-row.blade.php b/resources/views/components/docker-container-row.blade.php new file mode 100644 index 0000000..60aef84 --- /dev/null +++ b/resources/views/components/docker-container-row.blade.php @@ -0,0 +1,34 @@ +@props(['container', 'canOperate' => true]) +@php + // container state -> status-pill status (single source of truth for both the host Docker page + // and the per-server Docker tab). + $c = $container; + $pill = match (true) { + $c['state'] === 'running' => 'online', + $c['state'] === 'paused' => 'warning', + in_array($c['state'], ['created', 'restarting'], true) => 'pending', + default => 'offline', // exited / dead / removing + }; +@endphp +{{-- wire:click targets (viewLogs / action) resolve to the enclosing Livewire component — both + Docker\Index and Servers\ServerDocker expose the same method names, so the row works in either. --}} +
{{ $c['name'] }}
+{{ $c['image'] }}
+{{ $c['name'] }}
-{{ $c['image'] }}
-{{ $c['name'] }}
-{{ $c['image'] }}
-