refactor(terminal): host login targets the local machine only — drop the HOST/IP field

The "Clusev host" terminal is for the LOCAL machine Clusev runs on. Letting the operator
point it at an arbitrary host/server makes no sense (that is what fleet servers are for)
and the pre-filled host.docker.internal read as a confusing mandatory magic value.

- the host is now FIXED to host.docker.internal server-side (HostShell::HOST), not a form
  field; the modal asks only for the login — user + password|key, and the port.
- the rail tile sub-label shows just the login user (the host is implied/local).

Browser-verified: modal has no HOST/IP field (user/port/auth/password only); saving shows
the user on the tile; opening the host reaches the real host sshd (a wrong password yields
an SSH auth error, proving the connection), zero console errors. 16 terminal tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat/v1-foundation
boban 2026-06-25 20:00:04 +02:00
parent 0ec9f3664a
commit e1a886671b
6 changed files with 15 additions and 27 deletions

View File

@ -10,14 +10,16 @@ use Illuminate\Validation\ValidationException;
use LivewireUI\Modal\ModalComponent;
/**
* Form modal: configure the SSH login for the "Clusev host" terminal. Stores a single
* HostCredential (encrypted) so the terminal sidecar can SSH into the actual host instead of
* opening a shell in its own container. On edit, leaving the secret blank keeps the stored one.
* The clear-text secret is never echoed back into the form.
* Form modal: configure the SSH login for the "Clusev host" terminal. The target is ALWAYS the local
* machine Clusev runs on (the Docker host, reached at host.docker.internal fixed, not a free-form
* host, because an arbitrary machine is what fleet servers are for). The operator only supplies the
* login (user + password/key, and the port). Stores a single encrypted HostCredential; on edit a
* blank secret keeps the stored one, and the clear-text secret is never echoed back into the form.
*/
class HostShell extends ModalComponent
{
public string $host = 'host.docker.internal';
/** The host terminal always targets the local machine; not operator-editable. */
private const HOST = 'host.docker.internal';
public int $port = 22;
@ -42,7 +44,6 @@ class HostShell extends ModalComponent
$hc = HostCredential::current();
if ($hc !== null) {
$this->configured = true;
$this->host = $hc->host;
$this->port = $hc->port;
$this->username = $hc->username;
$this->authType = $hc->auth_type;
@ -56,7 +57,6 @@ class HostShell extends ModalComponent
protected function rules(): array
{
return [
'host' => ['required', 'string', 'max:255'],
'port' => ['required', 'integer', 'min:1', 'max:65535'],
'username' => ['required', 'string', 'max:120'],
'authType' => ['required', Rule::in(['password', 'key'])],
@ -73,7 +73,6 @@ class HostShell extends ModalComponent
protected function validationAttributes(): array
{
return [
'host' => __('terminal.host_field_host'),
'port' => __('terminal.host_field_port'),
'username' => __('terminal.host_field_user'),
'authType' => __('terminal.host_field_auth'),
@ -95,7 +94,7 @@ class HostShell extends ModalComponent
}
$hc = $existing ?? new HostCredential;
$hc->host = trim($data['host']);
$hc->host = self::HOST; // always the local machine — not operator-editable
$hc->port = (int) $data['port'];
$hc->username = trim($data['username']);
$hc->auth_type = $data['authType'] === 'key' ? 'key' : 'password';

View File

@ -102,7 +102,8 @@ class Index extends Component
'serverCount' => $serverCount,
'showSearch' => $serverCount > self::SEARCH_THRESHOLD,
'hostConfigured' => $host !== null,
'hostTarget' => $host !== null ? $host->username.'@'.$host->host : null,
// The host is always the local machine, so the tile shows just the login user.
'hostTarget' => $host?->username,
])->title($this->title());
}
}

View File

@ -28,8 +28,6 @@ return [
// Host-Login-Einrichtung (Clusev-Host-Terminal)
'host_setup_title' => 'Clusev-Host-Login',
'host_setup_subtitle' => 'SSH-Zugang zum Clusev-Host. Damit wird das Terminal ein echtes Login auf der Maschine — kein Container.',
'host_field_host' => 'Host / IP',
'host_field_host_hint' => 'Standard: die Maschine, auf der Clusev läuft (Docker-Host). So lassen — oder eine andere IP/Host eintragen.',
'host_field_port' => 'Port',
'host_field_user' => 'Benutzer',
'host_field_auth' => 'Authentifizierung',

View File

@ -28,8 +28,6 @@ return [
// Host login setup (Clusev host terminal)
'host_setup_title' => 'Clusev host login',
'host_setup_subtitle' => 'SSH access to the Clusev host. This makes the terminal a real login on the machine — not a container.',
'host_field_host' => 'Host / IP',
'host_field_host_hint' => 'Default: the machine Clusev runs on (the Docker host). Leave it — or enter another IP/host.',
'host_field_port' => 'Port',
'host_field_user' => 'User',
'host_field_auth' => 'Authentication',

View File

@ -10,13 +10,13 @@
</div>
<div class="mt-4 space-y-3">
{{-- Target is fixed to the local machine (the Docker host) only the login is asked for. --}}
<div class="grid grid-cols-1 gap-3 sm:grid-cols-3">
<div class="sm:col-span-2">
<label class="mb-1 block font-mono text-[11px] uppercase tracking-wider text-ink-3">{{ __('terminal.host_field_host') }}</label>
<input wire:model="host" type="text" placeholder="host.docker.internal"
<label class="mb-1 block font-mono text-[11px] uppercase tracking-wider text-ink-3">{{ __('terminal.host_field_user') }}</label>
<input wire:model="username" type="text" placeholder="root"
class="h-9 w-full rounded-md border border-line bg-inset px-3 font-mono text-sm text-ink placeholder:text-ink-4 focus:border-accent/40 focus:outline-none" />
@error('host')<p class="mt-1 flex items-center gap-1.5 font-mono text-[11px] text-offline"><x-icon name="alert" class="h-3.5 w-3.5 shrink-0" />{{ $message }}</p>@enderror
<p class="mt-1.5 font-mono text-[11px] leading-relaxed text-ink-4">{{ __('terminal.host_field_host_hint') }}</p>
@error('username')<p class="mt-1 flex items-center gap-1.5 font-mono text-[11px] text-offline"><x-icon name="alert" class="h-3.5 w-3.5 shrink-0" />{{ $message }}</p>@enderror
</div>
<div>
<label class="mb-1 block font-mono text-[11px] uppercase tracking-wider text-ink-3">{{ __('terminal.host_field_port') }}</label>
@ -26,13 +26,6 @@
</div>
</div>
<div>
<label class="mb-1 block font-mono text-[11px] uppercase tracking-wider text-ink-3">{{ __('terminal.host_field_user') }}</label>
<input wire:model="username" type="text" placeholder="root"
class="h-9 w-full rounded-md border border-line bg-inset px-3 font-mono text-sm text-ink placeholder:text-ink-4 focus:border-accent/40 focus:outline-none" />
@error('username')<p class="mt-1 flex items-center gap-1.5 font-mono text-[11px] text-offline"><x-icon name="alert" class="h-3.5 w-3.5 shrink-0" />{{ $message }}</p>@enderror
</div>
<div>
<label class="mb-1 block font-mono text-[11px] uppercase tracking-wider text-ink-3">{{ __('terminal.host_field_auth') }}</label>
<select wire:model.live="authType"

View File

@ -178,7 +178,6 @@ class TerminalTest extends TestCase
public function test_host_shell_modal_saves_the_singleton(): void
{
Livewire::test(HostShell::class)
->set('host', '10.0.0.1')
->set('port', 22)
->set('username', 'root')
->set('authType', 'password')
@ -188,7 +187,7 @@ class TerminalTest extends TestCase
$hc = HostCredential::current();
$this->assertNotNull($hc);
$this->assertSame('10.0.0.1', $hc->host);
$this->assertSame('host.docker.internal', $hc->host); // fixed to the local machine, not operator-set
$this->assertSame('root', $hc->username);
$this->assertSame('hunter2pw', $hc->secret); // round-trips through the encrypted cast
}