diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index 384b960ef..8a278476e 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -3153,3 +3153,46 @@ function generateDockerComposeServiceName(mixed $services, int $pullRequestId =
return $collection;
}
+
+/**
+ * Transform colon-delimited status format to human-readable parentheses format.
+ *
+ * Handles Docker container status formats with optional health check status and exclusion modifiers.
+ *
+ * Examples:
+ * - running:healthy → Running (healthy)
+ * - running:unhealthy:excluded → Running (unhealthy, excluded)
+ * - exited:excluded → Exited (excluded)
+ * - Proxy:running → Proxy:running (preserved as-is for headline formatting)
+ * - running → Running
+ *
+ * @param string $status The status string to format
+ * @return string The formatted status string
+ */
+function formatContainerStatus(string $status): string
+{
+ // Preserve Proxy statuses as-is (they follow different format)
+ if (str($status)->startsWith('Proxy')) {
+ return str($status)->headline()->value();
+ }
+
+ // Check for :excluded suffix
+ $isExcluded = str($status)->endsWith(':excluded');
+ $parts = explode(':', $status);
+
+ if ($isExcluded) {
+ if (count($parts) === 3) {
+ // Has health status: running:unhealthy:excluded → Running (unhealthy, excluded)
+ return str($parts[0])->headline().' ('.$parts[1].', excluded)';
+ } else {
+ // No health status: exited:excluded → Exited (excluded)
+ return str($parts[0])->headline().' (excluded)';
+ }
+ } elseif (count($parts) >= 2) {
+ // Regular colon format: running:healthy → Running (healthy)
+ return str($parts[0])->headline().' ('.$parts[1].')';
+ } else {
+ // Simple status: running → Running
+ return str($status)->headline()->value();
+ }
+}
diff --git a/resources/views/components/status/services.blade.php b/resources/views/components/status/services.blade.php
index 1897781ba..9c7a870c7 100644
--- a/resources/views/components/status/services.blade.php
+++ b/resources/views/components/status/services.blade.php
@@ -1,26 +1,5 @@
@php
- // Transform colon format to human-readable format for UI display
- // running:healthy → Running (healthy)
- // running:unhealthy:excluded → Running (unhealthy, excluded)
- // exited:excluded → Exited (excluded)
- $isExcluded = str($complexStatus)->endsWith(':excluded');
- $parts = explode(':', $complexStatus);
-
- if ($isExcluded) {
- if (count($parts) === 3) {
- // Has health status: running:unhealthy:excluded → Running (unhealthy, excluded)
- $displayStatus = str($parts[0])->headline() . ' (' . $parts[1] . ', excluded)';
- } else {
- // No health status: exited:excluded → Exited (excluded)
- $displayStatus = str($parts[0])->headline() . ' (excluded)';
- }
- } elseif (count($parts) >= 2 && !str($complexStatus)->startsWith('Proxy')) {
- // Regular colon format: running:healthy → Running (healthy)
- $displayStatus = str($parts[0])->headline() . ' (' . $parts[1] . ')';
- } else {
- // No transformation needed (simple status or already in parentheses format)
- $displayStatus = str($complexStatus)->headline();
- }
+ $displayStatus = formatContainerStatus($complexStatus);
@endphp
@if (str($displayStatus)->lower()->contains('running'))
diff --git a/resources/views/livewire/project/service/configuration.blade.php b/resources/views/livewire/project/service/configuration.blade.php
index cfe79e22d..9b81e4bec 100644
--- a/resources/views/livewire/project/service/configuration.blade.php
+++ b/resources/views/livewire/project/service/configuration.blade.php
@@ -90,31 +90,7 @@ class="w-4 h-4 dark:text-warning text-coollabs"
@endcan
@endif
- @php
- // Transform colon format to human-readable format
- // running:healthy → Running (healthy)
- // running:unhealthy:excluded → Running (unhealthy, excluded)
- $appStatus = $application->status;
- $isExcluded = str($appStatus)->endsWith(':excluded');
- $parts = explode(':', $appStatus);
-
- if ($isExcluded) {
- if (count($parts) === 3) {
- // Has health status: running:unhealthy:excluded → Running (unhealthy, excluded)
- $appStatus = str($parts[0])->headline() . ' (' . $parts[1] . ', excluded)';
- } else {
- // No health status: exited:excluded → Exited (excluded)
- $appStatus = str($parts[0])->headline() . ' (excluded)';
- }
- } elseif (count($parts) >= 2 && !str($appStatus)->startsWith('Proxy')) {
- // Regular colon format: running:healthy → Running (healthy)
- $appStatus = str($parts[0])->headline() . ' (' . $parts[1] . ')';
- } else {
- // Simple status or already in parentheses format
- $appStatus = str($appStatus)->headline();
- }
- @endphp
-
{{ $appStatus }}
+ {{ formatContainerStatus($application->status) }}
@if ($database->isBackupSolutionAvailable() || $database->is_migrated)
diff --git a/tests/Unit/FormatContainerStatusTest.php b/tests/Unit/FormatContainerStatusTest.php
new file mode 100644
index 000000000..f24aa8c52
--- /dev/null
+++ b/tests/Unit/FormatContainerStatusTest.php
@@ -0,0 +1,201 @@
+toBe('Running (healthy)');
+ });
+
+ it('transforms running:unhealthy to Running (unhealthy)', function () {
+ $result = formatContainerStatus('running:unhealthy');
+
+ expect($result)->toBe('Running (unhealthy)');
+ });
+
+ it('transforms exited:0 to Exited (0)', function () {
+ $result = formatContainerStatus('exited:0');
+
+ expect($result)->toBe('Exited (0)');
+ });
+
+ it('transforms restarting:starting to Restarting (starting)', function () {
+ $result = formatContainerStatus('restarting:starting');
+
+ expect($result)->toBe('Restarting (starting)');
+ });
+ });
+
+ describe('excluded suffix handling', function () {
+ it('transforms running:unhealthy:excluded to Running (unhealthy, excluded)', function () {
+ $result = formatContainerStatus('running:unhealthy:excluded');
+
+ expect($result)->toBe('Running (unhealthy, excluded)');
+ });
+
+ it('transforms running:healthy:excluded to Running (healthy, excluded)', function () {
+ $result = formatContainerStatus('running:healthy:excluded');
+
+ expect($result)->toBe('Running (healthy, excluded)');
+ });
+
+ it('transforms exited:excluded to Exited (excluded)', function () {
+ $result = formatContainerStatus('exited:excluded');
+
+ expect($result)->toBe('Exited (excluded)');
+ });
+
+ it('transforms stopped:excluded to Stopped (excluded)', function () {
+ $result = formatContainerStatus('stopped:excluded');
+
+ expect($result)->toBe('Stopped (excluded)');
+ });
+ });
+
+ describe('simple status format', function () {
+ it('transforms running to Running', function () {
+ $result = formatContainerStatus('running');
+
+ expect($result)->toBe('Running');
+ });
+
+ it('transforms exited to Exited', function () {
+ $result = formatContainerStatus('exited');
+
+ expect($result)->toBe('Exited');
+ });
+
+ it('transforms stopped to Stopped', function () {
+ $result = formatContainerStatus('stopped');
+
+ expect($result)->toBe('Stopped');
+ });
+
+ it('transforms restarting to Restarting', function () {
+ $result = formatContainerStatus('restarting');
+
+ expect($result)->toBe('Restarting');
+ });
+
+ it('transforms degraded to Degraded', function () {
+ $result = formatContainerStatus('degraded');
+
+ expect($result)->toBe('Degraded');
+ });
+ });
+
+ describe('Proxy status preservation', function () {
+ it('preserves Proxy:running without parsing colons', function () {
+ $result = formatContainerStatus('Proxy:running');
+
+ expect($result)->toBe('Proxy:running');
+ });
+
+ it('preserves Proxy:exited without parsing colons', function () {
+ $result = formatContainerStatus('Proxy:exited');
+
+ expect($result)->toBe('Proxy:exited');
+ });
+
+ it('preserves Proxy:healthy without parsing colons', function () {
+ $result = formatContainerStatus('Proxy:healthy');
+
+ expect($result)->toBe('Proxy:healthy');
+ });
+
+ it('applies headline formatting to Proxy statuses', function () {
+ $result = formatContainerStatus('proxy:running');
+
+ expect($result)->toBe('Proxy (running)');
+ });
+ });
+
+ describe('headline transformation', function () {
+ it('applies headline to simple lowercase status', function () {
+ $result = formatContainerStatus('running');
+
+ expect($result)->toBe('Running');
+ });
+
+ it('applies headline to uppercase status', function () {
+ // headline() adds spaces between capital letters
+ $result = formatContainerStatus('RUNNING');
+
+ expect($result)->toBe('R U N N I N G');
+ });
+
+ it('applies headline to mixed case status', function () {
+ // headline() adds spaces between capital letters
+ $result = formatContainerStatus('RuNnInG');
+
+ expect($result)->toBe('Ru Nn In G');
+ });
+
+ it('applies headline to first part of colon format', function () {
+ // headline() adds spaces between capital letters
+ $result = formatContainerStatus('RUNNING:healthy');
+
+ expect($result)->toBe('R U N N I N G (healthy)');
+ });
+ });
+
+ describe('edge cases', function () {
+ it('handles empty string gracefully', function () {
+ $result = formatContainerStatus('');
+
+ expect($result)->toBe('');
+ });
+
+ it('handles multiple colons beyond expected format', function () {
+ // Only first two parts should be used (or three with :excluded)
+ $result = formatContainerStatus('running:healthy:extra:data');
+
+ expect($result)->toBe('Running (healthy)');
+ });
+
+ it('handles status with spaces in health part', function () {
+ $result = formatContainerStatus('running:health check failed');
+
+ expect($result)->toBe('Running (health check failed)');
+ });
+
+ it('handles single colon with empty second part', function () {
+ $result = formatContainerStatus('running:');
+
+ expect($result)->toBe('Running ()');
+ });
+ });
+
+ describe('real-world scenarios', function () {
+ it('handles typical running healthy container', function () {
+ $result = formatContainerStatus('running:healthy');
+
+ expect($result)->toBe('Running (healthy)');
+ });
+
+ it('handles degraded container with health issues', function () {
+ $result = formatContainerStatus('degraded:unhealthy');
+
+ expect($result)->toBe('Degraded (unhealthy)');
+ });
+
+ it('handles excluded unhealthy container', function () {
+ $result = formatContainerStatus('running:unhealthy:excluded');
+
+ expect($result)->toBe('Running (unhealthy, excluded)');
+ });
+
+ it('handles proxy container status', function () {
+ $result = formatContainerStatus('Proxy:running');
+
+ expect($result)->toBe('Proxy:running');
+ });
+
+ it('handles stopped container', function () {
+ $result = formatContainerStatus('stopped');
+
+ expect($result)->toBe('Stopped');
+ });
+ });
+});