feat(server): move management toggle to connection header and show connection first

Replace the dev-only management ownership section with an Enable/Disable
management button in the Connection section header, and place the
Connection section above the Server overview.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2026-09-28 14:56:42 +02:00
co-authored by Claude Opus 5.5
parent 2e67b70eec
commit 15909fa641
2 changed files with 111 additions and 142 deletions
+104 -137
View File
@@ -63,143 +63,6 @@
<x-unsaved-bar action="submit"
targets="name,description,ip,user,port,connectionTimeout,serverTimezone,wildcardDomain" />
<x-application.settings-section id="server-overview-section" title="Server overview"
helper="Connection health, provider state, operating system, and hardware details.">
<x-slot:actions>
@if ($provider)
<x-status-badge :label="$provider . ($providerStatus ? ' · ' . ucfirst($providerStatus) : '')"
:type="$providerStatusType" />
@if ($provider === 'Hetzner')
<x-forms.button type="button" class="size-8! px-0!"
wire:click.prevent="checkHetznerServerStatus(true)"
title="Refresh provider status">
<x-reicon name="refresh" class="size-3.5" />
</x-forms.button>
@elseif ($provider === 'DigitalOcean')
<x-forms.button type="button" class="size-8! px-0!"
wire:click.prevent="checkDigitalOceanDropletStatus(true)"
title="Refresh provider status">
<x-reicon name="refresh" class="size-3.5" />
</x-forms.button>
@elseif ($provider === 'Vultr')
<x-forms.button type="button" class="size-8! px-0!"
wire:click.prevent="checkVultrInstanceStatus(true)"
title="Refresh provider status">
<x-reicon name="refresh" class="size-3.5" />
</x-forms.button>
@endif
@if ($server->cloudProviderToken)
@if ($provider === 'Hetzner' && !$server->isFunctional() && $hetznerServerStatus === 'off')
<x-forms.button type="button" wire:click.prevent="startHetznerServer" isHighlighted
canGate="update" :canResource="$server">
Power On
</x-forms.button>
@elseif ($provider === 'DigitalOcean' && $digitalOceanDropletStatus === 'off')
<x-forms.button type="button" wire:click.prevent="startDigitalOceanDroplet"
isHighlighted canGate="update" :canResource="$server">
Power On
</x-forms.button>
@elseif ($provider === 'Vultr' && $vultrInstanceStatus === 'stopped')
<x-forms.button type="button" wire:click.prevent="startVultrInstance" isHighlighted
canGate="update" :canResource="$server">
Power On
</x-forms.button>
@endif
@endif
@endif
@if ($server->server_metadata)
<x-forms.button type="button" class="size-8! px-0!"
wire:click="refreshServerMetadata" title="Refresh server details">
<x-reicon name="refresh" class="size-3.5" />
</x-forms.button>
@endif
@if ($server->isTransferredAway())
<x-status-badge label="Transferred away" type="warning" />
@else
<x-status-badge :label="$server->isFunctional() ? 'Ready' : 'Validation required'"
:type="$server->isFunctional() ? 'success' : 'warning'" />
@endif
</x-slot:actions>
<div class="flex items-start gap-3">
<div
class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-neutral-100 text-neutral-600 dark:bg-white/[0.06] dark:text-fg-dim">
<x-reicon name="servers" class="size-4.5" />
</div>
<div class="min-w-0">
<p class="truncate text-sm font-medium text-neutral-950 dark:text-fg">
{{ $server->name }}
</p>
<p class="mt-1 text-xs leading-5 text-neutral-500 dark:text-fg-dim">
@if ($server->isTransferredAway())
This server was migrated away from this Coolify instance and cannot be managed here.
@elseif ($server->isFunctional())
The server is reachable, validated, and ready to host resources.
@else
Validate the SSH connection before using this server.
@endif
</p>
</div>
</div>
@if ($server->server_metadata)
@include('livewire.server.partials.server-details', ['server' => $server])
@else
<div class="mt-4 border-t border-neutral-200 pt-4 dark:border-white/[0.08]">
<x-forms.button type="button" wire:click="refreshServerMetadata">
<x-reicon name="refresh" class="size-3.5" />
Fetch server details
</x-forms.button>
</div>
@endif
</x-application.settings-section>
@if (isDev())
<x-application.settings-section id="server-management-ownership-section"
title="Management ownership (Dev)"
helper="Choose whether this Coolify instance can control this server.">
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div class="min-w-0">
<div class="text-sm font-medium text-neutral-950 dark:text-fg">
Manage this server from this instance
</div>
<p class="mt-1 max-w-2xl text-xs leading-5 text-neutral-500 dark:text-fg-dim">
@if ($server->isTransferredAway())
Automations are disabled here. Existing workloads continue to run on the server.
@else
This instance runs deployments, webhooks, scheduled tasks, backups, proxy updates,
and monitoring for this server.
@endif
</p>
</div>
<button type="button" role="switch"
aria-checked="{{ $server->isTransferredAway() ? 'false' : 'true' }}"
wire:click="toggleManagement"
wire:loading.attr="disabled"
wire:target="toggleManagement"
wire:confirm="{{ $server->isTransferredAway()
? 'Take ownership of this server? Confirm that no other Coolify instance manages it.'
: 'Stop managing this server from this instance? Deployments, webhooks, scheduled tasks, backups, proxy updates, and monitoring will be disabled here.' }}"
@cannot('update', $server) disabled @endcannot
class="relative inline-flex h-7 w-12 shrink-0 items-center rounded-full border transition-colors disabled:cursor-not-allowed disabled:opacity-50 {{ $server->isTransferredAway()
? 'border-neutral-300 bg-neutral-200 dark:border-white/[0.14] dark:bg-white/[0.08]'
: 'border-coollabs bg-coollabs dark:border-warning dark:bg-warning' }}">
<span class="sr-only">Manage this server from this instance</span>
<span aria-hidden="true"
class="inline-block size-5 rounded-full bg-white shadow-sm transition-transform dark:bg-black {{ $server->isTransferredAway() ? 'translate-x-1' : 'translate-x-6' }}"></span>
</button>
</div>
@if ($server->isTransferredAway())
<x-callout type="warning" title="Management is disabled on this instance" class="mt-4">
Before you enable management, disable this server on every other Coolify instance. Two
active instances can cause conflicting deployments, proxy changes, backups, and restarts.
</x-callout>
@endif
</x-application.settings-section>
@endif
<x-application.settings-section id="server-connection-section" title="Connection"
helper="Configure how Coolify identifies, reaches, and validates this server.">
<x-slot:actions>
@@ -260,6 +123,25 @@
{{ $server->isFunctional() ? 'Revalidate connection' : 'Validate connection' }}
</x-forms.button>
</x-process-dialog>
@if (isDev())
<x-forms.button type="button" wire:click="toggleManagement"
wire:confirm="{{ $server->isTransferredAway()
? 'Enable management? Confirm that no other Coolify instance manages this server. Two active instances can cause conflicting deployments, proxy changes, backups, and restarts.'
: 'Disable management? Deployments, webhooks, scheduled tasks, backups, proxy updates, and monitoring will stop on this instance. Existing workloads continue to run.' }}"
title="{{ $server->isTransferredAway()
? 'Let this instance run deployments, webhooks, scheduled tasks, backups, proxy updates, and monitoring for this server'
: 'Stop deployments, webhooks, scheduled tasks, backups, proxy updates, and monitoring for this server on this instance' }}"
canGate="update" :canResource="$server">
<x-reicon name="{{ $server->isTransferredAway() ? 'play-circle' : 'stop-circle' }}" class="size-3.5" />
{{ $server->isTransferredAway() ? 'Enable management' : 'Disable management' }}
</x-forms.button>
@endif
@if ($server->isTransferredAway())
<x-status-badge label="Transferred away" type="warning" />
@else
<x-status-badge :label="$server->isFunctional() ? 'Ready' : 'Validation required'"
:type="$server->isFunctional() ? 'success' : 'warning'" />
@endif
</x-slot:actions>
@if ($server->isTransferredAway())
@@ -335,6 +217,91 @@
@endif
</x-application.settings-section>
<x-application.settings-section id="server-overview-section" title="Server overview"
helper="Provider state, operating system, and hardware details.">
<x-slot:actions>
@if ($provider)
<x-status-badge :label="$provider . ($providerStatus ? ' · ' . ucfirst($providerStatus) : '')"
:type="$providerStatusType" />
@if ($provider === 'Hetzner')
<x-forms.button type="button" class="size-8! px-0!"
wire:click.prevent="checkHetznerServerStatus(true)"
title="Refresh provider status">
<x-reicon name="refresh" class="size-3.5" />
</x-forms.button>
@elseif ($provider === 'DigitalOcean')
<x-forms.button type="button" class="size-8! px-0!"
wire:click.prevent="checkDigitalOceanDropletStatus(true)"
title="Refresh provider status">
<x-reicon name="refresh" class="size-3.5" />
</x-forms.button>
@elseif ($provider === 'Vultr')
<x-forms.button type="button" class="size-8! px-0!"
wire:click.prevent="checkVultrInstanceStatus(true)"
title="Refresh provider status">
<x-reicon name="refresh" class="size-3.5" />
</x-forms.button>
@endif
@if ($server->cloudProviderToken)
@if ($provider === 'Hetzner' && !$server->isFunctional() && $hetznerServerStatus === 'off')
<x-forms.button type="button" wire:click.prevent="startHetznerServer" isHighlighted
canGate="update" :canResource="$server">
Power On
</x-forms.button>
@elseif ($provider === 'DigitalOcean' && $digitalOceanDropletStatus === 'off')
<x-forms.button type="button" wire:click.prevent="startDigitalOceanDroplet"
isHighlighted canGate="update" :canResource="$server">
Power On
</x-forms.button>
@elseif ($provider === 'Vultr' && $vultrInstanceStatus === 'stopped')
<x-forms.button type="button" wire:click.prevent="startVultrInstance" isHighlighted
canGate="update" :canResource="$server">
Power On
</x-forms.button>
@endif
@endif
@endif
@if ($server->server_metadata)
<x-forms.button type="button" class="size-8! px-0!"
wire:click="refreshServerMetadata" title="Refresh server details">
<x-reicon name="refresh" class="size-3.5" />
</x-forms.button>
@endif
</x-slot:actions>
<div class="flex items-start gap-3">
<div
class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-neutral-100 text-neutral-600 dark:bg-white/[0.06] dark:text-fg-dim">
<x-reicon name="servers" class="size-4.5" />
</div>
<div class="min-w-0">
<p class="truncate text-sm font-medium text-neutral-950 dark:text-fg">
{{ $server->name }}
</p>
<p class="mt-1 text-xs leading-5 text-neutral-500 dark:text-fg-dim">
@if ($server->isTransferredAway())
This server was migrated away from this Coolify instance and cannot be managed here.
@elseif ($server->isFunctional())
The server is reachable, validated, and ready to host resources.
@else
Validate the SSH connection before using this server.
@endif
</p>
</div>
</div>
@if ($server->server_metadata)
@include('livewire.server.partials.server-details', ['server' => $server])
@else
<div class="mt-4 border-t border-neutral-200 pt-4 dark:border-white/[0.08]">
<x-forms.button type="button" wire:click="refreshServerMetadata">
<x-reicon name="refresh" class="size-3.5" />
Fetch server details
</x-forms.button>
</div>
@endif
</x-application.settings-section>
@if ($server->validation_logs)
<x-application.settings-section title="Previous validation output"
helper="The latest output produced while checking this server.">
@@ -144,18 +144,20 @@ test('team members cannot change server management ownership', function () {
expect($this->server->fresh()->isTransferredAway())->toBeFalse();
});
test('server overview explains the management ownership switch', function () {
test('server overview shows a management button that matches the ownership state', function () {
Livewire::test(Show::class, ['server_uuid' => $this->server->uuid])
->assertSee('Manage this server from this instance')
->assertSee('This instance runs deployments, webhooks, scheduled tasks, backups, proxy updates,')
->assertSee('and monitoring for this server.');
->assertSee('Disable management')
->assertDontSee('Enable management')
->call('toggleManagement')
->assertSee('Enable management')
->assertDontSee('Disable management');
});
test('management ownership is not available outside development', function () {
config(['app.env' => 'production']);
Livewire::test(Show::class, ['server_uuid' => $this->server->uuid])
->assertDontSee('Manage this server from this instance')
->assertDontSee('Disable management')
->call('toggleManagement')
->assertNotFound();