v4.0.0-beta.456 (#7673)

This commit is contained in:
Andras Bacsai 2025-12-18 09:58:24 +01:00 committed by GitHub
commit b6d0d303c8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
124 changed files with 2688 additions and 716 deletions

View file

@ -10,6 +10,42 @@ ## Project Overview
Coolify is an open-source, self-hostable platform for deploying applications and managing servers - an alternative to Heroku/Netlify/Vercel. It's built with Laravel (PHP) and uses Docker for containerization. Coolify is an open-source, self-hostable platform for deploying applications and managing servers - an alternative to Heroku/Netlify/Vercel. It's built with Laravel (PHP) and uses Docker for containerization.
## Git Worktree Shared Dependencies
This repository uses git worktrees for parallel development with **automatic shared dependency setup** via Conductor.
### How It Works
The `conductor.json` setup script (`scripts/conductor-setup.sh`) automatically:
1. Creates symlinks from worktree's `node_modules` and `vendor` to the main repository's directories
2. All worktrees share the same dependencies from the main repository
3. This happens automatically when Conductor creates a new worktree
### Benefits
- **Save disk space**: Only one copy of dependencies across all worktrees
- **Faster setup**: No need to run `npm install` or `composer install` for each worktree
- **Consistent versions**: All worktrees use the same dependency versions
- **Auto-configured**: Handled by Conductor's setup script
- **Simple**: Uses the main repo's existing directories, no extra folders
### Manual Setup (If Needed)
If you need to set up symlinks manually or for non-Conductor worktrees:
```bash
# From the worktree directory
rm -rf node_modules vendor
ln -sf ../../node_modules node_modules
ln -sf ../../vendor vendor
```
### Important Notes
- Dependencies are shared from the main repository (`$CONDUCTOR_ROOT_PATH`)
- Run `npm install` or `composer install` from the main repo or any worktree to update all
- If different branches need different dependency versions, this won't work - remove symlinks and use separate directories
## Development Commands ## Development Commands
### Frontend Development ### Frontend Development

View file

@ -51,7 +51,7 @@ public function handle(StandaloneClickhouse $database)
], ],
'labels' => defaultDatabaseLabels($this->database)->toArray(), 'labels' => defaultDatabaseLabels($this->database)->toArray(),
'healthcheck' => [ 'healthcheck' => [
'test' => "clickhouse-client --password {$this->database->clickhouse_admin_password} --query 'SELECT 1'", 'test' => "clickhouse-client --user {$this->database->clickhouse_admin_user} --password {$this->database->clickhouse_admin_password} --query 'SELECT 1'",
'interval' => '5s', 'interval' => '5s',
'timeout' => '5s', 'timeout' => '5s',
'retries' => 10, 'retries' => 10,
@ -152,12 +152,16 @@ private function generate_environment_variables()
$environment_variables->push("$env->key=$env->real_value"); $environment_variables->push("$env->key=$env->real_value");
} }
if ($environment_variables->filter(fn ($env) => str($env)->contains('CLICKHOUSE_ADMIN_USER'))->isEmpty()) { if ($environment_variables->filter(fn ($env) => str($env)->contains('CLICKHOUSE_USER'))->isEmpty()) {
$environment_variables->push("CLICKHOUSE_ADMIN_USER={$this->database->clickhouse_admin_user}"); $environment_variables->push("CLICKHOUSE_USER={$this->database->clickhouse_admin_user}");
} }
if ($environment_variables->filter(fn ($env) => str($env)->contains('CLICKHOUSE_ADMIN_PASSWORD'))->isEmpty()) { if ($environment_variables->filter(fn ($env) => str($env)->contains('CLICKHOUSE_PASSWORD'))->isEmpty()) {
$environment_variables->push("CLICKHOUSE_ADMIN_PASSWORD={$this->database->clickhouse_admin_password}"); $environment_variables->push("CLICKHOUSE_PASSWORD={$this->database->clickhouse_admin_password}");
}
if ($environment_variables->filter(fn ($env) => str($env)->contains('CLICKHOUSE_DB'))->isEmpty()) {
$environment_variables->push("CLICKHOUSE_DB={$this->database->clickhouse_db}");
} }
add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables); add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);

View file

