fix(status): show running:starting while a health check is in its start period

Closes #9411

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andras BacsaiandClaude Opus 5.5 committed 2026-11-03 20:49:21 +01:00
1 parent 3895ca05f3
commit 666ac84efb
4 files changed
+44 -3

No files matched your search

+15 -2
View File
@@ -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 {
+1 -1
View File
@@ -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';
+2
View File
@@ -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',
@@ -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');
});
});