From d8972e97c976c0111c3399bf41e38da9c19ca562 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 13 May 2026 09:31:28 +0200 Subject: [PATCH 01/12] fix(previews): clean up closed PR previews after update failures Catch and report failures while updating closed pull request status so preview deployment cleanup still runs for closed GitHub pull request webhooks. Add coverage for cleanup continuing when GitHub comment cleanup fails. --- app/Jobs/ProcessGithubPullRequestWebhook.php | 24 +++++-- .../ProcessGithubPullRequestWebhookTest.php | 72 +++++++++++++++++++ 2 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 tests/Feature/ProcessGithubPullRequestWebhookTest.php diff --git a/app/Jobs/ProcessGithubPullRequestWebhook.php b/app/Jobs/ProcessGithubPullRequestWebhook.php index 54e386676..5a390e8ed 100644 --- a/app/Jobs/ProcessGithubPullRequestWebhook.php +++ b/app/Jobs/ProcessGithubPullRequestWebhook.php @@ -14,6 +14,7 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Throwable; use Visus\Cuid2\Cuid2; class ProcessGithubPullRequestWebhook implements ShouldBeEncrypted, ShouldQueue @@ -70,16 +71,25 @@ private function handleClosedAction(Application $application): void ->first(); if ($found) { - ApplicationPullRequestUpdateJob::dispatchSync( - application: $application, - preview: $found, - status: ProcessStatus::CLOSED - ); - - CleanupPreviewDeployment::run($application, $this->pullRequestId, $found); + try { + $this->dispatchPullRequestClosedUpdate($application, $found); + } catch (Throwable $e) { + report($e); + } finally { + CleanupPreviewDeployment::run($application, $this->pullRequestId, $found); + } } } + protected function dispatchPullRequestClosedUpdate(Application $application, ApplicationPreview $preview): void + { + ApplicationPullRequestUpdateJob::dispatchSync( + application: $application, + preview: $preview, + status: ProcessStatus::CLOSED + ); + } + private function handleOpenAction(Application $application, ?GithubApp $githubApp): void { if (! $application->isPRDeployable()) { diff --git a/tests/Feature/ProcessGithubPullRequestWebhookTest.php b/tests/Feature/ProcessGithubPullRequestWebhookTest.php new file mode 100644 index 000000000..6e0e9241e --- /dev/null +++ b/tests/Feature/ProcessGithubPullRequestWebhookTest.php @@ -0,0 +1,72 @@ + InstanceSettings::firstOrCreate(['id' => 0])); + + $this->team = Team::factory()->create(); + $this->server = Server::factory()->create(['team_id' => $this->team->id]); + $this->destination = StandaloneDocker::where('server_id', $this->server->id)->first(); + $this->project = Project::factory()->create(['team_id' => $this->team->id]); + $this->environment = Environment::factory()->create(['project_id' => $this->project->id]); + + $this->application = Application::factory()->create([ + 'environment_id' => $this->environment->id, + 'destination_id' => $this->destination->id, + 'destination_type' => $this->destination->getMorphClass(), + ]); +}); + +it('cleans up a closed pull request preview when pull request comment cleanup fails', function () { + $preview = ApplicationPreview::create([ + 'application_id' => $this->application->id, + 'pull_request_id' => 42, + 'pull_request_html_url' => 'https://github.com/example/repo/pull/42', + ]); + + CleanupPreviewDeployment::shouldRun() + ->once() + ->withArgs(fn (Application $application, int $pullRequestId, ApplicationPreview $applicationPreview): bool => $application->is($this->application) + && $pullRequestId === 42 + && $applicationPreview->is($preview)) + ->andReturn([ + 'cancelled_deployments' => 0, + 'killed_containers' => 0, + 'status' => 'success', + ]); + + $job = new class( + applicationId: $this->application->id, + githubAppId: null, + action: 'closed', + pullRequestId: 42, + pullRequestHtmlUrl: 'https://github.com/example/repo/pull/42', + pullRequestTitle: null, + beforeSha: null, + afterSha: null, + commitSha: 'HEAD', + authorAssociation: 'OWNER', + fullName: 'example/repo', + ) extends ProcessGithubPullRequestWebhook + { + protected function dispatchPullRequestClosedUpdate(Application $application, ApplicationPreview $preview): void + { + throw new RuntimeException('GitHub comment cleanup failed.'); + } + }; + + $job->handle(); +}); From 7053f560c5d01ec64d2b7da45c15b83cd66be70b Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 4 Jun 2026 15:41:38 +0200 Subject: [PATCH 02/12] chore(navbar): replace theme selector with cycle button --- resources/views/components/navbar.blade.php | 36 ++++----------------- tests/Unit/NavbarThemeSwitcherTest.php | 13 ++++++++ 2 files changed, 20 insertions(+), 29 deletions(-) create mode 100644 tests/Unit/NavbarThemeSwitcherTest.php diff --git a/resources/views/components/navbar.blade.php b/resources/views/components/navbar.blade.php index ecd798cc2..c2ca28a2d 100644 --- a/resources/views/components/navbar.blade.php +++ b/resources/views/components/navbar.blade.php @@ -376,7 +376,11 @@ class="{{ request()->is('settings*') ? 'menu-item-active menu-item' : 'menu-item
  • - - + Theme + +@elseif ($as === 'a') + class([ + 'inline-flex h-5 max-w-full items-center gap-1 rounded-sm border px-1.5 text-xs font-medium leading-4 transition-colors', + $typeClasses[$type] ?? $typeClasses['neutral'], + ]) }}> + {{ collect([$label, $status])->filter()->join(' ') }} + +@else + class([ + 'inline-flex h-5 max-w-full items-center gap-1 rounded-sm border px-1.5 text-xs font-medium leading-4', + $typeClasses[$type] ?? $typeClasses['neutral'], + ]) }}> + {{ collect([$label, $status])->filter()->join(' ') }} + +@endif diff --git a/resources/views/components/status/degraded.blade.php b/resources/views/components/status/degraded.blade.php index fbd068c72..99ae2d4cd 100644 --- a/resources/views/components/status/degraded.blade.php +++ b/resources/views/components/status/degraded.blade.php @@ -2,32 +2,18 @@ 'status' => 'Degraded', ]) @php - // Handle both colon format (backend) and parentheses format (from services.blade.php) - // degraded:unhealthy → Degraded (unhealthy) - // degraded (unhealthy) → degraded (unhealthy) (already formatted, display as-is) - if (str($status)->contains('(')) { - // Already in parentheses format from services.blade.php - use as-is $displayStatus = $status; - $healthStatus = str($status)->after('(')->before(')')->trim()->value(); - } elseif (str($status)->contains(':') && !str($status)->startsWith('Proxy')) { - // Colon format from backend - transform it + $healthStatus = null; + } elseif (str($status)->contains(':') && ! str($status)->startsWith('Proxy')) { $parts = explode(':', $status); - $displayStatus = str($parts[0])->headline(); + $displayStatus = str($parts[0])->headline()->value(); $healthStatus = $parts[1] ?? null; } else { - // Simple status without health - $displayStatus = str($status)->headline(); + $displayStatus = str($status)->headline()->value(); $healthStatus = null; } + + $badgeStatus = $healthStatus ? "{$displayStatus} ({$healthStatus})" : $displayStatus; @endphp -
    - - -
    -
    {{ $displayStatus }}
    - @if ($healthStatus && !str($displayStatus)->contains('(')) -
    ({{ $healthStatus }})
    - @endif -
    -
    + diff --git a/resources/views/components/status/index.blade.php b/resources/views/components/status/index.blade.php index b54e35bd5..7e8fd50a5 100644 --- a/resources/views/components/status/index.blade.php +++ b/resources/views/components/status/index.blade.php @@ -32,19 +32,9 @@ @endif @if (!str($resource->status)->contains('exited') && $showRefreshButton) - - + @endif diff --git a/resources/views/components/status/restarting.blade.php b/resources/views/components/status/restarting.blade.php index 353bf1097..8ed7494e8 100644 --- a/resources/views/components/status/restarting.blade.php +++ b/resources/views/components/status/restarting.blade.php @@ -5,42 +5,23 @@ 'noLoading' => false, ]) @php - // Handle both colon format (backend) and parentheses format (from services.blade.php) - // starting:unknown → Starting (unknown) - // starting (unknown) → starting (unknown) (already formatted, display as-is) - if (str($status)->contains('(')) { - // Already in parentheses format from services.blade.php - use as-is $displayStatus = $status; - $healthStatus = str($status)->after('(')->before(')')->trim()->value(); - } elseif (str($status)->contains(':') && !str($status)->startsWith('Proxy')) { - // Colon format from backend - transform it + $healthStatus = null; + } elseif (str($status)->contains(':') && ! str($status)->startsWith('Proxy')) { $parts = explode(':', $status); - $displayStatus = str($parts[0])->headline(); + $displayStatus = str($parts[0])->headline()->value(); $healthStatus = $parts[1] ?? null; } else { - // Simple status without health - $displayStatus = str($status)->headline(); + $displayStatus = str($status)->headline()->value(); $healthStatus = null; } + + $badgeStatus = $healthStatus ? "{$displayStatus} ({$healthStatus})" : $displayStatus; @endphp -
    - @if (!$noLoading) - - @endif - -
    -
    - @if ($lastDeploymentLink) - - {{ $displayStatus }} - - @else - {{ $displayStatus }} - @endif -
    - @if ($healthStatus && !str($displayStatus)->contains('(')) -
    ({{ $healthStatus }})
    - @endif -
    -
    +@if ($lastDeploymentLink) + +@else + +@endif diff --git a/resources/views/components/status/running.blade.php b/resources/views/components/status/running.blade.php index eb2964a86..7686d30bb 100644 --- a/resources/views/components/status/running.blade.php +++ b/resources/views/components/status/running.blade.php @@ -5,80 +5,47 @@ 'noLoading' => false, ]) @php - // Handle both colon format (backend) and parentheses format (from services.blade.php) - // running:healthy → Running (healthy) - // running (healthy) → running (healthy) (already formatted, display as-is) - if (str($status)->contains('(')) { - // Already in parentheses format from services.blade.php - use as-is $displayStatus = $status; - $healthStatus = str($status)->after('(')->before(')')->trim()->value(); - } elseif (str($status)->contains(':') && !str($status)->startsWith('Proxy')) { - // Colon format from backend - transform it + $healthStatus = null; + } elseif (str($status)->contains(':') && ! str($status)->startsWith('Proxy')) { $parts = explode(':', $status); - $displayStatus = str($parts[0])->headline(); + $displayStatus = str($parts[0])->headline()->value(); $healthStatus = $parts[1] ?? null; } else { - // Simple status without health - $displayStatus = str($status)->headline(); + $displayStatus = str($status)->headline()->value(); $healthStatus = null; } + + $badgeStatus = $healthStatus ? "{$displayStatus} ({$healthStatus})" : $displayStatus; + $showUnknownHelper = ! str($status)->startsWith('Proxy') && (str($status)->contains('unknown') || str($healthStatus)->contains('unknown')); + $showUnhealthyHelper = ! str($status)->startsWith('Proxy') && (str($status)->contains('unhealthy') || str($healthStatus)->contains('unhealthy')); @endphp -
    -
    -
    -
    -
    - @if ($lastDeploymentLink) - - {{ $displayStatus }} - - @else - {{ $displayStatus }} - @endif -
    - @if ($healthStatus && !str($displayStatus)->contains('(')) -
    ({{ $healthStatus }})
    - @endif - @php - $showUnknownHelper = - !str($status)->startsWith('Proxy') && - (str($status)->contains('unknown') || str($healthStatus)->contains('unknown')); - $showUnhealthyHelper = - !str($status)->startsWith('Proxy') && - (str($status)->contains('unhealthy') || str($healthStatus)->contains('unhealthy')); - @endphp - @if ($showUnknownHelper) -
    +
    + @if ($lastDeploymentLink) + + @else + + @endif + @if ($showUnknownHelper) +
    - +
    - @endif - @if ($showUnhealthyHelper) -
    + @endif + @if ($showUnhealthyHelper) +
    - +
    - @endif -
    - + @endif
    diff --git a/resources/views/components/status/services.blade.php b/resources/views/components/status/services.blade.php index ff1862c73..0f90751b5 100644 --- a/resources/views/components/status/services.blade.php +++ b/resources/views/components/status/services.blade.php @@ -14,19 +14,9 @@ @endif @if (!str($complexStatus)->contains('exited') && $showRefreshButton) - - + @endif
    diff --git a/resources/views/components/status/stopped.blade.php b/resources/views/components/status/stopped.blade.php index 0af349d40..221ba5e67 100644 --- a/resources/views/components/status/stopped.blade.php +++ b/resources/views/components/status/stopped.blade.php @@ -3,46 +3,12 @@ 'noLoading' => false, ]) @php - // Handle both colon format (backend) and parentheses format (from services.blade.php) - // For exited containers, health status is hidden (health checks don't run on stopped containers) - // exited:unhealthy → Exited - // exited (unhealthy) → Exited - if (str($status)->contains('(')) { - // Already in parentheses format from services.blade.php - use as-is - $displayStatus = $status; - $healthStatus = str($status)->after('(')->before(')')->trim()->value(); - - // Don't show health status for exited containers (health checks don't run on stopped containers) - if (str($displayStatus)->lower()->contains('exited')) { - $displayStatus = str($status)->before('(')->trim()->headline(); - $healthStatus = null; - } + $displayStatus = str($status)->before('(')->trim()->headline()->value(); } elseif (str($status)->contains(':')) { - // Colon format from backend - transform it - $parts = explode(':', $status); - $displayStatus = str($parts[0])->headline(); - $healthStatus = $parts[1] ?? null; - - // Don't show health status for exited containers (health checks don't run on stopped containers) - if (str($displayStatus)->lower()->contains('exited')) { - $healthStatus = null; - } + $displayStatus = str(explode(':', $status)[0])->headline()->value(); } else { - // Simple status without health - $displayStatus = str($status)->headline(); - $healthStatus = null; + $displayStatus = str($status)->headline()->value(); } @endphp -
    - @if (!$noLoading) - - @endif - -
    -
    {{ $displayStatus }}
    - @if ($healthStatus && !str($displayStatus)->contains('(')) -
    ({{ $healthStatus }})
    - @endif -
    -
    + diff --git a/resources/views/livewire/project/service/resource-card.blade.php b/resources/views/livewire/project/service/resource-card.blade.php index 47fb00914..95af5a732 100644 --- a/resources/views/livewire/project/service/resource-card.blade.php +++ b/resources/views/livewire/project/service/resource-card.blade.php @@ -43,7 +43,15 @@ class="w-4 h-4 dark:text-warning text-coollabs" @endcan @endif -
    $isApplication, 'text-xs'])>{{ formatContainerStatus($resource->status) }}
    +
    $isApplication])> + @if (str($resource->status)->contains('running')) + + @elseif (str($resource->status)->contains(['starting', 'restarting', 'degraded'])) + + @else + + @endif +
    @if ($isDatabase && ($resource->isBackupSolutionAvailable() || $resource->is_migrated)) diff --git a/resources/views/livewire/server/navbar.blade.php b/resources/views/livewire/server/navbar.blade.php index bf55ca7f6..ed7d59980 100644 --- a/resources/views/livewire/server/navbar.blade.php +++ b/resources/views/livewire/server/navbar.blade.php @@ -12,65 +12,50 @@ -
    -

    Server

    - @if ($server->proxySet()) -
    -
    - @if ($proxyStatus === 'running') - - @elseif ($proxyStatus === 'restarting') - - @elseif ($proxyStatus === 'stopping') - - @elseif ($proxyStatus === 'starting') - - @elseif (data_get($server, 'proxy.force_stop')) -
    - -
    - @elseif ($proxyStatus === 'exited') -
    - +
    +
    +

    Server

    + @if ($server->proxySet() || $server->isSentinelEnabled()) +
    + @if ($server->proxySet()) +
    + @if ($proxyStatus === 'running') + + @elseif ($proxyStatus === 'restarting') + + @elseif ($proxyStatus === 'stopping') + + @elseif ($proxyStatus === 'starting') + + @elseif (data_get($server, 'proxy.force_stop')) + + @elseif ($proxyStatus === 'exited') + + @endif +
    @endif -
    -
    - Checking Ports Availability... -
    - @if ($proxyStatus !== 'exited') - - + @if ($server->isSentinelEnabled()) + @if ($server->isSentinelLive()) + + @else + + @endif + @endif + @if ($server->proxySet()) + @endif
    -
    - @endif - @if ($server->isSentinelEnabled()) -
    -
    - @if ($server->isSentinelLive()) - - @else - - @endif -
    -
    - @endif + @endif +
    +
    {{ data_get($server, 'name') }}
    -
    {{ data_get($server, 'name') }}