diff --git a/app/Livewire/Server/Sentinel.php b/app/Livewire/Server/Sentinel.php new file mode 100644 index 000000000..20135f3c9 --- /dev/null +++ b/app/Livewire/Server/Sentinel.php @@ -0,0 +1,182 @@ +server->team_id ?? auth()->user()->currentTeam()->id; + + return [ + "echo-private:team.{$teamId},SentinelRestarted" => 'handleSentinelRestarted', + ]; + } + + public function mount(string $server_uuid) + { + try { + $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); + $this->parameters = get_route_parameters(); + $this->syncData(); + } catch (\Throwable) { + return redirect()->route('server.index'); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->authorize('update', $this->server); + $this->validate(); + $this->server->settings->is_metrics_enabled = $this->isMetricsEnabled; + $this->server->settings->sentinel_token = $this->sentinelToken; + $this->server->settings->sentinel_metrics_refresh_rate_seconds = $this->sentinelMetricsRefreshRateSeconds; + $this->server->settings->sentinel_metrics_history_days = $this->sentinelMetricsHistoryDays; + $this->server->settings->sentinel_push_interval_seconds = $this->sentinelPushIntervalSeconds; + $this->server->settings->sentinel_custom_url = $this->sentinelCustomUrl; + $this->server->settings->is_sentinel_enabled = $this->isSentinelEnabled; + $this->server->settings->is_sentinel_debug_enabled = $this->isSentinelDebugEnabled; + $this->server->settings->save(); + } else { + $this->isMetricsEnabled = $this->server->settings->is_metrics_enabled; + $this->sentinelToken = $this->server->settings->sentinel_token; + $this->sentinelMetricsRefreshRateSeconds = $this->server->settings->sentinel_metrics_refresh_rate_seconds; + $this->sentinelMetricsHistoryDays = $this->server->settings->sentinel_metrics_history_days; + $this->sentinelPushIntervalSeconds = $this->server->settings->sentinel_push_interval_seconds; + $this->sentinelCustomUrl = $this->server->settings->sentinel_custom_url; + $this->isSentinelEnabled = $this->server->settings->is_sentinel_enabled; + $this->isSentinelDebugEnabled = $this->server->settings->is_sentinel_debug_enabled; + $this->sentinelUpdatedAt = $this->server->sentinel_updated_at; + } + } + + public function handleSentinelRestarted($event) + { + if ($event['serverUuid'] === $this->server->uuid) { + $this->server->refresh(); + $this->syncData(); + $this->dispatch('success', 'Sentinel has been restarted successfully.'); + } + } + + public function restartSentinel() + { + try { + $this->authorize('manageSentinel', $this->server); + $customImage = isDev() ? $this->sentinelCustomDockerImage : null; + $this->server->restartSentinel($customImage); + $this->dispatch('info', 'Restarting Sentinel.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function updatedIsSentinelDebugEnabled($value) + { + try { + $this->submit(); + $this->restartSentinel(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function updatedIsMetricsEnabled($value) + { + try { + $this->submit(); + $this->restartSentinel(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function updatedIsSentinelEnabled($value) + { + try { + $this->authorize('manageSentinel', $this->server); + if ($value === true) { + if ($this->server->isBuildServer()) { + $this->isSentinelEnabled = false; + $this->dispatch('error', 'Sentinel cannot be enabled on build servers.'); + + return; + } + $customImage = isDev() ? $this->sentinelCustomDockerImage : null; + StartSentinel::run($this->server, true, null, $customImage); + } else { + $this->isMetricsEnabled = false; + $this->isSentinelDebugEnabled = false; + StopSentinel::dispatch($this->server); + } + $this->submit(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function regenerateSentinelToken() + { + try { + $this->authorize('manageSentinel', $this->server); + $this->server->settings->generateSentinelToken(); + $this->dispatch('success', 'Token regenerated. Restarting Sentinel.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function submit() + { + try { + $this->syncData(true); + $this->dispatch('success', 'Sentinel settings updated.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function render() + { + return view('livewire.server.sentinel'); + } +} diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php index 7a4a1c480..cc55da491 100644 --- a/app/Livewire/Server/Show.php +++ b/app/Livewire/Server/Show.php @@ -81,6 +81,8 @@ class Show extends Component public ?int $selectedHetznerTokenId = null; + public ?string $manualHetznerServerId = null; + public ?array $matchedHetznerServer = null; public ?string $hetznerSearchError = null; @@ -447,6 +449,10 @@ public function handleServerValidated($event = null) // Update validation state $this->isValidating = $this->server->is_validating ?? false; + + // Reload Hetzner tokens in case the linking section should now be shown + $this->loadHetznerTokens(); + $this->dispatch('refreshServerShow'); $this->dispatch('refreshServer'); } @@ -524,6 +530,47 @@ public function searchHetznerServer(): void } } + public function searchHetznerServerById(): void + { + $this->hetznerSearchError = null; + $this->hetznerNoMatchFound = false; + $this->matchedHetznerServer = null; + + if (! $this->selectedHetznerTokenId) { + $this->hetznerSearchError = 'Please select a Hetzner token first.'; + + return; + } + + if (! $this->manualHetznerServerId) { + $this->hetznerSearchError = 'Please enter a Hetzner Server ID.'; + + return; + } + + try { + $this->authorize('update', $this->server); + + $token = $this->availableHetznerTokens->firstWhere('id', $this->selectedHetznerTokenId); + if (! $token) { + $this->hetznerSearchError = 'Invalid token selected.'; + + return; + } + + $hetznerService = new HetznerService($token->token); + $serverData = $hetznerService->getServer((int) $this->manualHetznerServerId); + + if (! empty($serverData)) { + $this->matchedHetznerServer = $serverData; + } else { + $this->hetznerNoMatchFound = true; + } + } catch (\Throwable $e) { + $this->hetznerSearchError = 'Failed to fetch Hetzner server: '.$e->getMessage(); + } + } + public function linkToHetzner() { if (! $this->matchedHetznerServer) { @@ -564,6 +611,7 @@ public function linkToHetzner() // Clear the linking state $this->matchedHetznerServer = null; $this->selectedHetznerTokenId = null; + $this->manualHetznerServerId = null; $this->hetznerNoMatchFound = false; $this->hetznerSearchError = null; diff --git a/app/Livewire/Server/Swarm.php b/app/Livewire/Server/Swarm.php new file mode 100644 index 000000000..e3e441ea0 --- /dev/null +++ b/app/Livewire/Server/Swarm.php @@ -0,0 +1,59 @@ +server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); + $this->parameters = get_route_parameters(); + $this->syncData(); + } catch (\Throwable) { + return redirect()->route('server.index'); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->authorize('update', $this->server); + $this->server->settings->is_swarm_manager = $this->isSwarmManager; + $this->server->settings->is_swarm_worker = $this->isSwarmWorker; + $this->server->settings->save(); + } else { + $this->isSwarmManager = $this->server->settings->is_swarm_manager; + $this->isSwarmWorker = $this->server->settings->is_swarm_worker; + } + } + + public function instantSave() + { + try { + $this->syncData(true); + $this->dispatch('success', 'Swarm settings updated.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function render() + { + return view('livewire.server.swarm'); + } +} diff --git a/resources/views/components/server/sidebar.blade.php b/resources/views/components/server/sidebar.blade.php index 2697b4c0b..3f5bcafac 100644 --- a/resources/views/components/server/sidebar.blade.php +++ b/resources/views/components/server/sidebar.blade.php @@ -6,6 +6,16 @@ href="{{ route('server.advanced', ['server_uuid' => $server->uuid]) }}">Advanced @endif + @if (!$server->isBuildServer() && !$server->settings->is_cloudflare_tunnel) + Swarm + + @endif + @if ($server->isFunctional() && !$server->isSwarm() && !$server->isBuildServer()) + Sentinel + + @endif Private Key diff --git a/resources/views/livewire/server/sentinel.blade.php b/resources/views/livewire/server/sentinel.blade.php new file mode 100644 index 000000000..4016a30e4 --- /dev/null +++ b/resources/views/livewire/server/sentinel.blade.php @@ -0,0 +1,110 @@ +
+ + {{ data_get_str($server, 'name')->limit(10) }} > Sentinel | Coolify + + +
+ +
+
+
+

Sentinel

+ + @if ($server->isSentinelEnabled()) +
+ @if ($server->isSentinelLive()) + + Save + Restart + + Sentinel Logs + + + + Logs + + @else + + Save + Sync + + Sentinel Logs + + + + Logs + + @endif +
+ @endif +
+
+
+ + @if ($server->isSentinelEnabled()) + @if (isDev()) + + @endif + + @else + @if (isDev()) + + @endif + + @endif +
+ @if (isDev() && $server->isSentinelEnabled()) +
+ +
+ @endif + @if ($server->isSentinelEnabled()) +
+ + Regenerate +
+ + + +
+
+ + + +
+
+ @endif +
+
+
+
+
diff --git a/resources/views/livewire/server/show.blade.php b/resources/views/livewire/server/show.blade.php index a8344df05..f58dc058b 100644 --- a/resources/views/livewire/server/show.blade.php +++ b/resources/views/livewire/server/show.blade.php @@ -285,37 +285,6 @@ class="w-full input opacity-50 cursor-not-allowed" @endif - @if (!$server->isBuildServer() && !$server->settings->is_cloudflare_tunnel) -

Swarm (experimental) -

-
Read the docs here. -
-
- @if ($server->settings->is_swarm_worker) - - @else - - @endif - - @if ($server->settings->is_swarm_manager) - - @else - - @endif -
- @endif @endif @@ -337,6 +306,20 @@ class="w-full input opacity-50 cursor-not-allowed" @endforeach +
+ +
+ + Search by ID + Searching... + +
OR
@@ -354,10 +337,14 @@ class="w-full input opacity-50 cursor-not-allowed" @if ($hetznerNoMatchFound)

- No Hetzner server found matching IP: {{ $server->ip }} + @if ($manualHetznerServerId) + No Hetzner server found with ID: {{ $manualHetznerServerId }} + @else + No Hetzner server found matching IP: {{ $server->ip }} + @endif

- Try a different token or verify the server IP is correct. + Try a different token, enter the Server ID manually, or verify the details are correct.

@endif @@ -378,116 +365,6 @@ class="w-full input opacity-50 cursor-not-allowed" @endif @endif - @if ($server->isFunctional() && !$server->isSwarm() && !$server->isBuildServer()) -
-
-

Sentinel

- - @if ($server->isSentinelEnabled()) -
- @if ($server->isSentinelLive()) - - Save - Restart - - Sentinel Logs - - - - Logs - - @else - - Save - Sync - - Sentinel Logs - - - - Logs - - @endif -
- @endif -
-
-
- - @if ($server->isSentinelEnabled()) - @if (isDev()) - - @endif - - @else - @if (isDev()) - - @endif - - @endif -
- @if (isDev() && $server->isSentinelEnabled()) -
- -
- @endif - @if ($server->isSentinelEnabled()) -
- - Regenerate -
- - - -
-
- - - -
-
- @endif -
-
- @endif diff --git a/resources/views/livewire/server/swarm.blade.php b/resources/views/livewire/server/swarm.blade.php new file mode 100644 index 000000000..1d18e2d31 --- /dev/null +++ b/resources/views/livewire/server/swarm.blade.php @@ -0,0 +1,43 @@ +
+ + {{ data_get_str($server, 'name')->limit(10) }} > Swarm | Coolify + + +
+ +
+
+
+

Swarm (experimental)

+
+
Read the docs here. +
+
+ +
+ @if ($server->settings->is_swarm_worker) + + @else + + @endif + + @if ($server->settings->is_swarm_manager) + + @else + + @endif +
+
+
+
diff --git a/routes/web.php b/routes/web.php index 703f80ab5..2a9072299 100644 --- a/routes/web.php +++ b/routes/web.php @@ -56,7 +56,9 @@ use App\Livewire\Server\Resources as ResourcesShow; use App\Livewire\Server\Security\Patches; use App\Livewire\Server\Security\TerminalAccess; +use App\Livewire\Server\Sentinel as ServerSentinel; use App\Livewire\Server\Show as ServerShow; +use App\Livewire\Server\Swarm as ServerSwarm; use App\Livewire\Settings\Advanced as SettingsAdvanced; use App\Livewire\Settings\Index as SettingsIndex; use App\Livewire\Settings\Updates as SettingsUpdates; @@ -251,6 +253,8 @@ Route::prefix('server/{server_uuid}')->group(function () { Route::get('/', ServerShow::class)->name('server.show'); Route::get('/advanced', ServerAdvanced::class)->name('server.advanced'); + Route::get('/swarm', ServerSwarm::class)->name('server.swarm'); + Route::get('/sentinel', ServerSentinel::class)->name('server.sentinel'); Route::get('/private-key', PrivateKeyShow::class)->name('server.private-key'); Route::get('/cloud-provider-token', CloudProviderTokenShow::class)->name('server.cloud-provider-token'); Route::get('/ca-certificate', CaCertificateShow::class)->name('server.ca-certificate');