From 666ac84efbdd6468628465ecd0425d2f4c018c74 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 3 Nov 2026 20:49:21 +0100 Subject: [PATCH] fix(status): show running:starting while a health check is in its start period Closes #9411 Co-Authored-By: Claude Opus 5.5 --- app/Services/ContainerStatusAggregator.php | 17 +++++++++++-- resources/js/environment-canvas.js | 2 +- resources/js/environment-canvas.test.js | 2 ++ tests/Unit/ContainerStatusAggregatorTest.php | 26 ++++++++++++++++++++ 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/app/Services/ContainerStatusAggregator.php b/app/Services/ContainerStatusAggregator.php index f963c0a6a1..74538968b7 100644 --- a/app/Services/ContainerStatusAggregator.php +++ b/app/Services/ContainerStatusAggregator.php @@ -21,7 +21,7 @@ use Illuminate\Support\Facades\Log; * 2. Restarting → degraded:unhealthy (or restarting:unknown if preserveRestarting=true) * 3. Mixed (running + exited) → degraded:unhealthy * 4. Mixed (running + starting) → starting:unknown - * 5. Running → running:healthy/unhealthy/unknown + * 5. Running → running:unhealthy/starting/unknown/healthy (health check still in its start period → starting) * 6. Dead/Removing → degraded:unhealthy * 7. Paused → paused:unknown * 8. Starting/Created → starting:unknown @@ -101,6 +101,7 @@ class ContainerStatusAggregator $hasRestarting = false; $hasUnhealthy = false; $hasUnknown = false; + $hasHealthStarting = false; $hasExited = false; $hasStarting = false; $hasPaused = false; @@ -124,6 +125,9 @@ class ContainerStatusAggregator if (str($status)->contains('unknown')) { $hasUnknown = true; } + if (str($status)->contains('starting')) { + $hasHealthStarting = true; + } } elseif (str($status)->contains('exited')) { $hasExited = true; } elseif (str($status)->contains('created') || str($status)->contains('starting')) { @@ -141,6 +145,7 @@ class ContainerStatusAggregator $hasRestarting, $hasUnhealthy, $hasUnknown, + $hasHealthStarting, $hasExited, $hasStarting, $hasPaused, @@ -178,6 +183,7 @@ class ContainerStatusAggregator $hasRestarting = false; $hasUnhealthy = false; $hasUnknown = false; + $hasHealthStarting = false; $hasExited = false; $hasStarting = false; $hasPaused = false; @@ -194,7 +200,9 @@ class ContainerStatusAggregator $hasRunning = true; if ($health === 'unhealthy') { $hasUnhealthy = true; - } elseif (is_null($health) || $health === 'starting') { + } elseif ($health === 'starting') { + $hasHealthStarting = true; + } elseif (is_null($health)) { $hasUnknown = true; } } elseif ($state === 'exited') { @@ -214,6 +222,7 @@ class ContainerStatusAggregator $hasRestarting, $hasUnhealthy, $hasUnknown, + $hasHealthStarting, $hasExited, $hasStarting, $hasPaused, @@ -231,6 +240,7 @@ class ContainerStatusAggregator * @param bool $hasRestarting Has at least one restarting container * @param bool $hasUnhealthy Has at least one unhealthy container * @param bool $hasUnknown Has at least one container with unknown health + * @param bool $hasHealthStarting Has at least one running container whose health check is still in its start period * @param bool $hasExited Has at least one exited container * @param bool $hasStarting Has at least one starting/created container * @param bool $hasPaused Has at least one paused container @@ -245,6 +255,7 @@ class ContainerStatusAggregator bool $hasRestarting, bool $hasUnhealthy, bool $hasUnknown, + bool $hasHealthStarting, bool $hasExited, bool $hasStarting, bool $hasPaused, @@ -281,6 +292,8 @@ class ContainerStatusAggregator if ($hasRunning) { if ($hasUnhealthy) { return 'running:unhealthy'; + } elseif ($hasHealthStarting) { + return 'running:starting'; } elseif ($hasUnknown) { return 'running:unknown'; } else { diff --git a/resources/js/environment-canvas.js b/resources/js/environment-canvas.js index a792bc17b2..2f9cda185e 100644 --- a/resources/js/environment-canvas.js +++ b/resources/js/environment-canvas.js @@ -570,7 +570,7 @@ export function statusTone(status) { const value = String(status ?? '').trim().toLowerCase(); if (!value) return 'idle'; const [state, health = ''] = value.replace(/\)$/, '').split(/\s*[:(]\s*/); - if (BUSY.includes(state)) return 'busy'; + if (BUSY.includes(state) || health === 'starting') return 'busy'; if (BAD.includes(state)) return 'bad'; if (state === 'degraded') return 'warn'; if (state === 'running' || state === 'healthy') return health === 'unhealthy' ? 'warn' : 'ok'; diff --git a/resources/js/environment-canvas.test.js b/resources/js/environment-canvas.test.js index 740ec6aa6f..074a8d60aa 100644 --- a/resources/js/environment-canvas.test.js +++ b/resources/js/environment-canvas.test.js @@ -179,6 +179,8 @@ test('maps Coolify statuses to tones', () => { 'running:unknown': 'ok', running: 'ok', 'running (healthy)': 'ok', + 'running:starting': 'busy', + 'running (starting)': 'busy', 'running:unhealthy': 'warn', 'degraded:unhealthy': 'warn', 'exited:unhealthy': 'bad', diff --git a/tests/Unit/ContainerStatusAggregatorTest.php b/tests/Unit/ContainerStatusAggregatorTest.php index 0a40206ac7..e97c4d6792 100644 --- a/tests/Unit/ContainerStatusAggregatorTest.php +++ b/tests/Unit/ContainerStatusAggregatorTest.php @@ -636,3 +636,29 @@ describe('maxRestartCount validation', function () { expect($result)->toBe('exited'); }); }); + +describe('health check in start period', function () { + test('keeps running:starting from status strings', function (array $statuses, string $expected) { + expect($this->aggregator->aggregateFromStrings(collect($statuses)))->toBe($expected); + })->with([ + 'one starting container' => [['running:starting'], 'running:starting'], + 'starting with healthy' => [['running:healthy', 'running:starting'], 'running:starting'], + 'starting with no health check' => [['running:unknown', 'running:starting'], 'running:starting'], + 'unhealthy wins over starting' => [['running:starting', 'running:unhealthy'], 'running:unhealthy'], + ]); + + test('keeps running:starting from container objects', function () { + $containers = collect([ + ['State' => ['Status' => 'running', 'Health' => ['Status' => 'starting']]], + ['State' => ['Status' => 'running', 'Health' => ['Status' => 'healthy']]], + ]); + + expect($this->aggregator->aggregateFromContainers($containers))->toBe('running:starting'); + }); + + test('keeps running:starting for a compose service', function () { + $compose = "services:\n nginx:\n image: nginx:alpine\n"; + + expect($this->aggregator->aggregateForCompose(collect(['nginx' => 'running:starting']), $compose))->toBe('running:starting'); + }); +});