diff --git a/app/Actions/Database/StartDatabase.php b/app/Actions/Database/StartDatabase.php index 7735c77d8..cd7e08328 100644 --- a/app/Actions/Database/StartDatabase.php +++ b/app/Actions/Database/StartDatabase.php @@ -28,7 +28,11 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St if (! $server->isFunctional()) { return 'Server is not functional'; } - $database->resetRestartLimit(); + $database->update([ + 'restart_count' => 0, + 'last_restart_at' => null, + 'last_restart_type' => null, + ]); switch ($database->getMorphClass()) { case StandalonePostgresql::class: $activity = StartPostgresql::run($database); diff --git a/app/Actions/Database/StopDatabase.php b/app/Actions/Database/StopDatabase.php index f3c591acf..8005311b5 100644 --- a/app/Actions/Database/StopDatabase.php +++ b/app/Actions/Database/StopDatabase.php @@ -32,7 +32,11 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St // Reset restart tracking when database is manually stopped $database->update(['status' => 'exited']); if ($resetRestartCount) { - $database->resetRestartLimit(); + $database->update([ + 'restart_count' => 0, + 'last_restart_at' => null, + 'last_restart_type' => null, + ]); } if ($dockerCleanup) { diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php index be098e481..c69bd1855 100644 --- a/app/Actions/Docker/GetContainersStatus.php +++ b/app/Actions/Docker/GetContainersStatus.php @@ -5,7 +5,6 @@ use App\Actions\Application\StopApplication; use App\Actions\Application\StopApplicationPreview; use App\Actions\Database\StartDatabaseProxy; -use App\Actions\Database\StopDatabase; use App\Actions\Database\StopDatabaseProxy; use App\Actions\Service\StopServiceApplication; use App\Actions\Shared\ComplexStatusCheck; @@ -249,9 +248,12 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti $database->update($updateData); - if ($database->trackRestartCount((int) $restartCount)) { - StopDatabase::dispatch($database, false, false, false); - $database->team()?->notify(new ApplicationRestartLimitReached($database)); + if ($restartCount > ($database->restart_count ?? 0)) { + $database->update([ + 'restart_count' => (int) $restartCount, + 'last_restart_at' => now(), + 'last_restart_type' => 'crash', + ]); } if ($isPublic) { @@ -357,7 +359,9 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti continue; } - if (! $exitedService->stoppedAfterRestartLimit()) { + if ($exitedService instanceof ServiceDatabase) { + $exitedService->update(['status' => 'exited']); + } elseif (! $exitedService->stoppedAfterRestartLimit()) { $exitedService->update([ 'status' => 'exited', 'restart_count' => 0, @@ -424,9 +428,6 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti $notRunningDatabases = $databases->pluck('id')->diff($foundDatabases); foreach ($notRunningDatabases as $database) { $database = $databases->where('id', $database)->first(); - if ($database->stoppedAfterRestartLimit()) { - continue; - } if (str($database->status)->startsWith('exited')) { continue; } @@ -442,7 +443,6 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti 'restart_count' => 0, 'last_restart_at' => null, 'last_restart_type' => null, - 'restart_limit_reached' => false, ]); // Stop proxy if database was public @@ -582,7 +582,7 @@ private function aggregateServiceContainerStatuses($services) $restartCount = isset($this->serviceContainerRestartCounts) ? ($this->serviceContainerRestartCounts->get($key)?->max() ?? 0) : 0; - if ($subResource->trackRestartCount($restartCount)) { + if (! $subResource instanceof ServiceDatabase && $subResource->trackRestartCount($restartCount)) { StopServiceApplication::dispatch($subResource, false, false); $subResource->team()?->notify(new ApplicationRestartLimitReached($subResource)); diff --git a/app/Actions/Service/StartService.php b/app/Actions/Service/StartService.php index 13371d126..3dc5c98b3 100644 --- a/app/Actions/Service/StartService.php +++ b/app/Actions/Service/StartService.php @@ -25,7 +25,6 @@ public function handle(Service $service, bool $pullLatestImages = false, bool $s $service->saveComposeConfigs(); $service->isConfigurationChanged(save: true); $service->applications()->get()->each->resetRestartLimit(); - $service->databases()->get()->each->resetRestartLimit(); $workdir = $service->workdir(); // $commands[] = "cd {$workdir}"; $commands[] = "echo 'Saved configuration files to {$workdir}.'"; diff --git a/app/Actions/Service/StopService.php b/app/Actions/Service/StopService.php index 341687d0d..52d9edda1 100644 --- a/app/Actions/Service/StopService.php +++ b/app/Actions/Service/StopService.php @@ -55,7 +55,6 @@ public function handle(Service $service, bool $deleteConnectedNetworks = false, }); $dbs->each(function ($database): void { $database->update(['status' => 'exited']); - $database->resetRestartLimit(); }); if ($deleteConnectedNetworks) { diff --git a/app/Actions/Service/StopServiceApplication.php b/app/Actions/Service/StopServiceApplication.php index fa93a7880..11ad337ed 100644 --- a/app/Actions/Service/StopServiceApplication.php +++ b/app/Actions/Service/StopServiceApplication.php @@ -30,7 +30,7 @@ public function handle(ServiceApplication|ServiceDatabase $serviceApplication, b instant_remote_process($commands, $server, throwError: ! $removeContainer); $serviceApplication->update(['status' => 'exited']); - if ($resetRestartCount) { + if ($resetRestartCount && $serviceApplication instanceof ServiceApplication) { $serviceApplication->resetRestartLimit(); } ServiceStatusChanged::dispatch($service->environment->project->team->id); diff --git a/app/Jobs/CheckTraefikVersionForServerJob.php b/app/Jobs/CheckTraefikVersionForServerJob.php index 054a739bc..e56b93c9e 100644 --- a/app/Jobs/CheckTraefikVersionForServerJob.php +++ b/app/Jobs/CheckTraefikVersionForServerJob.php @@ -2,6 +2,8 @@ namespace App\Jobs; +use App\Enums\ProxyStatus; +use App\Enums\ProxyTypes; use App\Events\ProxyStatusChangedUI; use App\Models\Server; use App\Notifications\Server\TraefikVersionOutdated; @@ -33,8 +35,13 @@ public function __construct( */ public function handle(): void { + $this->server->refresh(); $this->clearOutdatedInfo(); + if ($this->server->proxyType() !== ProxyTypes::TRAEFIK->value || $this->server->proxy->get('status') !== ProxyStatus::RUNNING->value) { + return; + } + // Detect current version (makes SSH call) $currentVersion = getTraefikVersionFromDockerCompose($this->server); @@ -116,7 +123,10 @@ public function handle(): void private function clearOutdatedInfo(): void { - $this->server->update(['traefik_outdated_info' => null]); + $this->server->update([ + 'detected_traefik_version' => null, + 'traefik_outdated_info' => null, + ]); } /** diff --git a/app/Jobs/CheckTraefikVersionJob.php b/app/Jobs/CheckTraefikVersionJob.php index ac94aa23f..0a9eeba00 100644 --- a/app/Jobs/CheckTraefikVersionJob.php +++ b/app/Jobs/CheckTraefikVersionJob.php @@ -19,6 +19,20 @@ class CheckTraefikVersionJob implements ShouldBeEncrypted, ShouldQueue public function handle(): void { + Server::query() + ->where(function ($query) { + $query->whereNull('proxy') + ->orWhere('proxy->type', '!=', ProxyTypes::TRAEFIK->value); + }) + ->where(function ($query) { + $query->whereNotNull('detected_traefik_version') + ->orWhereNotNull('traefik_outdated_info'); + }) + ->update([ + 'detected_traefik_version' => null, + 'traefik_outdated_info' => null, + ]); + // Load versions from cached data $traefikVersions = get_traefik_versions(); diff --git a/app/Jobs/PushServerUpdateJob.php b/app/Jobs/PushServerUpdateJob.php index 0e73ee41b..ef83d1944 100644 --- a/app/Jobs/PushServerUpdateJob.php +++ b/app/Jobs/PushServerUpdateJob.php @@ -5,7 +5,6 @@ use App\Actions\Application\StopApplication; use App\Actions\Application\StopApplicationPreview; use App\Actions\Database\StartDatabaseProxy; -use App\Actions\Database\StopDatabase; use App\Actions\Database\StopDatabaseProxy; use App\Actions\Proxy\CheckProxy; use App\Actions\Proxy\StartProxy; @@ -483,7 +482,7 @@ private function loadServices(): Collection ]) ->with([ 'applications:id,service_id,status,last_online_at,restart_count,max_restart_count,restart_limit_reached,last_restart_at,last_restart_type', - 'databases:id,service_id,status,last_online_at,is_public,name,restart_count,max_restart_count,restart_limit_reached,last_restart_at,last_restart_type', + 'databases:id,service_id,status,last_online_at,is_public,name', ]) ->get(); } @@ -506,8 +505,6 @@ private function loadDatabases(): Collection 'restart_count', 'last_restart_at', 'last_restart_type', - 'max_restart_count', - 'restart_limit_reached', ]; return collect([ @@ -675,7 +672,7 @@ private function aggregateServiceContainerStatuses() } $restartCount = $this->serviceContainerRestartCounts->get($key)?->max() ?? 0; - if ($subResource->trackRestartCount($restartCount)) { + if (! $subResource instanceof ServiceDatabase && $subResource->trackRestartCount($restartCount)) { StopServiceApplication::dispatch($subResource, false, false); $subResource->team()?->notify(new ApplicationRestartLimitReached($subResource)); @@ -821,11 +818,12 @@ private function updateDatabaseStatus(string $databaseUuid, string $containerSta $database->status = $containerStatus; $database->save(); } - if (is_numeric($restartCount) && $database->trackRestartCount((int) $restartCount)) { - StopDatabase::dispatch($database, false, false, false); - $database->team()?->notify(new ApplicationRestartLimitReached($database)); - - return; + if (is_numeric($restartCount) && $restartCount > ($database->restart_count ?? 0)) { + $database->update([ + 'restart_count' => (int) $restartCount, + 'last_restart_at' => now(), + 'last_restart_type' => 'crash', + ]); } if (! $this->isCompleteSnapshot()) { return; @@ -883,16 +881,12 @@ private function updateNotFoundDatabaseStatus() $notFoundDatabaseUuids->each(function ($databaseUuid) { $database = $this->databasesByUuid->get($databaseUuid); if ($database) { - if ($database->stoppedAfterRestartLimit()) { - return; - } if (! str($database->status)->startsWith('exited')) { $database->update([ 'status' => 'exited', 'restart_count' => 0, 'last_restart_at' => null, 'last_restart_type' => null, - 'restart_limit_reached' => false, ]); } if ($database->is_public) { @@ -918,9 +912,8 @@ private function updateNotFoundServiceStatus() // Batch update service databases if ($notFoundServiceDatabaseIds->isNotEmpty()) { ServiceDatabase::whereIn('id', $notFoundServiceDatabaseIds) - ->where('restart_limit_reached', false) ->where('status', '!=', 'exited') - ->update(['status' => 'exited', 'restart_count' => 0, 'last_restart_at' => null, 'last_restart_type' => null]); + ->update(['status' => 'exited']); } } diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php index 68cb52a92..296fd4da5 100644 --- a/app/Livewire/Server/Proxy.php +++ b/app/Livewire/Server/Proxy.php @@ -106,6 +106,8 @@ public function changeProxy() try { $this->authorize('update', $this->server); $this->server->proxy = null; + $this->server->detected_traefik_version = null; + $this->server->traefik_outdated_info = null; $this->server->save(); $this->dispatch('reloadWindow'); diff --git a/app/Models/Server.php b/app/Models/Server.php index f7a4bf20c..dccbed15e 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -1812,6 +1812,8 @@ public function changeProxy(string $proxyType, bool $async = true) $this->proxy->set('last_saved_proxy_configuration', null); $this->proxy->set('last_saved_settings', null); $this->proxy->set('last_applied_settings', null); + $this->detected_traefik_version = null; + $this->traefik_outdated_info = null; $this->save(); if ($this->proxySet()) { if ($async) { diff --git a/app/Models/ServiceDatabase.php b/app/Models/ServiceDatabase.php index c932791a7..603d11a7f 100644 --- a/app/Models/ServiceDatabase.php +++ b/app/Models/ServiceDatabase.php @@ -2,13 +2,12 @@ namespace App\Models; -use App\Traits\HasRestartLimit; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\SoftDeletes; class ServiceDatabase extends BaseModel { - use HasFactory, HasRestartLimit, SoftDeletes; + use HasFactory, SoftDeletes; protected $fillable = [ 'service_id', diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index ee8558bb0..7ca45cc3b 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -5,7 +5,6 @@ use App\Traits\ClearsGlobalSearchCache; use App\Traits\HasDatabaseHealthCheck; use App\Traits\HasMetrics; -use App\Traits\HasRestartLimit; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -13,7 +12,7 @@ class StandaloneClickhouse extends BaseModel { - use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasRestartLimit, HasSafeStringAttribute, SoftDeletes; + use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; protected $fillable = [ 'uuid', diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php index c72841955..769d9f00c 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -5,7 +5,6 @@ use App\Traits\ClearsGlobalSearchCache; use App\Traits\HasDatabaseHealthCheck; use App\Traits\HasMetrics; -use App\Traits\HasRestartLimit; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -13,7 +12,7 @@ class StandaloneDragonfly extends BaseModel { - use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasRestartLimit, HasSafeStringAttribute, SoftDeletes; + use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; protected $fillable = [ 'uuid', diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index 9c4e4aa66..15a1fe2f8 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -5,7 +5,6 @@ use App\Traits\ClearsGlobalSearchCache; use App\Traits\HasDatabaseHealthCheck; use App\Traits\HasMetrics; -use App\Traits\HasRestartLimit; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -13,7 +12,7 @@ class StandaloneKeydb extends BaseModel { - use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasRestartLimit, HasSafeStringAttribute, SoftDeletes; + use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; protected $fillable = [ 'uuid', diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index 4eada8a00..378d36395 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -5,7 +5,6 @@ use App\Traits\ClearsGlobalSearchCache; use App\Traits\HasDatabaseHealthCheck; use App\Traits\HasMetrics; -use App\Traits\HasRestartLimit; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -14,7 +13,7 @@ class StandaloneMariadb extends BaseModel { - use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasRestartLimit, HasSafeStringAttribute, SoftDeletes; + use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; protected $fillable = [ 'uuid', diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index d0a68d4e7..1010ca5f3 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -5,7 +5,6 @@ use App\Traits\ClearsGlobalSearchCache; use App\Traits\HasDatabaseHealthCheck; use App\Traits\HasMetrics; -use App\Traits\HasRestartLimit; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -13,7 +12,7 @@ class StandaloneMongodb extends BaseModel { - use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasRestartLimit, HasSafeStringAttribute, SoftDeletes; + use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; protected $fillable = [ 'uuid', diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index 4f9e73719..90828bf01 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -5,7 +5,6 @@ use App\Traits\ClearsGlobalSearchCache; use App\Traits\HasDatabaseHealthCheck; use App\Traits\HasMetrics; -use App\Traits\HasRestartLimit; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -13,7 +12,7 @@ class StandaloneMysql extends BaseModel { - use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasRestartLimit, HasSafeStringAttribute, SoftDeletes; + use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; protected $fillable = [ 'uuid', diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index 602728855..e7db81285 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -5,7 +5,6 @@ use App\Traits\ClearsGlobalSearchCache; use App\Traits\HasDatabaseHealthCheck; use App\Traits\HasMetrics; -use App\Traits\HasRestartLimit; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -13,7 +12,7 @@ class StandalonePostgresql extends BaseModel { - use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasRestartLimit, HasSafeStringAttribute, SoftDeletes; + use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; protected $fillable = [ 'uuid', diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index 8a74824be..326261190 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -5,7 +5,6 @@ use App\Traits\ClearsGlobalSearchCache; use App\Traits\HasDatabaseHealthCheck; use App\Traits\HasMetrics; -use App\Traits\HasRestartLimit; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -13,7 +12,7 @@ class StandaloneRedis extends BaseModel { - use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasRestartLimit, HasSafeStringAttribute, SoftDeletes; + use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; protected $fillable = [ 'uuid', diff --git a/database/migrations/2026_09_04_132827_remove_restart_limits_from_databases.php b/database/migrations/2026_09_04_132827_remove_restart_limits_from_databases.php new file mode 100644 index 000000000..710578fd9 --- /dev/null +++ b/database/migrations/2026_09_04_132827_remove_restart_limits_from_databases.php @@ -0,0 +1,56 @@ +dropColumn(['max_restart_count', 'restart_limit_reached']); + }); + } + + Schema::table('service_databases', function (Blueprint $table) { + $table->dropColumn([ + 'restart_count', + 'max_restart_count', + 'restart_limit_reached', + 'last_restart_at', + 'last_restart_type', + ]); + }); + } + + public function down(): void + { + foreach (self::STANDALONE_DATABASE_TABLES as $tableName) { + Schema::table($tableName, function (Blueprint $table) { + $table->integer('max_restart_count')->default(10); + $table->boolean('restart_limit_reached')->default(false); + }); + } + + Schema::table('service_databases', function (Blueprint $table) { + $table->integer('restart_count')->default(0); + $table->integer('max_restart_count')->default(10); + $table->boolean('restart_limit_reached')->default(false); + $table->timestamp('last_restart_at')->nullable(); + $table->string('last_restart_type', 10)->nullable(); + }); + } +}; diff --git a/resources/views/components/application/restart-limit-warning.blade.php b/resources/views/components/application/restart-limit-warning.blade.php index 3d08c803d..5a35623fc 100644 --- a/resources/views/components/application/restart-limit-warning.blade.php +++ b/resources/views/components/application/restart-limit-warning.blade.php @@ -1,6 +1,6 @@ @props(['application']) -@if ($application->stoppedAfterRestartLimit()) +@if (method_exists($application, 'stoppedAfterRestartLimit') && $application->stoppedAfterRestartLimit()) @php($restartLimit = method_exists($application, 'restartLimitMaximum') ? $application->restartLimitMaximum() : ($application->max_restart_count ?? 0)) @php($displayRestartCount = max($application->restart_count ?? 0, $restartLimit)) - Deploy - Fill required variables first - +
+ + + +
@endcan - - - @endif @@ -287,10 +300,26 @@ class="listbox-panel top-full! right-0! left-auto! mt-1! w-64! min-w-0!" role="m @endcan @else - - - + @can('deploy', $service) +
+ + +
+ @endcan @endif diff --git a/tests/Feature/AllResourceRestartLimitsTest.php b/tests/Feature/AllResourceRestartLimitsTest.php index 8418b552c..7da82f7d6 100644 --- a/tests/Feature/AllResourceRestartLimitsTest.php +++ b/tests/Feature/AllResourceRestartLimitsTest.php @@ -1,5 +1,8 @@ toContain(HasRestartLimit::class); $resource = new $modelClass; @@ -34,17 +38,8 @@ 'last_restart_at' => 'datetime', ]); })->with([ - ApplicationPreview::class, - ServiceApplication::class, - ServiceDatabase::class, - StandaloneClickhouse::class, - StandaloneDragonfly::class, - StandaloneKeydb::class, - StandaloneMariadb::class, - StandaloneMongodb::class, - StandaloneMysql::class, - StandalonePostgresql::class, - StandaloneRedis::class, + [ApplicationPreview::class], + [ServiceApplication::class], ]); it('collects restart counts for preview and service containers from both status sources', function () { @@ -157,7 +152,7 @@ ->toContain('Application::query()'); }); -it('adds restart limit columns to previews services and standalone databases', function () { +it('limits restarts only for applications', function () { $migrations = collect(glob(database_path('migrations/*.php'))) ->map(fn (string $path): string => file_get_contents($path)) ->implode("\n"); @@ -165,19 +160,62 @@ expect($migrations) ->toContain("'application_previews'") ->toContain("'service_applications'") - ->toContain("'service_databases'") ->toContain("'max_restart_count'") - ->toContain("'restart_limit_reached'"); + ->toContain("'restart_limit_reached'") + ->toContain("dropColumn(['max_restart_count', 'restart_limit_reached'])"); - $restartLimitMigrations = collect(glob(database_path('migrations/*_add_restart_limit_to_*.php'))); + $databaseModels = [ + ServiceDatabase::class, + StandalonePostgresql::class, + StandaloneRedis::class, + StandaloneMongodb::class, + StandaloneMysql::class, + StandaloneMariadb::class, + StandaloneKeydb::class, + StandaloneDragonfly::class, + StandaloneClickhouse::class, + ]; - expect($restartLimitMigrations)->toHaveCount(11); - expect($restartLimitMigrations->map( - fn (string $path): string => substr(basename($path), 0, 17) - )->unique())->toHaveCount(11); - $restartLimitMigrations->each(function (string $path): void { - expect(file_get_contents($path))->not->toContain('foreach ('); - }); + foreach ($databaseModels as $databaseModel) { + expect(class_uses_recursive($databaseModel))->not->toContain(HasRestartLimit::class); + } + + foreach ([ + 'service_databases', + 'standalone_postgresqls', + 'standalone_redis', + 'standalone_mongodbs', + 'standalone_mysqls', + 'standalone_mariadbs', + 'standalone_keydbs', + 'standalone_dragonflies', + 'standalone_clickhouses', + ] as $databaseTable) { + expect(Schema::hasColumn($databaseTable, 'max_restart_count'))->toBeFalse() + ->and(Schema::hasColumn($databaseTable, 'restart_limit_reached'))->toBeFalse(); + } + + foreach ([GetContainersStatus::class, PushServerUpdateJob::class] as $statusUpdater) { + $source = file_get_contents((new ReflectionClass($statusUpdater))->getFileName()); + + expect($source) + ->not->toContain('$database->trackRestartCount') + ->not->toContain('$database->stoppedAfterRestartLimit()'); + } + + $stopServiceResource = file_get_contents((new ReflectionClass(StopServiceApplication::class))->getFileName()); + + expect($stopServiceResource) + ->toContain('$resetRestartCount && $serviceApplication instanceof ServiceApplication'); +}); + +it('does not render restart limit warnings for service databases', function () { + $html = Blade::render( + '', + ['database' => new ServiceDatabase], + ); + + expect(trim($html))->toBeEmpty(); }); it('atomically claims a resource restart limit once and can reset it', function () { diff --git a/tests/Feature/ResourceHeadingUnifiedNavbarTest.php b/tests/Feature/ResourceHeadingUnifiedNavbarTest.php index ce69f26ef..f7951e93a 100644 --- a/tests/Feature/ResourceHeadingUnifiedNavbarTest.php +++ b/tests/Feature/ResourceHeadingUnifiedNavbarTest.php @@ -82,21 +82,31 @@ } }); -it('links the service header missing variables warning to environment variables', function () { +it('shows disabled deploy actions when service variables are missing', function () { $heading = file_get_contents(resource_path('views/livewire/project/service/heading.blade.php')); $mobileActions = str($heading) ->after('
') ->before("@teleport('#resource-action-hud-slot')") ->toString(); + $desktopActions = str($heading) + ->after("@teleport('#resource-action-hud-slot')") + ->before('@endteleport') + ->toString(); - expect($heading) - ->toContain("route('project.service.environment-variables'") - ->toContain('Required variables missing') + expect($mobileActions) + ->toContain('id="service-mobile-actions"') + ->toContain('aria-disabled="true"') + ->toContain('Deploy') + ->toContain('missing required env vars') ->toContain('href="{{ $environmentVariablesUrl }}"') - ->and($mobileActions) - ->toContain('Fill required variables first') - ->toContain('disabled') - ->toContain('Deploy'); + ->toContain('underline') + ->and($desktopActions) + ->toContain('id="service-desktop-actions"') + ->toContain('aria-disabled="true"') + ->toContain('Deploy') + ->toContain('missing required env vars') + ->toContain('href="{{ $environmentVariablesUrl }}"') + ->toContain('underline'); }); it('places the account menu beside the desktop sidebar toggle while retaining it on mobile', function () { diff --git a/tests/Feature/TraefikVersionStateTest.php b/tests/Feature/TraefikVersionStateTest.php index 3dad5423e..de3cf8e17 100644 --- a/tests/Feature/TraefikVersionStateTest.php +++ b/tests/Feature/TraefikVersionStateTest.php @@ -1,12 +1,16 @@ hasCurrentTraefikOutdatedInfo())->toBeTrue(); }); -it('clears stale outdated information before detecting the current version', function () { +it('clears stale Traefik version state before detecting the current version', function () { $team = Team::factory()->create(); $server = Server::factory()->create([ 'team_id' => $team->id, @@ -204,6 +208,80 @@ $server->refresh(); - expect($server->detected_traefik_version)->toBe('3.6.23') + expect($server->detected_traefik_version)->toBeNull() ->and($server->traefik_outdated_info)->toBeNull(); }); + +it('clears Traefik version state when the proxy changes', function () { + $team = Team::factory()->create(); + $server = Server::factory()->create([ + 'team_id' => $team->id, + 'proxy' => [ + 'type' => ProxyTypes::TRAEFIK->value, + 'status' => 'running', + ], + 'detected_traefik_version' => '3.6.23', + 'traefik_outdated_info' => [ + 'current' => '3.6.23', + 'latest' => '3.7.8', + 'type' => 'minor_upgrade', + ], + ]); + + $server->changeProxy(ProxyTypes::NONE->value); + + expect($server->refresh()->detected_traefik_version)->toBeNull() + ->and($server->traefik_outdated_info)->toBeNull(); +}); + +it('cleans stale Traefik version state while selecting servers to check', function () { + Bus::fake(); + Cache::put('coolify:versions:all', [ + 'traefik' => ['v3.7' => '3.7.8'], + ]); + + $team = Team::factory()->create(); + $server = Server::factory()->create([ + 'team_id' => $team->id, + 'proxy' => [ + 'type' => ProxyTypes::NONE->value, + 'status' => 'exited', + ], + 'detected_traefik_version' => '3.6.23', + 'traefik_outdated_info' => [ + 'current' => '3.6.23', + 'latest' => '3.7.8', + 'type' => 'minor_upgrade', + ], + ]); + + (new CheckTraefikVersionJob)->handle(); + + expect($server->refresh()->detected_traefik_version)->toBeNull() + ->and($server->traefik_outdated_info)->toBeNull(); +}); + +it('does not inspect a server after its Traefik proxy has been disabled', function () { + $team = Team::factory()->create(); + $server = Server::factory()->create([ + 'team_id' => $team->id, + 'proxy' => [ + 'type' => ProxyTypes::NONE->value, + 'status' => 'exited', + ], + 'detected_traefik_version' => '3.6.23', + 'traefik_outdated_info' => [ + 'current' => '3.6.23', + 'latest' => '3.7.8', + 'type' => 'minor_upgrade', + ], + ]); + Event::fake(); + + (new CheckTraefikVersionForServerJob($server, ['v3.7' => '3.7.8']))->handle(); + + expect($server->refresh()->detected_traefik_version)->toBeNull() + ->and($server->traefik_outdated_info)->toBeNull(); + + Event::assertNotDispatched(ProxyStatusChangedUI::class); +}); diff --git a/tests/Unit/StopActionsPersistStatusTest.php b/tests/Unit/StopActionsPersistStatusTest.php index 35b6052dd..67a7a8c6f 100644 --- a/tests/Unit/StopActionsPersistStatusTest.php +++ b/tests/Unit/StopActionsPersistStatusTest.php @@ -21,7 +21,7 @@ ->toContain("\$application->update(['status' => 'exited']);") ->toContain('$application->resetRestartLimit();') ->toContain("\$database->update(['status' => 'exited']);") - ->toContain('$database->resetRestartLimit();'); + ->not->toContain('$database->resetRestartLimit();'); }); it('persists exited status when stopping an individual service resource', function () {