diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml
new file mode 100644
index 000000000..d300267f1
--- /dev/null
+++ b/.github/workflows/claude.yml
@@ -0,0 +1,50 @@
+name: Claude Code
+
+on:
+ issue_comment:
+ types: [created]
+ pull_request_review_comment:
+ types: [created]
+ issues:
+ types: [opened, assigned]
+ pull_request_review:
+ types: [submitted]
+
+jobs:
+ claude:
+ if: |
+ (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
+ (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
+ (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
+ (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: read
+ issues: read
+ id-token: write
+ actions: read # Required for Claude to read CI results on PRs
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: Run Claude Code
+ id: claude
+ uses: anthropics/claude-code-action@v1
+ with:
+ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
+
+ # This is an optional setting that allows Claude to read CI results on PRs
+ additional_permissions: |
+ actions: read
+
+ # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
+ # prompt: 'Update the pull request description to include a summary of changes.'
+
+ # Optional: Add claude_args to customize behavior and configuration
+ # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
+ # or https://code.claude.com/docs/en/cli-reference for available options
+ # claude_args: '--allowed-tools Bash(gh pr:*)'
+
diff --git a/app/Actions/Application/StopApplication.php b/app/Actions/Application/StopApplication.php
index 94651a3c1..e86e30f04 100644
--- a/app/Actions/Application/StopApplication.php
+++ b/app/Actions/Application/StopApplication.php
@@ -55,6 +55,14 @@ public function handle(Application $application, bool $previewDeployments = fals
return $e->getMessage();
}
}
+
+ // Reset restart tracking when application is manually stopped
+ $application->update([
+ 'restart_count' => 0,
+ 'last_restart_at' => null,
+ 'last_restart_type' => null,
+ ]);
+
ServiceStatusChanged::dispatch($application->environment->project->team->id);
}
}
diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php
index 863691e1e..58f3cda4e 100644
--- a/app/Actions/Database/StartKeydb.php
+++ b/app/Actions/Database/StartKeydb.php
@@ -5,7 +5,6 @@
use App\Helpers\SslHelper;
use App\Models\SslCertificate;
use App\Models\StandaloneKeydb;
-use Illuminate\Support\Facades\Storage;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@@ -270,10 +269,9 @@ private function add_custom_keydb()
return;
}
$filename = 'keydb.conf';
- Storage::disk('local')->put("tmp/keydb.conf_{$this->database->uuid}", $this->database->keydb_conf);
- $path = Storage::path("tmp/keydb.conf_{$this->database->uuid}");
- instant_scp($path, "{$this->configuration_dir}/{$filename}", $this->database->destination->server);
- Storage::disk('local')->delete("tmp/keydb.conf_{$this->database->uuid}");
+ $content = $this->database->keydb_conf;
+ $content_base64 = base64_encode($content);
+ $this->commands[] = "echo '{$content_base64}' | base64 -d | tee $this->configuration_dir/{$filename} > /dev/null";
}
private function buildStartCommand(): string
diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php
index 2eaf82fdd..4e4f3ce53 100644
--- a/app/Actions/Database/StartRedis.php
+++ b/app/Actions/Database/StartRedis.php
@@ -5,7 +5,6 @@
use App\Helpers\SslHelper;
use App\Models\SslCertificate;
use App\Models\StandaloneRedis;
-use Illuminate\Support\Facades\Storage;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@@ -316,9 +315,8 @@ private function add_custom_redis()
return;
}
$filename = 'redis.conf';
- Storage::disk('local')->put("tmp/redis.conf_{$this->database->uuid}", $this->database->redis_conf);
- $path = Storage::path("tmp/redis.conf_{$this->database->uuid}");
- instant_scp($path, "{$this->configuration_dir}/{$filename}", $this->database->destination->server);
- Storage::disk('local')->delete("tmp/redis.conf_{$this->database->uuid}");
+ $content = $this->database->redis_conf;
+ $content_base64 = base64_encode($content);
+ $this->commands[] = "echo '{$content_base64}' | base64 -d | tee $this->configuration_dir/{$filename} > /dev/null";
}
}
diff --git a/app/Actions/Database/StopDatabase.php b/app/Actions/Database/StopDatabase.php
index c024c14e1..4dde509ab 100644
--- a/app/Actions/Database/StopDatabase.php
+++ b/app/Actions/Database/StopDatabase.php
@@ -28,6 +28,13 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St
$this->stopContainer($database, $database->uuid, 30);
+ // Reset restart tracking when database is manually stopped
+ $database->update([
+ 'restart_count' => 0,
+ 'last_restart_at' => null,
+ 'last_restart_type' => null,
+ ]);
+
if ($dockerCleanup) {
CleanupDocker::dispatch($server, false, false);
}
diff --git a/app/Listeners/ProxyStatusChangedNotification.php b/app/Listeners/ProxyStatusChangedNotification.php
index 1d99e7057..30ecb2d8d 100644
--- a/app/Listeners/ProxyStatusChangedNotification.php
+++ b/app/Listeners/ProxyStatusChangedNotification.php
@@ -29,7 +29,8 @@ public function handle(ProxyStatusChanged $event)
$server->proxy->set('status', $status);
$server->save();
- ProxyStatusChangedUI::dispatch($server->team_id);
+ $versionCheckDispatched = false;
+
if ($status === 'running') {
$server->setupDefaultRedirect();
$server->setupDynamicProxyConfiguration();
@@ -40,7 +41,9 @@ public function handle(ProxyStatusChanged $event)
if ($server->proxyType() === ProxyTypes::TRAEFIK->value) {
$traefikVersions = get_traefik_versions();
if ($traefikVersions !== null) {
+ // Version check job will dispatch ProxyStatusChangedUI when complete
CheckTraefikVersionForServerJob::dispatch($server, $traefikVersions);
+ $versionCheckDispatched = true;
} else {
Log::warning('Traefik version check skipped after proxy status change: versions.json data unavailable', [
'server_id' => $server->id,
@@ -49,6 +52,13 @@ public function handle(ProxyStatusChanged $event)
}
}
}
+
+ // Only dispatch UI refresh if version check wasn't dispatched
+ // (version check job handles its own UI refresh with updated version data)
+ if (! $versionCheckDispatched) {
+ ProxyStatusChangedUI::dispatch($server->team_id);
+ }
+
if ($status === 'created') {
instant_remote_process([
'docker rm -f coolify-proxy',
diff --git a/app/Livewire/Destination/New/Docker.php b/app/Livewire/Destination/New/Docker.php
index 819ac3ecd..70751fa03 100644
--- a/app/Livewire/Destination/New/Docker.php
+++ b/app/Livewire/Destination/New/Docker.php
@@ -95,7 +95,7 @@ public function submit()
]);
}
}
- $this->redirect(route('destination.show', $docker->uuid));
+ redirectRoute($this, 'destination.show', [$docker->uuid]);
} catch (\Throwable $e) {
return handleError($e, $this);
}
diff --git a/app/Livewire/GlobalSearch.php b/app/Livewire/GlobalSearch.php
index 237076acc..e4108668b 100644
--- a/app/Livewire/GlobalSearch.php
+++ b/app/Livewire/GlobalSearch.php
@@ -1314,10 +1314,10 @@ private function completeResourceCreation()
'server_id' => $this->selectedServerId,
];
- $this->redirect(route('project.resource.create', [
+ redirectRoute($this, 'project.resource.create', [
'project_uuid' => $this->selectedProjectUuid,
'environment_uuid' => $this->selectedEnvironmentUuid,
- ] + $queryParams));
+ ] + $queryParams);
}
}
@@ -1336,6 +1336,42 @@ public function cancelResourceSelection()
$this->autoOpenResource = null;
}
+ public function goBack()
+ {
+ // From Environment Selection → go back to Project (if multiple) or further
+ if ($this->selectedProjectUuid !== null) {
+ $this->selectedProjectUuid = null;
+ $this->selectedEnvironmentUuid = null;
+ if (count($this->availableProjects) > 1) {
+ return; // Stop here - user can choose a project
+ }
+ }
+
+ // From Project Selection → go back to Destination (if multiple) or further
+ if ($this->selectedDestinationUuid !== null) {
+ $this->selectedDestinationUuid = null;
+ $this->selectedProjectUuid = null;
+ $this->selectedEnvironmentUuid = null;
+ if (count($this->availableDestinations) > 1) {
+ return; // Stop here - user can choose a destination
+ }
+ }
+
+ // From Destination Selection → go back to Server (if multiple) or cancel
+ if ($this->selectedServerId !== null) {
+ $this->selectedServerId = null;
+ $this->selectedDestinationUuid = null;
+ $this->selectedProjectUuid = null;
+ $this->selectedEnvironmentUuid = null;
+ if (count($this->availableServers) > 1) {
+ return; // Stop here - user can choose a server
+ }
+ }
+
+ // All previous steps were auto-selected, cancel entirely
+ $this->cancelResourceSelection();
+ }
+
public function getFilteredCreatableItemsProperty()
{
$query = strtolower(trim($this->searchQuery));
diff --git a/app/Livewire/NavbarDeleteTeam.php b/app/Livewire/NavbarDeleteTeam.php
index 9508c2adc..a8c932912 100644
--- a/app/Livewire/NavbarDeleteTeam.php
+++ b/app/Livewire/NavbarDeleteTeam.php
@@ -37,7 +37,7 @@ public function delete($password)
refreshSession();
- return redirect()->route('team.index');
+ return redirectRoute($this, 'team.index');
}
public function render()
diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php
index c84de9d8d..dffe1ec67 100644
--- a/app/Livewire/Project/Application/General.php
+++ b/app/Livewire/Project/Application/General.php
@@ -558,8 +558,11 @@ public function loadComposeFile($isInit = false, $showToast = true, ?string $res
$this->dispatch('refreshStorages');
$this->dispatch('refreshEnvs');
} catch (\Throwable $e) {
- $this->application->docker_compose_location = $this->initialDockerComposeLocation;
- $this->application->save();
+ // Refresh model to get restored values from Application::loadComposeFile
+ $this->application->refresh();
+ // Sync restored values back to component properties for UI update
+
+ $this->syncData();
return handleError($e, $this);
} finally {
@@ -936,73 +939,6 @@ public function downloadConfig()
]);
}
- private function updateServiceEnvironmentVariables()
- {
- $domains = collect(json_decode($this->application->docker_compose_domains, true)) ?? collect([]);
-
- foreach ($domains as $serviceName => $service) {
- $serviceNameFormatted = str($serviceName)->upper()->replace('-', '_')->replace('.', '_');
- $domain = data_get($service, 'domain');
- // Delete SERVICE_FQDN_ and SERVICE_URL_ variables if domain is removed
- $this->application->environment_variables()->where('resourceable_type', Application::class)
- ->where('resourceable_id', $this->application->id)
- ->where('key', 'LIKE', "SERVICE_FQDN_{$serviceNameFormatted}%")
- ->delete();
-
- $this->application->environment_variables()->where('resourceable_type', Application::class)
- ->where('resourceable_id', $this->application->id)
- ->where('key', 'LIKE', "SERVICE_URL_{$serviceNameFormatted}%")
- ->delete();
-
- if ($domain) {
- // Create or update SERVICE_FQDN_ and SERVICE_URL_ variables
- $fqdn = Url::fromString($domain);
- $port = $fqdn->getPort();
- $path = $fqdn->getPath();
- $urlValue = $fqdn->getScheme().'://'.$fqdn->getHost();
- if ($path !== '/') {
- $urlValue = $urlValue.$path;
- }
- $fqdnValue = str($domain)->after('://');
- if ($path !== '/') {
- $fqdnValue = $fqdnValue.$path;
- }
-
- // Create/update SERVICE_FQDN_
- $this->application->environment_variables()->updateOrCreate([
- 'key' => "SERVICE_FQDN_{$serviceNameFormatted}",
- ], [
- 'value' => $fqdnValue,
- 'is_preview' => false,
- ]);
-
- // Create/update SERVICE_URL_
- $this->application->environment_variables()->updateOrCreate([
- 'key' => "SERVICE_URL_{$serviceNameFormatted}",
- ], [
- 'value' => $urlValue,
- 'is_preview' => false,
- ]);
- // Create/update port-specific variables if port exists
- if (filled($port)) {
- $this->application->environment_variables()->updateOrCreate([
- 'key' => "SERVICE_FQDN_{$serviceNameFormatted}_{$port}",
- ], [
- 'value' => $fqdnValue,
- 'is_preview' => false,
- ]);
-
- $this->application->environment_variables()->updateOrCreate([
- 'key' => "SERVICE_URL_{$serviceNameFormatted}_{$port}",
- ], [
- 'value' => $urlValue,
- 'is_preview' => false,
- ]);
- }
- }
- }
- }
-
public function getDetectedPortInfoProperty(): ?array
{
$detectedPort = $this->application->detectPortFromEnvironment();
diff --git a/app/Livewire/Project/Application/Rollback.php b/app/Livewire/Project/Application/Rollback.php
index e53784db5..e8edf72fa 100644
--- a/app/Livewire/Project/Application/Rollback.php
+++ b/app/Livewire/Project/Application/Rollback.php
@@ -66,7 +66,7 @@ public function rollbackImage($commit)
return;
}
- return redirect()->route('project.application.deployment.show', [
+ return redirectRoute($this, 'project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $deployment_uuid,
diff --git a/app/Livewire/Project/DeleteEnvironment.php b/app/Livewire/Project/DeleteEnvironment.php
index e97206081..aa6e95975 100644
--- a/app/Livewire/Project/DeleteEnvironment.php
+++ b/app/Livewire/Project/DeleteEnvironment.php
@@ -39,7 +39,7 @@ public function delete()
if ($environment->isEmpty()) {
$environment->delete();
- return redirect()->route('project.show', parameters: ['project_uuid' => $this->parameters['project_uuid']]);
+ return redirectRoute($this, 'project.show', ['project_uuid' => $this->parameters['project_uuid']]);
}
return $this->dispatch('error', "Environment {$environment->name} has defined resources, please delete them first.");
diff --git a/app/Livewire/Project/DeleteProject.php b/app/Livewire/Project/DeleteProject.php
index 26b35b2e7..a018046fd 100644
--- a/app/Livewire/Project/DeleteProject.php
+++ b/app/Livewire/Project/DeleteProject.php
@@ -35,7 +35,7 @@ public function delete()
if ($project->isEmpty()) {
$project->delete();
- return redirect()->route('project.index');
+ return redirectRoute($this, 'project.index');
}
return $this->dispatch('error', "Project {$project->name} has resources defined, please delete them first.");
diff --git a/app/Livewire/Project/EnvironmentEdit.php b/app/Livewire/Project/EnvironmentEdit.php
index d57be2cc8..529b9d7b1 100644
--- a/app/Livewire/Project/EnvironmentEdit.php
+++ b/app/Livewire/Project/EnvironmentEdit.php
@@ -63,7 +63,7 @@ public function submit()
{
try {
$this->syncData(true);
- $this->redirectRoute('project.environment.edit', [
+ redirectRoute($this, 'project.environment.edit', [
'environment_uuid' => $this->environment->uuid,
'project_uuid' => $this->project->uuid,
]);
diff --git a/app/Livewire/Project/New/DockerImage.php b/app/Livewire/Project/New/DockerImage.php
index 9d04ca9a5..8aff83153 100644
--- a/app/Livewire/Project/New/DockerImage.php
+++ b/app/Livewire/Project/New/DockerImage.php
@@ -154,7 +154,7 @@ public function submit()
'fqdn' => $fqdn,
]);
- return redirect()->route('project.application.configuration', [
+ return redirectRoute($this, 'project.application.configuration', [
'application_uuid' => $application->uuid,
'environment_uuid' => $environment->uuid,
'project_uuid' => $project->uuid,
diff --git a/app/Livewire/Project/New/EmptyProject.php b/app/Livewire/Project/New/EmptyProject.php
index 54cfc4b4d..0360365a9 100644
--- a/app/Livewire/Project/New/EmptyProject.php
+++ b/app/Livewire/Project/New/EmptyProject.php
@@ -16,6 +16,6 @@ public function createEmptyProject()
'uuid' => (string) new Cuid2,
]);
- return redirect()->route('project.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $project->environments->first()->uuid]);
+ return redirectRoute($this, 'project.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $project->environments->first()->uuid]);
}
}
diff --git a/app/Livewire/Project/Service/Database.php b/app/Livewire/Project/Service/Database.php
index 1e183c6bc..301c39dee 100644
--- a/app/Livewire/Project/Service/Database.php
+++ b/app/Livewire/Project/Service/Database.php
@@ -100,7 +100,7 @@ public function delete($password)
$this->database->delete();
$this->dispatch('success', 'Database deleted.');
- return redirect()->route('project.service.configuration', $this->parameters);
+ return redirectRoute($this, 'project.service.configuration', $this->parameters);
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -164,7 +164,7 @@ public function convertToApplication()
$serviceDatabase->delete();
});
- return redirect()->route('project.service.configuration', $redirectParams);
+ return redirectRoute($this, 'project.service.configuration', $redirectParams);
} catch (\Throwable $e) {
return handleError($e, $this);
}
diff --git a/app/Livewire/Project/Shared/Danger.php b/app/Livewire/Project/Shared/Danger.php
index 8bf3c7438..1b15c6367 100644
--- a/app/Livewire/Project/Shared/Danger.php
+++ b/app/Livewire/Project/Shared/Danger.php
@@ -111,7 +111,7 @@ public function delete($password)
$this->docker_cleanup
);
- return redirect()->route('project.resource.index', [
+ return redirectRoute($this, 'project.resource.index', [
'project_uuid' => $this->projectUuid,
'environment_uuid' => $this->environmentUuid,
]);
diff --git a/app/Livewire/Project/Shared/Destination.php b/app/Livewire/Project/Shared/Destination.php
index ffd18b35c..7ab81b7d1 100644
--- a/app/Livewire/Project/Shared/Destination.php
+++ b/app/Livewire/Project/Shared/Destination.php
@@ -97,7 +97,7 @@ public function redeploy(int $network_id, int $server_id)
return;
}
- return redirect()->route('project.application.deployment.show', [
+ return redirectRoute($this, 'project.application.deployment.show', [
'project_uuid' => data_get($this->resource, 'environment.project.uuid'),
'application_uuid' => data_get($this->resource, 'uuid'),
'deployment_uuid' => $deployment_uuid,
diff --git a/app/Livewire/Project/Shared/Terminal.php b/app/Livewire/Project/Shared/Terminal.php
index 3c2abc84c..ae68b2354 100644
--- a/app/Livewire/Project/Shared/Terminal.php
+++ b/app/Livewire/Project/Shared/Terminal.php
@@ -57,7 +57,14 @@ public function sendTerminalCommand($isContainer, $identifier, $serverUuid)
$shellCommand = 'PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && '.
'if [ -f ~/.profile ]; then . ~/.profile; fi && '.
'if [ -n "$SHELL" ] && [ -x "$SHELL" ]; then exec $SHELL; else sh; fi';
- $command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$escapedIdentifier} sh -c '{$shellCommand}'");
+
+ // Add sudo for non-root users to access Docker socket
+ $dockerCommand = "docker exec -it {$escapedIdentifier} sh -c '{$shellCommand}'";
+ if ($server->isNonRoot()) {
+ $dockerCommand = "sudo {$dockerCommand}";
+ }
+
+ $command = SshMultiplexingHelper::generateSshCommand($server, $dockerCommand);
} else {
$shellCommand = 'PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && '.
'if [ -f ~/.profile ]; then . ~/.profile; fi && '.
diff --git a/app/Livewire/Project/Show.php b/app/Livewire/Project/Show.php
index 7e828d14c..e884abb4e 100644
--- a/app/Livewire/Project/Show.php
+++ b/app/Livewire/Project/Show.php
@@ -48,7 +48,7 @@ public function submit()
'uuid' => (string) new Cuid2,
]);
- return redirect()->route('project.resource.index', [
+ return redirectRoute($this, 'project.resource.index', [
'project_uuid' => $this->project->uuid,
'environment_uuid' => $environment->uuid,
]);
@@ -59,7 +59,7 @@ public function submit()
public function navigateToEnvironment($projectUuid, $environmentUuid)
{
- return redirect()->route('project.resource.index', [
+ return redirectRoute($this, 'project.resource.index', [
'project_uuid' => $projectUuid,
'environment_uuid' => $environmentUuid,
]);
diff --git a/app/Livewire/Security/PrivateKey/Create.php b/app/Livewire/Security/PrivateKey/Create.php
index 656e73958..8b7ba73dd 100644
--- a/app/Livewire/Security/PrivateKey/Create.php
+++ b/app/Livewire/Security/PrivateKey/Create.php
@@ -114,7 +114,7 @@ private function validatePrivateKey()
private function redirectAfterCreation(PrivateKey $privateKey)
{
return $this->from === 'server'
- ? redirect()->route('dashboard')
- : redirect()->route('security.private-key.show', ['private_key_uuid' => $privateKey->uuid]);
+ ? redirectRoute($this, 'dashboard')
+ : redirectRoute($this, 'security.private-key.show', ['private_key_uuid' => $privateKey->uuid]);
}
}
diff --git a/app/Livewire/Security/PrivateKey/Show.php b/app/Livewire/Security/PrivateKey/Show.php
index c292d14a3..6be190689 100644
--- a/app/Livewire/Security/PrivateKey/Show.php
+++ b/app/Livewire/Security/PrivateKey/Show.php
@@ -107,7 +107,7 @@ public function delete()
$this->private_key->safeDelete();
currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get();
- return redirect()->route('security.private-key.index');
+ return redirectRoute($this, 'security.private-key.index');
} catch (\Exception $e) {
$this->dispatch('error', $e->getMessage());
} catch (\Throwable $e) {
diff --git a/app/Livewire/Server/Delete.php b/app/Livewire/Server/Delete.php
index 27a6e7aca..e7b64b805 100644
--- a/app/Livewire/Server/Delete.php
+++ b/app/Livewire/Server/Delete.php
@@ -46,7 +46,7 @@ public function delete($password)
$this->server->team_id
);
- return redirect()->route('server.index');
+ return redirectRoute($this, 'server.index');
} catch (\Throwable $e) {
return handleError($e, $this);
}
diff --git a/app/Livewire/Server/New/ByHetzner.php b/app/Livewire/Server/New/ByHetzner.php
index f7d12dbc1..f1ffa60f2 100644
--- a/app/Livewire/Server/New/ByHetzner.php
+++ b/app/Livewire/Server/New/ByHetzner.php
@@ -567,10 +567,10 @@ public function submit()
]);
refreshSession();
- return $this->redirect(route('server.show', $server->uuid));
+ return redirectRoute($this, 'server.show', [$server->uuid]);
}
- return redirect()->route('server.show', $server->uuid);
+ return redirectRoute($this, 'server.show', [$server->uuid]);
} catch (\Throwable $e) {
return handleError($e, $this);
}
diff --git a/app/Livewire/Server/New/ByIp.php b/app/Livewire/Server/New/ByIp.php
index 35526d59e..1f4cdf607 100644
--- a/app/Livewire/Server/New/ByIp.php
+++ b/app/Livewire/Server/New/ByIp.php
@@ -128,7 +128,7 @@ public function submit()
$server->settings->is_build_server = $this->is_build_server;
$server->settings->save();
- return redirect()->route('server.show', $server->uuid);
+ return redirectRoute($this, 'server.show', [$server->uuid]);
} catch (\Throwable $e) {
return handleError($e, $this);
}
diff --git a/app/Livewire/Source/Github/Create.php b/app/Livewire/Source/Github/Create.php
index 2f1482c89..4ece6a92f 100644
--- a/app/Livewire/Source/Github/Create.php
+++ b/app/Livewire/Source/Github/Create.php
@@ -58,7 +58,7 @@ public function createGitHubApp()
session(['from' => session('from') + ['source_id' => $github_app->id]]);
}
- return redirect()->route('source.github.show', ['github_app_uuid' => $github_app->uuid]);
+ return redirectRoute($this, 'source.github.show', ['github_app_uuid' => $github_app->uuid]);
} catch (\Throwable $e) {
return handleError($e, $this);
}
diff --git a/app/Livewire/Storage/Create.php b/app/Livewire/Storage/Create.php
index 9efeb948c..eda20342b 100644
--- a/app/Livewire/Storage/Create.php
+++ b/app/Livewire/Storage/Create.php
@@ -116,7 +116,7 @@ public function submit()
$this->storage->testConnection();
$this->storage->save();
- return redirect()->route('storage.show', $this->storage->uuid);
+ return redirectRoute($this, 'storage.show', [$this->storage->uuid]);
} catch (\Throwable $e) {
$this->dispatch('error', 'Failed to create storage.', $e->getMessage());
// return handleError($e, $this);
diff --git a/app/Livewire/Team/Create.php b/app/Livewire/Team/Create.php
index cd15be67d..0bcfb5631 100644
--- a/app/Livewire/Team/Create.php
+++ b/app/Livewire/Team/Create.php
@@ -37,7 +37,7 @@ public function submit()
auth()->user()->teams()->attach($team, ['role' => 'admin']);
refreshSession($team);
- return redirect()->route('team.index');
+ return redirectRoute($this, 'team.index');
} catch (\Throwable $e) {
return handleError($e, $this);
}
diff --git a/app/Models/Application.php b/app/Models/Application.php
index 5006d0ff8..40e41c2a7 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -1584,6 +1584,11 @@ public function loadComposeFile($isInit = false, ?string $restoreBaseDirectory =
try {
$composeFileContent = instant_remote_process($commands, $this->destination->server);
} catch (\Exception $e) {
+ // Restore original values on failure only
+ $this->docker_compose_location = $initialDockerComposeLocation;
+ $this->base_directory = $initialBaseDirectory;
+ $this->save();
+
if (str($e->getMessage())->contains('No such file')) {
throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile
Check if you used the right extension (.yaml or .yml) in the compose file name.");
}
@@ -1595,9 +1600,7 @@ public function loadComposeFile($isInit = false, ?string $restoreBaseDirectory =
}
throw new \RuntimeException($e->getMessage());
} finally {
- $this->docker_compose_location = $initialDockerComposeLocation;
- $this->base_directory = $initialBaseDirectory;
- $this->save();
+ // Cleanup only - restoration happens in catch block
$commands = collect([
"rm -rf /tmp/{$uuid}",
]);
@@ -1643,6 +1646,11 @@ public function loadComposeFile($isInit = false, ?string $restoreBaseDirectory =
'initialDockerComposeLocation' => $this->docker_compose_location,
];
} else {
+ // Restore original values before throwing
+ $this->docker_compose_location = $initialDockerComposeLocation;
+ $this->base_directory = $initialBaseDirectory;
+ $this->save();
+
throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile
Check if you used the right extension (.yaml or .yml) in the compose file name.");
}
}
diff --git a/app/Models/Server.php b/app/Models/Server.php
index be39e3f8d..fd1ce3e69 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -682,9 +682,16 @@ public function getCpuMetrics(int $mins = 5)
}
$cpu = json_decode($cpu, true);
- return collect($cpu)->map(function ($metric) {
+ $metrics = collect($cpu)->map(function ($metric) {
return [(int) $metric['time'], (float) $metric['percent']];
- });
+ })->toArray();
+
+ // Downsample for intervals > 60 minutes to prevent browser freeze
+ if ($mins > 60 && count($metrics) > 1000) {
+ $metrics = $this->downsampleLTTB($metrics, 1000);
+ }
+
+ return collect($metrics);
}
}
@@ -702,16 +709,99 @@ public function getMemoryMetrics(int $mins = 5)
throw new \Exception($error);
}
$memory = json_decode($memory, true);
- $parsedCollection = collect($memory)->map(function ($metric) {
+ $metrics = collect($memory)->map(function ($metric) {
$usedPercent = $metric['usedPercent'] ?? 0.0;
return [(int) $metric['time'], (float) $usedPercent];
- });
+ })->toArray();
- return $parsedCollection->toArray();
+ // Downsample for intervals > 60 minutes to prevent browser freeze
+ if ($mins > 60 && count($metrics) > 1000) {
+ $metrics = $this->downsampleLTTB($metrics, 1000);
+ }
+
+ return collect($metrics);
}
}
+ /**
+ * Downsample metrics using the Largest-Triangle-Three-Buckets (LTTB) algorithm.
+ * This preserves the visual shape of the data better than simple averaging.
+ *
+ * @param array $data Array of [timestamp, value] pairs
+ * @param int $threshold Target number of points
+ * @return array Downsampled data
+ */
+ private function downsampleLTTB(array $data, int $threshold): array
+ {
+ $dataLength = count($data);
+
+ // Return unchanged if threshold >= data length, or if threshold <= 2
+ // (threshold <= 2 would cause division by zero in bucket calculation)
+ if ($threshold >= $dataLength || $threshold <= 2) {
+ return $data;
+ }
+
+ $sampled = [];
+ $sampled[] = $data[0]; // Always keep first point
+
+ $bucketSize = ($dataLength - 2) / ($threshold - 2);
+
+ $a = 0; // Index of previous selected point
+
+ for ($i = 0; $i < $threshold - 2; $i++) {
+ // Calculate bucket range
+ $bucketStart = (int) floor(($i + 1) * $bucketSize) + 1;
+ $bucketEnd = (int) floor(($i + 2) * $bucketSize) + 1;
+ $bucketEnd = min($bucketEnd, $dataLength - 1);
+
+ // Calculate average point for next bucket (used as reference)
+ $nextBucketStart = (int) floor(($i + 2) * $bucketSize) + 1;
+ $nextBucketEnd = (int) floor(($i + 3) * $bucketSize) + 1;
+ $nextBucketEnd = min($nextBucketEnd, $dataLength - 1);
+
+ $avgX = 0;
+ $avgY = 0;
+ $nextBucketCount = $nextBucketEnd - $nextBucketStart + 1;
+
+ if ($nextBucketCount > 0) {
+ for ($j = $nextBucketStart; $j <= $nextBucketEnd; $j++) {
+ $avgX += $data[$j][0];
+ $avgY += $data[$j][1];
+ }
+ $avgX /= $nextBucketCount;
+ $avgY /= $nextBucketCount;
+ }
+
+ // Find point in current bucket with largest triangle area
+ $maxArea = -1;
+ $maxAreaIndex = $bucketStart;
+
+ $pointAX = $data[$a][0];
+ $pointAY = $data[$a][1];
+
+ for ($j = $bucketStart; $j <= $bucketEnd; $j++) {
+ // Triangle area calculation
+ $area = abs(
+ ($pointAX - $avgX) * ($data[$j][1] - $pointAY) -
+ ($pointAX - $data[$j][0]) * ($avgY - $pointAY)
+ ) * 0.5;
+
+ if ($area > $maxArea) {
+ $maxArea = $area;
+ $maxAreaIndex = $j;
+ }
+ }
+
+ $sampled[] = $data[$maxAreaIndex];
+ $a = $maxAreaIndex;
+ }
+
+ $sampled[] = $data[$dataLength - 1]; // Always keep last point
+
+ return $sampled;
+ }
+
public function getDiskUsage(): ?string
{
return instant_remote_process(['df / --output=pcent | tr -cd 0-9'], $this, false);
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index e73328474..9fc1e6f1c 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -2934,6 +2934,23 @@ function wireNavigate(): string
}
}
+/**
+ * Redirect to a named route with SPA navigation support.
+ * Automatically uses wire:navigate when is_wire_navigate_enabled is true.
+ */
+function redirectRoute(Livewire\Component $component, string $name, array $parameters = []): mixed
+{
+ $navigate = true;
+
+ try {
+ $navigate = instanceSettings()->is_wire_navigate_enabled ?? true;
+ } catch (\Exception $e) {
+ $navigate = true;
+ }
+
+ return $component->redirectRoute($name, $parameters, navigate: $navigate);
+}
+
function getHelperVersion(): string
{
$settings = instanceSettings();
diff --git a/config/constants.php b/config/constants.php
index 07141bd17..930db2f61 100644
--- a/config/constants.php
+++ b/config/constants.php
@@ -2,7 +2,7 @@
return [
'coolify' => [
- 'version' => '4.0.0-beta.459',
+ 'version' => '4.0.0-beta.460',
'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 8a7441054..c6419600c 100644
--- a/other/nightly/versions.json
+++ b/other/nightly/versions.json
@@ -1,10 +1,10 @@
{
"coolify": {
"v4": {
- "version": "4.0.0-beta.459"
+ "version": "4.0.0-beta.460"
},
"nightly": {
- "version": "4.0.0-beta.460"
+ "version": "4.0.0-beta.461"
},
"helper": {
"version": "1.0.12"
diff --git a/resources/views/components/server/sidebar.blade.php b/resources/views/components/server/sidebar.blade.php
index 3f5bcafac..e485ec545 100644
--- a/resources/views/components/server/sidebar.blade.php
+++ b/resources/views/components/server/sidebar.blade.php
@@ -6,11 +6,6 @@
href="{{ route('server.advanced', ['server_uuid' => $server->uuid]) }}">Advanced
@endif
- @if (!$server->isBuildServer() && !$server->settings->is_cloudflare_tunnel)
-
- @endif
@if ($server->isFunctional() && !$server->isSwarm() && !$server->isBuildServer())
@if ($server->hetzner_server_id)
-
@endif
@@ -45,6 +41,11 @@
@endif
+ @if (!$server->isBuildServer() && !$server->settings->is_cloudflare_tunnel)
+
+ @endif
@if (!$server->isLocalhost())
diff --git a/resources/views/components/upgrade-progress.blade.php b/resources/views/components/upgrade-progress.blade.php
index 1418ca6c9..cc7d7d188 100644
--- a/resources/views/components/upgrade-progress.blade.php
+++ b/resources/views/components/upgrade-progress.blade.php
@@ -17,7 +17,7 @@
@@ -26,7 +26,7 @@
-
@@ -52,7 +52,7 @@
@@ -61,7 +61,7 @@
-
+
@@ -73,7 +73,7 @@
Helper
@@ -87,7 +87,7 @@
@@ -96,7 +96,7 @@
-
+
@@ -108,7 +108,7 @@
Image
@@ -122,7 +122,7 @@
@@ -131,7 +131,7 @@
-
+
@@ -143,7 +143,7 @@
Restart
diff --git a/resources/views/livewire/global-search.blade.php b/resources/views/livewire/global-search.blade.php
index 8c073ecab..27b8d2821 100644
--- a/resources/views/livewire/global-search.blade.php
+++ b/resources/views/livewire/global-search.blade.php
@@ -323,7 +323,7 @@ class="mt-2 bg-white dark:bg-coolgray-100 rounded-lg shadow-xl ring-1 ring-neutr