diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index cc1a44f9a..8571f8981 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -3176,8 +3176,7 @@ private function graceful_shutdown_container(string $containerName) try { $timeout = isDev() ? 1 : 30; $this->execute_remote_command( - ["docker stop -t $timeout $containerName", 'hidden' => true, 'ignore_errors' => true], - ["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true] + ["docker stop -t $timeout $containerName", 'hidden' => true, 'ignore_errors' => true] ); } catch (Exception $error) { $this->application_deployment_queue->addLogEntry("Error stopping container $containerName: ".$error->getMessage(), 'stderr'); diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index a585baa69..d44f014b4 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -669,7 +669,7 @@ private function upload_to_s3(): void $this->add_to_error_output($e->getMessage()); throw $e; } finally { - $command = "docker rm -f backup-of-{$this->backup_log_uuid}"; + $command = "docker stop backup-of-{$this->backup_log_uuid}"; instant_remote_process([$command], $this->server, true, false, null, disableMultiplexing: true); } } diff --git a/app/Livewire/Project/Application/Deployment/Show.php b/app/Livewire/Project/Application/Deployment/Show.php index b0f5df0c8..c204a49f1 100644 --- a/app/Livewire/Project/Application/Deployment/Show.php +++ b/app/Livewire/Project/Application/Deployment/Show.php @@ -105,16 +105,7 @@ public function polling() public function getLogLinesProperty() { - return decode_remote_command_output($this->application_deployment_queue)->map(function ($logLine) { - $logLine['line'] = e($logLine['line']); - $logLine['line'] = preg_replace( - '/(https?:\/\/[^\s]+)/', - '$1', - $logLine['line'], - ); - - return $logLine; - }); + return decode_remote_command_output($this->application_deployment_queue); } public function copyLogs(): string 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/config/constants.php b/config/constants.php index 807dc88e0..9907bf8e0 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.456', + 'version' => '4.0.0-beta.457', 'helper_version' => '1.0.12', 'realtime_version' => '1.0.10', 'self_hosted' => env('SELF_HOSTED', true), diff --git a/other/nightly/versions.json b/other/nightly/versions.json index 9a5570f41..23c1f467e 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.456" + "version": "4.0.0-beta.457" }, "nightly": { - "version": "4.0.0-beta.457" + "version": "4.0.0-beta.458" }, "helper": { "version": "1.0.12" 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/project/application/deployment/show.blade.php b/resources/views/livewire/project/application/deployment/show.blade.php index 6aaf3e257..2b6afe75a 100644 --- a/resources/views/livewire/project/application/deployment/show.blade.php +++ b/resources/views/livewire/project/application/deployment/show.blade.php @@ -11,13 +11,8 @@ rafId: null, showTimestamps: true, searchQuery: '', - renderTrigger: 0, + matchCount: 0, deploymentId: '{{ $application_deployment_queue->deployment_uuid ?? 'deployment' }}', - // Cache for decoded HTML to avoid repeated DOMParser calls - decodeCache: new Map(), - // Cache for match count to avoid repeated DOM queries - matchCountCache: null, - lastSearchQuery: '', makeFullscreen() { this.fullscreen = !this.fullscreen; }, @@ -31,7 +26,6 @@ if (!this.alwaysScroll) return; this.rafId = requestAnimationFrame(() => { this.scrollToBottom(); - // Schedule next scroll after a reasonable delay (250ms instead of 100ms) if (this.alwaysScroll) { setTimeout(() => this.scheduleScroll(), 250); } @@ -48,10 +42,6 @@ } } }, - matchesSearch(text) { - if (!this.searchQuery.trim()) return true; - return text.toLowerCase().includes(this.searchQuery.toLowerCase()); - }, hasActiveLogSelection() { const selection = window.getSelection(); if (!selection || selection.isCollapsed || !selection.toString().trim()) { @@ -63,86 +53,59 @@ return logsContainer.contains(range.commonAncestorContainer); }, decodeHtml(text) { - // Return cached result if available - if (this.decodeCache.has(text)) { - return this.decodeCache.get(text); - } - // Decode HTML entities with max iteration limit - let decoded = text; - let prev = ''; - let iterations = 0; - const maxIterations = 3; - - while (decoded !== prev && iterations < maxIterations) { - prev = decoded; - const doc = new DOMParser().parseFromString(decoded, 'text/html'); - decoded = doc.documentElement.textContent; - iterations++; - } - // Cache the result (limit cache size to prevent memory bloat) - if (this.decodeCache.size > 5000) { - // Clear oldest entries when cache gets too large - const firstKey = this.decodeCache.keys().next().value; - this.decodeCache.delete(firstKey); - } - this.decodeCache.set(text, decoded); - return decoded; + const doc = new DOMParser().parseFromString(text, 'text/html'); + return doc.documentElement.textContent; }, - renderHighlightedLog(el, text) { - // Skip re-render if user has text selected in logs - if (el.textContent && this.hasActiveLogSelection()) { - return; - } + highlightText(el, text, query) { + if (this.hasActiveLogSelection()) return; - const decoded = this.decodeHtml(text); el.textContent = ''; - - if (!this.searchQuery.trim()) { - el.textContent = decoded; - return; - } - - const query = this.searchQuery.toLowerCase(); - const lowerText = decoded.toLowerCase(); + const lowerText = text.toLowerCase(); let lastIndex = 0; - let index = lowerText.indexOf(query, lastIndex); + while (index !== -1) { if (index > lastIndex) { - el.appendChild(document.createTextNode(decoded.substring(lastIndex, index))); + el.appendChild(document.createTextNode(text.substring(lastIndex, index))); } const mark = document.createElement('span'); mark.className = 'log-highlight'; - mark.textContent = decoded.substring(index, index + this.searchQuery.length); + mark.textContent = text.substring(index, index + query.length); el.appendChild(mark); - - lastIndex = index + this.searchQuery.length; + lastIndex = index + query.length; index = lowerText.indexOf(query, lastIndex); } - if (lastIndex < decoded.length) { - el.appendChild(document.createTextNode(decoded.substring(lastIndex))); + if (lastIndex < text.length) { + el.appendChild(document.createTextNode(text.substring(lastIndex))); } }, - getMatchCount() { - if (!this.searchQuery.trim()) return 0; - // Return cached count if search query hasn't changed - if (this.lastSearchQuery === this.searchQuery && this.matchCountCache !== null) { - return this.matchCountCache; - } + applySearch() { const logs = document.getElementById('logs'); - if (!logs) return 0; + if (!logs) return; const lines = logs.querySelectorAll('[data-log-line]'); + const query = this.searchQuery.trim().toLowerCase(); let count = 0; - const query = this.searchQuery.toLowerCase(); + lines.forEach(line => { - if (line.dataset.logContent && line.dataset.logContent.toLowerCase().includes(query)) { - count++; + const content = (line.dataset.logContent || '').toLowerCase(); + const textSpan = line.querySelector('[data-line-text]'); + const matches = !query || content.includes(query); + + line.classList.toggle('hidden', !matches); + if (matches && query) count++; + + if (textSpan) { + const originalText = this.decodeHtml(textSpan.dataset.lineText || ''); + if (!query) { + textSpan.textContent = originalText; + } else if (matches) { + this.highlightText(textSpan, originalText, query); + } } }); - this.matchCountCache = count; - this.lastSearchQuery = this.searchQuery; - return count; + + this.matchCount = query ? count : 0; }, downloadLogs() { const logs = document.getElementById('logs'); @@ -173,35 +136,30 @@ } }, init() { - // Prevent Livewire from morphing logs container when text is selected - Livewire.hook('morph.updating', ({ el, component, toEl, skip }) => { - if (el.id === 'logs' && this.hasActiveLogSelection()) { - skip(); + // Watch search query changes + this.$watch('searchQuery', () => { + this.applySearch(); + }); + + // Apply search after Livewire updates + Livewire.hook('morph.updated', ({ el }) => { + if (el.id === 'logs') { + this.$nextTick(() => { + this.applySearch(); + if (this.alwaysScroll) { + this.scrollToBottom(); + } + }); } }); - // Re-render logs after Livewire updates (debounced) - let renderTimeout = null; - const debouncedRender = () => { - clearTimeout(renderTimeout); - renderTimeout = setTimeout(() => { - this.matchCountCache = null; // Invalidate match cache on new content - this.renderTrigger++; - }, 100); - }; - document.addEventListener('livewire:navigated', () => { - this.$nextTick(debouncedRender); - }); - Livewire.hook('commit', ({ succeed }) => { - succeed(() => { - this.$nextTick(debouncedRender); - }); - }); + // Stop auto-scroll when deployment finishes Livewire.on('deploymentFinished', () => { setTimeout(() => { this.stopScroll(); }, 500); }); + // Start auto-scroll if deployment is in progress if (this.alwaysScroll) { this.scheduleScroll(); @@ -229,7 +187,7 @@ class="flex items-center justify-between gap-2 px-4 py-2 border-b dark:border-co {{ Str::headline(data_get($application_deployment_queue, 'status')) }} @endif -
@@ -324,7 +282,7 @@ class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text- class="flex flex-col overflow-y-auto p-2 px-4 min-h-4 scrollbar" :class="fullscreen ? 'flex-1' : 'max-h-[30rem]'">
-
No matches found.
@@ -334,19 +292,19 @@ class="text-gray-500 dark:text-gray-400 py-2"> $searchableContent = $line['timestamp'] . ' ' . $lineContent; @endphp
isset($line['command']) && $line['command'], 'flex gap-2 log-line', ])> {{ $line['timestamp'] }} - $line['hidden'], - 'text-red-500' => $line['stderr'], - 'font-bold' => isset($line['command']) && $line['command'], - 'whitespace-pre-wrap', - ]) - x-effect="renderTrigger; searchQuery; renderHighlightedLog($el, $el.dataset.lineText)"> + $line['hidden'], + 'text-red-500' => $line['stderr'], + 'font-bold' => isset($line['command']) && $line['command'], + 'whitespace-pre-wrap', + ])>{{ $lineContent }}
@empty No logs yet. diff --git a/resources/views/livewire/project/shared/get-logs.blade.php b/resources/views/livewire/project/shared/get-logs.blade.php index d4a4208f5..03c049874 100644 --- a/resources/views/livewire/project/shared/get-logs.blade.php +++ b/resources/views/livewire/project/shared/get-logs.blade.php @@ -104,6 +104,10 @@ const range = selection.getRangeAt(0); return logsContainer.contains(range.commonAncestorContainer); }, + decodeHtml(text) { + const doc = new DOMParser().parseFromString(text, 'text/html'); + return doc.documentElement.textContent; + }, applySearch() { const logs = document.getElementById('logs'); if (!logs) return; @@ -121,7 +125,7 @@ // Update highlighting if (textSpan) { - const originalText = textSpan.dataset.lineText || ''; + const originalText = this.decodeHtml(textSpan.dataset.lineText || ''); if (!query) { textSpan.textContent = originalText; } else if (matches) { @@ -188,16 +192,28 @@ this.applySearch(); }); - // Apply colors after Livewire updates + // Handler for applying colors and search after DOM changes + const applyAfterUpdate = () => { + this.$nextTick(() => { + this.applyColorLogs(); + this.applySearch(); + if (this.alwaysScroll) { + this.scrollToBottom(); + } + }); + }; + + // Apply colors after Livewire updates (existing content) Livewire.hook('morph.updated', ({ el }) => { if (el.id === 'logs') { - this.$nextTick(() => { - this.applyColorLogs(); - this.applySearch(); - if (this.alwaysScroll) { - this.scrollToBottom(); - } - }); + applyAfterUpdate(); + } + }); + + // Apply colors after Livewire adds new content (initial load) + Livewire.hook('morph.added', ({ el }) => { + if (el.id === 'logs') { + applyAfterUpdate(); } }); } @@ -375,7 +391,7 @@ class="flex overflow-y-auto overflow-x-hidden flex-col px-4 py-2 w-full min-w-0 class="text-gray-500 dark:text-gray-400 py-2"> No matches found.
- @foreach ($displayLines as $line) + @foreach ($displayLines as $index => $line) @php // Parse timestamp from log line (ISO 8601 format: 2025-12-04T11:48:39.136764033Z) $timestamp = ''; @@ -392,11 +408,13 @@ class="text-gray-500 dark:text-gray-400 py-2"> $monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; $monthName = $monthNames[(int)$month - 1] ?? $month; - // Format: 2025-Dec-04 09:44:58.198879 - $timestamp = "{$year}-{$monthName}-{$day} {$time}.{$microseconds}"; + // Format for display: 2025-Dec-04 09:44:58 + $timestamp = "{$year}-{$monthName}-{$day} {$time}"; + // Include microseconds in key for uniqueness + $lineKey = "{$timestamp}.{$microseconds}"; } @endphp -
+
@if ($timestamp && $showTimeStamps) {{ $timestamp }} @endif diff --git a/resources/views/livewire/security/cloud-provider-tokens.blade.php b/resources/views/livewire/security/cloud-provider-tokens.blade.php index 32a2cd2ab..6369134a8 100644 --- a/resources/views/livewire/security/cloud-provider-tokens.blade.php +++ b/resources/views/livewire/security/cloud-provider-tokens.blade.php @@ -23,12 +23,12 @@ class="flex flex-col gap-1 p-2 border dark:border-coolgray-200 hover:no-underlin
@can('view', $savedToken) - Validate Token + Validate @endcan @can('delete', $savedToken) - +
+ +
+
+
+

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'); diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json index 5c482630b..c3e33b582 100644 --- a/templates/service-templates-latest.json +++ b/templates/service-templates-latest.json @@ -4350,7 +4350,7 @@ "umami": { "documentation": "https://umami.is?utm_source=coolify.io", "slogan": "Umami is web analytics platform which provides insights into visitor behavior without compromising user privacy.", - "compose": "c2VydmljZXM6CiAgdW1hbWk6CiAgICBpbWFnZTogJ2doY3IuaW8vdW1hbWktc29mdHdhcmUvdW1hbWk6My4wLjInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9VTUFNSV8zMDAwCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcG9zdGdyZXNxbDo1NDMyLyRQT1NUR1JFU19EQicKICAgICAgLSBEQVRBQkFTRV9UWVBFPXBvc3RncmVzCiAgICAgIC0gQVBQX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9VTUFNSQogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAvYXBpL2hlYXJ0YmVhdCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi11bWFtaX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", + "compose": "c2VydmljZXM6CiAgdW1hbWk6CiAgICBpbWFnZTogJ2doY3IuaW8vdW1hbWktc29mdHdhcmUvdW1hbWk6My4wLjMnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9VTUFNSV8zMDAwCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcG9zdGdyZXNxbDo1NDMyLyRQT1NUR1JFU19EQicKICAgICAgLSBEQVRBQkFTRV9UWVBFPXBvc3RncmVzCiAgICAgIC0gQVBQX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9VTUFNSQogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAvYXBpL2hlYXJ0YmVhdCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi11bWFtaX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", "tags": [ "analytics", "insights", diff --git a/templates/service-templates.json b/templates/service-templates.json index 226657fad..aae653dac 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -4350,7 +4350,7 @@ "umami": { "documentation": "https://umami.is?utm_source=coolify.io", "slogan": "Umami is web analytics platform which provides insights into visitor behavior without compromising user privacy.", - "compose": "c2VydmljZXM6CiAgdW1hbWk6CiAgICBpbWFnZTogJ2doY3IuaW8vdW1hbWktc29mdHdhcmUvdW1hbWk6My4wLjInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fVU1BTUlfMzAwMAogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzcWw6NTQzMi8kUE9TVEdSRVNfREInCiAgICAgIC0gREFUQUJBU0VfVFlQRT1wb3N0Z3JlcwogICAgICAtIEFQUF9TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfVU1BTUkKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwL2FwaS9oZWFydGJlYXQnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotdW1hbWl9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", + "compose": "c2VydmljZXM6CiAgdW1hbWk6CiAgICBpbWFnZTogJ2doY3IuaW8vdW1hbWktc29mdHdhcmUvdW1hbWk6My4wLjMnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fVU1BTUlfMzAwMAogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzcWw6NTQzMi8kUE9TVEdSRVNfREInCiAgICAgIC0gREFUQUJBU0VfVFlQRT1wb3N0Z3JlcwogICAgICAtIEFQUF9TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfVU1BTUkKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwL2FwaS9oZWFydGJlYXQnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotdW1hbWl9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", "tags": [ "analytics", "insights", diff --git a/versions.json b/versions.json index 9a5570f41..23c1f467e 100644 --- a/versions.json +++ b/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.456" + "version": "4.0.0-beta.457" }, "nightly": { - "version": "4.0.0-beta.457" + "version": "4.0.0-beta.458" }, "helper": { "version": "1.0.12"