From 498b189286c0c2dacacf9d90f9a3e7d8d9d6b4d1 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 19 Nov 2025 10:54:51 +0100 Subject: [PATCH] fix: correct status for services with all containers excluded from health checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When all containers are excluded from health checks, display their actual status with :excluded suffix instead of misleading hardcoded statuses. This prevents broken UI state with incorrect action buttons and provides clarity that monitoring is disabled. 🤖 Generated with Claude Code Co-Authored-By: Claude --- app/Actions/Docker/GetContainersStatus.php | 8 +- app/Actions/Shared/ComplexStatusCheck.php | 51 +++++++- app/Models/Service.php | 78 +++++++++++- .../components/status/services.blade.php | 25 ++-- .../project/service/heading.blade.php | 4 +- tests/Unit/ContainerHealthStatusTest.php | 116 ++++++++++++++++++ tests/Unit/ExcludeFromHealthCheckTest.php | 66 ++++++++-- 7 files changed, 317 insertions(+), 31 deletions(-) create mode 100644 tests/Unit/ContainerHealthStatusTest.php diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php index c7f4055f0..ef5cc37aa 100644 --- a/app/Actions/Docker/GetContainersStatus.php +++ b/app/Actions/Docker/GetContainersStatus.php @@ -98,11 +98,13 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti $labels = data_get($container, 'Config.Labels'); } $containerStatus = data_get($container, 'State.Status'); - $containerHealth = data_get($container, 'State.Health.Status', 'unhealthy'); + $containerHealth = data_get($container, 'State.Health.Status'); if ($containerStatus === 'restarting') { - $containerStatus = "restarting ($containerHealth)"; + $healthSuffix = $containerHealth ?? 'unknown'; + $containerStatus = "restarting ($healthSuffix)"; } else { - $containerStatus = "$containerStatus ($containerHealth)"; + $healthSuffix = $containerHealth ?? 'unknown'; + $containerStatus = "$containerStatus ($healthSuffix)"; } $labels = Arr::undot(format_docker_labels_to_json($labels)); $applicationId = data_get($labels, 'coolify.applicationId'); diff --git a/app/Actions/Shared/ComplexStatusCheck.php b/app/Actions/Shared/ComplexStatusCheck.php index fbaa8cae5..1013c73e0 100644 --- a/app/Actions/Shared/ComplexStatusCheck.php +++ b/app/Actions/Shared/ComplexStatusCheck.php @@ -97,14 +97,14 @@ private function aggregateContainerStatuses($application, $containers) $relevantContainerCount++; $containerStatus = data_get($container, 'State.Status'); - $containerHealth = data_get($container, 'State.Health.Status', 'unhealthy'); + $containerHealth = data_get($container, 'State.Health.Status'); if ($containerStatus === 'restarting') { $hasRestarting = true; $hasUnhealthy = true; } elseif ($containerStatus === 'running') { $hasRunning = true; - if ($containerHealth === 'unhealthy') { + if ($containerHealth && $containerHealth === 'unhealthy') { $hasUnhealthy = true; } } elseif ($containerStatus === 'exited') { @@ -113,8 +113,53 @@ private function aggregateContainerStatuses($application, $containers) } } + // If all containers are excluded, calculate status from excluded containers + // but mark it with :excluded to indicate monitoring is disabled if ($relevantContainerCount === 0) { - return 'exited:healthy'; + $excludedHasRunning = false; + $excludedHasRestarting = false; + $excludedHasUnhealthy = false; + $excludedHasExited = false; + + foreach ($containers as $container) { + $labels = data_get($container, 'Config.Labels', []); + $serviceName = data_get($labels, 'com.docker.compose.service'); + + // Only process excluded containers + if (! $serviceName || ! $excludedContainers->contains($serviceName)) { + continue; + } + + $containerStatus = data_get($container, 'State.Status'); + $containerHealth = data_get($container, 'State.Health.Status'); + + if ($containerStatus === 'restarting') { + $excludedHasRestarting = true; + $excludedHasUnhealthy = true; + } elseif ($containerStatus === 'running') { + $excludedHasRunning = true; + if ($containerHealth && $containerHealth === 'unhealthy') { + $excludedHasUnhealthy = true; + } + } elseif ($containerStatus === 'exited') { + $excludedHasExited = true; + $excludedHasUnhealthy = true; + } + } + + if ($excludedHasRestarting) { + return 'degraded:excluded'; + } + + if ($excludedHasRunning && $excludedHasExited) { + return 'degraded:excluded'; + } + + if ($excludedHasRunning) { + return 'running:excluded'; + } + + return 'exited:excluded'; } if ($hasRestarting) { diff --git a/app/Models/Service.php b/app/Models/Service.php index 15ee2d1bc..c98c20121 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -184,11 +184,13 @@ public function getStatusAttribute() $complexStatus = null; $complexHealth = null; + $hasNonExcluded = false; foreach ($applications as $application) { if ($application->exclude_from_status) { continue; } + $hasNonExcluded = true; $status = str($application->status)->before('(')->trim(); $health = str($application->status)->between('(', ')')->trim(); if ($complexStatus === 'degraded') { @@ -218,6 +220,7 @@ public function getStatusAttribute() if ($database->exclude_from_status) { continue; } + $hasNonExcluded = true; $status = str($database->status)->before('(')->trim(); $health = str($database->status)->between('(', ')')->trim(); if ($complexStatus === 'degraded') { @@ -244,9 +247,78 @@ public function getStatusAttribute() } } - // If all services are excluded from status checks, return a default exited status - if ($complexStatus === null && $complexHealth === null) { - return 'exited:healthy'; + // If all services are excluded from status checks, calculate status from excluded containers + // but mark it with :excluded to indicate monitoring is disabled + if (! $hasNonExcluded && ($complexStatus === null && $complexHealth === null)) { + $excludedStatus = null; + $excludedHealth = null; + + // Calculate status from excluded containers + foreach ($applications as $application) { + $status = str($application->status)->before('(')->trim(); + $health = str($application->status)->between('(', ')')->trim(); + if ($excludedStatus === 'degraded') { + continue; + } + if ($status->startsWith('running')) { + if ($excludedStatus === 'exited') { + $excludedStatus = 'degraded'; + } else { + $excludedStatus = 'running'; + } + } elseif ($status->startsWith('restarting')) { + $excludedStatus = 'degraded'; + } elseif ($status->startsWith('exited')) { + $excludedStatus = 'exited'; + } + if ($health->value() === 'healthy') { + if ($excludedHealth === 'unhealthy') { + continue; + } + $excludedHealth = 'healthy'; + } else { + $excludedHealth = 'unhealthy'; + } + } + + foreach ($databases as $database) { + $status = str($database->status)->before('(')->trim(); + $health = str($database->status)->between('(', ')')->trim(); + if ($excludedStatus === 'degraded') { + continue; + } + if ($status->startsWith('running')) { + if ($excludedStatus === 'exited') { + $excludedStatus = 'degraded'; + } else { + $excludedStatus = 'running'; + } + } elseif ($status->startsWith('restarting')) { + $excludedStatus = 'degraded'; + } elseif ($status->startsWith('exited')) { + $excludedStatus = 'exited'; + } + if ($health->value() === 'healthy') { + if ($excludedHealth === 'unhealthy') { + continue; + } + $excludedHealth = 'healthy'; + } else { + $excludedHealth = 'unhealthy'; + } + } + + // Return status with :excluded suffix to indicate monitoring is disabled + if ($excludedStatus && $excludedHealth) { + return "{$excludedStatus}:excluded"; + } + + // If no status was calculated at all (no containers exist), return unknown + if ($excludedStatus === null && $excludedHealth === null) { + return 'unknown:excluded'; + } + + return 'exited:excluded'; } return "{$complexStatus}:{$complexHealth}"; diff --git a/resources/views/components/status/services.blade.php b/resources/views/components/status/services.blade.php index 7ea55099f..87db0d64c 100644 --- a/resources/views/components/status/services.blade.php +++ b/resources/views/components/status/services.blade.php @@ -1,13 +1,20 @@ -@if (str($complexStatus)->contains('running')) - -@elseif(str($complexStatus)->contains('starting')) - -@elseif(str($complexStatus)->contains('restarting')) - -@elseif(str($complexStatus)->contains('degraded')) - +@php + $isExcluded = str($complexStatus)->endsWith(':excluded'); + $displayStatus = $isExcluded ? str($complexStatus)->beforeLast(':excluded') : $complexStatus; +@endphp +@if (str($displayStatus)->contains('running')) + +@elseif(str($displayStatus)->contains('starting')) + +@elseif(str($displayStatus)->contains('restarting')) + +@elseif(str($displayStatus)->contains('degraded')) + @else - + +@endif +@if ($isExcluded) + (Monitoring Disabled) @endif @if (!str($complexStatus)->contains('exited') && $showRefreshButton)