From a5b3d3a53638717fd1ad97d6054039b7a7f73077 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 15 Apr 2026 14:24:41 +0200 Subject: [PATCH 1/2] fix(migrations): guard uuid column addition and filter teamless servers - Skip uuid column creation if it already exists to prevent duplicate column errors on re-run - Use chunkById instead of orderBy+chunk for efficient pagination - Filter servers by whereHas('team') to avoid processing orphaned servers without a team relationship --- ...redefined_server_variables_to_existing_servers.php | 2 +- ...720_add_uuid_to_local_persistent_volumes_table.php | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/database/migrations/2025_12_24_133707_add_predefined_server_variables_to_existing_servers.php b/database/migrations/2025_12_24_133707_add_predefined_server_variables_to_existing_servers.php index c67987e67..77fcf96a1 100644 --- a/database/migrations/2025_12_24_133707_add_predefined_server_variables_to_existing_servers.php +++ b/database/migrations/2025_12_24_133707_add_predefined_server_variables_to_existing_servers.php @@ -11,7 +11,7 @@ */ public function up(): void { - Server::query()->chunk(100, function ($servers) { + Server::query()->whereHas('team')->chunk(100, function ($servers) { foreach ($servers as $server) { $existingKeys = SharedEnvironmentVariable::where('type', 'server') ->where('server_id', $server->id) diff --git a/database/migrations/2026_03_23_101720_add_uuid_to_local_persistent_volumes_table.php b/database/migrations/2026_03_23_101720_add_uuid_to_local_persistent_volumes_table.php index 6b4fb690d..ab279c592 100644 --- a/database/migrations/2026_03_23_101720_add_uuid_to_local_persistent_volumes_table.php +++ b/database/migrations/2026_03_23_101720_add_uuid_to_local_persistent_volumes_table.php @@ -10,14 +10,15 @@ { public function up(): void { - Schema::table('local_persistent_volumes', function (Blueprint $table) { - $table->string('uuid')->nullable()->after('id'); - }); + if (! Schema::hasColumn('local_persistent_volumes', 'uuid')) { + Schema::table('local_persistent_volumes', function (Blueprint $table) { + $table->string('uuid')->nullable()->after('id'); + }); + } DB::table('local_persistent_volumes') ->whereNull('uuid') - ->orderBy('id') - ->chunk(1000, function ($volumes) { + ->chunkById(1000, function ($volumes) { foreach ($volumes as $volume) { DB::table('local_persistent_volumes') ->where('id', $volume->id) From 3a8f52ce16f3828884e48334895a15de3ade7755 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 15 Apr 2026 15:12:29 +0200 Subject: [PATCH 2/2] fix(team): mark servers unreachable when subscription ends Set unreachable_count to 3 and unreachable_notification_sent to true on all team servers in subscriptionEnded(), so the existing cleanup command can pick them up after the 7-day grace period. Also adds feature tests for the subscription-ended cleanup flow and casts server IP to string in existing unreachable server tests to fix type comparison. --- app/Models/Team.php | 4 +- .../Feature/CleanupUnreachableServersTest.php | 6 +- .../CleanupUnsubscribedServersTest.php | 78 +++++++++++++++++++ 3 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 tests/Feature/CleanupUnsubscribedServersTest.php diff --git a/app/Models/Team.php b/app/Models/Team.php index d5d564444..0fbcfe0c6 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -233,6 +233,9 @@ public function subscriptionEnded() 'is_reachable' => false, ]); ServerReachabilityChanged::dispatch($server); + $server->unreachable_count = 3; + $server->unreachable_notification_sent = true; + $server->save(); } } @@ -344,5 +347,4 @@ public function webhookNotificationSettings() { return $this->hasOne(WebhookNotificationSettings::class); } - } diff --git a/tests/Feature/CleanupUnreachableServersTest.php b/tests/Feature/CleanupUnreachableServersTest.php index edfd0511c..c06944969 100644 --- a/tests/Feature/CleanupUnreachableServersTest.php +++ b/tests/Feature/CleanupUnreachableServersTest.php @@ -30,7 +30,7 @@ 'updated_at' => now()->subDays(8), ]); - $originalIp = $server->ip; + $originalIp = (string) $server->ip; $this->artisan('cleanup:unreachable-servers')->assertSuccessful(); @@ -47,7 +47,7 @@ 'updated_at' => now()->subDays(3), ]); - $originalIp = $server->ip; + $originalIp = (string) $server->ip; $this->artisan('cleanup:unreachable-servers')->assertSuccessful(); @@ -64,7 +64,7 @@ 'updated_at' => now()->subDays(8), ]); - $originalIp = $server->ip; + $originalIp = (string) $server->ip; $this->artisan('cleanup:unreachable-servers')->assertSuccessful(); diff --git a/tests/Feature/CleanupUnsubscribedServersTest.php b/tests/Feature/CleanupUnsubscribedServersTest.php new file mode 100644 index 000000000..ee3fdb02d --- /dev/null +++ b/tests/Feature/CleanupUnsubscribedServersTest.php @@ -0,0 +1,78 @@ +create(); + Subscription::create([ + 'team_id' => $team->id, + 'stripe_invoice_paid' => true, + ]); + $server = Server::factory()->create([ + 'team_id' => $team->id, + 'unreachable_count' => 0, + 'unreachable_notification_sent' => false, + ]); + + $team->subscriptionEnded(); + + $server->refresh(); + expect($server->unreachable_count)->toBe(3); + expect($server->unreachable_notification_sent)->toBeTrue(); +}); + +it('cleans up unsubscribed server IP after 7 days via cleanup command', function () { + $team = Team::factory()->create(); + $server = Server::factory()->create([ + 'team_id' => $team->id, + 'unreachable_count' => 3, + 'unreachable_notification_sent' => true, + 'updated_at' => now()->subDays(8), + ]); + + $this->artisan('cleanup:unreachable-servers')->assertSuccessful(); + + $server->refresh(); + expect($server->ip)->toBe('1.2.3.4'); +}); + +it('does not clean up unsubscribed server IP within 7 day grace period', function () { + $team = Team::factory()->create(); + $server = Server::factory()->create([ + 'team_id' => $team->id, + 'unreachable_count' => 3, + 'unreachable_notification_sent' => true, + 'updated_at' => now()->subDays(3), + ]); + + $originalIp = (string) $server->ip; + + $this->artisan('cleanup:unreachable-servers')->assertSuccessful(); + + $server->refresh(); + expect((string) $server->ip)->toBe($originalIp); +}); + +it('does not affect servers with active subscriptions', function () { + $team = Team::factory()->create(); + Subscription::create([ + 'team_id' => $team->id, + 'stripe_invoice_paid' => true, + ]); + $server = Server::factory()->create([ + 'team_id' => $team->id, + 'unreachable_count' => 0, + 'unreachable_notification_sent' => false, + ]); + + $originalCount = $server->unreachable_count; + $originalNotification = $server->unreachable_notification_sent; + + expect($originalCount)->toBe(0); + expect($originalNotification)->toBeFalse(); +});