@ -199,12 +199,26 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti
$isPublic = data_get($database, 'is_public'); $isPublic = data_get($database, 'is_public');
$foundDatabases[] = $database->id; $foundDatabases[] = $database->id;
$statusFromDb = $database->status; $statusFromDb = $database->status;
// Track restart count for databases (single-container)
$restartCount = data_get($container, 'RestartCount', 0);
$previousRestartCount = $database->restart_count ?? 0;
if ($statusFromDb !== $containerStatus) { if ($statusFromDb !== $containerStatus) {
$database->update(['status' => $containerStatus]); $updateData = ['status' => $containerStatus];
} else { } else {
$database->update(['last_online_at' => now()]); $updateData = ['last_online_at' => now()];
} }
// Update restart tracking if restart count increased
if ($restartCount > $previousRestartCount) {
$updateData['restart_count'] = $restartCount;
$updateData['last_restart_at'] = now();
$updateData['last_restart_type'] = 'crash';
}
$database->update($updateData);
if ($isPublic) { if ($isPublic) {
$foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) { $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) {
if ($this->server->isSwarm()) { if ($this->server->isSwarm()) {
@ -365,7 +379,13 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti
if (str($database->status)->startsWith('exited')) { if (str($database->status)->startsWith('exited')) {
continue; continue;
} }
$database->update(['status' => 'exited']); // Reset restart tracking when database exits completely
$database->update([
'status' => 'exited',
'restart_count' => 0,
'last_restart_at' => null,
'last_restart_type' => null,
]);
$name = data_get($database, 'name'); $name = data_get($database, 'name');
$fqdn = data_get($database, 'fqdn'); $fqdn = data_get($database, 'fqdn');

View file

@ -237,8 +237,9 @@ public function handle()
$this->foundProxy = true; $this->foundProxy = true;
} elseif ($type === 'service' && $this->isRunning($containerStatus)) { } elseif ($type === 'service' && $this->isRunning($containerStatus)) {
} else { } else {
if ($this->allDatabaseUuids->contains($uuid) && $this->isRunning($containerStatus)) { if ($this->allDatabaseUuids->contains($uuid) && $this->isActiveOrTransient($containerStatus)) {
$this->foundDatabaseUuids->push($uuid); $this->foundDatabaseUuids->push($uuid);
// TCP proxy should only be started/managed when database is actually running
if ($this->allTcpProxyUuids->contains($uuid) && $this->isRunning($containerStatus)) { if ($this->allTcpProxyUuids->contains($uuid) && $this->isRunning($containerStatus)) {
$this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: true); $this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: true);
} else { } else {
@ -503,20 +504,28 @@ private function updateDatabaseStatus(string $databaseUuid, string $containerSta
private function updateNotFoundDatabaseStatus() private function updateNotFoundDatabaseStatus()
{ {
$notFoundDatabaseUuids = $this->allDatabaseUuids->diff($this->foundDatabaseUuids); $notFoundDatabaseUuids = $this->allDatabaseUuids->diff($this->foundDatabaseUuids);
if ($notFoundDatabaseUuids->isNotEmpty()) { if ($notFoundDatabaseUuids->isEmpty()) {
$notFoundDatabaseUuids->each(function ($databaseUuid) { return;
$database = $this->databases->where('uuid', $databaseUuid)->first();
if ($database) {
if ($database->status !== 'exited') {
$database->status = 'exited';
$database->save();
}
if ($database->is_public) {
StopDatabaseProxy::dispatch($database);
}
}
});
} }
// Only protection: Verify we received any container data at all
// If containers collection is completely empty, Sentinel might have failed
if ($this->containers->isEmpty()) {
return;
}
$notFoundDatabaseUuids->each(function ($databaseUuid) {
$database = $this->databases->where('uuid', $databaseUuid)->first();
if ($database) {
if (! str($database->status)->startsWith('exited')) {
$database->status = 'exited';
$database->save();
}
if ($database->is_public) {
StopDatabaseProxy::dispatch($database);
}
}
});
} }
private function updateServiceSubStatus(string $serviceId, string $subType, string $subId, string $containerStatus) private function updateServiceSubStatus(string $serviceId, string $subType, string $subId, string $containerStatus)
@ -576,6 +585,23 @@ private function isRunning(string $containerStatus)
return str($containerStatus)->contains('running'); return str($containerStatus)->contains('running');
} }
/**
* Check if container is in an active or transient state.
* Active states: running
* Transient states: restarting, starting, created, paused
*
* These states indicate the container exists and should be tracked.
* Terminal states (exited, dead, removing) should NOT be tracked.
*/
private function isActiveOrTransient(string $containerStatus): bool
{
return str($containerStatus)->contains('running') ||
str($containerStatus)->contains('restarting') ||
str($containerStatus)->contains('starting') ||
str($containerStatus)->contains('created') ||
str($containerStatus)->contains('paused');
}
private function checkLogDrainContainer() private function checkLogDrainContainer()
{ {
if ($this->server->isLogDrainEnabled() && $this->foundLogDrainContainer === false) { if ($this->server->isLogDrainEnabled() && $this->foundLogDrainContainer === false) {

View file

@ -1314,11 +1314,6 @@ private function completeResourceCreation()
'server_id' => $this->selectedServerId, 'server_id' => $this->selectedServerId,
]; ];
// PostgreSQL requires a database_image parameter
if ($this->selectedResourceType === 'postgresql') {
$queryParams['database_image'] = 'postgres:16-alpine';
}
$this->redirect(route('project.resource.create', [ $this->redirect(route('project.resource.create', [
'project_uuid' => $this->selectedProjectUuid, 'project_uuid' => $this->selectedProjectUuid,
'environment_uuid' => $this->selectedEnvironmentUuid, 'environment_uuid' => $this->selectedEnvironmentUuid,

View file

@ -117,6 +117,19 @@ public function getLogLinesProperty()
}); });
} }
public function copyLogs(): string
{
$logs = decode_remote_command_output($this->application_deployment_queue)
->map(function ($line) {
return $line['timestamp'].' '.
(isset($line['command']) && $line['command'] ? '[CMD]: ' : '').
trim($line['line']);
})
->join("\n");
return sanitizeLogsForExport($logs);
}
public function render() public function render()
{ {
return view('livewire.project.application.deployment.show'); return view('livewire.project.application.deployment.show');

View file

@ -53,6 +53,8 @@ class Select extends Component
protected $queryString = [ protected $queryString = [
'server_id', 'server_id',
'type' => ['except' => ''],
'destination_uuid' => ['except' => '', 'as' => 'destination'],
]; ];
public function mount() public function mount()
@ -66,6 +68,20 @@ public function mount()
$project = Project::whereUuid($projectUuid)->firstOrFail(); $project = Project::whereUuid($projectUuid)->firstOrFail();
$this->environments = $project->environments; $this->environments = $project->environments;
$this->selectedEnvironment = $this->environments->where('uuid', data_get($this->parameters, 'environment_uuid'))->firstOrFail()->name; $this->selectedEnvironment = $this->environments->where('uuid', data_get($this->parameters, 'environment_uuid'))->firstOrFail()->name;
// Check if we have all required params for PostgreSQL type selection
// This handles navigation from global search
$queryType = request()->query('type');
$queryServerId = request()->query('server_id');
$queryDestination = request()->query('destination');
if ($queryType === 'postgresql' && $queryServerId !== null && $queryDestination) {
$this->type = $queryType;
$this->server_id = $queryServerId;
$this->destination_uuid = $queryDestination;
$this->server = Server::find($queryServerId);
$this->current_step = 'select-postgresql-type';
}
} catch (\Exception $e) { } catch (\Exception $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View file

@ -35,6 +35,13 @@ public function mount()
if (in_array($type, DATABASE_TYPES)) { if (in_array($type, DATABASE_TYPES)) {
if ($type->value() === 'postgresql') { if ($type->value() === 'postgresql') {
// PostgreSQL requires database_image to be explicitly set
// If not provided, fall through to Select component for version selection
if (! $database_image) {
$this->type = $type->value();
return;
}
$database = create_standalone_postgresql( $database = create_standalone_postgresql(
environmentId: $environment->id, environmentId: $environment->id,
destinationUuid: $destination_uuid, destinationUuid: $destination_uuid,

View file

@ -67,11 +67,6 @@ public function mount()
} }
} }
public function doSomethingWithThisChunkOfOutput($output)
{
$this->outputs .= removeAnsiColors($output);
}
public function instantSave() public function instantSave()
{ {
if (! is_null($this->resource)) { if (! is_null($this->resource)) {
@ -162,23 +157,32 @@ public function getLogs($refresh = false)
$sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command); $sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
} }
} }
if ($refresh) { // Collect new logs into temporary variable first to prevent flickering
$this->outputs = ''; // (avoids clearing output before new data is ready)
} $newOutputs = '';
Process::run($sshCommand, function (string $type, string $output) { Process::run($sshCommand, function (string $type, string $output) use (&$newOutputs) {
$this->doSomethingWithThisChunkOfOutput($output); $newOutputs .= removeAnsiColors($output);
}); });
if ($this->showTimeStamps) { if ($this->showTimeStamps) {
$this->outputs = str($this->outputs)->split('/\n/')->sort(function ($a, $b) { $newOutputs = str($newOutputs)->split('/\n/')->sort(function ($a, $b) {
$a = explode(' ', $a); $a = explode(' ', $a);
$b = explode(' ', $b); $b = explode(' ', $b);
return $a[0] <=> $b[0]; return $a[0] <=> $b[0];
})->join("\n"); })->join("\n");
} }
// Only update outputs after new data is ready (atomic update prevents flicker)
$this->outputs = $newOutputs;
} }
} }
public function copyLogs(): string
{
return sanitizeLogsForExport($this->outputs);
}
public function render() public function render()
{ {
return view('livewire.project.shared.get-logs'); return view('livewire.project.shared.get-logs');

View file

@ -4,7 +4,6 @@
use App\Models\Server; use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection;
use Livewire\Component; use Livewire\Component;
class Resources extends Component class Resources extends Component
@ -15,7 +14,7 @@ class Resources extends Component
public $parameters = []; public $parameters = [];
public Collection $containers; public array $unmanagedContainers = [];
public $activeTab = 'managed'; public $activeTab = 'managed';
@ -64,7 +63,7 @@ public function loadManagedContainers()
{ {
try { try {
$this->activeTab = 'managed'; $this->activeTab = 'managed';
$this->containers = $this->server->refresh()->definedResources(); $this->server->refresh();
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
@ -74,7 +73,7 @@ public function loadUnmanagedContainers()
{ {
$this->activeTab = 'unmanaged'; $this->activeTab = 'unmanaged';
try { try {
$this->containers = $this->server->loadUnmanagedContainers(); $this->unmanagedContainers = $this->server->loadUnmanagedContainers()->toArray();
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
@ -82,14 +81,12 @@ public function loadUnmanagedContainers()
public function mount() public function mount()
{ {
$this->containers = collect();
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
try { try {
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first(); $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) { if (is_null($this->server)) {
return redirect()->route('server.index'); return redirect()->route('server.index');
} }
$this->loadManagedContainers();
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View file

@ -38,6 +38,9 @@ class Advanced extends Component
#[Validate('boolean')] #[Validate('boolean')]
public bool $disable_two_step_confirmation; public bool $disable_two_step_confirmation;
#[Validate('boolean')]
public bool $is_wire_navigate_enabled;
public function rules() public function rules()
{ {
return [ return [
@ -50,6 +53,7 @@ public function rules()
'allowed_ips' => ['nullable', 'string', new ValidIpOrCidr], 'allowed_ips' => ['nullable', 'string', new ValidIpOrCidr],
'is_sponsorship_popup_enabled' => 'boolean', 'is_sponsorship_popup_enabled' => 'boolean',
'disable_two_step_confirmation' => 'boolean', 'disable_two_step_confirmation' => 'boolean',
'is_wire_navigate_enabled' => 'boolean',
]; ];
} }
@ -68,6 +72,7 @@ public function mount()
$this->is_api_enabled = $this->settings->is_api_enabled; $this->is_api_enabled = $this->settings->is_api_enabled;
$this->disable_two_step_confirmation = $this->settings->disable_two_step_confirmation; $this->disable_two_step_confirmation = $this->settings->disable_two_step_confirmation;
$this->is_sponsorship_popup_enabled = $this->settings->is_sponsorship_popup_enabled; $this->is_sponsorship_popup_enabled = $this->settings->is_sponsorship_popup_enabled;
$this->is_wire_navigate_enabled = $this->settings->is_wire_navigate_enabled ?? true;
} }
public function submit() public function submit()
@ -146,6 +151,7 @@ public function instantSave()
$this->settings->allowed_ips = $this->allowed_ips; $this->settings->allowed_ips = $this->allowed_ips;
$this->settings->is_sponsorship_popup_enabled = $this->is_sponsorship_popup_enabled; $this->settings->is_sponsorship_popup_enabled = $this->is_sponsorship_popup_enabled;
$this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation; $this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation;
$this->settings->is_wire_navigate_enabled = $this->is_wire_navigate_enabled;
$this->settings->save(); $this->settings->save();
$this->dispatch('success', 'Settings updated!'); $this->dispatch('success', 'Settings updated!');
} catch (\Exception $e) { } catch (\Exception $e) {

View file

@ -17,11 +17,14 @@ class Upgrade extends Component
public string $currentVersion = ''; public string $currentVersion = '';
public bool $devMode = false;
protected $listeners = ['updateAvailable' => 'checkUpdate']; protected $listeners = ['updateAvailable' => 'checkUpdate'];
public function mount() public function mount()
{ {
$this->currentVersion = config('constants.coolify.version'); $this->currentVersion = config('constants.coolify.version');
$this->devMode = isDev();
} }
public function checkUpdate() public function checkUpdate()

View file

@ -77,21 +77,21 @@ public function generate_preview_fqdn()
if ($this->application->fqdn) { if ($this->application->fqdn) {
if (str($this->application->fqdn)->contains(',')) { if (str($this->application->fqdn)->contains(',')) {
$url = Url::fromString(str($this->application->fqdn)->explode(',')[0]); $url = Url::fromString(str($this->application->fqdn)->explode(',')[0]);
$preview_fqdn = getFqdnWithoutPort(str($this->application->fqdn)->explode(',')[0]);
} else { } else {
$url = Url::fromString($this->application->fqdn); $url = Url::fromString($this->application->fqdn);
if ($this->fqdn) {
$preview_fqdn = getFqdnWithoutPort($this->fqdn);
}
} }
$template = $this->application->preview_url_template; $template = $this->application->preview_url_template;
$host = $url->getHost(); $host = $url->getHost();
$schema = $url->getScheme(); $schema = $url->getScheme();
$portInt = $url->getPort();
$port = $portInt !== null ? ':'.$portInt : '';
$urlPath = $url->getPath();
$path = ($urlPath !== '' && $urlPath !== '/') ? $urlPath : '';
$random = new Cuid2; $random = new Cuid2;
$preview_fqdn = str_replace('{{random}}', $random, $template); $preview_fqdn = str_replace('{{random}}', $random, $template);
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
$preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn); $preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn);
$preview_fqdn = "$schema://$preview_fqdn"; $preview_fqdn = "$schema://$preview_fqdn{$port}{$path}";
$this->fqdn = $preview_fqdn; $this->fqdn = $preview_fqdn;
$this->save(); $this->save();
} }
@ -147,11 +147,13 @@ public function generate_preview_fqdn_compose()
$schema = $url->getScheme(); $schema = $url->getScheme();
$portInt = $url->getPort(); $portInt = $url->getPort();
$port = $portInt !== null ? ':'.$portInt : ''; $port = $portInt !== null ? ':'.$portInt : '';
$urlPath = $url->getPath();
$path = ($urlPath !== '' && $urlPath !== '/') ? $urlPath : '';
$random = new Cuid2; $random = new Cuid2;
$preview_fqdn = str_replace('{{random}}', $random, $template); $preview_fqdn = str_replace('{{random}}', $random, $template);
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
$preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn); $preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn);
$preview_fqdn = "$schema://$preview_fqdn{$port}"; $preview_fqdn = "$schema://$preview_fqdn{$port}{$path}";
$preview_domains[] = $preview_fqdn; $preview_domains[] = $preview_fqdn;
} }

View file

@ -29,6 +29,7 @@ class InstanceSettings extends Model
'auto_update_frequency' => 'string', 'auto_update_frequency' => 'string',
'update_check_frequency' => 'string', 'update_check_frequency' => 'string',
'sentinel_token' => 'encrypted', 'sentinel_token' => 'encrypted',
'is_wire_navigate_enabled' => 'boolean',
]; ];
protected static function booted(): void protected static function booted(): void

View file

@ -18,6 +18,9 @@ class StandaloneClickhouse extends BaseModel
protected $casts = [ protected $casts = [
'clickhouse_password' => 'encrypted', 'clickhouse_password' => 'encrypted',
'restart_count' => 'integer',
'last_restart_at' => 'datetime',
'last_restart_type' => 'string',
]; ];
protected static function booted() protected static function booted()
@ -25,7 +28,7 @@ protected static function booted()
static::created(function ($database) { static::created(function ($database) {
LocalPersistentVolume::create([ LocalPersistentVolume::create([
'name' => 'clickhouse-data-'.$database->uuid, 'name' => 'clickhouse-data-'.$database->uuid,
'mount_path' => '/bitnami/clickhouse', 'mount_path' => '/var/lib/clickhouse',
'host_path' => null, 'host_path' => null,
'resource_id' => $database->id, 'resource_id' => $database->id,
'resource_type' => $database->getMorphClass(), 'resource_type' => $database->getMorphClass(),
@ -246,8 +249,9 @@ protected function internalDbUrl(): Attribute
get: function () { get: function () {
$encodedUser = rawurlencode($this->clickhouse_admin_user); $encodedUser = rawurlencode($this->clickhouse_admin_user);
$encodedPass = rawurlencode($this->clickhouse_admin_password); $encodedPass = rawurlencode($this->clickhouse_admin_password);
$database = $this->clickhouse_db ?? 'default';
return "clickhouse://{$encodedUser}:{$encodedPass}@{$this->uuid}:9000/{$this->clickhouse_db}"; return "clickhouse://{$encodedUser}:{$encodedPass}@{$this->uuid}:9000/{$database}";
}, },
); );
} }
@ -263,8 +267,9 @@ protected function externalDbUrl(): Attribute
} }
$encodedUser = rawurlencode($this->clickhouse_admin_user); $encodedUser = rawurlencode($this->clickhouse_admin_user);
$encodedPass = rawurlencode($this->clickhouse_admin_password); $encodedPass = rawurlencode($this->clickhouse_admin_password);
$database = $this->clickhouse_db ?? 'default';
return "clickhouse://{$encodedUser}:{$encodedPass}@{$serverIp}:{$this->public_port}/{$this->clickhouse_db}"; return "clickhouse://{$encodedUser}:{$encodedPass}@{$serverIp}:{$this->public_port}/{$database}";
} }
return null; return null;

View file

@ -18,6 +18,9 @@ class StandaloneDragonfly extends BaseModel
protected $casts = [ protected $casts = [
'dragonfly_password' => 'encrypted', 'dragonfly_password' => 'encrypted',
'restart_count' => 'integer',
'last_restart_at' => 'datetime',
'last_restart_type' => 'string',
]; ];
protected static function booted() protected static function booted()

View file

@ -18,6 +18,9 @@ class StandaloneKeydb extends BaseModel
protected $casts = [ protected $casts = [
'keydb_password' => 'encrypted', 'keydb_password' => 'encrypted',
'restart_count' => 'integer',
'last_restart_at' => 'datetime',
'last_restart_type' => 'string',
]; ];
protected static function booted() protected static function booted()

View file

@ -19,6 +19,9 @@ class StandaloneMariadb extends BaseModel
protected $casts = [ protected $casts = [
'mariadb_password' => 'encrypted', 'mariadb_password' => 'encrypted',
'restart_count' => 'integer',
'last_restart_at' => 'datetime',
'last_restart_type' => 'string',
]; ];
protected static function booted() protected static function booted()

View file

@ -16,6 +16,12 @@ class StandaloneMongodb extends BaseModel
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
protected $casts = [
'restart_count' => 'integer',
'last_restart_at' => 'datetime',
'last_restart_type' => 'string',
];
protected static function booted() protected static function booted()
{ {
static::created(function ($database) { static::created(function ($database) {

View file

@ -19,6 +19,9 @@ class StandaloneMysql extends BaseModel
protected $casts = [ protected $casts = [
'mysql_password' => 'encrypted', 'mysql_password' => 'encrypted',
'mysql_root_password' => 'encrypted', 'mysql_root_password' => 'encrypted',
'restart_count' => 'integer',
'last_restart_at' => 'datetime',
'last_restart_type' => 'string',
]; ];
protected static function booted() protected static function booted()

View file

@ -19,6 +19,9 @@ class StandalonePostgresql extends BaseModel
protected $casts = [ protected $casts = [
'init_scripts' => 'array', 'init_scripts' => 'array',
'postgres_password' => 'encrypted', 'postgres_password' => 'encrypted',
'restart_count' => 'integer',
'last_restart_at' => 'datetime',
'last_restart_type' => 'string',
]; ];
protected static function booted() protected static function booted()

View file

@ -16,6 +16,12 @@ class StandaloneRedis extends BaseModel
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
protected $casts = [
'restart_count' => 'integer',
'last_restart_at' => 'datetime',
'last_restart_type' => 'string',
];
protected static function booted() protected static function booted()
{ {
static::created(function ($database) { static::created(function ($database) {

View file

@ -269,9 +269,41 @@ function remove_iip($text)
// Ensure the input is valid UTF-8 before processing // Ensure the input is valid UTF-8 before processing
$text = sanitize_utf8_text($text); $text = sanitize_utf8_text($text);
// Git access tokens
$text = preg_replace('/x-access-token:.*?(?=@)/', 'x-access-token:'.REDACTED, $text); $text = preg_replace('/x-access-token:.*?(?=@)/', 'x-access-token:'.REDACTED, $text);
return preg_replace('/\x1b\[[0-9;]*m/', '', $text); // ANSI color codes
$text = preg_replace('/\x1b\[[0-9;]*m/', '', $text);
// Generic URLs with passwords (covers database URLs, ftp, amqp, ssh, etc.)
// (protocol://user:password@host → protocol://user:<REDACTED>@host)
$text = preg_replace('/((?:postgres|mysql|mongodb|rediss?|mariadb|ftp|sftp|ssh|amqp|amqps|ldap|ldaps|s3):\/\/[^:]+:)[^@]+(@)/i', '$1'.REDACTED.'$2', $text);
// Email addresses
$text = preg_replace('/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/', REDACTED, $text);
// Bearer/JWT tokens
$text = preg_replace('/Bearer\s+[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+/i', 'Bearer '.REDACTED, $text);
// GitHub tokens (ghp_ = personal, gho_ = OAuth, ghu_ = user-to-server, ghs_ = server-to-server, ghr_ = refresh)
$text = preg_replace('/\b(gh[pousr]_[A-Za-z0-9_]{36,})\b/', REDACTED, $text);
// GitLab tokens (glpat- = personal access token, glcbt- = CI build token, glrt- = runner token)
$text = preg_replace('/\b(gl(?:pat|cbt|rt)-[A-Za-z0-9\-_]{20,})\b/', REDACTED, $text);
// AWS credentials (Access Key ID starts with AKIA, ABIA, ACCA, ASIA)
$text = preg_replace('/\b(A(?:KIA|BIA|CCA|SIA)[A-Z0-9]{16})\b/', REDACTED, $text);
// AWS Secret Access Key (40 character base64-ish string, typically follows access key)
$text = preg_replace('/(aws_secret_access_key|AWS_SECRET_ACCESS_KEY)[=:]\s*[\'"]?([A-Za-z0-9\/+=]{40})[\'"]?/i', '$1='.REDACTED, $text);
// API keys (common patterns)
$text = preg_replace('/(api[_-]?key|apikey|api[_-]?secret|secret[_-]?key)[=:]\s*[\'"]?[A-Za-z0-9\-_]{16,}[\'"]?/i', '$1='.REDACTED, $text);
// Private key blocks
$text = preg_replace('/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/', REDACTED, $text);
return $text;
} }
/** /**

View file

@ -672,6 +672,12 @@ function removeAnsiColors($text)
return preg_replace('/\e[[][A-Za-z0-9];?[0-9]*m?/', '', $text); return preg_replace('/\e[[][A-Za-z0-9];?[0-9]*m?/', '', $text);
} }
function sanitizeLogsForExport(string $text): string
{
// All sanitization is now handled by remove_iip()
return remove_iip($text);
}
function getTopLevelNetworks(Service|Application $resource) function getTopLevelNetworks(Service|Application $resource)
{ {
if ($resource->getMorphClass() === \App\Models\Service::class) { if ($resource->getMorphClass() === \App\Models\Service::class) {
@ -2916,6 +2922,18 @@ function instanceSettings()
return InstanceSettings::get(); return InstanceSettings::get();
} }
function wireNavigate(): string
{
try {
$settings = instanceSettings();
// Return wire:navigate.hover for SPA navigation with prefetching, or empty string if disabled
return ($settings->is_wire_navigate_enabled ?? true) ? 'wire:navigate.hover' : '';
} catch (\Exception $e) {
return 'wire:navigate.hover';
}
}
function getHelperVersion(): string function getHelperVersion(): string
{ {
$settings = instanceSettings(); $settings = instanceSettings();

View file

@ -2,7 +2,7 @@
return [ return [
'coolify' => [ 'coolify' => [
'version' => '4.0.0-beta.455', 'version' => '4.0.0-beta.456',
'helper_version' => '1.0.12', 'helper_version' => '1.0.12',
'realtime_version' => '1.0.10', 'realtime_version' => '1.0.10',
'self_hosted' => env('SELF_HOSTED', true), 'self_hosted' => env('SELF_HOSTED', true),

View file

@ -0,0 +1,69 @@
<?php
use App\Models\LocalPersistentVolume;
use App\Models\StandaloneClickhouse;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* Migrates existing ClickHouse instances from Bitnami/BinamiLegacy images
* to the official clickhouse/clickhouse-server image.
*/
public function up(): void
{
// Add clickhouse_db column if it doesn't exist
if (! Schema::hasColumn('standalone_clickhouses', 'clickhouse_db')) {
Schema::table('standalone_clickhouses', function (Blueprint $table) {
$table->string('clickhouse_db')
->default('default')
->after('clickhouse_admin_password');
});
}
// Change the default value for the 'image' column to the official image
Schema::table('standalone_clickhouses', function (Blueprint $table) {
$table->string('image')->default('clickhouse/clickhouse-server:25.11')->change();
});
// Update existing ClickHouse instances from Bitnami images to official image
StandaloneClickhouse::where(function ($query) {
$query->where('image', 'like', '%bitnami/clickhouse%')
->orWhere('image', 'like', '%bitnamilegacy/clickhouse%');
})
->update([
'image' => 'clickhouse/clickhouse-server:25.11',
'clickhouse_db' => DB::raw("COALESCE(clickhouse_db, 'default')"),
]);
// Update volume mount paths from Bitnami to official image paths
LocalPersistentVolume::where('resource_type', StandaloneClickhouse::class)
->where('mount_path', '/bitnami/clickhouse')
->update(['mount_path' => '/var/lib/clickhouse']);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
// Revert the default value for the 'image' column
Schema::table('standalone_clickhouses', function (Blueprint $table) {
$table->string('image')->default('bitnamilegacy/clickhouse')->change();
});
// Revert existing ClickHouse instances back to Bitnami image
StandaloneClickhouse::where('image', 'clickhouse/clickhouse-server:25.11')
->update(['image' => 'bitnamilegacy/clickhouse']);
// Revert volume mount paths
LocalPersistentVolume::where('resource_type', StandaloneClickhouse::class)
->where('mount_path', '/var/lib/clickhouse')
->update(['mount_path' => '/bitnami/clickhouse']);
}
};

View file

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('instance_settings', function (Blueprint $table) {
$table->boolean('is_wire_navigate_enabled')->default(true);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('instance_settings', function (Blueprint $table) {
$table->dropColumn('is_wire_navigate_enabled');
});
}
};

View file

@ -0,0 +1,66 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* The standalone database tables to add restart tracking columns to.
*/
private array $tables = [
'standalone_postgresqls',
'standalone_mysqls',
'standalone_mariadbs',
'standalone_redis',
'standalone_mongodbs',
'standalone_keydbs',
'standalone_dragonflies',
'standalone_clickhouses',
];
/**
* Run the migrations.
*/
public function up(): void
{
foreach ($this->tables as $table) {
if (! Schema::hasColumn($table, 'restart_count')) {
Schema::table($table, function (Blueprint $blueprint) {
$blueprint->integer('restart_count')->default(0)->after('status');
});
}
if (! Schema::hasColumn($table, 'last_restart_at')) {
Schema::table($table, function (Blueprint $blueprint) {
$blueprint->timestamp('last_restart_at')->nullable()->after('restart_count');
});
}
if (! Schema::hasColumn($table, 'last_restart_type')) {
Schema::table($table, function (Blueprint $blueprint) {
$blueprint->string('last_restart_type', 10)->nullable()->after('last_restart_at');
});
}
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$columns = ['restart_count', 'last_restart_at', 'last_restart_type'];
foreach ($this->tables as $table) {
foreach ($columns as $column) {
if (Schema::hasColumn($table, $column)) {
Schema::table($table, function (Blueprint $blueprint) use ($column) {
$blueprint->dropColumn($column);
});
}
}
}
}
};

View file

@ -29,9 +29,14 @@ if [ $EUID != 0 ]; then
exit exit
fi fi
echo -e "Welcome to Coolify Installer!" echo ""
echo -e "This script will install everything for you. Sit back and relax." echo "=========================================="
echo -e "Source code: https://github.com/coollabsio/coolify/blob/v4.x/scripts/install.sh" echo " Coolify Installation - ${DATE}"
echo "=========================================="
echo ""
echo "Welcome to Coolify Installer!"
echo "This script will install everything for you. Sit back and relax."
echo "Source code: https://github.com/coollabsio/coolify/blob/v4.x/scripts/install.sh"
# Predefined root user # Predefined root user
ROOT_USERNAME=${ROOT_USERNAME:-} ROOT_USERNAME=${ROOT_USERNAME:-}
@ -242,6 +247,29 @@ getAJoke() {
fi fi
} }
# Helper function to log with timestamp
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
# Helper function to log section headers
log_section() {
echo ""
echo "============================================================"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
echo "============================================================"
}
# Helper function to check if all required packages are installed
all_packages_installed() {
for pkg in curl wget git jq openssl; do
if ! command -v "$pkg" >/dev/null 2>&1; then
return 1
fi
done
return 0
}
# Check if the OS is manjaro, if so, change it to arch # Check if the OS is manjaro, if so, change it to arch
if [ "$OS_TYPE" = "manjaro" ] || [ "$OS_TYPE" = "manjaro-arm" ]; then if [ "$OS_TYPE" = "manjaro" ] || [ "$OS_TYPE" = "manjaro-arm" ]; then
OS_TYPE="arch" OS_TYPE="arch"
@ -288,9 +316,11 @@ if [ "$OS_TYPE" = 'amzn' ]; then
dnf install -y findutils >/dev/null dnf install -y findutils >/dev/null
fi fi
LATEST_VERSION=$(curl -L --silent $CDN/versions.json | grep -i version | xargs | awk '{print $2}' | tr -d ',') # Fetch versions.json once and parse all values from it
LATEST_HELPER_VERSION=$(curl -L --silent $CDN/versions.json | grep -i version | xargs | awk '{print $6}' | tr -d ',') VERSIONS_JSON=$(curl -L --silent $CDN/versions.json)
LATEST_REALTIME_VERSION=$(curl -L --silent $CDN/versions.json | grep -i version | xargs | awk '{print $8}' | tr -d ',') LATEST_VERSION=$(echo "$VERSIONS_JSON" | grep -i version | xargs | awk '{print $2}' | tr -d ',')
LATEST_HELPER_VERSION=$(echo "$VERSIONS_JSON" | grep -i version | xargs | awk '{print $6}' | tr -d ',')
LATEST_REALTIME_VERSION=$(echo "$VERSIONS_JSON" | grep -i version | xargs | awk '{print $8}' | tr -d ',')
if [ -z "$LATEST_HELPER_VERSION" ]; then if [ -z "$LATEST_HELPER_VERSION" ]; then
LATEST_HELPER_VERSION=latest LATEST_HELPER_VERSION=latest
@ -315,7 +345,7 @@ if [ "$1" != "" ]; then
LATEST_VERSION="${LATEST_VERSION#v}" LATEST_VERSION="${LATEST_VERSION#v}"
fi fi
echo -e "---------------------------------------------" echo "---------------------------------------------"
echo "| Operating System | $OS_TYPE $OS_VERSION" echo "| Operating System | $OS_TYPE $OS_VERSION"
echo "| Docker | $DOCKER_VERSION" echo "| Docker | $DOCKER_VERSION"
echo "| Coolify | $LATEST_VERSION" echo "| Coolify | $LATEST_VERSION"
@ -323,46 +353,61 @@ echo "| Helper | $LATEST_HELPER_VERSION"
echo "| Realtime | $LATEST_REALTIME_VERSION" echo "| Realtime | $LATEST_REALTIME_VERSION"
echo "| Docker Pool | $DOCKER_ADDRESS_POOL_BASE (size $DOCKER_ADDRESS_POOL_SIZE)" echo "| Docker Pool | $DOCKER_ADDRESS_POOL_BASE (size $DOCKER_ADDRESS_POOL_SIZE)"
echo "| Registry URL | $REGISTRY_URL" echo "| Registry URL | $REGISTRY_URL"
echo -e "---------------------------------------------\n" echo "---------------------------------------------"
echo -e "1. Installing required packages (curl, wget, git, jq, openssl). " echo ""
case "$OS_TYPE" in log_section "Step 1/9: Installing required packages"
arch) echo "1/9 Installing required packages (curl, wget, git, jq, openssl)..."
pacman -Sy --noconfirm --needed curl wget git jq openssl >/dev/null || true
;;
alpine)
sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
apk update >/dev/null
apk add curl wget git jq openssl >/dev/null
;;
ubuntu | debian | raspbian)
apt-get update -y >/dev/null
apt-get install -y curl wget git jq openssl >/dev/null
;;
centos | fedora | rhel | ol | rocky | almalinux | amzn)
if [ "$OS_TYPE" = "amzn" ]; then
dnf install -y wget git jq openssl >/dev/null
else
if ! command -v dnf >/dev/null; then
yum install -y dnf >/dev/null
fi
if ! command -v curl >/dev/null; then
dnf install -y curl >/dev/null
fi
dnf install -y wget git jq openssl >/dev/null
fi
;;
sles | opensuse-leap | opensuse-tumbleweed)
zypper refresh >/dev/null
zypper install -y curl wget git jq openssl >/dev/null
;;
*)
echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now."
exit
;;
esac
echo -e "2. Check OpenSSH server configuration. " # Track if apt-get update was run to avoid redundant calls later
APT_UPDATED=false
if all_packages_installed; then
log "All required packages already installed, skipping installation"
echo " - All required packages already installed."
else
case "$OS_TYPE" in
arch)
pacman -Sy --noconfirm --needed curl wget git jq openssl >/dev/null || true
;;
alpine)
sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
apk update >/dev/null
apk add curl wget git jq openssl >/dev/null
;;
ubuntu | debian | raspbian)
apt-get update -y >/dev/null
APT_UPDATED=true
apt-get install -y curl wget git jq openssl >/dev/null
;;
centos | fedora | rhel | ol | rocky | almalinux | amzn)
if [ "$OS_TYPE" = "amzn" ]; then
dnf install -y wget git jq openssl >/dev/null
else
if ! command -v dnf >/dev/null; then
yum install -y dnf >/dev/null
fi
if ! command -v curl >/dev/null; then
dnf install -y curl >/dev/null
fi
dnf install -y wget git jq openssl >/dev/null
fi
;;
sles | opensuse-leap | opensuse-tumbleweed)
zypper refresh >/dev/null
zypper install -y curl wget git jq openssl >/dev/null
;;
*)
echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now."
exit
;;
esac
log "Required packages installed successfully"
fi
echo " Done."
log_section "Step 2/9: Checking OpenSSH server configuration"
echo "2/9 Checking OpenSSH server configuration..."
# Detect OpenSSH server # Detect OpenSSH server
SSH_DETECTED=false SSH_DETECTED=false
@ -398,7 +443,10 @@ if [ "$SSH_DETECTED" = "false" ]; then
service sshd start >/dev/null 2>&1 service sshd start >/dev/null 2>&1
;; ;;
ubuntu | debian | raspbian) ubuntu | debian | raspbian)
apt-get update -y >/dev/null if [ "$APT_UPDATED" = false ]; then
apt-get update -y >/dev/null
APT_UPDATED=true
fi
apt-get install -y openssh-server >/dev/null apt-get install -y openssh-server >/dev/null
systemctl enable ssh >/dev/null 2>&1 systemctl enable ssh >/dev/null 2>&1
systemctl start ssh >/dev/null 2>&1 systemctl start ssh >/dev/null 2>&1
@ -465,7 +513,10 @@ install_docker() {
install_docker_manually() { install_docker_manually() {
case "$OS_TYPE" in case "$OS_TYPE" in
"ubuntu" | "debian" | "raspbian") "ubuntu" | "debian" | "raspbian")
apt-get update if [ "$APT_UPDATED" = false ]; then
apt-get update
APT_UPDATED=true
fi
apt-get install -y ca-certificates curl apt-get install -y ca-certificates curl
install -m 0755 -d /etc/apt/keyrings install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/$OS_TYPE/gpg -o /etc/apt/keyrings/docker.asc curl -fsSL https://download.docker.com/linux/$OS_TYPE/gpg -o /etc/apt/keyrings/docker.asc
@ -491,7 +542,8 @@ install_docker_manually() {
echo "Docker installed successfully." echo "Docker installed successfully."
fi fi
} }
echo -e "3. Check Docker Installation. " log_section "Step 3/9: Checking Docker installation"
echo "3/9 Checking Docker installation..."
if ! [ -x "$(command -v docker)" ]; then if ! [ -x "$(command -v docker)" ]; then
echo " - Docker is not installed. Installing Docker. It may take a while." echo " - Docker is not installed. Installing Docker. It may take a while."
getAJoke getAJoke
@ -575,7 +627,8 @@ else
echo " - Docker is installed." echo " - Docker is installed."
fi fi
echo -e "4. Check Docker Configuration. " log_section "Step 4/9: Checking Docker configuration"
echo "4/9 Checking Docker configuration..."
echo " - Network pool configuration: ${DOCKER_ADDRESS_POOL_BASE}/${DOCKER_ADDRESS_POOL_SIZE}" echo " - Network pool configuration: ${DOCKER_ADDRESS_POOL_BASE}/${DOCKER_ADDRESS_POOL_SIZE}"
echo " - To override existing configuration: DOCKER_POOL_FORCE_OVERRIDE=true" echo " - To override existing configuration: DOCKER_POOL_FORCE_OVERRIDE=true"
@ -704,13 +757,38 @@ else
fi fi
fi fi
echo -e "5. Download required files from CDN. " log_section "Step 5/9: Downloading required files from CDN"
curl -fsSL -L $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml echo "5/9 Downloading required files from CDN..."
curl -fsSL -L $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml log "Downloading configuration files in parallel..."
curl -fsSL -L $CDN/.env.production -o /data/coolify/source/.env.production
curl -fsSL -L $CDN/upgrade.sh -o /data/coolify/source/upgrade.sh
echo -e "6. Setting up environment variable file" # Download files in parallel for faster installation
curl -fsSL -L $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml &
PID1=$!
curl -fsSL -L $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml &
PID2=$!
curl -fsSL -L $CDN/.env.production -o /data/coolify/source/.env.production &
PID3=$!
curl -fsSL -L $CDN/upgrade.sh -o /data/coolify/source/upgrade.sh &
PID4=$!
# Wait for all downloads to complete and check for errors
DOWNLOAD_FAILED=false
for PID in $PID1 $PID2 $PID3 $PID4; do
if ! wait $PID; then
DOWNLOAD_FAILED=true
fi
done
if [ "$DOWNLOAD_FAILED" = true ]; then
echo " - ERROR: One or more downloads failed. Please check your network connection."
exit 1
fi
log "All configuration files downloaded successfully"
echo " Done."
log_section "Step 6/9: Setting up environment variable file"
echo "6/9 Setting up environment variable file..."
if [ -f "$ENV_FILE" ]; then if [ -f "$ENV_FILE" ]; then
# If .env exists, create backup # If .env exists, create backup
@ -725,8 +803,11 @@ else
echo " - No .env file found, copying .env.production to .env" echo " - No .env file found, copying .env.production to .env"
cp "/data/coolify/source/.env.production" "$ENV_FILE" cp "/data/coolify/source/.env.production" "$ENV_FILE"
fi fi
log "Environment file setup completed"
echo " Done."
echo -e "7. Checking and updating environment variables if necessary..." log_section "Step 7/9: Checking and updating environment variables"
echo "7/9 Checking and updating environment variables..."
update_env_var() { update_env_var() {
local key="$1" local key="$1"
@ -786,8 +867,11 @@ else
update_env_var "DOCKER_ADDRESS_POOL_SIZE" "$DOCKER_ADDRESS_POOL_SIZE" update_env_var "DOCKER_ADDRESS_POOL_SIZE" "$DOCKER_ADDRESS_POOL_SIZE"
fi fi
fi fi
log "Environment variables check completed"
echo " Done."
echo -e "8. Checking for SSH key for localhost access." log_section "Step 8/9: Checking SSH key for localhost access"
echo "8/9 Checking SSH key for localhost access..."
if [ ! -f ~/.ssh/authorized_keys ]; then if [ ! -f ~/.ssh/authorized_keys ]; then
mkdir -p ~/.ssh mkdir -p ~/.ssh
chmod 700 ~/.ssh chmod 700 ~/.ssh
@ -812,8 +896,11 @@ fi
chown -R 9999:root /data/coolify chown -R 9999:root /data/coolify
chmod -R 700 /data/coolify chmod -R 700 /data/coolify
log "SSH key check completed"
echo " Done."
echo -e "9. Installing Coolify ($LATEST_VERSION)" log_section "Step 9/9: Installing Coolify"
echo "9/9 Installing Coolify ($LATEST_VERSION)..."
echo -e " - It could take a while based on your server's performance, network speed, stars, etc." echo -e " - It could take a while based on your server's performance, network speed, stars, etc."
echo -e " - Please wait." echo -e " - Please wait."
getAJoke getAJoke
@ -824,11 +911,85 @@ else
bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" "${REGISTRY_URL:-ghcr.io}" "true" bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" "${REGISTRY_URL:-ghcr.io}" "true"
fi fi
echo " - Coolify installed successfully." echo " - Coolify installed successfully."
echo " - Waiting for Coolify to be ready..."
echo " - Waiting 20 seconds for Coolify database migrations to complete." # Wait for upgrade.sh background process to complete
getAJoke # upgrade.sh writes status to /data/coolify/source/.upgrade-status
# Status file format: step|message|timestamp
# Step 6 = "Upgrade complete", file deleted 10 seconds after
UPGRADE_STATUS_FILE="/data/coolify/source/.upgrade-status"
MAX_WAIT=180
WAITED=0
SEEN_STATUS_FILE=false
sleep 20 while [ $WAITED -lt $MAX_WAIT ]; do
if [ -f "$UPGRADE_STATUS_FILE" ]; then
SEEN_STATUS_FILE=true
STATUS=$(cat "$UPGRADE_STATUS_FILE" 2>/dev/null | cut -d'|' -f1)
MESSAGE=$(cat "$UPGRADE_STATUS_FILE" 2>/dev/null | cut -d'|' -f2)
if [ "$STATUS" = "6" ]; then
log "Upgrade completed: $MESSAGE"
echo " - Upgrade complete!"
break
elif [ "$STATUS" = "error" ]; then
echo " - ERROR: Upgrade failed: $MESSAGE"
echo " - Please check the upgrade logs: /data/coolify/source/upgrade-*.log"
exit 1
else
if [ $((WAITED % 10)) -eq 0 ]; then
echo " - Upgrade in progress: $MESSAGE (${WAITED}s)"
fi
fi
else
# Status file doesn't exist
if [ "$SEEN_STATUS_FILE" = true ]; then
# We saw the file before, now it's gone = upgrade completed and cleaned up
log "Upgrade status file cleaned up - upgrade complete"
echo " - Upgrade complete!"
break
fi
# Haven't seen status file yet - either very early or upgrade.sh hasn't started
if [ $((WAITED % 10)) -eq 0 ] && [ $WAITED -gt 0 ]; then
echo " - Waiting for upgrade process to start... (${WAITED}s)"
fi
fi
sleep 2
WAITED=$((WAITED + 2))
done
if [ $WAITED -ge $MAX_WAIT ]; then
if [ "$SEEN_STATUS_FILE" = false ]; then
# Never saw status file - fallback to old behavior (wait 20s + health check)
log "Status file not found, using fallback wait"
echo " - Status file not found, waiting 20 seconds..."
sleep 20
else
echo " - ERROR: Upgrade timed out after ${MAX_WAIT}s"
echo " - Please check the upgrade logs: /data/coolify/source/upgrade-*.log"
exit 1
fi
fi
# Final health verification - wait for container to be healthy
echo " - Verifying Coolify is healthy..."
HEALTH_WAIT=60
HEALTH_WAITED=0
while [ $HEALTH_WAITED -lt $HEALTH_WAIT ]; do
HEALTH=$(docker inspect --format='{{.State.Health.Status}}' coolify 2>/dev/null || echo "unknown")
if [ "$HEALTH" = "healthy" ]; then
log "Coolify container is healthy"
echo " - Coolify is ready!"
break
fi
sleep 2
HEALTH_WAITED=$((HEALTH_WAITED + 2))
done
if [ "$HEALTH" != "healthy" ]; then
echo " - ERROR: Coolify container is not healthy after ${HEALTH_WAIT}s. Status: $HEALTH"
echo " - Please check: docker logs coolify"
exit 1
fi
echo -e "\033[0;35m echo -e "\033[0;35m
____ _ _ _ _ _ ____ _ _ _ _ _
/ ___|___ _ __ __ _ _ __ __ _| |_ _ _| | __ _| |_(_) ___ _ __ ___| | / ___|___ _ __ __ _ _ __ __ _| |_ _ _| | __ _| |_(_) ___ _ __ ___| |
@ -838,8 +999,18 @@ echo -e "\033[0;35m
|___/ |___/
\033[0m" \033[0m"
IPV4_PUBLIC_IP=$(curl -4s https://ifconfig.io || true) # Fetch public IPs in parallel for faster completion
IPV6_PUBLIC_IP=$(curl -6s https://ifconfig.io || true) IPV4_TMP=$(mktemp)
IPV6_TMP=$(mktemp)
curl -4s --max-time 5 https://ifconfig.io > "$IPV4_TMP" 2>/dev/null &
IPV4_PID=$!
curl -6s --max-time 5 https://ifconfig.io > "$IPV6_TMP" 2>/dev/null &
IPV6_PID=$!
wait $IPV4_PID 2>/dev/null || true
wait $IPV6_PID 2>/dev/null || true
IPV4_PUBLIC_IP=$(cat "$IPV4_TMP" 2>/dev/null || true)
IPV6_PUBLIC_IP=$(cat "$IPV6_TMP" 2>/dev/null || true)
rm -f "$IPV4_TMP" "$IPV6_TMP"
echo -e "\nYour instance is ready to use!\n" echo -e "\nYour instance is ready to use!\n"
if [ -n "$IPV4_PUBLIC_IP" ]; then if [ -n "$IPV4_PUBLIC_IP" ]; then
@ -864,3 +1035,8 @@ if [ -n "$PRIVATE_IPS" ]; then
fi fi
echo -e "\nWARNING: It is highly recommended to backup your Environment variables file (/data/coolify/source/.env) to a safe location, outside of this server (e.g. into a Password Manager).\n" echo -e "\nWARNING: It is highly recommended to backup your Environment variables file (/data/coolify/source/.env) to a safe location, outside of this server (e.g. into a Password Manager).\n"
log_section "Installation Complete"
log "Coolify installation completed successfully"
log "Version: ${LATEST_VERSION}"
log "Log file: ${INSTALLATION_LOG_WITH_DATE}"

View file

@ -7,27 +7,101 @@ LATEST_HELPER_VERSION=${2:-latest}
REGISTRY_URL=${3:-ghcr.io} REGISTRY_URL=${3:-ghcr.io}
SKIP_BACKUP=${4:-false} SKIP_BACKUP=${4:-false}
ENV_FILE="/data/coolify/source/.env" ENV_FILE="/data/coolify/source/.env"
STATUS_FILE="/data/coolify/source/.upgrade-status"
DATE=$(date +%Y-%m-%d-%H-%M-%S) DATE=$(date +%Y-%m-%d-%H-%M-%S)
LOGFILE="/data/coolify/source/upgrade-${DATE}.log" LOGFILE="/data/coolify/source/upgrade-${DATE}.log"
curl -fsSL $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml # Helper function to log with timestamp
curl -fsSL $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml log() {
curl -fsSL $CDN/.env.production -o /data/coolify/source/.env.production echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >>"$LOGFILE"
}
# Helper function to log section headers
log_section() {
echo "" >>"$LOGFILE"
echo "============================================================" >>"$LOGFILE"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >>"$LOGFILE"
echo "============================================================" >>"$LOGFILE"
}
# Helper function to write upgrade status for API polling
write_status() {
local step="$1"
local message="$2"
echo "${step}|${message}|$(date -Iseconds)" > "$STATUS_FILE"
}
echo ""
echo "=========================================="
echo " Coolify Upgrade - ${DATE}"
echo "=========================================="
echo ""
# Initialize log file with header
echo "============================================================" >>"$LOGFILE"
echo "Coolify Upgrade Log" >>"$LOGFILE"
echo "Started: $(date '+%Y-%m-%d %H:%M:%S')" >>"$LOGFILE"
echo "Target Version: ${LATEST_IMAGE}" >>"$LOGFILE"
echo "Helper Version: ${LATEST_HELPER_VERSION}" >>"$LOGFILE"
echo "Registry URL: ${REGISTRY_URL}" >>"$LOGFILE"
echo "============================================================" >>"$LOGFILE"
log_section "Step 1/6: Downloading configuration files"
write_status "1" "Downloading configuration files"
echo "1/6 Downloading latest configuration files..."
log "Downloading docker-compose.yml from ${CDN}/docker-compose.yml"
curl -fsSL -L $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml
log "Downloading docker-compose.prod.yml from ${CDN}/docker-compose.prod.yml"
curl -fsSL -L $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml
log "Downloading .env.production from ${CDN}/.env.production"
curl -fsSL -L $CDN/.env.production -o /data/coolify/source/.env.production
log "Configuration files downloaded successfully"
echo " Done."
# Extract all images from docker-compose configuration
log "Extracting all images from docker-compose configuration..."
COMPOSE_FILES="-f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml"
# Check if custom compose file exists
if [ -f /data/coolify/source/docker-compose.custom.yml ]; then
COMPOSE_FILES="$COMPOSE_FILES -f /data/coolify/source/docker-compose.custom.yml"
log "Including custom docker-compose.yml in image extraction"
fi
# Get all unique images from docker compose config
# LATEST_IMAGE env var is needed for image substitution in compose files
IMAGES=$(LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file "$ENV_FILE" $COMPOSE_FILES config --images 2>/dev/null | sort -u)
if [ -z "$IMAGES" ]; then
log "ERROR: Failed to extract images from docker-compose files"
write_status "error" "Failed to parse docker-compose configuration"
echo " ERROR: Failed to parse docker-compose configuration. Aborting upgrade."
exit 1
fi
log "Images to pull:"
echo "$IMAGES" | while read img; do log " - $img"; done
# Backup existing .env file before making any changes # Backup existing .env file before making any changes
if [ "$SKIP_BACKUP" != "true" ]; then if [ "$SKIP_BACKUP" != "true" ]; then
if [ -f "$ENV_FILE" ]; then if [ -f "$ENV_FILE" ]; then
echo "Creating backup of existing .env file to .env-$DATE" >>"$LOGFILE" echo " Creating backup of .env file..."
log "Creating backup of .env file to .env-$DATE"
cp "$ENV_FILE" "$ENV_FILE-$DATE" cp "$ENV_FILE" "$ENV_FILE-$DATE"
log "Backup created: ${ENV_FILE}-${DATE}"
else else
echo "No existing .env file found to backup" >>"$LOGFILE" log "WARNING: No existing .env file found to backup"
fi fi
fi fi
echo "Merging .env.production values into .env" >>"$LOGFILE" log_section "Step 2/6: Updating environment configuration"
write_status "2" "Updating environment configuration"
echo ""
echo "2/6 Updating environment configuration..."
log "Merging .env.production values into .env"
awk -F '=' '!seen[$1]++' "$ENV_FILE" /data/coolify/source/.env.production > "$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE" awk -F '=' '!seen[$1]++' "$ENV_FILE" /data/coolify/source/.env.production > "$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE"
echo ".env file merged successfully" >>"$LOGFILE" log "Environment file merged successfully"
update_env_var() { update_env_var() {
local key="$1" local key="$1"
@ -36,73 +110,173 @@ update_env_var() {
# If variable "key=" exists but has no value, update the value of the existing line # If variable "key=" exists but has no value, update the value of the existing line
if grep -q "^${key}=$" "$ENV_FILE"; then if grep -q "^${key}=$" "$ENV_FILE"; then
sed -i "s|^${key}=$|${key}=${value}|" "$ENV_FILE" sed -i "s|^${key}=$|${key}=${value}|" "$ENV_FILE"
echo " - Updated value of ${key} as the current value was empty" >>"$LOGFILE" log "Updated ${key} (was empty)"
# If variable "key=" doesn't exist, append it to the file with value # If variable "key=" doesn't exist, append it to the file with value
elif ! grep -q "^${key}=" "$ENV_FILE"; then elif ! grep -q "^${key}=" "$ENV_FILE"; then
printf '%s=%s\n' "$key" "$value" >>"$ENV_FILE" printf '%s=%s\n' "$key" "$value" >>"$ENV_FILE"
echo " - Added ${key} with default value as the variable was missing" >>"$LOGFILE" log "Added ${key} (was missing)"
fi fi
} }
echo "Checking and updating environment variables if necessary..." >>"$LOGFILE" log "Checking environment variables..."
update_env_var "PUSHER_APP_ID" "$(openssl rand -hex 32)" update_env_var "PUSHER_APP_ID" "$(openssl rand -hex 32)"
update_env_var "PUSHER_APP_KEY" "$(openssl rand -hex 32)" update_env_var "PUSHER_APP_KEY" "$(openssl rand -hex 32)"
update_env_var "PUSHER_APP_SECRET" "$(openssl rand -hex 32)" update_env_var "PUSHER_APP_SECRET" "$(openssl rand -hex 32)"
log "Environment variables check complete"
echo " Done."
# Make sure coolify network exists # Make sure coolify network exists
# It is created when starting Coolify with docker compose # It is created when starting Coolify with docker compose
log "Checking Docker network 'coolify'..."
if ! docker network inspect coolify >/dev/null 2>&1; then if ! docker network inspect coolify >/dev/null 2>&1; then
log "Network 'coolify' does not exist, creating..."
if ! docker network create --attachable --ipv6 coolify 2>/dev/null; then if ! docker network create --attachable --ipv6 coolify 2>/dev/null; then
echo "Failed to create coolify network with ipv6. Trying without ipv6..." log "Failed to create network with IPv6, trying without IPv6..."
docker network create --attachable coolify 2>/dev/null docker network create --attachable coolify 2>/dev/null
log "Network 'coolify' created without IPv6"
else
log "Network 'coolify' created with IPv6 support"
fi fi
else
log "Network 'coolify' already exists"
fi fi
# Check if Docker config file exists # Check if Docker config file exists
DOCKER_CONFIG_MOUNT="" DOCKER_CONFIG_MOUNT=""
if [ -f /root/.docker/config.json ]; then if [ -f /root/.docker/config.json ]; then
DOCKER_CONFIG_MOUNT="-v /root/.docker/config.json:/root/.docker/config.json" DOCKER_CONFIG_MOUNT="-v /root/.docker/config.json:/root/.docker/config.json"
log "Docker config mount enabled: /root/.docker/config.json"
fi fi
# Pull all required images before stopping containers log_section "Step 3/6: Pulling Docker images"
# This ensures we don't take down the system if image pull fails (rate limits, network issues, etc.) write_status "3" "Pulling Docker images"
echo "Pulling required Docker images..." >>"$LOGFILE" echo ""
docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify:${LATEST_IMAGE}" >>"$LOGFILE" 2>&1 || { echo "Failed to pull Coolify image. Aborting upgrade." >>"$LOGFILE"; exit 1; } echo "3/6 Pulling Docker images..."
docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION}" >>"$LOGFILE" 2>&1 || { echo "Failed to pull Coolify helper image. Aborting upgrade." >>"$LOGFILE"; exit 1; } echo " This may take a few minutes depending on your connection."
docker pull postgres:15-alpine >>"$LOGFILE" 2>&1 || { echo "Failed to pull PostgreSQL image. Aborting upgrade." >>"$LOGFILE"; exit 1; }
docker pull redis:7-alpine >>"$LOGFILE" 2>&1 || { echo "Failed to pull Redis image. Aborting upgrade." >>"$LOGFILE"; exit 1; }
# Pull realtime image - version is hardcoded in docker-compose.prod.yml, extract it or use a known version
docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.10" >>"$LOGFILE" 2>&1 || { echo "Failed to pull Coolify realtime image. Aborting upgrade." >>"$LOGFILE"; exit 1; }
echo "All images pulled successfully." >>"$LOGFILE"
# Stop and remove existing Coolify containers to prevent conflicts # Also pull the helper image (not in compose files but needed for upgrade)
# This handles both old installations (project "source") and new ones (project "coolify") HELPER_IMAGE="${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION}"
# Use nohup to ensure the script continues even if SSH connection is lost echo " - Pulling $HELPER_IMAGE..."
echo "Starting container restart sequence (detached)..." >>"$LOGFILE" log "Pulling image: $HELPER_IMAGE"
if docker pull "$HELPER_IMAGE" >>"$LOGFILE" 2>&1; then
log "Successfully pulled $HELPER_IMAGE"
else
log "ERROR: Failed to pull $HELPER_IMAGE"
write_status "error" "Failed to pull $HELPER_IMAGE"
echo " ERROR: Failed to pull $HELPER_IMAGE. Aborting upgrade."
exit 1
fi
# Pull all images from compose config
# Using a for loop to avoid subshell issues with exit
for IMAGE in $IMAGES; do
if [ -n "$IMAGE" ]; then
echo " - Pulling $IMAGE..."
log "Pulling image: $IMAGE"
if docker pull "$IMAGE" >>"$LOGFILE" 2>&1; then
log "Successfully pulled $IMAGE"
else
log "ERROR: Failed to pull $IMAGE"
write_status "error" "Failed to pull $IMAGE"
echo " ERROR: Failed to pull $IMAGE. Aborting upgrade."
exit 1
fi
fi
done
log "All images pulled successfully"
echo " All images pulled successfully."
log_section "Step 4/6: Stopping and restarting containers"
write_status "4" "Stopping containers"
echo ""
echo "4/6 Stopping containers and starting new ones..."
echo " This step will restart all Coolify containers."
echo " Check the log file for details: ${LOGFILE}"
# From this point forward, we need to ensure the script continues even if
# the SSH connection is lost (which happens when coolify container stops)
# We use a subshell with nohup to ensure completion
log "Starting container restart sequence (detached)..."
nohup bash -c " nohup bash -c "
LOGFILE='$LOGFILE' LOGFILE='$LOGFILE'
STATUS_FILE='$STATUS_FILE'
DOCKER_CONFIG_MOUNT='$DOCKER_CONFIG_MOUNT' DOCKER_CONFIG_MOUNT='$DOCKER_CONFIG_MOUNT'
REGISTRY_URL='$REGISTRY_URL' REGISTRY_URL='$REGISTRY_URL'
LATEST_HELPER_VERSION='$LATEST_HELPER_VERSION' LATEST_HELPER_VERSION='$LATEST_HELPER_VERSION'
LATEST_IMAGE='$LATEST_IMAGE' LATEST_IMAGE='$LATEST_IMAGE'
log() {
echo \"[\$(date '+%Y-%m-%d %H:%M:%S')] \$1\" >>\"\$LOGFILE\"
}
write_status() {
echo \"\$1|\$2|\$(date -Iseconds)\" > \"\$STATUS_FILE\"
}
# Stop and remove containers # Stop and remove containers
echo 'Stopping existing Coolify containers...' >>\"\$LOGFILE\"
for container in coolify coolify-db coolify-redis coolify-realtime; do for container in coolify coolify-db coolify-redis coolify-realtime; do
if docker ps -a --format '{{.Names}}' | grep -q \"^\${container}\$\"; then if docker ps -a --format '{{.Names}}' | grep -q \"^\${container}\$\"; then
log \"Stopping container: \${container}\"
docker stop \"\$container\" >>\"\$LOGFILE\" 2>&1 || true docker stop \"\$container\" >>\"\$LOGFILE\" 2>&1 || true
log \"Removing container: \${container}\"
docker rm \"\$container\" >>\"\$LOGFILE\" 2>&1 || true docker rm \"\$container\" >>\"\$LOGFILE\" 2>&1 || true
echo \" - Removed container: \$container\" >>\"\$LOGFILE\" log \"Container \${container} stopped and removed\"
else
log \"Container \${container} not found (skipping)\"
fi fi
done done
log \"Container cleanup complete\"
# Start new containers # Start new containers
echo '' >>\"\$LOGFILE\"
echo '============================================================' >>\"\$LOGFILE\"
log 'Step 5/6: Starting new containers'
echo '============================================================' >>\"\$LOGFILE\"
write_status '5' 'Starting new containers'
if [ -f /data/coolify/source/docker-compose.custom.yml ]; then if [ -f /data/coolify/source/docker-compose.custom.yml ]; then
echo 'docker-compose.custom.yml detected.' >>\"\$LOGFILE\" log 'Using custom docker-compose.yml'
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1 log 'Running docker compose up with custom configuration...'
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1
else else
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1 log 'Using standard docker-compose configuration'
log 'Running docker compose up...'
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1
fi fi
echo 'Upgrade completed.' >>\"\$LOGFILE\" log 'Docker compose up completed'
# Final log entry
echo '' >>\"\$LOGFILE\"
echo '============================================================' >>\"\$LOGFILE\"
log 'Step 6/6: Upgrade complete'
echo '============================================================' >>\"\$LOGFILE\"
write_status '6' 'Upgrade complete'
log 'Coolify upgrade completed successfully'
log \"Version: \${LATEST_IMAGE}\"
echo '' >>\"\$LOGFILE\"
echo '============================================================' >>\"\$LOGFILE\"
echo \"Upgrade completed: \$(date '+%Y-%m-%d %H:%M:%S')\" >>\"\$LOGFILE\"
echo '============================================================' >>\"\$LOGFILE\"
# Clean up status file after a short delay to allow frontend to read completion
sleep 10
rm -f \"\$STATUS_FILE\"
log 'Status file cleaned up'
" >>"$LOGFILE" 2>&1 & " >>"$LOGFILE" 2>&1 &
# Give the background process a moment to start
sleep 2
log "Container restart sequence started in background (PID: $!)"
echo ""
echo "5/6 Containers are being restarted in the background..."
echo "6/6 Upgrade process initiated!"
echo ""
echo "=========================================="
echo " Coolify upgrade to ${LATEST_IMAGE} in progress"
echo "=========================================="
echo ""
echo " The upgrade will continue in the background."
echo " Coolify will be available again shortly."
echo " Log file: ${LOGFILE}"

View file

@ -1,10 +1,10 @@
{ {
"coolify": { "coolify": {
"v4": { "v4": {
"version": "4.0.0-beta.455" "version": "4.0.0-beta.456"
}, },
"nightly": { "nightly": {
"version": "4.0.0-beta.456" "version": "4.0.0-beta.457"
}, },
"helper": { "helper": {
"version": "1.0.12" "version": "1.0.12"
@ -17,13 +17,13 @@
} }
}, },
"traefik": { "traefik": {
"v3.6": "3.6.1", "v3.6": "3.6.5",
"v3.5": "3.5.6", "v3.5": "3.5.6",
"v3.4": "3.4.5", "v3.4": "3.4.5",
"v3.3": "3.3.7", "v3.3": "3.3.7",
"v3.2": "3.2.5", "v3.2": "3.2.5",
"v3.1": "3.1.7", "v3.1": "3.1.7",
"v3.0": "3.0.4", "v3.0": "3.0.4",
"v2.11": "2.11.31" "v2.11": "2.11.32"
} }
} }

12
public/svgs/appflowy.svg Normal file
View file

@ -0,0 +1,12 @@
<svg width="352" height="351" viewBox="0 0 352 351" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M351.694 211.848C341.593 266.47 297.512 314.204 246.499 341.01C240.117 344.355 237.564 346.269 230.367 346.732H333.912C336.243 346.805 338.566 346.41 340.741 345.57C342.917 344.73 344.902 343.462 346.579 341.84C348.255 340.219 349.589 338.278 350.502 336.132C351.415 333.985 351.888 331.678 351.892 329.346V211.848H351.694Z" fill="#F7931E" />
<path d="M238.823 343.613C236.728 343.767 234.625 343.767 232.529 343.613H238.823Z" fill="#FFCE00" />
<path d="M135.699 106.437C134.136 107.713 132.574 108.902 130.967 110.024C104.756 128.466 24.7373 188.283 6.62514 162.534C-11.1349 137.269 7.57146 65.591 53.6331 31.1053C54.5134 30.401 55.3937 29.7848 56.296 29.1466C106.319 -5.99935 143.732 -1.1357 161.822 24.6351C178.812 48.7993 159.797 86.6741 135.699 106.437Z" fill="#8427E0" />
<path d="M331.277 161.796C306.519 179.182 267.455 158.891 248.111 134.023C247.319 133.011 246.57 131.976 245.91 130.964C227.49 104.731 167.674 24.7118 193.422 6.62164C219.171 -11.4685 293.094 8.29421 326.81 56.2925C327.558 57.3489 328.284 58.4933 329.01 59.4396C361.691 108.01 356.542 144.08 331.277 161.796Z" fill="#00B5FF" />
<path d="M298.158 319.655C297.278 320.337 296.42 320.975 295.561 321.569C245.538 356.781 208.126 351.83 190.036 326.103C173.068 301.895 192.038 264.086 216.136 244.301C217.677 243.003 219.284 241.792 220.89 240.67C247.101 222.25 327.12 162.455 345.21 188.182C362.97 213.447 344.264 285.169 298.158 319.655Z" fill="#FFBD00" />
<path d="M158.493 344.09C132.745 362.158 58.8216 342.418 25.1062 294.419C24.402 293.451 23.7638 292.483 23.1036 291.514C-9.79756 242.922 -4.6038 206.653 20.6167 188.915C45.3531 171.529 84.4383 191.798 103.761 216.689C104.553 217.701 105.301 218.714 105.962 219.748C124.47 245.981 184.264 326 158.493 344.09Z" fill="#E3006D" />
<path d="M135.695 106.437C99.9326 122.15 24.0948 154.853 12.695 128.642C3.14372 106.635 20.8377 61.5636 53.6509 31.1053C54.5312 30.401 55.4115 29.7848 56.3138 29.1466C106.315 -5.99935 143.727 -1.1357 161.818 24.6351C178.807 48.7993 159.793 86.6742 135.695 106.437Z" fill="#9327FF" />
<path d="M331.29 161.778C306.531 179.164 267.468 158.873 248.124 134.004C232.036 97.3179 201.402 24.7152 227.04 13.6014C250.148 3.58802 298.543 23.5048 328.935 59.421C361.704 107.992 356.554 144.062 331.29 161.778Z" fill="#00C8FF" />
<path d="M298.158 319.642C297.278 320.324 296.42 320.963 295.561 321.557C245.538 356.769 208.126 351.817 190.036 326.09C173.068 301.882 192.038 264.073 216.136 244.289C251.877 228.553 327.758 195.872 339.158 222.061C348.709 244.09 331.015 289.162 298.158 319.642Z" fill="#FFCE00" />
<path d="M124.838 337.114C101.818 347.105 53.556 327.276 23.1417 291.514C-9.80345 242.922 -4.60969 206.653 20.6108 188.915C45.3472 171.53 84.4324 191.798 103.755 216.689C119.864 253.397 150.477 325.978 124.838 337.114Z" fill="#FB006D" />
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -292,3 +292,31 @@ @utility dz-button {
@utility xterm { @utility xterm {
@apply p-2; @apply p-2;
} }
/* Log line optimization - uses content-visibility for lazy rendering of off-screen log lines */
@utility log-line {
content-visibility: auto;
contain-intrinsic-size: auto 1.5em;
}
/* Search highlight styling for logs */
@utility log-highlight {
@apply bg-warning/40 dark:bg-warning/30;
}
/* Log level color classes */
@utility log-error {
@apply bg-red-500/10 dark:bg-red-500/15;
}
@utility log-warning {
@apply bg-yellow-500/10 dark:bg-yellow-500/15;
}
@utility log-debug {
@apply bg-purple-500/10 dark:bg-purple-500/15;
}
@utility log-info {
@apply bg-blue-500/10 dark:bg-blue-500/15;
}

View file

@ -1,6 +1,6 @@
<div class="flex flex-col items-center justify-center h-32"> <div class="flex flex-col items-center justify-center h-32">
<span class="text-xl font-bold dark:text-white">You have reached the limit of {{ $name }} you can create.</span> <span class="text-xl font-bold dark:text-white">You have reached the limit of {{ $name }} you can create.</span>
<span>Please <a class="dark:text-white underline "href="{{ route('subscription.show') }}">upgrade your <span>Please <a class="dark:text-white underline" {{ wireNavigate() }} href="{{ route('subscription.show') }}">upgrade your
subscription</a> to create more subscription</a> to create more
{{ $name }}.</span> {{ $name }}.</span>
</div> </div>

View file

@ -79,7 +79,7 @@
}"> }">
<div class="flex lg:pt-6 pt-4 pb-4 pl-2"> <div class="flex lg:pt-6 pt-4 pb-4 pl-2">
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<a href="/" class="text-2xl font-bold tracking-wide dark:text-white hover:opacity-80 transition-opacity">Coolify</a> <a href="/" {{ wireNavigate() }} class="text-2xl font-bold tracking-wide dark:text-white hover:opacity-80 transition-opacity">Coolify</a>
<x-version /> <x-version />
</div> </div>
<div> <div>
@ -105,7 +105,7 @@ class="px-1 py-0.5 text-xs font-semibold text-neutral-500 dark:text-neutral-400
<ul role="list" class="flex flex-col h-full space-y-1.5"> <ul role="list" class="flex flex-col h-full space-y-1.5">
@if (isSubscribed() || !isCloud()) @if (isSubscribed() || !isCloud())
<li> <li>
<a title="Dashboard" href="/" <a title="Dashboard" href="/" {{ wireNavigate() }}
class="{{ request()->is('/') ? 'menu-item-active menu-item' : 'menu-item' }}"> class="{{ request()->is('/') ? 'menu-item-active menu-item' : 'menu-item' }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" fill="none" viewBox="0 0 24 24" <svg xmlns="http://www.w3.org/2000/svg" class="icon" fill="none" viewBox="0 0 24 24"
stroke="currentColor"> stroke="currentColor">
@ -116,7 +116,7 @@ class="{{ request()->is('/') ? 'menu-item-active menu-item' : 'menu-item' }}">
</a> </a>
</li> </li>
<li> <li>
<a title="Projects" <a title="Projects" {{ wireNavigate() }}
class="{{ request()->is('project/*') || request()->is('projects') ? 'menu-item menu-item-active' : 'menu-item' }}" class="{{ request()->is('project/*') || request()->is('projects') ? 'menu-item menu-item-active' : 'menu-item' }}"
href="/projects"> href="/projects">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24"
@ -131,7 +131,7 @@ class="{{ request()->is('project/*') || request()->is('projects') ? 'menu-item m
</a> </a>
</li> </li>
<li> <li>
<a title="Servers" <a title="Servers" {{ wireNavigate() }}
class="{{ request()->is('server/*') || request()->is('servers') ? 'menu-item menu-item-active' : 'menu-item' }}" class="{{ request()->is('server/*') || request()->is('servers') ? 'menu-item menu-item-active' : 'menu-item' }}"
href="/servers"> href="/servers">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24"
@ -150,7 +150,7 @@ class="{{ request()->is('server/*') || request()->is('servers') ? 'menu-item men
</li> </li>
<li> <li>
<a title="Sources" <a title="Sources" {{ wireNavigate() }}
class="{{ request()->is('source*') ? 'menu-item-active menu-item' : 'menu-item' }}" class="{{ request()->is('source*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('source.all') }}"> href="{{ route('source.all') }}">
<svg class="icon" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg"> <svg class="icon" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg">
@ -161,7 +161,7 @@ class="{{ request()->is('source*') ? 'menu-item-active menu-item' : 'menu-item'
</a> </a>
</li> </li>
<li> <li>
<a title="Destinations" <a title="Destinations" {{ wireNavigate() }}
class="{{ request()->is('destination*') ? 'menu-item-active menu-item' : 'menu-item' }}" class="{{ request()->is('destination*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('destination.index') }}"> href="{{ route('destination.index') }}">
@ -174,7 +174,7 @@ class="{{ request()->is('destination*') ? 'menu-item-active menu-item' : 'menu-i
</a> </a>
</li> </li>
<li> <li>
<a title="S3 Storages" <a title="S3 Storages" {{ wireNavigate() }}
class="{{ request()->is('storages*') ? 'menu-item-active menu-item' : 'menu-item' }}" class="{{ request()->is('storages*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('storage.index') }}"> href="{{ route('storage.index') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24"> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
@ -189,7 +189,7 @@ class="{{ request()->is('storages*') ? 'menu-item-active menu-item' : 'menu-item
</a> </a>
</li> </li>
<li> <li>
<a title="Shared variables" <a title="Shared variables" {{ wireNavigate() }}
class="{{ request()->is('shared-variables*') ? 'menu-item-active menu-item' : 'menu-item' }}" class="{{ request()->is('shared-variables*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('shared-variables.index') }}"> href="{{ route('shared-variables.index') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24"> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
@ -204,7 +204,7 @@ class="{{ request()->is('shared-variables*') ? 'menu-item-active menu-item' : 'm
</a> </a>
</li> </li>
<li> <li>
<a title="Notifications" <a title="Notifications" {{ wireNavigate() }}
class="{{ request()->is('notifications*') ? 'menu-item-active menu-item' : 'menu-item' }}" class="{{ request()->is('notifications*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('notifications.email') }}"> href="{{ route('notifications.email') }}">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
@ -216,7 +216,7 @@ class="{{ request()->is('notifications*') ? 'menu-item-active menu-item' : 'menu
</a> </a>
</li> </li>
<li> <li>
<a title="Keys & Tokens" <a title="Keys & Tokens" {{ wireNavigate() }}
class="{{ request()->is('security*') ? 'menu-item-active menu-item' : 'menu-item' }}" class="{{ request()->is('security*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('security.private-key.index') }}"> href="{{ route('security.private-key.index') }}">
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
@ -228,7 +228,7 @@ class="{{ request()->is('security*') ? 'menu-item-active menu-item' : 'menu-item
</a> </a>
</li> </li>
<li> <li>
<a title="Tags" <a title="Tags" {{ wireNavigate() }}
class="{{ request()->is('tags*') ? 'menu-item-active menu-item' : 'menu-item' }}" class="{{ request()->is('tags*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('tags.show') }}"> href="{{ route('tags.show') }}">
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
@ -259,7 +259,7 @@ class="{{ request()->is('terminal*') ? 'menu-item-active menu-item' : 'menu-item
</li> </li>
@endcan @endcan
<li> <li>
<a title="Profile" <a title="Profile" {{ wireNavigate() }}
class="{{ request()->is('profile*') ? 'menu-item-active menu-item' : 'menu-item' }}" class="{{ request()->is('profile*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('profile') }}"> href="{{ route('profile') }}">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
@ -274,7 +274,7 @@ class="{{ request()->is('profile*') ? 'menu-item-active menu-item' : 'menu-item'
</a> </a>
</li> </li>
<li> <li>
<a title="Teams" <a title="Teams" {{ wireNavigate() }}
class="{{ request()->is('team*') ? 'menu-item-active menu-item' : 'menu-item' }}" class="{{ request()->is('team*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('team.index') }}"> href="{{ route('team.index') }}">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
@ -293,7 +293,7 @@ class="{{ request()->is('team*') ? 'menu-item-active menu-item' : 'menu-item' }}
</li> </li>
@if (isCloud() && auth()->user()->isAdmin()) @if (isCloud() && auth()->user()->isAdmin())
<li> <li>
<a title="Subscription" <a title="Subscription" {{ wireNavigate() }}
class="{{ request()->is('subscription*') ? 'menu-item-active menu-item' : 'menu-item' }}" class="{{ request()->is('subscription*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('subscription.show') }}"> href="{{ route('subscription.show') }}">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
@ -308,7 +308,7 @@ class="{{ request()->is('subscription*') ? 'menu-item-active menu-item' : 'menu-
@if (isInstanceAdmin()) @if (isInstanceAdmin())
<li> <li>
<a title="Settings" <a title="Settings" {{ wireNavigate() }}
class="{{ request()->is('settings*') ? 'menu-item-active menu-item' : 'menu-item' }}" class="{{ request()->is('settings*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="/settings"> href="/settings">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
@ -327,7 +327,7 @@ class="{{ request()->is('settings*') ? 'menu-item-active menu-item' : 'menu-item
@if (isCloud() || isDev()) @if (isCloud() || isDev())
@if (isInstanceAdmin() || session('impersonating')) @if (isInstanceAdmin() || session('impersonating'))
<li> <li>
<a title="Admin" class="menu-item" href="/admin"> <a title="Admin" class="menu-item" href="/admin" {{ wireNavigate() }}>
<svg class="text-pink-500 icon" viewBox="0 0 256 256" <svg class="text-pink-500 icon" viewBox="0 0 256 256"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" <path fill="currentColor"

View file

@ -3,27 +3,27 @@
<div class="subtitle">Get notified about your infrastructure.</div> <div class="subtitle">Get notified about your infrastructure.</div>
<div class="navbar-main"> <div class="navbar-main">
<nav class="flex items-center gap-6 min-h-10"> <nav class="flex items-center gap-6 min-h-10">
<a class="{{ request()->routeIs('notifications.email') ? 'dark:text-white' : '' }}" <a class="{{ request()->routeIs('notifications.email') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('notifications.email') }}"> href="{{ route('notifications.email') }}">
<button>Email</button> <button>Email</button>
</a> </a>
<a class="{{ request()->routeIs('notifications.discord') ? 'dark:text-white' : '' }}" <a class="{{ request()->routeIs('notifications.discord') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('notifications.discord') }}"> href="{{ route('notifications.discord') }}">
<button>Discord</button> <button>Discord</button>
</a> </a>
<a class="{{ request()->routeIs('notifications.telegram') ? 'dark:text-white' : '' }}" <a class="{{ request()->routeIs('notifications.telegram') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('notifications.telegram') }}"> href="{{ route('notifications.telegram') }}">
<button>Telegram</button> <button>Telegram</button>
</a> </a>
<a class="{{ request()->routeIs('notifications.slack') ? 'dark:text-white' : '' }}" <a class="{{ request()->routeIs('notifications.slack') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('notifications.slack') }}"> href="{{ route('notifications.slack') }}">
<button>Slack</button> <button>Slack</button>
</a> </a>
<a class="{{ request()->routeIs('notifications.pushover') ? 'dark:text-white' : '' }}" <a class="{{ request()->routeIs('notifications.pushover') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('notifications.pushover') }}"> href="{{ route('notifications.pushover') }}">
<button>Pushover</button> <button>Pushover</button>
</a> </a>
<a class="{{ request()->routeIs('notifications.webhook') ? 'dark:text-white' : '' }}" <a class="{{ request()->routeIs('notifications.webhook') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('notifications.webhook') }}"> href="{{ route('notifications.webhook') }}">
<button>Webhook</button> <button>Webhook</button>
</a> </a>

View file

@ -18,7 +18,7 @@
<!-- Project Level --> <!-- Project Level -->
<li class="inline-flex items-center" x-data="{ projectOpen: false, closeTimeout: null, toggle() { this.projectOpen = !this.projectOpen }, open() { clearTimeout(this.closeTimeout); this.projectOpen = true }, close() { this.closeTimeout = setTimeout(() => { this.projectOpen = false }, 100) } }"> <li class="inline-flex items-center" x-data="{ projectOpen: false, closeTimeout: null, toggle() { this.projectOpen = !this.projectOpen }, open() { clearTimeout(this.closeTimeout); this.projectOpen = true }, close() { this.closeTimeout = setTimeout(() => { this.projectOpen = false }, 100) } }">
<div class="flex items-center relative" @mouseenter="open()" @mouseleave="close()"> <div class="flex items-center relative" @mouseenter="open()" @mouseleave="close()">
<a class="text-xs truncate lg:text-sm hover:text-warning" <a class="text-xs truncate lg:text-sm hover:text-warning" {{ wireNavigate() }}
href="{{ route('project.show', ['project_uuid' => $currentProjectUuid]) }}"> href="{{ route('project.show', ['project_uuid' => $currentProjectUuid]) }}">
{{ data_get($resource, 'environment.project.name', 'Undefined Name') }} {{ data_get($resource, 'environment.project.name', 'Undefined Name') }}
</a> </a>
@ -36,7 +36,7 @@
x-transition:leave-end="opacity-0 scale-95" x-transition:leave-end="opacity-0 scale-95"
class="absolute z-20 top-full mt-1 w-56 -ml-2 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 border border-neutral-200 dark:border-coolgray-200 max-h-96 overflow-y-auto scrollbar"> class="absolute z-20 top-full mt-1 w-56 -ml-2 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 border border-neutral-200 dark:border-coolgray-200 max-h-96 overflow-y-auto scrollbar">
@foreach ($projects as $project) @foreach ($projects as $project)
<a href="{{ route('project.show', ['project_uuid' => $project->uuid]) }}" <a href="{{ route('project.show', ['project_uuid' => $project->uuid]) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200 {{ $project->uuid === $currentProjectUuid ? 'dark:text-warning font-semibold' : '' }}" class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200 {{ $project->uuid === $currentProjectUuid ? 'dark:text-warning font-semibold' : '' }}"
title="{{ $project->name }}"> title="{{ $project->name }}">
{{ $project->name }} {{ $project->name }}
@ -50,7 +50,7 @@ class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolg
<li class="inline-flex items-center" x-data="{ envOpen: false, activeEnv: null, envPositions: {}, activeRes: null, resPositions: {}, activeMenuEnv: null, menuPositions: {}, closeTimeout: null, envTimeout: null, resTimeout: null, menuTimeout: null, toggle() { this.envOpen = !this.envOpen; if (!this.envOpen) { this.activeEnv = null; this.activeRes = null; this.activeMenuEnv = null; } }, open() { clearTimeout(this.closeTimeout); this.envOpen = true }, close() { this.closeTimeout = setTimeout(() => { this.envOpen = false; this.activeEnv = null; this.activeRes = null; this.activeMenuEnv = null; }, 100) }, openEnv(id) { clearTimeout(this.closeTimeout); clearTimeout(this.envTimeout); this.activeEnv = id }, closeEnv() { this.envTimeout = setTimeout(() => { this.activeEnv = null; this.activeRes = null; this.activeMenuEnv = null; }, 100) }, openRes(id) { clearTimeout(this.envTimeout); clearTimeout(this.resTimeout); this.activeRes = id }, closeRes() { this.resTimeout = setTimeout(() => { this.activeRes = null; this.activeMenuEnv = null; }, 100) }, openMenu(id) { clearTimeout(this.resTimeout); clearTimeout(this.menuTimeout); this.activeMenuEnv = id }, closeMenu() { this.menuTimeout = setTimeout(() => { this.activeMenuEnv = null; }, 100) } }"> <li class="inline-flex items-center" x-data="{ envOpen: false, activeEnv: null, envPositions: {}, activeRes: null, resPositions: {}, activeMenuEnv: null, menuPositions: {}, closeTimeout: null, envTimeout: null, resTimeout: null, menuTimeout: null, toggle() { this.envOpen = !this.envOpen; if (!this.envOpen) { this.activeEnv = null; this.activeRes = null; this.activeMenuEnv = null; } }, open() { clearTimeout(this.closeTimeout); this.envOpen = true }, close() { this.closeTimeout = setTimeout(() => { this.envOpen = false; this.activeEnv = null; this.activeRes = null; this.activeMenuEnv = null; }, 100) }, openEnv(id) { clearTimeout(this.closeTimeout); clearTimeout(this.envTimeout); this.activeEnv = id }, closeEnv() { this.envTimeout = setTimeout(() => { this.activeEnv = null; this.activeRes = null; this.activeMenuEnv = null; }, 100) }, openRes(id) { clearTimeout(this.envTimeout); clearTimeout(this.resTimeout); this.activeRes = id }, closeRes() { this.resTimeout = setTimeout(() => { this.activeRes = null; this.activeMenuEnv = null; }, 100) }, openMenu(id) { clearTimeout(this.resTimeout); clearTimeout(this.menuTimeout); this.activeMenuEnv = id }, closeMenu() { this.menuTimeout = setTimeout(() => { this.activeMenuEnv = null; }, 100) } }">
<div class="flex items-center relative" @mouseenter="open()" <div class="flex items-center relative" @mouseenter="open()"
@mouseleave="close()"> @mouseleave="close()">
<a class="text-xs truncate lg:text-sm hover:text-warning" <a class="text-xs truncate lg:text-sm hover:text-warning" {{ wireNavigate() }}
href="{{ route('project.resource.index', [ href="{{ route('project.resource.index', [
'environment_uuid' => $currentEnvironmentUuid, 'environment_uuid' => $currentEnvironmentUuid,
'project_uuid' => $currentProjectUuid, 'project_uuid' => $currentProjectUuid,
@ -96,7 +96,7 @@ class="relative w-48 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 bor
<a href="{{ route('project.resource.index', [ <a href="{{ route('project.resource.index', [
'environment_uuid' => $environment->uuid, 'environment_uuid' => $environment->uuid,
'project_uuid' => $currentProjectUuid, 'project_uuid' => $currentProjectUuid,
]) }}" ]) }}" {{ wireNavigate() }}
class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200 {{ $environment->uuid === $currentEnvironmentUuid ? 'dark:text-warning font-semibold' : '' }}" class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200 {{ $environment->uuid === $currentEnvironmentUuid ? 'dark:text-warning font-semibold' : '' }}"
title="{{ $environment->name }}"> title="{{ $environment->name }}">
<span class="truncate">{{ $environment->name }}</span> <span class="truncate">{{ $environment->name }}</span>
@ -111,7 +111,7 @@ class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutra
</div> </div>
@endforeach @endforeach
<div class="border-t border-neutral-200 dark:border-coolgray-200 mt-1 pt-1"> <div class="border-t border-neutral-200 dark:border-coolgray-200 mt-1 pt-1">
<a href="{{ route('project.show', ['project_uuid' => $currentProjectUuid]) }}" <a href="{{ route('project.show', ['project_uuid' => $currentProjectUuid]) }}" {{ wireNavigate() }}
class="flex items-center gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200"> class="flex items-center gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
@ -178,7 +178,7 @@ class="relative w-48 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 bor
@endphp @endphp
<div @mouseenter="openRes('{{ $environment->uuid }}-{{ $res->uuid }}'); resPositions['{{ $environment->uuid }}-{{ $res->uuid }}'] = $el.offsetTop - ($el.closest('.overflow-y-auto')?.scrollTop || 0)" <div @mouseenter="openRes('{{ $environment->uuid }}-{{ $res->uuid }}'); resPositions['{{ $environment->uuid }}-{{ $res->uuid }}'] = $el.offsetTop - ($el.closest('.overflow-y-auto')?.scrollTop || 0)"
@mouseleave="closeRes()"> @mouseleave="closeRes()">
<a href="{{ $resRoute }}" <a href="{{ $resRoute }}" {{ wireNavigate() }}
class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200 {{ $isCurrentResource ? 'dark:text-warning font-semibold' : '' }}" class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200 {{ $isCurrentResource ? 'dark:text-warning font-semibold' : '' }}"
title="{{ $res->name }}{{ $resServerName ? ' ('.$resServerName.')' : '' }}"> title="{{ $res->name }}{{ $resServerName ? ' ('.$resServerName.')' : '' }}">
<span class="truncate">{{ $res->name }}@if($resServerName) <span class="text-xs text-neutral-400">({{ $resServerName }})</span>@endif</span> <span class="truncate">{{ $res->name }}@if($resServerName) <span class="text-xs text-neutral-400">({{ $resServerName }})</span>@endif</span>
@ -223,7 +223,7 @@ class="relative w-48 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 bor
@if ($resType === 'application') @if ($resType === 'application')
<div @mouseenter="openMenu('{{ $resKey }}-config'); menuPositions['{{ $resKey }}-config'] = $el.offsetTop - ($el.closest('.overflow-y-auto')?.scrollTop || 0)" <div @mouseenter="openMenu('{{ $resKey }}-config'); menuPositions['{{ $resKey }}-config'] = $el.offsetTop - ($el.closest('.overflow-y-auto')?.scrollTop || 0)"
@mouseleave="closeMenu()"> @mouseleave="closeMenu()">
<a href="{{ route('project.application.configuration', $resParams) }}" <a href="{{ route('project.application.configuration', $resParams) }}" {{ wireNavigate() }}
class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200"> class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
<span>Configuration</span> <span>Configuration</span>
<svg class="w-3 h-3 shrink-0" fill="none" <svg class="w-3 h-3 shrink-0" fill="none"
@ -233,9 +233,9 @@ class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutra
</svg> </svg>
</a> </a>
</div> </div>
<a href="{{ route('project.application.deployment.index', $resParams) }}" <a href="{{ route('project.application.deployment.index', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">Deployments</a> class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">Deployments</a>
<a href="{{ route('project.application.logs', $resParams) }}" <a href="{{ route('project.application.logs', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">Logs</a> class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">Logs</a>
@can('canAccessTerminal') @can('canAccessTerminal')
<a href="{{ route('project.application.command', $resParams) }}" <a href="{{ route('project.application.command', $resParams) }}"
@ -244,7 +244,7 @@ class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
@elseif ($resType === 'service') @elseif ($resType === 'service')
<div @mouseenter="openMenu('{{ $resKey }}-config'); menuPositions['{{ $resKey }}-config'] = $el.offsetTop - ($el.closest('.overflow-y-auto')?.scrollTop || 0)" <div @mouseenter="openMenu('{{ $resKey }}-config'); menuPositions['{{ $resKey }}-config'] = $el.offsetTop - ($el.closest('.overflow-y-auto')?.scrollTop || 0)"
@mouseleave="closeMenu()"> @mouseleave="closeMenu()">
<a href="{{ route('project.service.configuration', $resParams) }}" <a href="{{ route('project.service.configuration', $resParams) }}" {{ wireNavigate() }}
class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200"> class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
<span>Configuration</span> <span>Configuration</span>
<svg class="w-3 h-3 shrink-0" fill="none" <svg class="w-3 h-3 shrink-0" fill="none"
@ -254,7 +254,7 @@ class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutra
</svg> </svg>
</a> </a>
</div> </div>
<a href="{{ route('project.service.logs', $resParams) }}" <a href="{{ route('project.service.logs', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">Logs</a> class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">Logs</a>
@can('canAccessTerminal') @can('canAccessTerminal')
<a href="{{ route('project.service.command', $resParams) }}" <a href="{{ route('project.service.command', $resParams) }}"
@ -263,7 +263,7 @@ class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
@else @else
<div @mouseenter="openMenu('{{ $resKey }}-config'); menuPositions['{{ $resKey }}-config'] = $el.offsetTop - ($el.closest('.overflow-y-auto')?.scrollTop || 0)" <div @mouseenter="openMenu('{{ $resKey }}-config'); menuPositions['{{ $resKey }}-config'] = $el.offsetTop - ($el.closest('.overflow-y-auto')?.scrollTop || 0)"
@mouseleave="closeMenu()"> @mouseleave="closeMenu()">
<a href="{{ route('project.database.configuration', $resParams) }}" <a href="{{ route('project.database.configuration', $resParams) }}" {{ wireNavigate() }}
class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200"> class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
<span>Configuration</span> <span>Configuration</span>
<svg class="w-3 h-3 shrink-0" fill="none" <svg class="w-3 h-3 shrink-0" fill="none"
@ -273,7 +273,7 @@ class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutra
</svg> </svg>
</a> </a>
</div> </div>
<a href="{{ route('project.database.logs', $resParams) }}" <a href="{{ route('project.database.logs', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">Logs</a> class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">Logs</a>
@can('canAccessTerminal') @can('canAccessTerminal')
<a href="{{ route('project.database.command', $resParams) }}" <a href="{{ route('project.database.command', $resParams) }}"
@ -284,7 +284,7 @@ class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
$res->getMorphClass() === 'App\Models\StandaloneMongodb' || $res->getMorphClass() === 'App\Models\StandaloneMongodb' ||
$res->getMorphClass() === 'App\Models\StandaloneMysql' || $res->getMorphClass() === 'App\Models\StandaloneMysql' ||
$res->getMorphClass() === 'App\Models\StandaloneMariadb') $res->getMorphClass() === 'App\Models\StandaloneMariadb')
<a href="{{ route('project.database.backup.index', $resParams) }}" <a href="{{ route('project.database.backup.index', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">Backups</a> class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">Backups</a>
@endif @endif
@endif @endif
@ -300,90 +300,90 @@ class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
class="pl-1"> class="pl-1">
<div class="w-52 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 border border-neutral-200 dark:border-coolgray-200 max-h-96 overflow-y-auto scrollbar"> <div class="w-52 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 border border-neutral-200 dark:border-coolgray-200 max-h-96 overflow-y-auto scrollbar">
@if ($resType === 'application') @if ($resType === 'application')
<a href="{{ route('project.application.configuration', $resParams) }}" <a href="{{ route('project.application.configuration', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">General</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">General</a>
<a href="{{ route('project.application.environment-variables', $resParams) }}" <a href="{{ route('project.application.environment-variables', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Environment class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Environment
Variables</a> Variables</a>
<a href="{{ route('project.application.persistent-storage', $resParams) }}" <a href="{{ route('project.application.persistent-storage', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Persistent class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Persistent
Storage</a> Storage</a>
<a href="{{ route('project.application.source', $resParams) }}" <a href="{{ route('project.application.source', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Source</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Source</a>
<a href="{{ route('project.application.servers', $resParams) }}" <a href="{{ route('project.application.servers', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Servers</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Servers</a>
<a href="{{ route('project.application.scheduled-tasks.show', $resParams) }}" <a href="{{ route('project.application.scheduled-tasks.show', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Scheduled class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Scheduled
Tasks</a> Tasks</a>
<a href="{{ route('project.application.webhooks', $resParams) }}" <a href="{{ route('project.application.webhooks', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Webhooks</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Webhooks</a>
<a href="{{ route('project.application.preview-deployments', $resParams) }}" <a href="{{ route('project.application.preview-deployments', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Preview class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Preview
Deployments</a> Deployments</a>
<a href="{{ route('project.application.healthcheck', $resParams) }}" <a href="{{ route('project.application.healthcheck', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Healthcheck</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Healthcheck</a>
<a href="{{ route('project.application.rollback', $resParams) }}" <a href="{{ route('project.application.rollback', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Rollback</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Rollback</a>
<a href="{{ route('project.application.resource-limits', $resParams) }}" <a href="{{ route('project.application.resource-limits', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Resource class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Resource
Limits</a> Limits</a>
<a href="{{ route('project.application.resource-operations', $resParams) }}" <a href="{{ route('project.application.resource-operations', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Resource class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Resource
Operations</a> Operations</a>
<a href="{{ route('project.application.metrics', $resParams) }}" <a href="{{ route('project.application.metrics', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Metrics</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Metrics</a>
<a href="{{ route('project.application.tags', $resParams) }}" <a href="{{ route('project.application.tags', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Tags</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Tags</a>
<a href="{{ route('project.application.advanced', $resParams) }}" <a href="{{ route('project.application.advanced', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Advanced</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Advanced</a>
<a href="{{ route('project.application.danger', $resParams) }}" <a href="{{ route('project.application.danger', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200 text-red-500">Danger class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200 text-red-500">Danger
Zone</a> Zone</a>
@elseif ($resType === 'service') @elseif ($resType === 'service')
<a href="{{ route('project.service.configuration', $resParams) }}" <a href="{{ route('project.service.configuration', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">General</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">General</a>
<a href="{{ route('project.service.environment-variables', $resParams) }}" <a href="{{ route('project.service.environment-variables', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Environment class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Environment
Variables</a> Variables</a>
<a href="{{ route('project.service.storages', $resParams) }}" <a href="{{ route('project.service.storages', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Storages</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Storages</a>
<a href="{{ route('project.service.scheduled-tasks.show', $resParams) }}" <a href="{{ route('project.service.scheduled-tasks.show', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Scheduled class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Scheduled
Tasks</a> Tasks</a>
<a href="{{ route('project.service.webhooks', $resParams) }}" <a href="{{ route('project.service.webhooks', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Webhooks</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Webhooks</a>
<a href="{{ route('project.service.resource-operations', $resParams) }}" <a href="{{ route('project.service.resource-operations', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Resource class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Resource
Operations</a> Operations</a>
<a href="{{ route('project.service.tags', $resParams) }}" <a href="{{ route('project.service.tags', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Tags</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Tags</a>
<a href="{{ route('project.service.danger', $resParams) }}" <a href="{{ route('project.service.danger', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200 text-red-500">Danger class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200 text-red-500">Danger
Zone</a> Zone</a>
@else @else
<a href="{{ route('project.database.configuration', $resParams) }}" <a href="{{ route('project.database.configuration', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">General</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">General</a>
<a href="{{ route('project.database.environment-variables', $resParams) }}" <a href="{{ route('project.database.environment-variables', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Environment class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Environment
Variables</a> Variables</a>
<a href="{{ route('project.database.servers', $resParams) }}" <a href="{{ route('project.database.servers', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Servers</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Servers</a>
<a href="{{ route('project.database.persistent-storage', $resParams) }}" <a href="{{ route('project.database.persistent-storage', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Persistent class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Persistent
Storage</a> Storage</a>
<a href="{{ route('project.database.webhooks', $resParams) }}" <a href="{{ route('project.database.webhooks', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Webhooks</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Webhooks</a>
<a href="{{ route('project.database.resource-limits', $resParams) }}" <a href="{{ route('project.database.resource-limits', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Resource class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Resource
Limits</a> Limits</a>
<a href="{{ route('project.database.resource-operations', $resParams) }}" <a href="{{ route('project.database.resource-operations', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Resource class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Resource
Operations</a> Operations</a>
<a href="{{ route('project.database.metrics', $resParams) }}" <a href="{{ route('project.database.metrics', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Metrics</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Metrics</a>
<a href="{{ route('project.database.tags', $resParams) }}" <a href="{{ route('project.database.tags', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Tags</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Tags</a>
<a href="{{ route('project.database.danger', $resParams) }}" <a href="{{ route('project.database.danger', $resParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200 text-red-500">Danger class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200 text-red-500">Danger
Zone</a> Zone</a>
@endif @endif
@ -422,7 +422,7 @@ class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolg
<li class="inline-flex items-center" x-data="{ resourceOpen: false, activeMenu: null, menuPosition: 0, closeTimeout: null, menuTimeout: null, toggle() { this.resourceOpen = !this.resourceOpen; if (!this.resourceOpen) { this.activeMenu = null; } }, open() { clearTimeout(this.closeTimeout); this.resourceOpen = true }, close() { this.closeTimeout = setTimeout(() => { this.resourceOpen = false; this.activeMenu = null; }, 100) }, openMenu(id) { clearTimeout(this.closeTimeout); clearTimeout(this.menuTimeout); this.activeMenu = id }, closeMenu() { this.menuTimeout = setTimeout(() => { this.activeMenu = null; }, 100) } }"> <li class="inline-flex items-center" x-data="{ resourceOpen: false, activeMenu: null, menuPosition: 0, closeTimeout: null, menuTimeout: null, toggle() { this.resourceOpen = !this.resourceOpen; if (!this.resourceOpen) { this.activeMenu = null; } }, open() { clearTimeout(this.closeTimeout); this.resourceOpen = true }, close() { this.closeTimeout = setTimeout(() => { this.resourceOpen = false; this.activeMenu = null; }, 100) }, openMenu(id) { clearTimeout(this.closeTimeout); clearTimeout(this.menuTimeout); this.activeMenu = id }, closeMenu() { this.menuTimeout = setTimeout(() => { this.activeMenu = null; }, 100) } }">
<div class="flex items-center relative" @mouseenter="open()" <div class="flex items-center relative" @mouseenter="open()"
@mouseleave="close()"> @mouseleave="close()">
<a class="text-xs truncate lg:text-sm hover:text-warning" <a class="text-xs truncate lg:text-sm hover:text-warning" {{ wireNavigate() }}
href="{{ $isApplication href="{{ $isApplication
? route('project.application.configuration', $routeParams) ? route('project.application.configuration', $routeParams)
: ($isService : ($isService
@ -451,7 +451,7 @@ class="relative w-48 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 bor
@if ($isApplication) @if ($isApplication)
<!-- Application Main Menus --> <!-- Application Main Menus -->
<div @mouseenter="openMenu('config'); menuPosition = $el.offsetTop" @mouseleave="closeMenu()"> <div @mouseenter="openMenu('config'); menuPosition = $el.offsetTop" @mouseleave="closeMenu()">
<a href="{{ route('project.application.configuration', $routeParams) }}" <a href="{{ route('project.application.configuration', $routeParams) }}" {{ wireNavigate() }}
class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200"> class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
<span>Configuration</span> <span>Configuration</span>
<svg class="w-3 h-3 shrink-0" fill="none" stroke="currentColor" <svg class="w-3 h-3 shrink-0" fill="none" stroke="currentColor"
@ -461,11 +461,11 @@ class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutra
</svg> </svg>
</a> </a>
</div> </div>
<a href="{{ route('project.application.deployment.index', $routeParams) }}" <a href="{{ route('project.application.deployment.index', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200"> class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
Deployments Deployments
</a> </a>
<a href="{{ route('project.application.logs', $routeParams) }}" <a href="{{ route('project.application.logs', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200"> class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
Logs Logs
</a> </a>
@ -478,7 +478,7 @@ class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
@elseif ($isService) @elseif ($isService)
<!-- Service Main Menus --> <!-- Service Main Menus -->
<div @mouseenter="openMenu('config'); menuPosition = $el.offsetTop" @mouseleave="closeMenu()"> <div @mouseenter="openMenu('config'); menuPosition = $el.offsetTop" @mouseleave="closeMenu()">
<a href="{{ route('project.service.configuration', $routeParams) }}" <a href="{{ route('project.service.configuration', $routeParams) }}" {{ wireNavigate() }}
class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200"> class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
<span>Configuration</span> <span>Configuration</span>
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" <svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor"
@ -488,7 +488,7 @@ class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutra
</svg> </svg>
</a> </a>
</div> </div>
<a href="{{ route('project.service.logs', $routeParams) }}" <a href="{{ route('project.service.logs', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200"> class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
Logs Logs
</a> </a>
@ -501,7 +501,7 @@ class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
@else @else
<!-- Database Main Menus --> <!-- Database Main Menus -->
<div @mouseenter="openMenu('config'); menuPosition = $el.offsetTop" @mouseleave="closeMenu()"> <div @mouseenter="openMenu('config'); menuPosition = $el.offsetTop" @mouseleave="closeMenu()">
<a href="{{ route('project.database.configuration', $routeParams) }}" <a href="{{ route('project.database.configuration', $routeParams) }}" {{ wireNavigate() }}
class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200"> class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
<span>Configuration</span> <span>Configuration</span>
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" <svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor"
@ -511,7 +511,7 @@ class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutra
</svg> </svg>
</a> </a>
</div> </div>
<a href="{{ route('project.database.logs', $routeParams) }}" <a href="{{ route('project.database.logs', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200"> class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
Logs Logs
</a> </a>
@ -526,7 +526,7 @@ class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
$resourceType === 'App\Models\StandaloneMongodb' || $resourceType === 'App\Models\StandaloneMongodb' ||
$resourceType === 'App\Models\StandaloneMysql' || $resourceType === 'App\Models\StandaloneMysql' ||
$resourceType === 'App\Models\StandaloneMariadb') $resourceType === 'App\Models\StandaloneMariadb')
<a href="{{ route('project.database.backup.index', $routeParams) }}" <a href="{{ route('project.database.backup.index', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200"> class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
Backups Backups
</a> </a>
@ -543,90 +543,90 @@ class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
class="pl-1"> class="pl-1">
<div class="w-52 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 border border-neutral-200 dark:border-coolgray-200 max-h-96 overflow-y-auto scrollbar"> <div class="w-52 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 border border-neutral-200 dark:border-coolgray-200 max-h-96 overflow-y-auto scrollbar">
@if ($isApplication) @if ($isApplication)
<a href="{{ route('project.application.configuration', $routeParams) }}" <a href="{{ route('project.application.configuration', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">General</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">General</a>
<a href="{{ route('project.application.environment-variables', $routeParams) }}" <a href="{{ route('project.application.environment-variables', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Environment class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Environment
Variables</a> Variables</a>
<a href="{{ route('project.application.persistent-storage', $routeParams) }}" <a href="{{ route('project.application.persistent-storage', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Persistent class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Persistent
Storage</a> Storage</a>
<a href="{{ route('project.application.source', $routeParams) }}" <a href="{{ route('project.application.source', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Source</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Source</a>
<a href="{{ route('project.application.servers', $routeParams) }}" <a href="{{ route('project.application.servers', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Servers</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Servers</a>
<a href="{{ route('project.application.scheduled-tasks.show', $routeParams) }}" <a href="{{ route('project.application.scheduled-tasks.show', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Scheduled class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Scheduled
Tasks</a> Tasks</a>
<a href="{{ route('project.application.webhooks', $routeParams) }}" <a href="{{ route('project.application.webhooks', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Webhooks</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Webhooks</a>
<a href="{{ route('project.application.preview-deployments', $routeParams) }}" <a href="{{ route('project.application.preview-deployments', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Preview class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Preview
Deployments</a> Deployments</a>
<a href="{{ route('project.application.healthcheck', $routeParams) }}" <a href="{{ route('project.application.healthcheck', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Healthcheck</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Healthcheck</a>
<a href="{{ route('project.application.rollback', $routeParams) }}" <a href="{{ route('project.application.rollback', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Rollback</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Rollback</a>
<a href="{{ route('project.application.resource-limits', $routeParams) }}" <a href="{{ route('project.application.resource-limits', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Resource class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Resource
Limits</a> Limits</a>
<a href="{{ route('project.application.resource-operations', $routeParams) }}" <a href="{{ route('project.application.resource-operations', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Resource class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Resource
Operations</a> Operations</a>
<a href="{{ route('project.application.metrics', $routeParams) }}" <a href="{{ route('project.application.metrics', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Metrics</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Metrics</a>
<a href="{{ route('project.application.tags', $routeParams) }}" <a href="{{ route('project.application.tags', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Tags</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Tags</a>
<a href="{{ route('project.application.advanced', $routeParams) }}" <a href="{{ route('project.application.advanced', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Advanced</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Advanced</a>
<a href="{{ route('project.application.danger', $routeParams) }}" <a href="{{ route('project.application.danger', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200 text-red-500">Danger class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200 text-red-500">Danger
Zone</a> Zone</a>
@elseif ($isService) @elseif ($isService)
<a href="{{ route('project.service.configuration', $routeParams) }}" <a href="{{ route('project.service.configuration', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">General</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">General</a>
<a href="{{ route('project.service.environment-variables', $routeParams) }}" <a href="{{ route('project.service.environment-variables', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Environment class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Environment
Variables</a> Variables</a>
<a href="{{ route('project.service.storages', $routeParams) }}" <a href="{{ route('project.service.storages', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Storages</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Storages</a>
<a href="{{ route('project.service.scheduled-tasks.show', $routeParams) }}" <a href="{{ route('project.service.scheduled-tasks.show', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Scheduled class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Scheduled
Tasks</a> Tasks</a>
<a href="{{ route('project.service.webhooks', $routeParams) }}" <a href="{{ route('project.service.webhooks', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Webhooks</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Webhooks</a>
<a href="{{ route('project.service.resource-operations', $routeParams) }}" <a href="{{ route('project.service.resource-operations', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Resource class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Resource
Operations</a> Operations</a>
<a href="{{ route('project.service.tags', $routeParams) }}" <a href="{{ route('project.service.tags', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Tags</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Tags</a>
<a href="{{ route('project.service.danger', $routeParams) }}" <a href="{{ route('project.service.danger', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200 text-red-500">Danger class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200 text-red-500">Danger
Zone</a> Zone</a>
@else @else
<a href="{{ route('project.database.configuration', $routeParams) }}" <a href="{{ route('project.database.configuration', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">General</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">General</a>
<a href="{{ route('project.database.environment-variables', $routeParams) }}" <a href="{{ route('project.database.environment-variables', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Environment class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Environment
Variables</a> Variables</a>
<a href="{{ route('project.database.servers', $routeParams) }}" <a href="{{ route('project.database.servers', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Servers</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Servers</a>
<a href="{{ route('project.database.persistent-storage', $routeParams) }}" <a href="{{ route('project.database.persistent-storage', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Persistent class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Persistent
Storage</a> Storage</a>
<a href="{{ route('project.database.webhooks', $routeParams) }}" <a href="{{ route('project.database.webhooks', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Webhooks</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Webhooks</a>
<a href="{{ route('project.database.resource-limits', $routeParams) }}" <a href="{{ route('project.database.resource-limits', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Resource class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Resource
Limits</a> Limits</a>
<a href="{{ route('project.database.resource-operations', $routeParams) }}" <a href="{{ route('project.database.resource-operations', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Resource class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Resource
Operations</a> Operations</a>
<a href="{{ route('project.database.metrics', $routeParams) }}" <a href="{{ route('project.database.metrics', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Metrics</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Metrics</a>
<a href="{{ route('project.database.tags', $routeParams) }}" <a href="{{ route('project.database.tags', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Tags</a> class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Tags</a>
<a href="{{ route('project.database.danger', $routeParams) }}" <a href="{{ route('project.database.danger', $routeParams) }}" {{ wireNavigate() }}
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200 text-red-500">Danger class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200 text-red-500">Danger
Zone</a> Zone</a>
@endif @endif

View file

@ -3,20 +3,20 @@
<div class="subtitle">Security related settings.</div> <div class="subtitle">Security related settings.</div>
<div class="navbar-main"> <div class="navbar-main">
<nav class="flex items-center gap-6 scrollbar min-h-10"> <nav class="flex items-center gap-6 scrollbar min-h-10">
<a href="{{ route('security.private-key.index') }}"> <a href="{{ route('security.private-key.index') }}" {{ wireNavigate() }}>
<button>Private Keys</button> <button>Private Keys</button>
</a> </a>
@can('viewAny', App\Models\CloudProviderToken::class) @can('viewAny', App\Models\CloudProviderToken::class)
<a href="{{ route('security.cloud-tokens') }}"> <a href="{{ route('security.cloud-tokens') }}" {{ wireNavigate() }}>
<button>Cloud Tokens</button> <button>Cloud Tokens</button>
</a> </a>
@endcan @endcan
@can('viewAny', App\Models\CloudInitScript::class) @can('viewAny', App\Models\CloudInitScript::class)
<a href="{{ route('security.cloud-init-scripts') }}"> <a href="{{ route('security.cloud-init-scripts') }}" {{ wireNavigate() }}>
<button>Cloud-Init Scripts</button> <button>Cloud-Init Scripts</button>
</a> </a>
@endcan @endcan
<a href="{{ route('security.api-tokens') }}"> <a href="{{ route('security.api-tokens') }}" {{ wireNavigate() }}>
<button>API Tokens</button> <button>API Tokens</button>
</a> </a>
</nav> </nav>

View file

@ -1,10 +1,10 @@
<div class="flex flex-col items-start gap-2 min-w-fit"> <div class="flex flex-col items-start gap-2 min-w-fit">
<a class="{{ request()->routeIs('server.proxy') ? 'menu-item menu-item-active' : 'menu-item' }}" <a class="{{ request()->routeIs('server.proxy') ? 'menu-item menu-item-active' : 'menu-item' }}" {{ wireNavigate() }}
href="{{ route('server.proxy', $parameters) }}"> href="{{ route('server.proxy', $parameters) }}">
<button>Configuration</button> <button>Configuration</button>
</a> </a>
@if ($server->proxySet()) @if ($server->proxySet())
<a class="{{ request()->routeIs('server.proxy.dynamic-confs') ? 'menu-item menu-item-active' : 'menu-item' }}" <a class="{{ request()->routeIs('server.proxy.dynamic-confs') ? 'menu-item menu-item-active' : 'menu-item' }}" {{ wireNavigate() }}
href="{{ route('server.proxy.dynamic-confs', $parameters) }}"> href="{{ route('server.proxy.dynamic-confs', $parameters) }}">
<button>Dynamic Configurations</button> <button>Dynamic Configurations</button>
</a> </a>

View file

@ -1,5 +1,5 @@
<div class="flex flex-col items-start gap-2 min-w-fit"> <div class="flex flex-col items-start gap-2 min-w-fit">
<a class="{{ request()->routeIs('server.security.patches') ? 'menu-item menu-item-active' : 'menu-item' }}" <a class="{{ request()->routeIs('server.security.patches') ? 'menu-item menu-item-active' : 'menu-item' }}" {{ wireNavigate() }}
href="{{ route('server.security.patches', $parameters) }}"> href="{{ route('server.security.patches', $parameters) }}">
Server Patching Server Patching
</a> </a>

View file

@ -1,42 +1,42 @@
<div class="flex flex-col items-start gap-2 min-w-fit"> <div class="flex flex-col items-start gap-2 min-w-fit">
<a class="menu-item {{ $activeMenu === 'general' ? 'menu-item-active' : '' }}" <a class="menu-item {{ $activeMenu === 'general' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
href="{{ route('server.show', ['server_uuid' => $server->uuid]) }}">General</a> href="{{ route('server.show', ['server_uuid' => $server->uuid]) }}">General</a>
@if ($server->isFunctional()) @if ($server->isFunctional())
<a class="menu-item {{ $activeMenu === 'advanced' ? 'menu-item-active' : '' }}" <a class="menu-item {{ $activeMenu === 'advanced' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
href="{{ route('server.advanced', ['server_uuid' => $server->uuid]) }}">Advanced href="{{ route('server.advanced', ['server_uuid' => $server->uuid]) }}">Advanced
</a> </a>
@endif @endif
<a class="menu-item {{ $activeMenu === 'private-key' ? 'menu-item-active' : '' }}" <a class="menu-item {{ $activeMenu === 'private-key' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
href="{{ route('server.private-key', ['server_uuid' => $server->uuid]) }}">Private Key href="{{ route('server.private-key', ['server_uuid' => $server->uuid]) }}">Private Key
</a> </a>
@if ($server->hetzner_server_id) @if ($server->hetzner_server_id)
<a class="menu-item {{ $activeMenu === 'cloud-provider-token' ? 'menu-item-active' : '' }}" <a class="menu-item {{ $activeMenu === 'cloud-provider-token' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
href="{{ route('server.cloud-provider-token', ['server_uuid' => $server->uuid]) }}">Hetzner Token href="{{ route('server.cloud-provider-token', ['server_uuid' => $server->uuid]) }}">Hetzner Token
</a> </a>
@endif @endif
<a class="menu-item {{ $activeMenu === 'ca-certificate' ? 'menu-item-active' : '' }}" <a class="menu-item {{ $activeMenu === 'ca-certificate' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
href="{{ route('server.ca-certificate', ['server_uuid' => $server->uuid]) }}">CA Certificate href="{{ route('server.ca-certificate', ['server_uuid' => $server->uuid]) }}">CA Certificate
</a> </a>
@if (!$server->isLocalhost()) @if (!$server->isLocalhost())
<a class="menu-item {{ $activeMenu === 'cloudflare-tunnel' ? 'menu-item-active' : '' }}" <a class="menu-item {{ $activeMenu === 'cloudflare-tunnel' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
href="{{ route('server.cloudflare-tunnel', ['server_uuid' => $server->uuid]) }}">Cloudflare href="{{ route('server.cloudflare-tunnel', ['server_uuid' => $server->uuid]) }}">Cloudflare
Tunnel</a> Tunnel</a>
@endif @endif
@if ($server->isFunctional()) @if ($server->isFunctional())
<a class="menu-item {{ $activeMenu === 'docker-cleanup' ? 'menu-item-active' : '' }}" <a class="menu-item {{ $activeMenu === 'docker-cleanup' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
href="{{ route('server.docker-cleanup', ['server_uuid' => $server->uuid]) }}">Docker Cleanup href="{{ route('server.docker-cleanup', ['server_uuid' => $server->uuid]) }}">Docker Cleanup
</a> </a>
<a class="menu-item {{ $activeMenu === 'destinations' ? 'menu-item-active' : '' }}" <a class="menu-item {{ $activeMenu === 'destinations' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
href="{{ route('server.destinations', ['server_uuid' => $server->uuid]) }}">Destinations href="{{ route('server.destinations', ['server_uuid' => $server->uuid]) }}">Destinations
</a> </a>
<a class="menu-item {{ $activeMenu === 'log-drains' ? 'menu-item-active' : '' }}" <a class="menu-item {{ $activeMenu === 'log-drains' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
href="{{ route('server.log-drains', ['server_uuid' => $server->uuid]) }}">Log href="{{ route('server.log-drains', ['server_uuid' => $server->uuid]) }}">Log
Drains</a> Drains</a>
<a class="menu-item {{ $activeMenu === 'metrics' ? 'menu-item-active' : '' }}" <a class="menu-item {{ $activeMenu === 'metrics' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
href="{{ route('server.charts', ['server_uuid' => $server->uuid]) }}">Metrics</a> href="{{ route('server.charts', ['server_uuid' => $server->uuid]) }}">Metrics</a>
@endif @endif
@if (!$server->isLocalhost()) @if (!$server->isLocalhost())
<a class="menu-item {{ $activeMenu === 'danger' ? 'menu-item-active' : '' }}" <a class="menu-item {{ $activeMenu === 'danger' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
href="{{ route('server.delete', ['server_uuid' => $server->uuid]) }}">Danger</a> href="{{ route('server.delete', ['server_uuid' => $server->uuid]) }}">Danger</a>
@endif @endif
</div> </div>

View file

@ -3,19 +3,19 @@
<div class="subtitle">Instance wide settings for Coolify.</div> <div class="subtitle">Instance wide settings for Coolify.</div>
<div class="navbar-main"> <div class="navbar-main">
<nav class="flex items-center gap-6 min-h-10 whitespace-nowrap"> <nav class="flex items-center gap-6 min-h-10 whitespace-nowrap">
<a class="{{ request()->routeIs('settings.index') ? 'dark:text-white' : '' }}" <a class="{{ request()->routeIs('settings.index') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('settings.index') }}"> href="{{ route('settings.index') }}">
Configuration Configuration
</a> </a>
<a class="{{ request()->routeIs('settings.backup') ? 'dark:text-white' : '' }}" <a class="{{ request()->routeIs('settings.backup') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('settings.backup') }}"> href="{{ route('settings.backup') }}">
Backup Backup
</a> </a>
<a class="{{ request()->routeIs('settings.email') ? 'dark:text-white' : '' }}" <a class="{{ request()->routeIs('settings.email') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('settings.email') }}"> href="{{ route('settings.email') }}">
Transactional Email Transactional Email
</a> </a>
<a class="{{ request()->routeIs('settings.oauth') ? 'dark:text-white' : '' }}" <a class="{{ request()->routeIs('settings.oauth') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('settings.oauth') }}"> href="{{ route('settings.oauth') }}">
OAuth OAuth
</a> </a>

View file

@ -1,8 +1,8 @@
<div class="flex flex-col items-start gap-2 min-w-fit"> <div class="flex flex-col items-start gap-2 min-w-fit">
<a class="menu-item {{ $activeMenu === 'general' ? 'menu-item-active' : '' }}" <a class="menu-item {{ $activeMenu === 'general' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
href="{{ route('settings.index') }}">General</a> href="{{ route('settings.index') }}">General</a>
<a class="menu-item {{ $activeMenu === 'advanced' ? 'menu-item-active' : '' }}" <a class="menu-item {{ $activeMenu === 'advanced' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
href="{{ route('settings.advanced') }}">Advanced</a> href="{{ route('settings.advanced') }}">Advanced</a>
<a class="menu-item {{ $activeMenu === 'updates' ? 'menu-item-active' : '' }}" <a class="menu-item {{ $activeMenu === 'updates' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
href="{{ route('settings.updates') }}">Updates</a> href="{{ route('settings.updates') }}">Updates</a>
</div> </div>

View file

@ -8,15 +8,16 @@
<div class="subtitle">Team wide configurations.</div> <div class="subtitle">Team wide configurations.</div>
<div class="navbar-main"> <div class="navbar-main">
<nav class="flex items-center gap-6 min-h-10"> <nav class="flex items-center gap-6 min-h-10">
<a class="{{ request()->routeIs('team.index') ? 'dark:text-white' : '' }}" href="{{ route('team.index') }}"> <a class="{{ request()->routeIs('team.index') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('team.index') }}">
General General
</a> </a>
<a class="{{ request()->routeIs('team.member.index') ? 'dark:text-white' : '' }}" <a class="{{ request()->routeIs('team.member.index') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('team.member.index') }}"> href="{{ route('team.member.index') }}">
Members Members
</a> </a>
@if (isInstanceAdmin()) @if (isInstanceAdmin())
<a class="{{ request()->routeIs('team.admin-view') ? 'dark:text-white' : '' }}" <a class="{{ request()->routeIs('team.admin-view') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('team.admin-view') }}"> href="{{ route('team.admin-view') }}">
Admin View Admin View
</a> </a>

View file

@ -9,7 +9,7 @@
The currentStep variable is inherited from parent Alpine component (upgradeModal). The currentStep variable is inherited from parent Alpine component (upgradeModal).
--}} --}}
<div class="w-full max-w-md mx-auto" x-data="{ activeStep: {{ $step }} }" x-effect="activeStep = $el.closest('[x-data]')?.__x?.$data?.currentStep ?? {{ $step }}"> <div class="w-full" x-data="{ activeStep: {{ $step }} }" x-effect="activeStep = $el.closest('[x-data]')?.__x?.$data?.currentStep ?? {{ $step }}">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
{{-- Step 1: Preparing --}} {{-- Step 1: Preparing --}}
<div class="flex items-center flex-1"> <div class="flex items-center flex-1">

View file

@ -15,7 +15,7 @@
<a href="{{ url()->previous() }}"> <a href="{{ url()->previous() }}">
<x-forms.button>Go back</x-forms.button> <x-forms.button>Go back</x-forms.button>
</a> </a>
<a href="{{ route('dashboard') }}"> <a href="{{ route('dashboard') }}" {{ wireNavigate() }}>
<x-forms.button>Dashboard</x-forms.button> <x-forms.button>Dashboard</x-forms.button>
</a> </a>
<a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact <a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact

View file

@ -9,7 +9,7 @@
<a href="{{ url()->previous() }}"> <a href="{{ url()->previous() }}">
<x-forms.button>Go back</x-forms.button> <x-forms.button>Go back</x-forms.button>
</a> </a>
<a href="{{ route('dashboard') }}"> <a href="{{ route('dashboard') }}" {{ wireNavigate() }}>
<x-forms.button>Dashboard</x-forms.button> <x-forms.button>Dashboard</x-forms.button>
</a> </a>
<a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact <a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact

View file

@ -8,7 +8,7 @@
<a href="{{ url()->previous() }}"> <a href="{{ url()->previous() }}">
<x-forms.button>Go back</x-forms.button> <x-forms.button>Go back</x-forms.button>
</a> </a>
<a href="{{ route('dashboard') }}"> <a href="{{ route('dashboard') }}" {{ wireNavigate() }}>
<x-forms.button>Dashboard</x-forms.button> <x-forms.button>Dashboard</x-forms.button>
</a> </a>
<a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact <a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact

View file

@ -9,7 +9,7 @@
<a href="{{ url()->previous() }}"> <a href="{{ url()->previous() }}">
<x-forms.button>Go back</x-forms.button> <x-forms.button>Go back</x-forms.button>
</a> </a>
<a href="{{ route('dashboard') }}"> <a href="{{ route('dashboard') }}" {{ wireNavigate() }}>
<x-forms.button>Dashboard</x-forms.button> <x-forms.button>Dashboard</x-forms.button>
</a> </a>
<a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact <a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact

View file

@ -3,14 +3,14 @@
<div> <div>
<p class="font-mono font-semibold text-7xl dark:text-warning">404</p> <p class="font-mono font-semibold text-7xl dark:text-warning">404</p>
<h1 class="mt-4 font-bold tracking-tight dark:text-white">How did you get here?</h1> <h1 class="mt-4 font-bold tracking-tight dark:text-white">How did you get here?</h1>
<p class="text-base leading-7 dark:text-neutral-400 text-black">Sorry, we couldnt find the page youre looking <p class="text-base leading-7 dark:text-neutral-400 text-black">Sorry, we couldn't find the page you're looking
for. for.
</p> </p>
<div class="flex items-center mt-10 gap-x-2"> <div class="flex items-center mt-10 gap-x-2">
<a href="{{ url()->previous() }}"> <a href="{{ url()->previous() }}">
<x-forms.button>Go back</x-forms.button> <x-forms.button>Go back</x-forms.button>
</a> </a>
<a href="{{ route('dashboard') }}"> <a href="{{ route('dashboard') }}" {{ wireNavigate() }}>
<x-forms.button>Dashboard</x-forms.button> <x-forms.button>Dashboard</x-forms.button>
</a> </a>
<a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact <a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact

View file

@ -3,14 +3,14 @@
<div> <div>
<p class="font-mono font-semibold text-7xl dark:text-warning">419</p> <p class="font-mono font-semibold text-7xl dark:text-warning">419</p>
<h1 class="mt-4 font-bold tracking-tight dark:text-white">This page is definitely old, not like you!</h1> <h1 class="mt-4 font-bold tracking-tight dark:text-white">This page is definitely old, not like you!</h1>
<p class="text-base leading-7 dark:text-neutral-300 text-black">Sorry, we couldnt find the page youre looking <p class="text-base leading-7 dark:text-neutral-300 text-black">Sorry, we couldn't find the page you're looking
for. for.
</p> </p>
<div class="flex items-center mt-10 gap-x-2"> <div class="flex items-center mt-10 gap-x-2">
<a href="{{ url()->previous() }}"> <a href="{{ url()->previous() }}">
<x-forms.button>Go back</x-forms.button> <x-forms.button>Go back</x-forms.button>
</a> </a>
<a href="{{ route('dashboard') }}"> <a href="{{ route('dashboard') }}" {{ wireNavigate() }}>
<x-forms.button>Dashboard</x-forms.button> <x-forms.button>Dashboard</x-forms.button>
</a> </a>
<a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact <a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact

View file

@ -11,7 +11,7 @@
<a href="{{ url()->previous() }}"> <a href="{{ url()->previous() }}">
<x-forms.button>Go back</x-forms.button> <x-forms.button>Go back</x-forms.button>
</a> </a>
<a href="{{ route('dashboard') }}"> <a href="{{ route('dashboard') }}" {{ wireNavigate() }}>
<x-forms.button>Dashboard</x-forms.button> <x-forms.button>Dashboard</x-forms.button>
</a> </a>
<a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact <a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact

View file

@ -14,7 +14,7 @@
<a href="{{ url()->previous() }}"> <a href="{{ url()->previous() }}">
<x-forms.button>Go back</x-forms.button> <x-forms.button>Go back</x-forms.button>
</a> </a>
<a href="{{ route('dashboard') }}"> <a href="{{ route('dashboard') }}" {{ wireNavigate() }}>
<x-forms.button>Dashboard</x-forms.button> <x-forms.button>Dashboard</x-forms.button>
</a> </a>
<a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact <a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact

View file

@ -11,7 +11,7 @@
<a href="{{ url()->previous() }}"> <a href="{{ url()->previous() }}">
<x-forms.button>Go back</x-forms.button> <x-forms.button>Go back</x-forms.button>
</a> </a>
<a href="{{ route('dashboard') }}"> <a href="{{ route('dashboard') }}" {{ wireNavigate() }}>
<x-forms.button>Dashboard</x-forms.button> <x-forms.button>Dashboard</x-forms.button>
</a> </a>
<a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact <a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact

View file

@ -36,7 +36,7 @@ class="flex items-center justify-center size-4 text-white rounded hover:bg-coolg
<div class="grid grid-cols-1 gap-4 xl:grid-cols-2"> <div class="grid grid-cols-1 gap-4 xl:grid-cols-2">
@foreach ($projects as $project) @foreach ($projects as $project)
<div class="relative gap-2 cursor-pointer coolbox group"> <div class="relative gap-2 cursor-pointer coolbox group">
<a href="{{ $project->navigateTo() }}" class="absolute inset-0"></a> <a href="{{ $project->navigateTo() }}" {{ wireNavigate() }} class="absolute inset-0"></a>
<div class="flex flex-1 mx-6"> <div class="flex flex-1 mx-6">
<div class="flex flex-col justify-center flex-1"> <div class="flex flex-col justify-center flex-1">
<div class="box-title">{{ $project->name }}</div> <div class="box-title">{{ $project->name }}</div>
@ -47,7 +47,7 @@ class="flex items-center justify-center size-4 text-white rounded hover:bg-coolg
<div class="relative z-10 flex items-center justify-center gap-4 text-xs font-bold"> <div class="relative z-10 flex items-center justify-center gap-4 text-xs font-bold">
@if ($project->environments->first()) @if ($project->environments->first())
@can('createAnyResource') @can('createAnyResource')
<a class="hover:underline" <a class="hover:underline" {{ wireNavigate() }}
href="{{ route('project.resource.create', [ href="{{ route('project.resource.create', [
'project_uuid' => $project->uuid, 'project_uuid' => $project->uuid,
'environment_uuid' => $project->environments->first()->uuid, 'environment_uuid' => $project->environments->first()->uuid,
@ -57,7 +57,7 @@ class="flex items-center justify-center size-4 text-white rounded hover:bg-coolg
@endcan @endcan
@endif @endif
@can('update', $project) @can('update', $project)
<a class="hover:underline" <a class="hover:underline" {{ wireNavigate() }}
href="{{ route('project.edit', ['project_uuid' => $project->uuid]) }}"> href="{{ route('project.edit', ['project_uuid' => $project->uuid]) }}">
Settings Settings
</a> </a>
@ -74,7 +74,7 @@ class="flex items-center justify-center size-4 text-white rounded hover:bg-coolg
<x-modal-input buttonTitle="Add" title="New Project"> <x-modal-input buttonTitle="Add" title="New Project">
<livewire:project.add-empty /> <livewire:project.add-empty />
</x-modal-input> your first project or </x-modal-input> your first project or
go to the <a class="underline dark:text-white" href="{{ route('onboarding') }}">onboarding</a> page. go to the <a class="underline dark:text-white" href="{{ route('onboarding') }}" {{ wireNavigate() }}>onboarding</a> page.
</div> </div>
</div> </div>
@endif @endif
@ -101,7 +101,7 @@ class="flex items-center justify-center size-4 text-white rounded hover:bg-coolg
@if ($servers->count() > 0) @if ($servers->count() > 0)
<div class="grid grid-cols-1 gap-4 xl:grid-cols-2"> <div class="grid grid-cols-1 gap-4 xl:grid-cols-2">
@foreach ($servers as $server) @foreach ($servers as $server)
<a href="{{ route('server.show', ['server_uuid' => data_get($server, 'uuid')]) }}" <a href="{{ route('server.show', ['server_uuid' => data_get($server, 'uuid')]) }}" {{ wireNavigate() }}
@class([ @class([
'gap-2 border cursor-pointer coolbox group', 'gap-2 border cursor-pointer coolbox group',
'border-red-500' => 'border-red-500' =>
@ -138,7 +138,7 @@ class="flex items-center justify-center size-4 text-white rounded hover:bg-coolg
<livewire:security.private-key.create from="server" /> <livewire:security.private-key.create from="server" />
</x-modal-input> a private key </x-modal-input> a private key
or or
go to the <a class="underline dark:text-white" href="{{ route('onboarding') }}">onboarding</a> go to the <a class="underline dark:text-white" href="{{ route('onboarding') }}" {{ wireNavigate() }}>onboarding</a>
page. page.
</div> </div>
</div> </div>
@ -150,7 +150,7 @@ class="flex items-center justify-center size-4 text-white rounded hover:bg-coolg
<livewire:server.create /> <livewire:server.create />
</x-modal-input> your first server </x-modal-input> your first server
or or
go to the <a class="underline dark:text-white" href="{{ route('onboarding') }}">onboarding</a> go to the <a class="underline dark:text-white" href="{{ route('onboarding') }}" {{ wireNavigate() }}>onboarding</a>
page. page.
</div> </div>
</div> </div>

View file

@ -39,7 +39,7 @@ class="absolute bottom-full mb-2 w-80 max-h-96 overflow-y-auto rounded-lg shadow
<div class="p-4 space-y-3"> <div class="p-4 space-y-3">
@foreach ($this->deployments as $deployment) @foreach ($this->deployments as $deployment)
<a href="{{ $deployment->deployment_url }}" <a href="{{ $deployment->deployment_url }}" {{ wireNavigate() }}
class="flex items-start gap-3 p-3 rounded-lg dark:bg-coolgray-200 bg-gray-50 transition-all duration-200 hover:ring-2 hover:ring-coollabs dark:hover:ring-warning cursor-pointer"> class="flex items-start gap-3 p-3 rounded-lg dark:bg-coolgray-200 bg-gray-50 transition-all duration-200 hover:ring-2 hover:ring-coollabs dark:hover:ring-warning cursor-pointer">
<!-- Status indicator --> <!-- Status indicator -->
<div class="flex-shrink-0 mt-1"> <div class="flex-shrink-0 mt-1">

View file

@ -17,7 +17,7 @@
@forelse ($servers as $server) @forelse ($servers as $server)
@forelse ($server->destinations() as $destination) @forelse ($server->destinations() as $destination)
@if ($destination->getMorphClass() === 'App\Models\StandaloneDocker') @if ($destination->getMorphClass() === 'App\Models\StandaloneDocker')
<a class="coolbox group" <a class="coolbox group" {{ wireNavigate() }}
href="{{ route('destination.show', ['destination_uuid' => data_get($destination, 'uuid')]) }}"> href="{{ route('destination.show', ['destination_uuid' => data_get($destination, 'uuid')]) }}">
<div class="flex flex-col justify-center mx-6"> <div class="flex flex-col justify-center mx-6">
<div class="box-title">{{ $destination->name }}</div> <div class="box-title">{{ $destination->name }}</div>
@ -26,7 +26,7 @@
</a> </a>
@endif @endif
@if ($destination->getMorphClass() === 'App\Models\SwarmDocker') @if ($destination->getMorphClass() === 'App\Models\SwarmDocker')
<a class="coolbox group" <a class="coolbox group" {{ wireNavigate() }}
href="{{ route('destination.show', ['destination_uuid' => data_get($destination, 'uuid')]) }}"> href="{{ route('destination.show', ['destination_uuid' => data_get($destination, 'uuid')]) }}">
<div class="flex flex-col mx-6"> <div class="flex flex-col mx-6">
<div class="box-title">{{ $destination->name }}</div> <div class="box-title">{{ $destination->name }}</div>

View file

@ -70,6 +70,11 @@
}, },
openModal() { openModal() {
// Check if $wire is available (may not be after SPA navigation destroys/recreates component)
if (typeof $wire === 'undefined' || !$wire) {
console.warn('Global search: $wire not available, skipping open');
return;
}
this.modalOpen = true; this.modalOpen = true;
this.selectedIndex = -1; this.selectedIndex = -1;
this.isLoadingInitialData = true; this.isLoadingInitialData = true;
@ -79,6 +84,10 @@
this.creatableItems = $wire.creatableItems || []; this.creatableItems = $wire.creatableItems || [];
this.isLoadingInitialData = false; this.isLoadingInitialData = false;
setTimeout(() => this.$refs.searchInput?.focus(), 50); setTimeout(() => this.$refs.searchInput?.focus(), 50);
}).catch(() => {
// Handle case where component was destroyed during navigation
this.modalOpen = false;
this.isLoadingInitialData = false;
}); });
}, },
closeModal() { closeModal() {
@ -90,7 +99,10 @@
this.allSearchableItems = []; this.allSearchableItems = [];
// Ensure scroll is restored // Ensure scroll is restored
document.body.style.overflow = ''; document.body.style.overflow = '';
@this.closeSearchModal(); // Use $wire instead of @this for SPA navigation compatibility
if ($wire) {
$wire.closeSearchModal();
}
}, },
navigateResults(direction) { navigateResults(direction) {
const results = document.querySelectorAll('.search-result-item'); const results = document.querySelectorAll('.search-result-item');
@ -120,7 +132,7 @@
const trimmed = value.trim().toLowerCase(); const trimmed = value.trim().toLowerCase();
if (trimmed === '') { if (trimmed === '') {
if ($wire.isSelectingResource) { if (typeof $wire !== 'undefined' && $wire && $wire.isSelectingResource) {
$wire.cancelResourceSelection(); $wire.cancelResourceSelection();
} }
return; return;
@ -149,7 +161,7 @@
(item.quickcommand && item.quickcommand.toLowerCase().includes(trimmed)); (item.quickcommand && item.quickcommand.toLowerCase().includes(trimmed));
}); });
if (matchingItem) { if (matchingItem && typeof $wire !== 'undefined' && $wire) {
$wire.navigateToResource(matchingItem.type); $wire.navigateToResource(matchingItem.type);
} }
} }
@ -186,7 +198,7 @@
// If search query is empty, close the modal // If search query is empty, close the modal
if (!this.searchQuery || this.searchQuery === '') { if (!this.searchQuery || this.searchQuery === '') {
// Check if we're in a selection state using Alpine-accessible Livewire state // Check if we're in a selection state using Alpine-accessible Livewire state
if ($wire.isSelectingResource) { if (typeof $wire !== 'undefined' && $wire && $wire.isSelectingResource) {
$wire.cancelResourceSelection(); $wire.cancelResourceSelection();
setTimeout(() => this.$refs.searchInput?.focus(), 100); setTimeout(() => this.$refs.searchInput?.focus(), 100);
} else { } else {
@ -227,19 +239,23 @@
document.removeEventListener('keydown', arrowKeyHandler); document.removeEventListener('keydown', arrowKeyHandler);
}); });
// Watch for auto-open resource // Watch for auto-open resource (only if $wire is available)
this.$watch('$wire.autoOpenResource', value => { if (typeof $wire !== 'undefined' && $wire) {
if (value) { this.$watch('$wire.autoOpenResource', value => {
// Close search modal first if (value) {
this.closeModal(); // Close search modal first
// Open the specific resource modal after a short delay this.closeModal();
setTimeout(() => { // Open the specific resource modal after a short delay
this.$dispatch('open-create-modal-' + value); setTimeout(() => {
// Reset the value so it can trigger again this.$dispatch('open-create-modal-' + value);
@this.set('autoOpenResource', null); // Reset the value so it can trigger again
}, 150); if (typeof $wire !== 'undefined' && $wire) {
} $wire.set('autoOpenResource', null);
}); }
}, 150);
}
});
}
// Listen for closeSearchModal event from backend // Listen for closeSearchModal event from backend
window.addEventListener('closeSearchModal', () => { window.addEventListener('closeSearchModal', () => {

View file

@ -137,7 +137,7 @@ class="font-bold dark:text-white">Stripe</a></x-forms.button>
<div><span class="font-bold text-red-500">WARNING:</span> Your subscription is in over-due. If your <div><span class="font-bold text-red-500">WARNING:</span> Your subscription is in over-due. If your
latest latest
payment is not paid within a week, all automations <span class="font-bold text-red-500">will payment is not paid within a week, all automations <span class="font-bold text-red-500">will
be deactivated</span>. Visit <a href="{{ route('subscription.show') }}" be deactivated</span>. Visit <a href="{{ route('subscription.show') }}" {{ wireNavigate() }}
class="underline dark:text-white">/subscription</a> to check your subscription status or pay class="underline dark:text-white">/subscription</a> to check your subscription status or pay
your your
invoice (or check your email for the invoice). invoice (or check your email for the invoice).
@ -148,7 +148,7 @@ class="underline dark:text-white">/subscription</a> to check your subscription s
<x-banner :closable=false> <x-banner :closable=false>
<div><span class="font-bold text-red-500">WARNING:</span> The number of active servers exceeds the limit <div><span class="font-bold text-red-500">WARNING:</span> The number of active servers exceeds the limit
covered by your payment. If not resolved, some of your servers <span class="font-bold text-red-500">will covered by your payment. If not resolved, some of your servers <span class="font-bold text-red-500">will
be deactivated</span>. Visit <a href="{{ route('subscription.show') }}" be deactivated</span>. Visit <a href="{{ route('subscription.show') }}" {{ wireNavigate() }}
class="underline dark:text-white">/subscription</a> to update your subscription or remove some class="underline dark:text-white">/subscription</a> to update your subscription or remove some
servers. servers.
</div> </div>
@ -172,7 +172,7 @@ class="underline dark:text-white">/subscription</a> to update your subscription
highly recommended to enable at least highly recommended to enable at least
one one
notification channel to receive important alerts.<br>Visit <a notification channel to receive important alerts.<br>Visit <a
href="{{ route('notifications.email') }}" class="underline dark:text-white">/notification</a> to href="{{ route('notifications.email') }}" {{ wireNavigate() }} class="underline dark:text-white">/notification</a> to
enable notifications.</span> enable notifications.</span>
</x-slot:description> </x-slot:description>
<x-slot:button-text @click="disableNotification()"> <x-slot:button-text @click="disableNotification()">

View file

@ -8,27 +8,27 @@
<div class="flex flex-col h-full gap-8 sm:flex-row"> <div class="flex flex-col h-full gap-8 sm:flex-row">
<div class="flex flex-col items-start gap-2 min-w-fit"> <div class="flex flex-col items-start gap-2 min-w-fit">
<a class='menu-item' wire:current.exact="menu-item-active" <a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">General</a> href="{{ route('project.application.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">General</a>
<a class='menu-item' wire:current.exact="menu-item-active" <a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.advanced', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Advanced</a> href="{{ route('project.application.advanced', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Advanced</a>
@if ($application->destination->server->isSwarm()) @if ($application->destination->server->isSwarm())
<a class="menu-item" wire:current.exact="menu-item-active" <a class="menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.swarm', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Swarm href="{{ route('project.application.swarm', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Swarm
Configuration</a> Configuration</a>
@endif @endif
<a class='menu-item' wire:current.exact="menu-item-active" <a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.environment-variables', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Environment href="{{ route('project.application.environment-variables', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Environment
Variables</a> Variables</a>
<a class='menu-item' wire:current.exact="menu-item-active" <a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.persistent-storage', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Persistent href="{{ route('project.application.persistent-storage', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Persistent
Storage</a> Storage</a>
@if ($application->git_based()) @if ($application->git_based())
<a class='menu-item' wire:current.exact="menu-item-active" <a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.source', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Git href="{{ route('project.application.source', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Git
Source</a> Source</a>
@endif @endif
<a class="menu-item flex items-center gap-2" wire:current.exact="menu-item-active" <a class="menu-item flex items-center gap-2" {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.servers', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Servers href="{{ route('project.application.servers', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Servers
@if ($application->server_status == false) @if ($application->server_status == false)
<span title="One or more servers are unreachable or misconfigured."> <span title="One or more servers are unreachable or misconfigured.">
@ -46,33 +46,33 @@
</span> </span>
@endif @endif
</a> </a>
<a class="menu-item" wire:current.exact="menu-item-active" <a class="menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.scheduled-tasks.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Scheduled href="{{ route('project.application.scheduled-tasks.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Scheduled
Tasks</a> Tasks</a>
<a class="menu-item" wire:current.exact="menu-item-active" <a class="menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.webhooks', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Webhooks</a> href="{{ route('project.application.webhooks', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Webhooks</a>
@if ($application->deploymentType() !== 'deploy_key') @if ($application->deploymentType() !== 'deploy_key')
<a class="menu-item" wire:current.exact="menu-item-active" <a class="menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.preview-deployments', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Preview href="{{ route('project.application.preview-deployments', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Preview
Deployments</a> Deployments</a>
@endif @endif
@if ($application->build_pack !== 'dockercompose') @if ($application->build_pack !== 'dockercompose')
<a class="menu-item" wire:current.exact="menu-item-active" <a class="menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.healthcheck', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Healthcheck</a> href="{{ route('project.application.healthcheck', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Healthcheck</a>
@endif @endif
<a class="menu-item" wire:current.exact="menu-item-active" <a class="menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.rollback', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Rollback</a> href="{{ route('project.application.rollback', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Rollback</a>
<a class="menu-item" wire:current.exact="menu-item-active" <a class="menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.resource-limits', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Resource href="{{ route('project.application.resource-limits', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Resource
Limits</a> Limits</a>
<a class="menu-item" wire:current.exact="menu-item-active" <a class="menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.resource-operations', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Resource href="{{ route('project.application.resource-operations', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Resource
Operations</a> Operations</a>
<a class="menu-item" wire:current.exact="menu-item-active" <a class="menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.metrics', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Metrics</a> href="{{ route('project.application.metrics', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Metrics</a>
<a class="menu-item" wire:current.exact="menu-item-active" <a class="menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.tags', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Tags</a> href="{{ route('project.application.tags', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Tags</a>
<a class="menu-item" wire:current.exact="menu-item-active" <a class="menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.danger', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Danger href="{{ route('project.application.danger', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Danger
Zone</a> Zone</a>
</div> </div>

View file

@ -45,7 +45,7 @@
'border-error' => data_get($deployment, 'status') === 'failed', 'border-error' => data_get($deployment, 'status') === 'failed',
'border-success' => data_get($deployment, 'status') === 'finished', 'border-success' => data_get($deployment, 'status') === 'finished',
])> ])>
<a href="{{ $current_url . '/' . data_get($deployment, 'deployment_uuid') }}" class="block"> <a href="{{ $current_url . '/' . data_get($deployment, 'deployment_uuid') }}" {{ wireNavigate() }} class="block">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex items-center gap-2 mb-2"> <div class="flex items-center gap-2 mb-2">
<span @class([ <span @class([

View file

@ -8,26 +8,44 @@
<div x-data="{ <div x-data="{
fullscreen: @entangle('fullscreen'), fullscreen: @entangle('fullscreen'),
alwaysScroll: {{ $isKeepAliveOn ? 'true' : 'false' }}, alwaysScroll: {{ $isKeepAliveOn ? 'true' : 'false' }},
intervalId: null, rafId: null,
showTimestamps: true, showTimestamps: true,
searchQuery: '', searchQuery: '',
renderTrigger: 0, renderTrigger: 0,
deploymentId: '{{ $application_deployment_queue->deployment_uuid ?? 'deployment' }}', 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() { makeFullscreen() {
this.fullscreen = !this.fullscreen; this.fullscreen = !this.fullscreen;
}, },
scrollToBottom() {
const logsContainer = document.getElementById('logsContainer');
if (logsContainer) {
logsContainer.scrollTop = logsContainer.scrollHeight;
}
},
scheduleScroll() {
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);
}
});
},
toggleScroll() { toggleScroll() {
this.alwaysScroll = !this.alwaysScroll; this.alwaysScroll = !this.alwaysScroll;
if (this.alwaysScroll) { if (this.alwaysScroll) {
this.intervalId = setInterval(() => { this.scheduleScroll();
const logsContainer = document.getElementById('logsContainer');
if (logsContainer) {
logsContainer.scrollTop = logsContainer.scrollHeight;
}
}, 100);
} else { } else {
clearInterval(this.intervalId); if (this.rafId) {
this.intervalId = null; cancelAnimationFrame(this.rafId);
this.rafId = null;
}
} }
}, },
matchesSearch(text) { matchesSearch(text) {
@ -41,17 +59,19 @@
} }
const logsContainer = document.getElementById('logs'); const logsContainer = document.getElementById('logs');
if (!logsContainer) return false; if (!logsContainer) return false;
// Check if selection is within the logs container
const range = selection.getRangeAt(0); const range = selection.getRangeAt(0);
return logsContainer.contains(range.commonAncestorContainer); return logsContainer.contains(range.commonAncestorContainer);
}, },
decodeHtml(text) { decodeHtml(text) {
// Decode HTML entities, handling double-encoding with max iteration limit to prevent DoS // 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 decoded = text;
let prev = ''; let prev = '';
let iterations = 0; let iterations = 0;
const maxIterations = 3; // Prevent DoS from deeply nested HTML entities const maxIterations = 3;
while (decoded !== prev && iterations < maxIterations) { while (decoded !== prev && iterations < maxIterations) {
prev = decoded; prev = decoded;
@ -59,11 +79,17 @@
decoded = doc.documentElement.textContent; decoded = doc.documentElement.textContent;
iterations++; 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; return decoded;
}, },
renderHighlightedLog(el, text) { renderHighlightedLog(el, text) {
// Skip re-render if user has text selected in logs (preserves copy ability) // Skip re-render if user has text selected in logs
// But always render if the element is empty (initial render)
if (el.textContent && this.hasActiveLogSelection()) { if (el.textContent && this.hasActiveLogSelection()) {
return; return;
} }
@ -82,11 +108,9 @@
let index = lowerText.indexOf(query, lastIndex); let index = lowerText.indexOf(query, lastIndex);
while (index !== -1) { while (index !== -1) {
// Add text before match
if (index > lastIndex) { if (index > lastIndex) {
el.appendChild(document.createTextNode(decoded.substring(lastIndex, index))); el.appendChild(document.createTextNode(decoded.substring(lastIndex, index)));
} }
// Add highlighted match
const mark = document.createElement('span'); const mark = document.createElement('span');
mark.className = 'log-highlight'; mark.className = 'log-highlight';
mark.textContent = decoded.substring(index, index + this.searchQuery.length); mark.textContent = decoded.substring(index, index + this.searchQuery.length);
@ -96,22 +120,28 @@
index = lowerText.indexOf(query, lastIndex); index = lowerText.indexOf(query, lastIndex);
} }
// Add remaining text
if (lastIndex < decoded.length) { if (lastIndex < decoded.length) {
el.appendChild(document.createTextNode(decoded.substring(lastIndex))); el.appendChild(document.createTextNode(decoded.substring(lastIndex)));
} }
}, },
getMatchCount() { getMatchCount() {
if (!this.searchQuery.trim()) return 0; 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;
}
const logs = document.getElementById('logs'); const logs = document.getElementById('logs');
if (!logs) return 0; if (!logs) return 0;
const lines = logs.querySelectorAll('[data-log-line]'); const lines = logs.querySelectorAll('[data-log-line]');
let count = 0; let count = 0;
const query = this.searchQuery.toLowerCase();
lines.forEach(line => { lines.forEach(line => {
if (line.dataset.logContent && line.dataset.logContent.toLowerCase().includes(this.searchQuery.toLowerCase())) { if (line.dataset.logContent && line.dataset.logContent.toLowerCase().includes(query)) {
count++; count++;
} }
}); });
this.matchCountCache = count;
this.lastSearchQuery = this.searchQuery;
return count; return count;
}, },
downloadLogs() { downloadLogs() {
@ -135,15 +165,11 @@
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
}, },
stopScroll() { stopScroll() {
// Scroll to the end one final time before disabling this.scrollToBottom();
const logsContainer = document.getElementById('logsContainer');
if (logsContainer) {
logsContainer.scrollTop = logsContainer.scrollHeight;
}
this.alwaysScroll = false; this.alwaysScroll = false;
if (this.intervalId) { if (this.rafId) {
clearInterval(this.intervalId); cancelAnimationFrame(this.rafId);
this.intervalId = null; this.rafId = null;
} }
}, },
init() { init() {
@ -153,30 +179,32 @@
skip(); skip();
} }
}); });
// Re-render logs after Livewire updates // 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', () => { document.addEventListener('livewire:navigated', () => {
this.$nextTick(() => { this.renderTrigger++; }); this.$nextTick(debouncedRender);
}); });
Livewire.hook('commit', ({ succeed }) => { Livewire.hook('commit', ({ succeed }) => {
succeed(() => { succeed(() => {
this.$nextTick(() => { this.renderTrigger++; }); this.$nextTick(debouncedRender);
}); });
}); });
// Stop auto-scroll when deployment finishes // Stop auto-scroll when deployment finishes
Livewire.on('deploymentFinished', () => { Livewire.on('deploymentFinished', () => {
// Wait for DOM to update with final logs before scrolling to end
setTimeout(() => { setTimeout(() => {
this.stopScroll(); this.stopScroll();
}, 500); }, 500);
}); });
// Start auto-scroll if deployment is in progress // Start auto-scroll if deployment is in progress
if (this.alwaysScroll) { if (this.alwaysScroll) {
this.intervalId = setInterval(() => { this.scheduleScroll();
const logsContainer = document.getElementById('logsContainer');
if (logsContainer) {
logsContainer.scrollTop = logsContainer.scrollHeight;
}
}, 100);
} }
} }
}"> }">
@ -212,7 +240,7 @@ class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap"></span>
<path stroke-linecap="round" stroke-linejoin="round" <path stroke-linecap="round" stroke-linejoin="round"
d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" /> d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
</svg> </svg>
<input type="text" x-model="searchQuery" placeholder="Find in logs" <input type="text" x-model.debounce.300ms="searchQuery" placeholder="Find in logs"
class="input input-sm w-48 pl-8 pr-8 dark:bg-coolgray-200" /> class="input input-sm w-48 pl-8 pr-8 dark:bg-coolgray-200" />
<button x-show="searchQuery" x-on:click="searchQuery = ''" type="button" <button x-show="searchQuery" x-on:click="searchQuery = ''" type="button"
class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"> class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200">
@ -222,6 +250,21 @@ class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-6
</svg> </svg>
</button> </button>
</div> </div>
<button
x-on:click="
$wire.copyLogs().then(logs => {
navigator.clipboard.writeText(logs);
Livewire.dispatch('success', ['Logs copied to clipboard.']);
});
"
title="Copy Logs"
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75" />
</svg>
</button>
<button x-on:click="downloadLogs()" title="Download Logs" <button x-on:click="downloadLogs()" title="Download Logs"
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"> class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" <svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
@ -293,7 +336,7 @@ class="text-gray-500 dark:text-gray-400 py-2">
<div data-log-line data-log-content="{{ htmlspecialchars($searchableContent) }}" <div data-log-line data-log-content="{{ htmlspecialchars($searchableContent) }}"
x-effect="renderTrigger; searchQuery; $el.classList.toggle('hidden', !matchesSearch($el.dataset.logContent))" @class([ x-effect="renderTrigger; searchQuery; $el.classList.toggle('hidden', !matchesSearch($el.dataset.logContent))" @class([
'mt-2' => isset($line['command']) && $line['command'], 'mt-2' => isset($line['command']) && $line['command'],
'flex gap-2', 'flex gap-2 log-line',
])> ])>
<span x-show="showTimestamps" <span x-show="showTimestamps"
class="shrink-0 text-gray-500">{{ $line['timestamp'] }}</span> class="shrink-0 text-gray-500">{{ $line['timestamp'] }}</span>

View file

@ -236,11 +236,7 @@
@endif @endif
<div class="flex flex-col gap-2 pt-6 pb-10"> <div class="flex flex-col gap-2 pt-6 pb-10">
@if ($application->build_pack === 'dockercompose') @if ($application->build_pack === 'dockercompose')
@can('update', $application) <div class="flex flex-col gap-2" @can('update', $application) x-init="$wire.dispatch('loadCompose', true)" @endcan>
<div class="flex flex-col gap-2" x-init="$wire.dispatch('loadCompose', true)">
@else
<div class="flex flex-col gap-2">
@endcan
<div x-data="{ <div x-data="{
baseDir: '{{ $application->base_directory }}', baseDir: '{{ $application->base_directory }}',
composeLocation: '{{ $application->docker_compose_location }}', composeLocation: '{{ $application->docker_compose_location }}',

View file

@ -2,11 +2,11 @@
<x-resources.breadcrumbs :resource="$application" :parameters="$parameters" :title="$lastDeploymentInfo" :lastDeploymentLink="$lastDeploymentLink" /> <x-resources.breadcrumbs :resource="$application" :parameters="$parameters" :title="$lastDeploymentInfo" :lastDeploymentLink="$lastDeploymentLink" />
<div class="navbar-main"> <div class="navbar-main">
<nav class="flex shrink-0 gap-6 items-center whitespace-nowrap scrollbar min-h-10"> <nav class="flex shrink-0 gap-6 items-center whitespace-nowrap scrollbar min-h-10">
<a class="{{ request()->routeIs('project.application.configuration') ? 'dark:text-white' : '' }}" <a class="{{ request()->routeIs('project.application.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('project.application.configuration', $parameters) }}"> href="{{ route('project.application.configuration', $parameters) }}">
Configuration Configuration
</a> </a>
<a class="{{ request()->routeIs('project.application.deployment.index') ? 'dark:text-white' : '' }}" <a class="{{ request()->routeIs('project.application.deployment.index') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('project.application.deployment.index', $parameters) }}"> href="{{ route('project.application.deployment.index', $parameters) }}">
Deployments Deployments
</a> </a>

View file

@ -94,12 +94,12 @@ class="dark:text-warning">{{ $application->destination->server->name }}</span>.<
</a> </a>
@if (count($parameters) > 0) @if (count($parameters) > 0)
| |
<a <a {{ wireNavigate() }}
href="{{ route('project.application.deployment.index', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}"> href="{{ route('project.application.deployment.index', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}">
Deployment Logs Deployment Logs
</a> </a>
| |
<a <a {{ wireNavigate() }}
href="{{ route('project.application.logs', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}"> href="{{ route('project.application.logs', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}">
Application Logs Application Logs
</a> </a>

View file

@ -7,34 +7,34 @@
<livewire:project.database.heading :database="$database" /> <livewire:project.database.heading :database="$database" />
<div class="flex flex-col h-full gap-8 sm:flex-row"> <div class="flex flex-col h-full gap-8 sm:flex-row">
<div class="flex flex-col items-start gap-2 min-w-fit"> <div class="flex flex-col items-start gap-2 min-w-fit">
<a class='menu-item' wire:current.exact="menu-item-active" <a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.database.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">General</a> href="{{ route('project.database.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">General</a>
<a class='menu-item' wire:current.exact="menu-item-active" <a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.database.environment-variables', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Environment href="{{ route('project.database.environment-variables', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Environment
Variables</a> Variables</a>
<a class='menu-item' wire:current.exact="menu-item-active" <a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.database.servers', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Servers</a> href="{{ route('project.database.servers', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Servers</a>
<a class='menu-item' wire:current.exact="menu-item-active" <a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.database.persistent-storage', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Persistent href="{{ route('project.database.persistent-storage', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Persistent
Storage</a> Storage</a>
@can('update', $database) @can('update', $database)
<a class='menu-item' wire:current.exact="menu-item-active" <a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.database.import-backups', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Import href="{{ route('project.database.import-backups', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Import
Backups</a> Backups</a>
@endcan @endcan
<a class='menu-item' wire:current.exact="menu-item-active" <a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.database.webhooks', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Webhooks</a> href="{{ route('project.database.webhooks', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Webhooks</a>
<a class="menu-item" wire:current.exact="menu-item-active" <a class="menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.database.resource-limits', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Resource href="{{ route('project.database.resource-limits', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Resource
Limits</a> Limits</a>
<a class="menu-item" wire:current.exact="menu-item-active" <a class="menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.database.resource-operations', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Resource href="{{ route('project.database.resource-operations', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Resource
Operations</a> Operations</a>
<a class='menu-item' wire:current.exact="menu-item-active" <a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.database.metrics', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Metrics</a> href="{{ route('project.database.metrics', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Metrics</a>
<a class='menu-item' wire:current.exact="menu-item-active" <a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.database.tags', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Tags</a> href="{{ route('project.database.tags', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Tags</a>
<a class='menu-item' wire:current.exact="menu-item-active" <a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.database.danger', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Danger href="{{ route('project.database.danger', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Danger
Zone</a> Zone</a>
</div> </div>

View file

@ -11,7 +11,7 @@
<div class="navbar-main"> <div class="navbar-main">
<nav <nav
class="flex overflow-x-scroll shrink-0 gap-6 items-center whitespace-nowrap sm:overflow-x-hidden scrollbar min-h-10"> class="flex overflow-x-scroll shrink-0 gap-6 items-center whitespace-nowrap sm:overflow-x-hidden scrollbar min-h-10">
<a class="{{ request()->routeIs('project.database.configuration') ? 'dark:text-white' : '' }}" <a class="{{ request()->routeIs('project.database.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('project.database.configuration', $parameters) }}"> href="{{ route('project.database.configuration', $parameters) }}">
Configuration Configuration
</a> </a>
@ -31,7 +31,7 @@ class="flex overflow-x-scroll shrink-0 gap-6 items-center whitespace-nowrap sm:o
$database->getMorphClass() === 'App\Models\StandaloneMongodb' || $database->getMorphClass() === 'App\Models\StandaloneMongodb' ||
$database->getMorphClass() === 'App\Models\StandaloneMysql' || $database->getMorphClass() === 'App\Models\StandaloneMysql' ||
$database->getMorphClass() === 'App\Models\StandaloneMariadb') $database->getMorphClass() === 'App\Models\StandaloneMariadb')
<a class="{{ request()->routeIs('project.database.backup.index') ? 'dark:text-white' : '' }}" <a class="{{ request()->routeIs('project.database.backup.index') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('project.database.backup.index', $parameters) }}"> href="{{ route('project.database.backup.index', $parameters) }}">
Backups Backups
</a> </a>

View file

@ -28,7 +28,7 @@
</div> </div>
@endif @endif
<x-forms.input <x-forms.input
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>" helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' target='_blank' href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>"
placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k" placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k"
id="customDockerRunOptions" label="Custom Docker Options" canGate="update" :canResource="$database" /> id="customDockerRunOptions" label="Custom Docker Options" canGate="update" :canResource="$database" />
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">

View file

@ -49,7 +49,7 @@
@endif @endif
<div class="pt-2"> <div class="pt-2">
<x-forms.input <x-forms.input
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>" helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' {{ wireNavigate() }} href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>"
placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k" placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k"
id="customDockerRunOptions" label="Custom Docker Options" canGate="update" id="customDockerRunOptions" label="Custom Docker Options" canGate="update"
:canResource="$database" /> :canResource="$database" />

View file

@ -40,7 +40,7 @@
</div> </div>
@endif @endif
<x-forms.input <x-forms.input
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>" helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' {{ wireNavigate() }} href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>"
placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k" placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k"
id="customDockerRunOptions" label="Custom Docker Options" canGate="update" :canResource="$database" /> id="customDockerRunOptions" label="Custom Docker Options" canGate="update" :canResource="$database" />
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">

View file

@ -46,7 +46,7 @@
@endif @endif
<div class="pt-2"> <div class="pt-2">
<x-forms.input <x-forms.input
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>" helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' target='_blank' href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>"
placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k" placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k"
id="customDockerRunOptions" label="Custom Docker Options" canGate="update" :canResource="$database" /> id="customDockerRunOptions" label="Custom Docker Options" canGate="update" :canResource="$database" />
</div> </div>

View file

@ -58,7 +58,7 @@
placeholder="If empty, use default. See in docker docs." /> placeholder="If empty, use default. See in docker docs." />
</div> </div>
<x-forms.input <x-forms.input
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>" helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' {{ wireNavigate() }} href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>"
placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k" placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k"
id="customDockerRunOptions" label="Custom Docker Options" canGate="update" :canResource="$database" /> id="customDockerRunOptions" label="Custom Docker Options" canGate="update" :canResource="$database" />
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">

View file

@ -50,7 +50,7 @@
@endif @endif
</div> </div>
<x-forms.input <x-forms.input
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>" helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' {{ wireNavigate() }} href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>"
placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k" placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k"
id="customDockerRunOptions" label="Custom Docker Options" canGate="update" :canResource="$database" /> id="customDockerRunOptions" label="Custom Docker Options" canGate="update" :canResource="$database" />
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">

View file

@ -32,7 +32,7 @@
$backup->latest_log && $backup->latest_log &&
data_get($backup->latest_log, 'status') === 'success', data_get($backup->latest_log, 'status') === 'success',
'border-gray-200 dark:border-coolgray-300' => !$backup->latest_log, 'border-gray-200 dark:border-coolgray-300' => !$backup->latest_log,
]) ]) {{ wireNavigate() }}
href="{{ route('project.database.backup.execution', [...$parameters, 'backup_uuid' => $backup->uuid]) }}"> href="{{ route('project.database.backup.execution', [...$parameters, 'backup_uuid' => $backup->uuid]) }}">
@if ($backup->latest_log && data_get($backup->latest_log, 'status') === 'running') @if ($backup->latest_log && data_get($backup->latest_log, 'status') === 'running')
<div class="absolute top-2 right-2"> <div class="absolute top-2 right-2">

View file

@ -14,14 +14,14 @@
<ol class="flex flex-wrap items-center gap-y-1"> <ol class="flex flex-wrap items-center gap-y-1">
<li class="inline-flex items-center"> <li class="inline-flex items-center">
<div class="flex items-center"> <div class="flex items-center">
<a class="text-xs truncate lg:text-sm" <a class="text-xs truncate lg:text-sm" {{ wireNavigate() }}
href="{{ route('project.show', ['project_uuid' => $project->uuid]) }}"> href="{{ route('project.show', ['project_uuid' => $project->uuid]) }}">
{{ $project->name }}</a> {{ $project->name }}</a>
</div> </div>
</li> </li>
<li> <li>
<div class="flex items-center"> <div class="flex items-center">
<a class="text-xs truncate lg:text-sm" <a class="text-xs truncate lg:text-sm" {{ wireNavigate() }}
href="{{ route('project.resource.index', ['environment_uuid' => $environment->uuid, 'project_uuid' => $project->uuid]) }}"> href="{{ route('project.resource.index', ['environment_uuid' => $environment->uuid, 'project_uuid' => $project->uuid]) }}">
{{ $environment->name }} {{ $environment->name }}
</a> </a>

View file

@ -25,7 +25,7 @@
<div class="relative z-10 flex items-center justify-center gap-4 text-xs font-bold"> <div class="relative z-10 flex items-center justify-center gap-4 text-xs font-bold">
@if ($project->environments->first()) @if ($project->environments->first())
@can('createAnyResource') @can('createAnyResource')
<a class="hover:underline" <a class="hover:underline" {{ wireNavigate() }}
href="{{ route('project.resource.create', [ href="{{ route('project.resource.create', [
'project_uuid' => $project->uuid, 'project_uuid' => $project->uuid,
'environment_uuid' => $project->environments->first()->uuid, 'environment_uuid' => $project->environments->first()->uuid,
@ -35,7 +35,7 @@
@endcan @endcan
@endif @endif
@can('update', $project) @can('update', $project)
<a class="hover:underline" <a class="hover:underline" {{ wireNavigate() }}
href="{{ route('project.edit', ['project_uuid' => $project->uuid]) }}"> href="{{ route('project.edit', ['project_uuid' => $project->uuid]) }}">
Settings Settings
</a> </a>

View file

@ -38,7 +38,7 @@ class="loading loading-xs dark:text-warning loading-spinner"></span>
<div> <div>
No private keys found. No private keys found.
</div> </div>
<a href="{{ route('security.private-key.index') }}"> <a href="{{ route('security.private-key.index') }}" {{ wireNavigate() }}>
<x-forms.button>Create a new private key</x-forms.button> <x-forms.button>Create a new private key</x-forms.button>
</a> </a>
</div> </div>

View file

@ -385,7 +385,7 @@ function searchResources() {
<div class="flex flex-col justify-center gap-4 text-left xl:flex-row xl:flex-wrap"> <div class="flex flex-col justify-center gap-4 text-left xl:flex-row xl:flex-wrap">
@if ($onlyBuildServerAvailable) @if ($onlyBuildServerAvailable)
<div> Only build servers are available, you need at least one server that is not set as build <div> Only build servers are available, you need at least one server that is not set as build
server. <a class="underline dark:text-white" href="/servers"> server. <a class="underline dark:text-white" href="/servers" {{ wireNavigate() }}>
Go to servers page Go to servers page
</a> </div> </a> </div>
@else @else
@ -404,7 +404,7 @@ function searchResources() {
<div> <div>
<div>No validated & reachable servers found. <a class="underline dark:text-white" <div>No validated & reachable servers found. <a class="underline dark:text-white"
href="/servers"> href="/servers" {{ wireNavigate() }}>
Go to servers page Go to servers page
</a></div> </a></div>
</div> </div>

View file

@ -7,19 +7,19 @@
<h1>Resources</h1> <h1>Resources</h1>
@if ($environment->isEmpty()) @if ($environment->isEmpty())
@can('createAnyResource') @can('createAnyResource')
<a class="button" <a class="button" {{ wireNavigate() }}
href="{{ route('project.clone-me', ['project_uuid' => data_get($project, 'uuid'), 'environment_uuid' => data_get($environment, 'uuid')]) }}"> href="{{ route('project.clone-me', ['project_uuid' => data_get($project, 'uuid'), 'environment_uuid' => data_get($environment, 'uuid')]) }}">
Clone Clone
</a> </a>
@endcan @endcan
@else @else
@can('createAnyResource') @can('createAnyResource')
<a href="{{ route('project.resource.create', ['project_uuid' => data_get($parameters, 'project_uuid'), 'environment_uuid' => data_get($environment, 'uuid')]) }}" <a href="{{ route('project.resource.create', ['project_uuid' => data_get($parameters, 'project_uuid'), 'environment_uuid' => data_get($environment, 'uuid')]) }}" {{ wireNavigate() }}
class="button">+ class="button">+
New</a> New</a>
@endcan @endcan
@can('createAnyResource') @can('createAnyResource')
<a class="button" <a class="button" {{ wireNavigate() }}
href="{{ route('project.clone-me', ['project_uuid' => data_get($project, 'uuid'), 'environment_uuid' => data_get($environment, 'uuid')]) }}"> href="{{ route('project.clone-me', ['project_uuid' => data_get($project, 'uuid'), 'environment_uuid' => data_get($environment, 'uuid')]) }}">
Clone Clone
</a> </a>
@ -36,7 +36,7 @@ class="button">+
<ol class="flex items-center"> <ol class="flex items-center">
<li class="inline-flex items-center" x-data="{ projectOpen: false, toggle() { this.projectOpen = !this.projectOpen }, open() { this.projectOpen = true }, close() { this.projectOpen = false } }"> <li class="inline-flex items-center" x-data="{ projectOpen: false, toggle() { this.projectOpen = !this.projectOpen }, open() { this.projectOpen = true }, close() { this.projectOpen = false } }">
<div class="flex items-center relative" @mouseenter="open()" @mouseleave="close()"> <div class="flex items-center relative" @mouseenter="open()" @mouseleave="close()">
<a class="text-xs truncate lg:text-sm hover:text-warning" <a class="text-xs truncate lg:text-sm hover:text-warning" {{ wireNavigate() }}
href="{{ route('project.show', ['project_uuid' => data_get($parameters, 'project_uuid')]) }}"> href="{{ route('project.show', ['project_uuid' => data_get($parameters, 'project_uuid')]) }}">
{{ $project->name }}</a> {{ $project->name }}</a>
<button type="button" @click.stop="toggle()" class="px-1 text-warning"> <button type="button" @click.stop="toggle()" class="px-1 text-warning">
@ -66,7 +66,7 @@ class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolg
@endphp @endphp
<li class="inline-flex items-center" x-data="{ envOpen: false, activeEnv: null, envPositions: {}, activeRes: null, resPositions: {}, activeMenuEnv: null, menuPositions: {}, closeTimeout: null, envTimeout: null, resTimeout: null, menuTimeout: null, toggle() { this.envOpen = !this.envOpen; if (!this.envOpen) { this.activeEnv = null; this.activeRes = null; this.activeMenuEnv = null; } }, open() { clearTimeout(this.closeTimeout); this.envOpen = true }, close() { this.closeTimeout = setTimeout(() => { this.envOpen = false; this.activeEnv = null; this.activeRes = null; this.activeMenuEnv = null; }, 100) }, openEnv(id) { clearTimeout(this.closeTimeout); clearTimeout(this.envTimeout); this.activeEnv = id }, closeEnv() { this.envTimeout = setTimeout(() => { this.activeEnv = null; this.activeRes = null; this.activeMenuEnv = null; }, 100) }, openRes(id) { clearTimeout(this.envTimeout); clearTimeout(this.resTimeout); this.activeRes = id }, closeRes() { this.resTimeout = setTimeout(() => { this.activeRes = null; this.activeMenuEnv = null; }, 100) }, openMenu(id) { clearTimeout(this.resTimeout); clearTimeout(this.menuTimeout); this.activeMenuEnv = id }, closeMenu() { this.menuTimeout = setTimeout(() => { this.activeMenuEnv = null; }, 100) } }"> <li class="inline-flex items-center" x-data="{ envOpen: false, activeEnv: null, envPositions: {}, activeRes: null, resPositions: {}, activeMenuEnv: null, menuPositions: {}, closeTimeout: null, envTimeout: null, resTimeout: null, menuTimeout: null, toggle() { this.envOpen = !this.envOpen; if (!this.envOpen) { this.activeEnv = null; this.activeRes = null; this.activeMenuEnv = null; } }, open() { clearTimeout(this.closeTimeout); this.envOpen = true }, close() { this.closeTimeout = setTimeout(() => { this.envOpen = false; this.activeEnv = null; this.activeRes = null; this.activeMenuEnv = null; }, 100) }, openEnv(id) { clearTimeout(this.closeTimeout); clearTimeout(this.envTimeout); this.activeEnv = id }, closeEnv() { this.envTimeout = setTimeout(() => { this.activeEnv = null; this.activeRes = null; this.activeMenuEnv = null; }, 100) }, openRes(id) { clearTimeout(this.envTimeout); clearTimeout(this.resTimeout); this.activeRes = id }, closeRes() { this.resTimeout = setTimeout(() => { this.activeRes = null; this.activeMenuEnv = null; }, 100) }, openMenu(id) { clearTimeout(this.resTimeout); clearTimeout(this.menuTimeout); this.activeMenuEnv = id }, closeMenu() { this.menuTimeout = setTimeout(() => { this.activeMenuEnv = null; }, 100) } }">
<div class="flex items-center relative" @mouseenter="open()" @mouseleave="close()"> <div class="flex items-center relative" @mouseenter="open()" @mouseleave="close()">
<a class="text-xs truncate lg:text-sm hover:text-warning" <a class="text-xs truncate lg:text-sm hover:text-warning" {{ wireNavigate() }}
href="{{ route('project.resource.index', ['project_uuid' => data_get($parameters, 'project_uuid'), 'environment_uuid' => $environment->uuid]) }}"> href="{{ route('project.resource.index', ['project_uuid' => data_get($parameters, 'project_uuid'), 'environment_uuid' => $environment->uuid]) }}">
{{ $environment->name }} {{ $environment->name }}
</a> </a>
@ -106,7 +106,7 @@ class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutra
</div> </div>
@endforeach @endforeach
<div class="border-t border-neutral-200 dark:border-coolgray-200 mt-1 pt-1"> <div class="border-t border-neutral-200 dark:border-coolgray-200 mt-1 pt-1">
<a href="{{ route('project.show', ['project_uuid' => data_get($parameters, 'project_uuid')]) }}" <a href="{{ route('project.show', ['project_uuid' => data_get($parameters, 'project_uuid')]) }}" {{ wireNavigate() }}
class="flex items-center gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200"> class="flex items-center gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
@ -311,7 +311,7 @@ class="pl-1">
</div> </div>
@if ($environment->isEmpty()) @if ($environment->isEmpty())
@can('createAnyResource') @can('createAnyResource')
<a href="{{ route('project.resource.create', ['project_uuid' => data_get($parameters, 'project_uuid'), 'environment_uuid' => data_get($environment, 'uuid')]) }}" <a href="{{ route('project.resource.create', ['project_uuid' => data_get($parameters, 'project_uuid'), 'environment_uuid' => data_get($environment, 'uuid')]) }}" {{ wireNavigate() }}
class="items-center justify-center coolbox">+ Add Resource</a> class="items-center justify-center coolbox">+ Add Resource</a>
@else @else
<div <div
@ -352,7 +352,7 @@ class="font-semibold" x-text="search"></span>".</p>
class="grid grid-cols-1 gap-4 pt-4 lg:grid-cols-2 xl:grid-cols-3"> class="grid grid-cols-1 gap-4 pt-4 lg:grid-cols-2 xl:grid-cols-3">
<template x-for="item in filteredApplications" :key="item.uuid"> <template x-for="item in filteredApplications" :key="item.uuid">
<span> <span>
<a class="h-24 coolbox group" :href="item.hrefLink"> <a class="h-24 coolbox group" :href="item.hrefLink" {{ wireNavigate() }}>
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class="flex gap-2 px-4"> <div class="flex gap-2 px-4">
<div class="pb-2 truncate box-title" x-text="item.name"></div> <div class="pb-2 truncate box-title" x-text="item.name"></div>
@ -402,7 +402,7 @@ class="flex flex-wrap gap-1 pt-1 dark:group-hover:text-white group-hover:text-bl
class="grid grid-cols-1 gap-4 pt-4 lg:grid-cols-2 xl:grid-cols-3"> class="grid grid-cols-1 gap-4 pt-4 lg:grid-cols-2 xl:grid-cols-3">
<template x-for="item in filteredDatabases" :key="item.uuid"> <template x-for="item in filteredDatabases" :key="item.uuid">
<span> <span>
<a class="h-24 coolbox group" :href="item.hrefLink"> <a class="h-24 coolbox group" :href="item.hrefLink" {{ wireNavigate() }}>
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class="flex gap-2 px-4"> <div class="flex gap-2 px-4">
<div class="pb-2 truncate box-title" x-text="item.name"></div> <div class="pb-2 truncate box-title" x-text="item.name"></div>
@ -452,7 +452,7 @@ class="flex flex-wrap gap-1 pt-1 dark:group-hover:text-white group-hover:text-bl
class="grid grid-cols-1 gap-4 pt-4 lg:grid-cols-2 xl:grid-cols-3"> class="grid grid-cols-1 gap-4 pt-4 lg:grid-cols-2 xl:grid-cols-3">
<template x-for="item in filteredServices" :key="item.uuid"> <template x-for="item in filteredServices" :key="item.uuid">
<span> <span>
<a class="h-24 coolbox group" :href="item.hrefLink"> <a class="h-24 coolbox group" :href="item.hrefLink" {{ wireNavigate() }}>
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class="flex gap-2 px-4"> <div class="flex gap-2 px-4">
<div class="pb-2 truncate box-title" x-text="item.name"></div> <div class="pb-2 truncate box-title" x-text="item.name"></div>

View file

@ -8,27 +8,27 @@
<div class="flex flex-col items-start gap-2 min-w-fit"> <div class="flex flex-col items-start gap-2 min-w-fit">
<a class="menu-item sm:min-w-fit" target="_blank" href="{{ $service->documentation() }}">Documentation <a class="menu-item sm:min-w-fit" target="_blank" href="{{ $service->documentation() }}">Documentation
<x-external-link /></a> <x-external-link /></a>
<a class='menu-item' wire:current.exact="menu-item-active" <a class='menu-item' wire:current.exact="menu-item-active" {{ wireNavigate() }}
href="{{ route('project.service.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}">General</a> href="{{ route('project.service.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}">General</a>
<a class='menu-item' wire:current.exact="menu-item-active" <a class='menu-item' wire:current.exact="menu-item-active" {{ wireNavigate() }}
href="{{ route('project.service.environment-variables', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}">Environment href="{{ route('project.service.environment-variables', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}">Environment
Variables</a> Variables</a>
<a class='menu-item' wire:current.exact="menu-item-active" <a class='menu-item' wire:current.exact="menu-item-active" {{ wireNavigate() }}
href="{{ route('project.service.storages', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}">Persistent href="{{ route('project.service.storages', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}">Persistent
Storages</a> Storages</a>
<a class='menu-item' wire:current.exact="menu-item-active" <a class='menu-item' wire:current.exact="menu-item-active" {{ wireNavigate() }}
href="{{ route('project.service.scheduled-tasks.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}">Scheduled href="{{ route('project.service.scheduled-tasks.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}">Scheduled
Tasks</a> Tasks</a>
<a class='menu-item' wire:current.exact="menu-item-active" <a class='menu-item' wire:current.exact="menu-item-active" {{ wireNavigate() }}
href="{{ route('project.service.webhooks', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}">Webhooks</a> href="{{ route('project.service.webhooks', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}">Webhooks</a>
<a class='menu-item' wire:current.exact="menu-item-active" <a class='menu-item' wire:current.exact="menu-item-active" {{ wireNavigate() }}
href="{{ route('project.service.resource-operations', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}">Resource href="{{ route('project.service.resource-operations', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}">Resource
Operations</a> Operations</a>
<a class='menu-item' wire:current.exact="menu-item-active" <a class='menu-item' wire:current.exact="menu-item-active" {{ wireNavigate() }}
href="{{ route('project.service.tags', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}">Tags</a> href="{{ route('project.service.tags', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}">Tags</a>
<a class='menu-item' wire:current.exact="menu-item-active" <a class='menu-item' wire:current.exact="menu-item-active" {{ wireNavigate() }}
href="{{ route('project.service.danger', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}">Danger href="{{ route('project.service.danger', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}">Danger
Zone</a> Zone</a>
</div> </div>
@ -104,7 +104,7 @@ class="w-4 h-4 dark:text-warning text-coollabs"
<div class="pt-2 text-xs">{{ formatContainerStatus($application->status) }}</div> <div class="pt-2 text-xs">{{ formatContainerStatus($application->status) }}</div>
</div> </div>
<div class="flex items-center px-4"> <div class="flex items-center px-4">
<a class="mx-4 text-xs font-bold hover:underline" <a class="mx-4 text-xs font-bold hover:underline" {{ wireNavigate() }}
href="{{ route('project.service.index', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid, 'stack_service_uuid' => $application->uuid]) }}"> href="{{ route('project.service.index', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid, 'stack_service_uuid' => $application->uuid]) }}">
Settings Settings
</a> </a>
@ -154,12 +154,12 @@ class="w-4 h-4 dark:text-warning text-coollabs"
</div> </div>
<div class="flex items-center px-4"> <div class="flex items-center px-4">
@if ($database->isBackupSolutionAvailable() || $database->is_migrated) @if ($database->isBackupSolutionAvailable() || $database->is_migrated)
<a class="mx-4 text-xs font-bold hover:underline" <a class="mx-4 text-xs font-bold hover:underline" {{ wireNavigate() }}
href="{{ route('project.service.index', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid, 'stack_service_uuid' => $database->uuid]) }}#backups"> href="{{ route('project.service.index', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid, 'stack_service_uuid' => $database->uuid]) }}#backups">
Backups Backups
</a> </a>
@endif @endif
<a class="mx-4 text-xs font-bold hover:underline" <a class="mx-4 text-xs font-bold hover:underline" {{ wireNavigate() }}
href="{{ route('project.service.index', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid, 'stack_service_uuid' => $database->uuid]) }}"> href="{{ route('project.service.index', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid, 'stack_service_uuid' => $database->uuid]) }}">
Settings Settings
</a> </a>

View file

@ -10,7 +10,7 @@
<x-resources.breadcrumbs :resource="$service" :parameters="$parameters" /> <x-resources.breadcrumbs :resource="$service" :parameters="$parameters" />
<div class="navbar-main" x-data"> <div class="navbar-main" x-data">
<nav class="flex shrink-0 gap-6 items-center whitespace-nowrap scrollbar min-h-10"> <nav class="flex shrink-0 gap-6 items-center whitespace-nowrap scrollbar min-h-10">
<a class="{{ request()->routeIs('project.service.configuration') ? 'dark:text-white' : '' }}" <a class="{{ request()->routeIs('project.service.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('project.service.configuration', $parameters) }}"> href="{{ route('project.service.configuration', $parameters) }}">
<button>Configuration</button> <button>Configuration</button>
</a> </a>
@ -127,7 +127,7 @@
@else @else
<div class="flex flex-wrap order-first gap-2 items-center sm:order-last"> <div class="flex flex-wrap order-first gap-2 items-center sm:order-last">
<div class="text-error"> <div class="text-error">
Unable to deploy. <a class="underline font-bold cursor-pointer" Unable to deploy. <a class="underline font-bold cursor-pointer" {{ wireNavigate() }}
href="{{ route('project.service.environment-variables', $parameters) }}"> href="{{ route('project.service.environment-variables', $parameters) }}">
Required environment variables missing.</a> Required environment variables missing.</a>
</div> </div>

View file

@ -3,7 +3,7 @@
<div class="flex flex-col h-full gap-8 sm:flex-row"> <div class="flex flex-col h-full gap-8 sm:flex-row">
<div class="flex flex-col items-start gap-2 min-w-fit"> <div class="flex flex-col items-start gap-2 min-w-fit">
<a class="menu-item" <a class="menu-item"
class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-active' : '' }}" class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
href="{{ route('project.service.configuration', [...$parameters, 'stack_service_uuid' => null]) }}"> href="{{ route('project.service.configuration', [...$parameters, 'stack_service_uuid' => null]) }}">
<button><- Back</button> <button><- Back</button>
</a> </a>

View file

@ -5,17 +5,20 @@
logsLoaded: false, logsLoaded: false,
fullscreen: false, fullscreen: false,
alwaysScroll: false, alwaysScroll: false,
intervalId: null, rafId: null,
scrollDebounce: null, scrollDebounce: null,
colorLogs: localStorage.getItem('coolify-color-logs') === 'true', colorLogs: localStorage.getItem('coolify-color-logs') === 'true',
searchQuery: '', searchQuery: '',
renderTrigger: 0, matchCount: 0,
containerName: '{{ $container ?? "logs" }}', containerName: '{{ $container ?? "logs" }}',
makeFullscreen() { makeFullscreen() {
this.fullscreen = !this.fullscreen; this.fullscreen = !this.fullscreen;
if (this.fullscreen === false) { if (this.fullscreen === false) {
this.alwaysScroll = false; this.alwaysScroll = false;
clearInterval(this.intervalId); if (this.rafId) {
cancelAnimationFrame(this.rafId);
this.rafId = null;
}
} }
}, },
handleKeyDown(event) { handleKeyDown(event) {
@ -24,67 +27,72 @@
} }
}, },
isScrolling: false, isScrolling: false,
scrollToBottom() {
const logsContainer = document.getElementById('logsContainer');
if (logsContainer) {
this.isScrolling = true;
logsContainer.scrollTop = logsContainer.scrollHeight;
setTimeout(() => { this.isScrolling = false; }, 50);
}
},
scheduleScroll() {
if (!this.alwaysScroll) return;
this.rafId = requestAnimationFrame(() => {
this.scrollToBottom();
if (this.alwaysScroll) {
setTimeout(() => this.scheduleScroll(), 250);
}
});
},
toggleScroll() { toggleScroll() {
this.alwaysScroll = !this.alwaysScroll; this.alwaysScroll = !this.alwaysScroll;
if (this.alwaysScroll) { if (this.alwaysScroll) {
this.intervalId = setInterval(() => { this.scheduleScroll();
const logsContainer = document.getElementById('logsContainer');
if (logsContainer) {
this.isScrolling = true;
logsContainer.scrollTop = logsContainer.scrollHeight;
setTimeout(() => { this.isScrolling = false; }, 50);
}
}, 100);
} else { } else {
clearInterval(this.intervalId); if (this.rafId) {
this.intervalId = null; cancelAnimationFrame(this.rafId);
this.rafId = null;
}
} }
}, },
handleScroll(event) { handleScroll(event) {
// Skip if follow logs is disabled or this is a programmatic scroll
if (!this.alwaysScroll || this.isScrolling) return; if (!this.alwaysScroll || this.isScrolling) return;
// Debounce scroll handling to avoid false positives from DOM mutations
// when Livewire re-renders and adds new log lines
clearTimeout(this.scrollDebounce); clearTimeout(this.scrollDebounce);
this.scrollDebounce = setTimeout(() => { this.scrollDebounce = setTimeout(() => {
const el = event.target; const el = event.target;
const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight; const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
// Use larger threshold (100px) to avoid accidental disables
if (distanceFromBottom > 100) { if (distanceFromBottom > 100) {
this.alwaysScroll = false; this.alwaysScroll = false;
clearInterval(this.intervalId); if (this.rafId) {
this.intervalId = null; cancelAnimationFrame(this.rafId);
this.rafId = null;
}
} }
}, 150); }, 150);
}, },
toggleColorLogs() { toggleColorLogs() {
this.colorLogs = !this.colorLogs; this.colorLogs = !this.colorLogs;
localStorage.setItem('coolify-color-logs', this.colorLogs); localStorage.setItem('coolify-color-logs', this.colorLogs);
this.applyColorLogs();
}, },
getLogLevel(text) { applyColorLogs() {
const lowerText = text.toLowerCase(); const logs = document.getElementById('logs');
// Error detection (highest priority) if (!logs) return;
if (/\b(error|err|failed|failure|exception|fatal|panic|critical)\b/.test(lowerText)) { const lines = logs.querySelectorAll('[data-log-line]');
return 'error'; lines.forEach(line => {
} const content = (line.dataset.logContent || '').toLowerCase();
// Warning detection line.classList.remove('log-error', 'log-warning', 'log-debug', 'log-info');
if (/\b(warn|warning|wrn|caution)\b/.test(lowerText)) { if (!this.colorLogs) return;
return 'warning'; if (/\b(error|err|failed|failure|exception|fatal|panic|critical)\b/.test(content)) {
} line.classList.add('log-error');
// Debug detection } else if (/\b(warn|warning|wrn|caution)\b/.test(content)) {
if (/\b(debug|dbg|trace|verbose)\b/.test(lowerText)) { line.classList.add('log-warning');
return 'debug'; } else if (/\b(debug|dbg|trace|verbose)\b/.test(content)) {
} line.classList.add('log-debug');
// Info detection } else if (/\b(info|inf|notice)\b/.test(content)) {
if (/\b(info|inf|notice)\b/.test(lowerText)) { line.classList.add('log-info');
return 'info'; }
} });
return null;
},
matchesSearch(line) {
if (!this.searchQuery.trim()) return true;
return line.toLowerCase().includes(this.searchQuery.toLowerCase());
}, },
hasActiveLogSelection() { hasActiveLogSelection() {
const selection = window.getSelection(); const selection = window.getSelection();
@ -93,79 +101,62 @@
} }
const logsContainer = document.getElementById('logs'); const logsContainer = document.getElementById('logs');
if (!logsContainer) return false; if (!logsContainer) return false;
// Check if selection is within the logs container
const range = selection.getRangeAt(0); const range = selection.getRangeAt(0);
return logsContainer.contains(range.commonAncestorContainer); return logsContainer.contains(range.commonAncestorContainer);
}, },
decodeHtml(text) { applySearch() {
// Decode HTML entities, handling double-encoding with max iteration limit to prevent DoS const logs = document.getElementById('logs');
let decoded = text; if (!logs) return;
let prev = ''; const lines = logs.querySelectorAll('[data-log-line]');
let iterations = 0; const query = this.searchQuery.trim().toLowerCase();
const maxIterations = 3; // Prevent DoS from deeply nested HTML entities let count = 0;
while (decoded !== prev && iterations < maxIterations) { lines.forEach(line => {
prev = decoded; const content = (line.dataset.logContent || '').toLowerCase();
const doc = new DOMParser().parseFromString(decoded, 'text/html'); const textSpan = line.querySelector('[data-line-text]');
decoded = doc.documentElement.textContent; const matches = !query || content.includes(query);
iterations++;
}
return decoded;
},
renderHighlightedLog(el, text) {
// Skip re-render if user has text selected in logs (preserves copy ability)
// But always render if the element is empty (initial render)
if (el.textContent && this.hasActiveLogSelection()) {
return;
}
const decoded = this.decodeHtml(text); line.classList.toggle('hidden', !matches);
el.textContent = ''; if (matches && query) count++;
if (!this.searchQuery.trim()) { // Update highlighting
el.textContent = decoded; if (textSpan) {
return; const originalText = textSpan.dataset.lineText || '';
} if (!query) {
textSpan.textContent = originalText;
const query = this.searchQuery.toLowerCase(); } else if (matches) {
const lowerText = decoded.toLowerCase(); this.highlightText(textSpan, originalText, query);
let lastIndex = 0; }
}
let index = lowerText.indexOf(query, lastIndex); });
while (index !== -1) {
// Add text before match this.matchCount = query ? count : 0;
if (index > lastIndex) { },
el.appendChild(document.createTextNode(decoded.substring(lastIndex, index))); highlightText(el, text, query) {
// Skip if user has selection
if (this.hasActiveLogSelection()) return;
el.textContent = '';
const lowerText = text.toLowerCase();
let lastIndex = 0;
let index = lowerText.indexOf(query, lastIndex);
while (index !== -1) {
if (index > lastIndex) {
el.appendChild(document.createTextNode(text.substring(lastIndex, index)));
} }
// Add highlighted match
const mark = document.createElement('span'); const mark = document.createElement('span');
mark.className = 'log-highlight'; mark.className = 'log-highlight';
mark.textContent = decoded.substring(index, index + this.searchQuery.length); mark.textContent = text.substring(index, index + query.length);
el.appendChild(mark); el.appendChild(mark);
lastIndex = index + query.length;
lastIndex = index + this.searchQuery.length;
index = lowerText.indexOf(query, lastIndex); index = lowerText.indexOf(query, lastIndex);
} }
// Add remaining text if (lastIndex < text.length) {
if (lastIndex < decoded.length) { el.appendChild(document.createTextNode(text.substring(lastIndex)));
el.appendChild(document.createTextNode(decoded.substring(lastIndex)));
} }
}, },
getMatchCount() {
if (!this.searchQuery.trim()) return 0;
const logs = document.getElementById('logs');
if (!logs) return 0;
const lines = logs.querySelectorAll('[data-log-line]');
let count = 0;
lines.forEach(line => {
if (line.textContent.toLowerCase().includes(this.searchQuery.toLowerCase())) {
count++;
}
});
return count;
},
downloadLogs() { downloadLogs() {
const logs = document.getElementById('logs'); const logs = document.getElementById('logs');
if (!logs) return; if (!logs) return;
@ -191,17 +182,23 @@
this.$wire.getLogs(true); this.$wire.getLogs(true);
this.logsLoaded = true; this.logsLoaded = true;
} }
// Prevent Livewire from morphing logs container when text is selected
Livewire.hook('morph.updating', ({ el, component, toEl, skip }) => { // Watch search query changes
if (el.id === 'logs' && this.hasActiveLogSelection()) { this.$watch('searchQuery', () => {
skip(); this.applySearch();
}
}); });
// Re-render logs after Livewire updates
Livewire.hook('commit', ({ succeed }) => { // Apply colors after Livewire updates
succeed(() => { Livewire.hook('morph.updated', ({ el }) => {
this.$nextTick(() => { this.renderTrigger++; }); if (el.id === 'logs') {
}); this.$nextTick(() => {
this.applyColorLogs();
this.applySearch();
if (this.alwaysScroll) {
this.scrollToBottom();
}
});
}
}); });
} }
}" @keydown.window="handleKeyDown($event)"> }" @keydown.window="handleKeyDown($event)">
@ -242,7 +239,7 @@ class="absolute left-2 top-1/2 -translate-y-1/2 text-xs text-gray-400 pointer-ev
title="Number of Lines" {{ $streamLogs ? 'readonly' : '' }} title="Number of Lines" {{ $streamLogs ? 'readonly' : '' }}
class="input input-sm w-32 pl-11 text-center dark:bg-coolgray-300" /> class="input input-sm w-32 pl-11 text-center dark:bg-coolgray-300" />
</form> </form>
<span x-show="searchQuery.trim()" x-text="getMatchCount() + ' matches'" <span x-show="searchQuery.trim()" x-text="matchCount + ' matches'"
class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap"></span> class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap"></span>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@ -288,6 +285,21 @@ class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-
</svg> </svg>
@endif @endif
</button> </button>
<button
x-on:click="
$wire.copyLogs().then(logs => {
navigator.clipboard.writeText(logs);
Livewire.dispatch('success', ['Logs copied to clipboard.']);
});
"
title="Copy Logs"
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75" />
</svg>
</button>
<button x-on:click="downloadLogs()" title="Download Logs" <button x-on:click="downloadLogs()" title="Download Logs"
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"> class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" <svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
@ -359,7 +371,7 @@ class="flex overflow-y-auto overflow-x-hidden flex-col px-4 py-2 w-full min-w-0
Showing last {{ number_format($maxDisplayLines) }} of {{ number_format($totalLines) }} lines Showing last {{ number_format($maxDisplayLines) }} of {{ number_format($totalLines) }} lines
</div> </div>
@endif @endif
<div x-show="searchQuery.trim() && getMatchCount() === 0" <div x-show="searchQuery.trim() && matchCount === 0"
class="text-gray-500 dark:text-gray-400 py-2"> class="text-gray-500 dark:text-gray-400 py-2">
No matches found. No matches found.
</div> </div>
@ -383,23 +395,12 @@ class="text-gray-500 dark:text-gray-400 py-2">
// Format: 2025-Dec-04 09:44:58.198879 // Format: 2025-Dec-04 09:44:58.198879
$timestamp = "{$year}-{$monthName}-{$day} {$time}.{$microseconds}"; $timestamp = "{$year}-{$monthName}-{$day} {$time}.{$microseconds}";
} }
@endphp @endphp
<div data-log-line data-log-content="{{ $line }}" <div data-log-line data-log-content="{{ $line }}" class="flex gap-2 log-line">
x-effect="renderTrigger; searchQuery; $el.classList.toggle('hidden', !matchesSearch($el.dataset.logContent))"
x-bind:class="{
'bg-red-500/10 dark:bg-red-500/15': colorLogs && getLogLevel($el.dataset.logContent) === 'error',
'bg-yellow-500/10 dark:bg-yellow-500/15': colorLogs && getLogLevel($el.dataset.logContent) === 'warning',
'bg-purple-500/10 dark:bg-purple-500/15': colorLogs && getLogLevel($el.dataset.logContent) === 'debug',
'bg-blue-500/10 dark:bg-blue-500/15': colorLogs && getLogLevel($el.dataset.logContent) === 'info',
}"
class="flex gap-2">
@if ($timestamp && $showTimeStamps) @if ($timestamp && $showTimeStamps)
<span class="shrink-0 text-gray-500">{{ $timestamp }}</span> <span class="shrink-0 text-gray-500">{{ $timestamp }}</span>
@endif @endif
<span data-line-text="{{ $logContent }}" <span data-line-text="{{ $logContent }}" class="whitespace-pre-wrap break-all">{{ $logContent }}</span>
x-effect="renderTrigger; searchQuery; renderHighlightedLog($el, $el.dataset.lineText)"
class="whitespace-pre-wrap break-all"></span>
</div> </div>
@endforeach @endforeach
</div> </div>

View file

@ -8,7 +8,7 @@
<div class="alert alert-warning">Metrics are not available for Docker Compose applications yet!</div> <div class="alert alert-warning">Metrics are not available for Docker Compose applications yet!</div>
@elseif(!$resource->destination->server->isMetricsEnabled()) @elseif(!$resource->destination->server->isMetricsEnabled())
<div class="alert alert-warning">Metrics are only available for servers with Sentinel & Metrics enabled!</div> <div class="alert alert-warning">Metrics are only available for servers with Sentinel & Metrics enabled!</div>
<div>Go to <a class="underline dark:text-white" href="{{ route('server.show', $resource->destination->server->uuid) }}">Server settings</a> to enable it.</div> <div>Go to <a class="underline dark:text-white" href="{{ route('server.show', $resource->destination->server->uuid) }}" {{ wireNavigate() }}>Server settings</a> to enable it.</div>
@else @else
@if (!str($resource->status)->contains('running')) @if (!str($resource->status)->contains('running'))
<div class="alert alert-warning">Metrics are only available when the application container is running!</div> <div class="alert alert-warning">Metrics are only available when the application container is running!</div>

View file

@ -14,7 +14,7 @@
<div class="flex flex-col flex-wrap gap-2 pt-4"> <div class="flex flex-col flex-wrap gap-2 pt-4">
@forelse($resource->scheduled_tasks as $task) @forelse($resource->scheduled_tasks as $task)
@if ($resource->type() == 'application') @if ($resource->type() == 'application')
<a class="coolbox" <a class="coolbox" {{ wireNavigate() }}
href="{{ route('project.application.scheduled-tasks', [...$parameters, 'task_uuid' => $task->uuid]) }}"> href="{{ route('project.application.scheduled-tasks', [...$parameters, 'task_uuid' => $task->uuid]) }}">
<span class="flex flex-col"> <span class="flex flex-col">
<span class="text-lg font-bold">{{ $task->name }} <span class="text-lg font-bold">{{ $task->name }}
@ -29,7 +29,7 @@
</span> </span>
</a> </a>
@elseif ($resource->type() == 'service') @elseif ($resource->type() == 'service')
<a class="coolbox" <a class="coolbox" {{ wireNavigate() }}
href="{{ route('project.service.scheduled-tasks', [...$parameters, 'task_uuid' => $task->uuid]) }}"> href="{{ route('project.service.scheduled-tasks', [...$parameters, 'task_uuid' => $task->uuid]) }}">
<span class="flex flex-col"> <span class="flex flex-col">
<span class="text-lg font-bold">{{ $task->name }} <span class="text-lg font-bold">{{ $task->name }}

View file

@ -23,7 +23,7 @@
@forelse ($project->environments->sortBy('created_at') as $environment) @forelse ($project->environments->sortBy('created_at') as $environment)
<div class="gap-2 coolbox group"> <div class="gap-2 coolbox group">
<div class="flex flex-1 mx-6"> <div class="flex flex-1 mx-6">
<a class="flex flex-col justify-center flex-1" <a class="flex flex-col justify-center flex-1" {{ wireNavigate() }}
href="{{ route('project.resource.index', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid]) }}"> href="{{ route('project.resource.index', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid]) }}">
<div class="font-bold dark:text-white"> {{ $environment->name }}</div> <div class="font-bold dark:text-white"> {{ $environment->name }}</div>
<div class="description"> <div class="description">
@ -31,7 +31,7 @@
</a> </a>
@can('update', $project) @can('update', $project)
<div class="flex items-center justify-center gap-2 text-xs"> <div class="flex items-center justify-center gap-2 text-xs">
<a class="font-bold hover:underline" <a class="font-bold hover:underline" {{ wireNavigate() }}
href="{{ route('project.environment.edit', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid]) }}"> href="{{ route('project.environment.edit', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid]) }}">
Settings Settings
</a> </a>

View file

@ -7,7 +7,7 @@
<h2>API Tokens</h2> <h2>API Tokens</h2>
@if (!$isApiEnabled) @if (!$isApiEnabled)
<div>API is disabled. If you want to use the API, please enable it in the <a <div>API is disabled. If you want to use the API, please enable it in the <a
href="{{ route('settings.advanced') }}" class="underline dark:text-white">Settings</a> menu.</div> href="{{ route('settings.advanced') }}" class="underline dark:text-white" {{ wireNavigate() }}>Settings</a> menu.</div>
@else @else
<div>Tokens are created with the current team as scope.</div> <div>Tokens are created with the current team as scope.</div>
</div> </div>

View file

@ -17,7 +17,7 @@
@can('view', $key) @can('view', $key)
{{-- Admin/Owner: Clickable link --}} {{-- Admin/Owner: Clickable link --}}
<a class="coolbox group" <a class="coolbox group"
href="{{ route('security.private-key.show', ['private_key_uuid' => data_get($key, 'uuid')]) }}"> href="{{ route('security.private-key.show', ['private_key_uuid' => data_get($key, 'uuid')]) }}" {{ wireNavigate() }}>
<div class="flex flex-col justify-center mx-6"> <div class="flex flex-col justify-center mx-6">
<div class="box-title"> <div class="box-title">
{{ data_get($key, 'name') }} {{ data_get($key, 'name') }}

View file

@ -289,7 +289,7 @@
</div> </div>
@else @else
<div>Metrics are disabled for this server. Enable them in <a class="underline dark:text-white" <div>Metrics are disabled for this server. Enable them in <a class="underline dark:text-white"
href="{{ route('server.show', ['server_uuid' => $server->uuid]) }}">General</a> settings.</div> href="{{ route('server.show', ['server_uuid' => $server->uuid]) }}" {{ wireNavigate() }}>General</a> settings.</div>
@endif @endif
</div> </div>
</div> </div>

View file

@ -20,12 +20,12 @@
<h4 class="pt-4 pb-2">Available Destinations</h4> <h4 class="pt-4 pb-2">Available Destinations</h4>
<div class="flex gap-2"> <div class="flex gap-2">
@foreach ($server->standaloneDockers as $docker) @foreach ($server->standaloneDockers as $docker)
<a href="{{ route('destination.show', ['destination_uuid' => data_get($docker, 'uuid')]) }}"> <a href="{{ route('destination.show', ['destination_uuid' => data_get($docker, 'uuid')]) }}" {{ wireNavigate() }}>
<x-forms.button>{{ data_get($docker, 'network') }} </x-forms.button> <x-forms.button>{{ data_get($docker, 'network') }} </x-forms.button>
</a> </a>
@endforeach @endforeach
@foreach ($server->swarmDockers as $docker) @foreach ($server->swarmDockers as $docker)
<a href="{{ route('destination.show', ['destination_uuid' => data_get($docker, 'uuid')]) }}"> <a href="{{ route('destination.show', ['destination_uuid' => data_get($docker, 'uuid')]) }}" {{ wireNavigate() }}>
<x-forms.button>{{ data_get($docker, 'network') }} </x-forms.button> <x-forms.button>{{ data_get($docker, 'network') }} </x-forms.button>
</a> </a>
@endforeach @endforeach

View file

@ -13,7 +13,7 @@
<div class="subtitle">All your servers are here.</div> <div class="subtitle">All your servers are here.</div>
<div class="grid gap-4 lg:grid-cols-2 -mt-1"> <div class="grid gap-4 lg:grid-cols-2 -mt-1">
@forelse ($servers as $server) @forelse ($servers as $server)
<a href="{{ route('server.show', ['server_uuid' => data_get($server, 'uuid')]) }}" <a href="{{ route('server.show', ['server_uuid' => data_get($server, 'uuid')]) }}" {{ wireNavigate() }}
@class([ @class([
'gap-2 border cursor-pointer coolbox group', 'gap-2 border cursor-pointer coolbox group',
'border-red-500' => 'border-red-500' =>

View file

@ -65,14 +65,14 @@ class="mx-1 dark:hover:fill-white fill-black dark:fill-warning">
class="flex items-center gap-6 overflow-x-scroll sm:overflow-x-hidden scrollbar min-h-10 whitespace-nowrap pt-2"> class="flex items-center gap-6 overflow-x-scroll sm:overflow-x-hidden scrollbar min-h-10 whitespace-nowrap pt-2">
<a class="{{ request()->routeIs('server.show') ? 'dark:text-white' : '' }}" href="{{ route('server.show', [ <a class="{{ request()->routeIs('server.show') ? 'dark:text-white' : '' }}" href="{{ route('server.show', [
'server_uuid' => data_get($server, 'uuid'), 'server_uuid' => data_get($server, 'uuid'),
]) }}"> ]) }}" {{ wireNavigate() }}>
Configuration Configuration
</a> </a>
@if (!$server->isSwarmWorker() && !$server->settings->is_build_server) @if (!$server->isSwarmWorker() && !$server->settings->is_build_server)
<a class="{{ request()->routeIs('server.proxy') ? 'dark:text-white' : '' }} flex items-center gap-1" href="{{ route('server.proxy', [ <a class="{{ request()->routeIs('server.proxy') ? 'dark:text-white' : '' }} flex items-center gap-1" href="{{ route('server.proxy', [
'server_uuid' => data_get($server, 'uuid'), 'server_uuid' => data_get($server, 'uuid'),
]) }}"> ]) }}" {{ wireNavigate() }}>
Proxy Proxy
@if ($this->hasTraefikOutdated) @if ($this->hasTraefikOutdated)
<svg class="w-4 h-4 text-warning" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"> <svg class="w-4 h-4 text-warning" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
@ -84,7 +84,7 @@ class="flex items-center gap-6 overflow-x-scroll sm:overflow-x-hidden scrollbar
@endif @endif
<a class="{{ request()->routeIs('server.resources') ? 'dark:text-white' : '' }}" href="{{ route('server.resources', [ <a class="{{ request()->routeIs('server.resources') ? 'dark:text-white' : '' }}" href="{{ route('server.resources', [
'server_uuid' => data_get($server, 'uuid'), 'server_uuid' => data_get($server, 'uuid'),
]) }}"> ]) }}" {{ wireNavigate() }}>
Resources Resources
</a> </a>
@can('canAccessTerminal') @can('canAccessTerminal')
@ -97,7 +97,7 @@ class="flex items-center gap-6 overflow-x-scroll sm:overflow-x-hidden scrollbar
@can('update', $server) @can('update', $server)
<a class="{{ request()->routeIs('server.security.patches') ? 'dark:text-white' : '' }}" href="{{ route('server.security.patches', [ <a class="{{ request()->routeIs('server.security.patches') ? 'dark:text-white' : '' }}" href="{{ route('server.security.patches', [
'server_uuid' => data_get($server, 'uuid'), 'server_uuid' => data_get($server, 'uuid'),
]) }}"> ]) }}" {{ wireNavigate() }}>
Security Security
</a> </a>
@endcan @endcan

View file

@ -32,8 +32,11 @@
</div> </div>
</div> </div>
</div> </div>
@if ($containers->count() > 0) @if ($activeTab === 'managed')
@if ($activeTab === 'managed') @php
$managedResources = $server->definedResources()->sortBy('name', SORT_NATURAL);
@endphp
@if ($managedResources->count() > 0)
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
@ -59,7 +62,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@forelse ($server->definedResources()->sortBy('name',SORT_NATURAL) as $resource) @foreach ($managedResources as $resource)
<tr> <tr>
<td class="px-5 py-4 text-sm whitespace-nowrap"> <td class="px-5 py-4 text-sm whitespace-nowrap">
{{ data_get($resource->project(), 'name') }} {{ data_get($resource->project(), 'name') }}
@ -68,7 +71,7 @@
{{ data_get($resource, 'environment.name') }} {{ data_get($resource, 'environment.name') }}
</td> </td>
<td class="px-5 py-4 text-sm whitespace-nowrap hover:underline"> <td class="px-5 py-4 text-sm whitespace-nowrap hover:underline">
<a class="" <a class="" {{ wireNavigate() }}
href="{{ $resource->link() }}">{{ $resource->name }} href="{{ $resource->link() }}">{{ $resource->name }}
<x-internal-link /></a> <x-internal-link /></a>
</td> </td>
@ -83,8 +86,7 @@
@endif @endif
</td> </td>
</tr> </tr>
@empty @endforeach
@endforelse
</tbody> </tbody>
</table> </table>
</div> </div>
@ -92,7 +94,11 @@
</div> </div>
</div> </div>
</div> </div>
@elseif ($activeTab === 'unmanaged') @else
<div>No managed resources found.</div>
@endif
@elseif ($activeTab === 'unmanaged')
@if (count($unmanagedContainers) > 0)
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
@ -116,7 +122,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@forelse ($containers->sortBy('name',SORT_NATURAL) as $resource) @foreach (collect($unmanagedContainers)->sortBy('name', SORT_NATURAL) as $resource)
<tr> <tr>
<td class="px-5 py-4 text-sm whitespace-nowrap"> <td class="px-5 py-4 text-sm whitespace-nowrap">
{{ data_get($resource, 'Names') }} {{ data_get($resource, 'Names') }}
@ -146,19 +152,15 @@
@endif @endif
</td> </td>
</tr> </tr>
@empty @endforeach
@endforelse
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@endif </div>
@else @else
@if ($activeTab === 'managed')
<div>No managed resources found.</div>
@elseif ($activeTab === 'unmanaged')
<div>No unmanaged resources found.</div> <div>No unmanaged resources found.</div>
@endif @endif
@endif @endif

View file

@ -19,7 +19,7 @@
<span class="text-xs text-neutral-500">(experimental)</span> <span class="text-xs text-neutral-500">(experimental)</span>
<x-helper <x-helper
helper="Only available for apt, dnf and zypper package managers atm, more coming helper="Only available for apt, dnf and zypper package managers atm, more coming
soon.<br/>Status notifications sent every week.<br/>You can disable notifications in the <a class='dark:text-white underline' href='{{ route('notifications.email') }}'>notification settings</a>." /> soon.<br/>Status notifications sent every week.<br/>You can disable notifications in the <a class='dark:text-white underline' href='{{ route('notifications.email') }}' {{ wireNavigate() }}>notification settings</a>." />
@if (isDev()) @if (isDev())
<x-forms.button type="button" wire:click="sendTestEmail"> <x-forms.button type="button" wire:click="sendTestEmail">
Send Test Email (dev only)</x-forms.button> Send Test Email (dev only)</x-forms.button>

View file

@ -43,7 +43,7 @@
Please validate your server to enable Instance Backup. Please validate your server to enable Instance Backup.
</div> </div>
<a href="{{ route('server.show', [$server->uuid]) }}" <a href="{{ route('server.show', [$server->uuid]) }}"
class="text-black hover:text-gray-700 dark:text-white dark:hover:text-gray-200 underline"> class="text-black hover:text-gray-700 dark:text-white dark:hover:text-gray-200 underline" {{ wireNavigate() }}>
Go to Server Settings to Validate Go to Server Settings to Validate
</a> </a>
</div> </div>

View file

@ -50,9 +50,14 @@ class="flex flex-col h-full gap-8 sm:flex-row">
environments! environments!
</x-callout> </x-callout>
@endif @endif
<h4 class="pt-4">UI Settings</h4>
<div class="md:w-96">
<x-forms.checkbox instantSave id="is_wire_navigate_enabled" label="SPA Navigation"
helper="Enable single-page application (SPA) style navigation with prefetching on hover. When enabled, page transitions are smoother without full page reloads and pages are prefetched when hovering over links. Disable if you experience navigation issues." />
</div>
<h4 class="pt-4">Confirmation Settings</h4> <h4 class="pt-4">Confirmation Settings</h4>
<div class="md:w-96"> <div class="md:w-96">
<x-forms.checkbox instantSave id=" is_sponsorship_popup_enabled" label="Show Sponsorship Popup" <x-forms.checkbox instantSave id="is_sponsorship_popup_enabled" label="Show Sponsorship Popup"
helper="Show monthly sponsorship reminders to support Coolify development. Disable to hide these messages permanently." /> helper="Show monthly sponsorship reminders to support Coolify development. Disable to hide these messages permanently." />
</div> </div>
</div> </div>

View file

@ -15,7 +15,7 @@
href="{{ route('shared-variables.environment.show', [ href="{{ route('shared-variables.environment.show', [
'project_uuid' => $project->uuid, 'project_uuid' => $project->uuid,
'environment_uuid' => $environment->uuid, 'environment_uuid' => $environment->uuid,
]) }}"> ]) }}" {{ wireNavigate() }}>
<div class="flex flex-col justify-center flex-1 mx-6 "> <div class="flex flex-col justify-center flex-1 mx-6 ">
<div class="box-title"> {{ $environment->name }}</div> <div class="box-title"> {{ $environment->name }}</div>
<div class="box-description"> <div class="box-description">

View file

@ -8,19 +8,19 @@
<div class="subtitle">Set Team / Project / Environment wide variables.</div> <div class="subtitle">Set Team / Project / Environment wide variables.</div>
<div class="flex flex-col gap-2 -mt-1"> <div class="flex flex-col gap-2 -mt-1">
<a class="coolbox group" href="{{ route('shared-variables.team.index') }}"> <a class="coolbox group" href="{{ route('shared-variables.team.index') }}" {{ wireNavigate() }}>
<div class="flex flex-col justify-center mx-6"> <div class="flex flex-col justify-center mx-6">
<div class="box-title">Team wide</div> <div class="box-title">Team wide</div>
<div class="box-description">Usable for all resources in a team.</div> <div class="box-description">Usable for all resources in a team.</div>
</div> </div>
</a> </a>
<a class="coolbox group" href="{{ route('shared-variables.project.index') }}"> <a class="coolbox group" href="{{ route('shared-variables.project.index') }}" {{ wireNavigate() }}>
<div class="flex flex-col justify-center mx-6"> <div class="flex flex-col justify-center mx-6">
<div class="box-title">Project wide</div> <div class="box-title">Project wide</div>
<div class="box-description">Usable for all resources in a project.</div> <div class="box-description">Usable for all resources in a project.</div>
</div> </div>
</a> </a>
<a class="coolbox group" href="{{ route('shared-variables.environment.index') }}"> <a class="coolbox group" href="{{ route('shared-variables.environment.index') }}" {{ wireNavigate() }}>
<div class="flex flex-col justify-center mx-6"> <div class="flex flex-col justify-center mx-6">
<div class="box-title">Environment wide</div> <div class="box-title">Environment wide</div>
<div class="box-description">Usable for all resources in an environment.</div> <div class="box-description">Usable for all resources in an environment.</div>

View file

@ -9,7 +9,7 @@
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
@forelse ($projects as $project) @forelse ($projects as $project)
<a class="coolbox group" <a class="coolbox group"
href="{{ route('shared-variables.project.show', ['project_uuid' => data_get($project, 'uuid')]) }}"> href="{{ route('shared-variables.project.show', ['project_uuid' => data_get($project, 'uuid')]) }}" {{ wireNavigate() }}>
<div class="flex flex-col justify-center mx-6 "> <div class="flex flex-col justify-center mx-6 ">
<div class="box-title">{{ $project->name }}</div> <div class="box-title">{{ $project->name }}</div>
<div class="box-description "> <div class="box-description ">

View file

@ -184,6 +184,7 @@ class="bg-transparent border-transparent hover:bg-transparent hover:border-trans
</td> </td>
<td class="px-5 py-4 text-sm whitespace-nowrap"><a <td class="px-5 py-4 text-sm whitespace-nowrap"><a
class="" class=""
{{ wireNavigate() }}
href="{{ $resource->link() }}">{{ $resource->name }} href="{{ $resource->link() }}">{{ $resource->name }}
<x-internal-link /></a> <x-internal-link /></a>
</td> </td>

Some files were not shown because too many files have changed in this diff Show more