From 9b07be2d68af39e46c131a7d36bf4c0b56edd4c3 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 14 Sep 2026 13:36:46 +0200 Subject: [PATCH 01/11] chore(helper): bump helper version to 1.0.17 --- config/constants.php | 2 +- other/nightly/versions.json | 2 +- versions.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/constants.php b/config/constants.php index 7e649668d8..a69b697f30 100644 --- a/config/constants.php +++ b/config/constants.php @@ -3,7 +3,7 @@ return [ 'coolify' => [ 'version' => env('COOLIFY_VERSION') ?: '4.3.19', - 'helper_version' => '1.0.16', + 'helper_version' => '1.0.17', 'realtime_version' => '1.0.19', 'railpack_version' => '0.23.0', 'self_hosted' => env('SELF_HOSTED', true), diff --git a/other/nightly/versions.json b/other/nightly/versions.json index d426bdb007..50e20a4066 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -7,7 +7,7 @@ "version": "4.4-rc.1" }, "helper": { - "version": "1.0.16" + "version": "1.0.17" }, "realtime": { "version": "1.0.18" diff --git a/versions.json b/versions.json index d426bdb007..50e20a4066 100644 --- a/versions.json +++ b/versions.json @@ -7,7 +7,7 @@ "version": "4.4-rc.1" }, "helper": { - "version": "1.0.16" + "version": "1.0.17" }, "realtime": { "version": "1.0.18" From 22fbbd74ffc18f152302ebf3649a0d74385b9d9f Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 14 Sep 2026 15:43:52 +0200 Subject: [PATCH 02/11] fix(sentinel): restore hourly version checks for enabled servers Dispatch Sentinel checks hourly for eligible servers, including healthy installations, while continuing to exclude build servers. --- app/Jobs/ServerManagerJob.php | 6 ++++++ .../Server/SentinelUpdateCheckIndependenceTest.php | 12 ++++++------ tests/Unit/ServerManagerJobSentinelCheckTest.php | 7 ++++--- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/app/Jobs/ServerManagerJob.php b/app/Jobs/ServerManagerJob.php index 171d4e6949..20b60b9fd2 100644 --- a/app/Jobs/ServerManagerJob.php +++ b/app/Jobs/ServerManagerJob.php @@ -166,6 +166,12 @@ class ServerManagerJob implements ShouldBeEncrypted, ShouldQueue } } + if ($server->isSentinelEnabled() + && shouldRunCronNow('0 * * * *', $serverTimezone, "sentinel-version-check:{$server->id}", $this->executionTime) + ) { + CheckAndStartSentinelJob::dispatch($server); + } + // Dispatch ServerStorageCheckJob if due (only when Sentinel is out of sync or disabled) // When Sentinel is active, PushServerUpdateJob handles storage checks with real-time data if ($sentinelOutOfSync) { diff --git a/tests/Feature/Server/SentinelUpdateCheckIndependenceTest.php b/tests/Feature/Server/SentinelUpdateCheckIndependenceTest.php index 080a3ee7c8..d3ccb5c478 100644 --- a/tests/Feature/Server/SentinelUpdateCheckIndependenceTest.php +++ b/tests/Feature/Server/SentinelUpdateCheckIndependenceTest.php @@ -13,6 +13,7 @@ uses(RefreshDatabase::class); beforeEach(function () { Queue::fake(); + InstanceSettings::forceCreate(['id' => 0]); // Create user (which automatically creates a team) $user = User::factory()->create(); @@ -123,10 +124,9 @@ it('respects server timezone when checking sentinel updates', function () { }); }); -it('does not dispatch sentinel check for servers without sentinel enabled', function () { - // Disable sentinel +it('does not dispatch sentinel check for build servers', function () { $this->server->settings->update([ - 'is_sentinel_enabled' => false, + 'is_build_server' => true, ]); $instanceSettings = InstanceSettings::first(); @@ -144,13 +144,13 @@ it('does not dispatch sentinel check for servers without sentinel enabled', func Queue::assertNotPushed(CheckAndStartSentinelJob::class); }); -it('handles multiple servers with different sentinel configurations', function () { - // Create a second server with sentinel disabled +it('handles multiple servers with different sentinel eligibility', function () { + // Create a second server that cannot run Sentinel $server2 = Server::factory()->create([ 'team_id' => $this->team->id, ]); $server2->settings->update([ - 'is_sentinel_enabled' => false, + 'is_build_server' => true, 'server_timezone' => 'UTC', ]); diff --git a/tests/Unit/ServerManagerJobSentinelCheckTest.php b/tests/Unit/ServerManagerJobSentinelCheckTest.php index 4666565fe4..f2a49bad3d 100644 --- a/tests/Unit/ServerManagerJobSentinelCheckTest.php +++ b/tests/Unit/ServerManagerJobSentinelCheckTest.php @@ -18,7 +18,7 @@ afterEach(function () { Carbon::setTestNow(); }); -it('does not dispatch CheckAndStartSentinelJob hourly anymore', function () { +it('dispatches an hourly Sentinel version check for a healthy Sentinel', function () { $settings = Mockery::mock(InstanceSettings::class); $settings->instance_timezone = 'UTC'; $this->app->instance(InstanceSettings::class, $settings); @@ -39,8 +39,9 @@ it('does not dispatch CheckAndStartSentinelJob hourly anymore', function () { $job = new ServerManagerJob; $job->handle(); - // Hourly CheckAndStartSentinelJob dispatch was removed — ServerCheckJob handles it when Sentinel is out of sync - Queue::assertNotPushed(CheckAndStartSentinelJob::class); + Queue::assertPushed(CheckAndStartSentinelJob::class, function ($job) use ($server) { + return $job->server->id === $server->id; + }); }); it('does not schedule periodic Sentinel restart checks', function () { From 246b2cfe0ac7f1ece50b46706f693df1b88ec07b Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 14 Sep 2026 17:01:53 +0200 Subject: [PATCH 03/11] chore(release): bump Coolify version to 4.3.20 --- config/constants.php | 2 +- other/nightly/versions.json | 2 +- versions.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/constants.php b/config/constants.php index a69b697f30..3f8335134f 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => env('COOLIFY_VERSION') ?: '4.3.19', + 'version' => env('COOLIFY_VERSION') ?: '4.3.20', 'helper_version' => '1.0.17', 'realtime_version' => '1.0.19', 'railpack_version' => '0.23.0', diff --git a/other/nightly/versions.json b/other/nightly/versions.json index 50e20a4066..6818286b29 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,7 +1,7 @@ { "coolify": { "v4": { - "version": "4.3.19" + "version": "4.3.20" }, "nightly": { "version": "4.4-rc.1" diff --git a/versions.json b/versions.json index 50e20a4066..6818286b29 100644 --- a/versions.json +++ b/versions.json @@ -1,7 +1,7 @@ { "coolify": { "v4": { - "version": "4.3.19" + "version": "4.3.20" }, "nightly": { "version": "4.4-rc.1" From 398f32712aa431118255f3c68879c3b96d5791d2 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:41:06 +0200 Subject: [PATCH 04/11] docs(ai): consolidate engineering lessons --- .ai/lessons.md | 127 +++++++++++++------------------------------------ 1 file changed, 34 insertions(+), 93 deletions(-) diff --git a/.ai/lessons.md b/.ai/lessons.md index db826478c3..43de6bf878 100644 --- a/.ai/lessons.md +++ b/.ai/lessons.md @@ -1,102 +1,43 @@ # Lessons -## Check prior fixes before changing a repeated symptom -- When a reported regression matches a recent fix, inspect that fix and reproduce why it no longer works before adding another workaround. -- Do not claim a redirect or lifecycle root cause from an effects assertion alone. Prove the reported HTTP or browser failure first. -- Preserve SPA navigation when it is a product requirement. Do not replace it with a full-page redirect to mask a deletion race; fix the ordering or state race instead. +## Prove regressions before changing code +- Reproduce the reported failure on the unchanged baseline before adding a fix. +- When a symptom matches an earlier fix, inspect that fix and prove why it no longer works before adding another workaround. +- Test old reports against the current branch because later changes can make the report obsolete. +- Use the same regression test before and after the production change so the result shows the behavior difference. -## Confirm which surface becomes the modal -- When a user wants two settings pages replaced by a modal, identify the parent page that owns the trigger and confirm that the complete child settings view moves into that modal. -- Do not make one child page a modal inside the other child page when the user wants both child URLs removed. -- When the modal itself supplies the title and subtitle, do not repeat page-style section cards inside it. Use a flat input layout and one footer for actions. -- Put destructive actions on the footer's left. Put conversion and the primary Save action on the right, with Save last. -- Do not repeat domain-port guidance in a resource settings modal when domain ports have their own input in the domain editor. -- A flat modal form can still use a bordered summary box for a distinct linked resource, such as the domain count and Manage domains action. -- For compact modal headers, show the descriptive subtitle as hover text on an underlined title instead of adding a second visible line. -- Reuse `x-helper` and the plain `underline underline-offset-4` trigger for title help. Do not use a native `title` tooltip or a dotted underline when the project already has a shared title-tooltip pattern. +## Verify the complete user flow +- Do not use a passing unit test, a successful build, or a healthy process as proof for a reported UI failure. +- Verify the exact live flow, persisted state, relevant logs, and queue state when they affect the result. +- When the request covers more than one interface or resource type, inventory and verify each supported path. -## Alpine x-transition + tw-animate-css exit animations flash at the end -- Symptom: a modal/overlay fades out, then flashes fully visible for 1-2 frames before it disappears. -- Cause: `animate-out` keyframes default to `animation-fill-mode: none`. The element snaps back to its natural state when the keyframe ends. Alpine hides the element (display: none) only after its own timer (read from `transition-duration`), which starts ~2 rAF later than the animation. The gap shows the element at full opacity. -- Rule: every `x-transition:leave` that uses tw-animate-css `animate-out` MUST also include `fill-mode-forwards`. -- Rule: when a user reports UI flicker, check ALL layers of the animation stack (state reset timing, spinner flash, keyframe fill mode, focus restore) before you report the fix as complete. My first fix covered state reset and spinner only; the fill-mode snap was the visible one. +## Preserve product scope +- Do not replace required SPA navigation with a full-page redirect to hide a lifecycle or ordering defect. +- Do not add billing restrictions, live reconciliation, or fallback behavior unless the request includes them. +- Treat implementation constraints as details. Do not expand a requested team-level control into a more complex policy model. -## Displayed defaults must not become stored overrides -- When an edit form shows an inherited or computed default, trace an unchanged save and a related-field edit through persistence. -- Preserve the inherited state when the displayed value still equals the computed default; store an override only when the user selects a different value. +## Keep dynamic Livewire identities stable +- In dynamic lists, key components and actions with immutable record identities, not counts, indexes, or array positions. +- Use targeted refresh events. Do not refresh a parent and a child that the parent can remove or hide during the same operation. +- Prove lifecycle and redirect causes directly; an effects assertion alone is not sufficient. -## Prove regressions against the unchanged baseline -- For a bug fix, run the same regression test before and after the production change. Use a stash when requested so the failure and success come from the exact same test. +## Keep modal structure consistent +- Identify the parent page that owns a modal trigger and move the complete requested workflow into that modal. +- Use a flat form layout when the modal already supplies its title and description. +- Put destructive actions on the footer's left and primary actions last on the right. +- Use shared section, helper, tooltip, and icon-button components instead of local variants. +- Keep validation, preview, and save controls in a fixed footer when the body is large. -## Apply shared domain UX to every supported resource type -- When a user asks for domain-management behavior, inventory every resource that can edit domains before implementation. -- Do not stop at the resource type named in the original report when the requested UX is meant to be consistent across Coolify. +## Verify layered UI behavior visually +- Inspect the real layout with all conditional elements visible, especially compound status badges. +- For animation flicker, inspect state timing, loading indicators, keyframe fill mode, and focus restoration. +- Add `fill-mode-forwards` to Alpine leave transitions that use tw-animate-css `animate-out` so the element does not flash before Alpine hides it. -## Verify manual and generated domain paths separately -- Domain regeneration and manual hostname edits must start the same post-save DNS check. -- Add explicit regression coverage for both entry paths across every active domain editor. +## Preserve inherited values and clear API semantics +- An unchanged displayed default must remain inherited; store an override only when the user selects a different value. +- Expose named API values for special modes. Keep existing numeric sentinels only as compatibility aliases unless a breaking change is requested. -## Do not treat a runtime restart as behavior verification -- A healthy restarted container proves only that the process started. -- For a reported UI failure, verify the exact user flow and inspect the resulting persisted state before claiming the fix works. - -## Prove the reported live flow before reporting a UI fix -- Do not use unit tests or a healthy process as proof for a reported live UI failure. -- After the user repeats the flow, inspect the exact persisted record, request logs, queue state, and deployed source before stating that it works. - -## Start DNS checks only for DNS-relevant edits -- Compare the previous and saved scheme and hostname before a post-save DNS check. -- Do not restart DNS checks for indexing, redirect, path, or internal-port-only changes. - -## Include automatically added domains in post-save DNS checks -- Compare the configured domain list before and after Save. -- Start checks for each newly added counterpart, even when the edited domain itself did not change. - -## Use one DNS progress pattern -- All DNS check entry points must set the domain badge to the same `checking` state. -- Do not use separate loading feedback on Check all or per-domain action buttons when the badge is the progress indicator. -- Verify the rendered badge uses the spinner slot instead of the default status dot. - -## Confirm whether old reports still apply before changing code -- For an old issue, first test the current branch and inspect later fixes. Do not assume that the historical reproduction still needs a new code change. - -## Compare routing identity, not complete domain URLs -- Domain-conflict checks must treat `http://host` and `https://host` as the same routing identity. -- Reproduce reports with the exact stored schemes before stating that duplicate detection works. - -## Verify reported fixes against the running development app -- When a user asks for before-and-after verification, test the unchanged and fixed production code against the same Jean Run environment. -- Cover each requested interface, such as UI and API, and record the exact URL, response, persisted state, and relevant logs. - -## Do not infer that “Pro” means paid -- When the user calls a setting “Pro,” confirm whether it means advanced-user functionality or a subscription entitlement. -- Do not add billing or Cloud-only checks unless the user explicitly requests them. - -## Verify compound status layouts visually -- When a status component can render more than one badge, give its root an explicit horizontal flex layout. -- Inspect the real top-bar layout with every conditional badge visible before calling a UI change complete. - -## Keep a requested security control at its stated scope -- If the user specifies one team-level redaction flag, do not introduce per-secret policy questions. -- Explain storage constraints as implementation details, then preserve the requested single control. - -## Use shared section title helpers in edit modals -- When modal section descriptions should appear on hover, use `x-application.settings-section` instead of a manual heading and visible paragraph. -- Keep text labels for direct actions such as Back up now. Use a standard icon button with a tooltip for familiar secondary actions such as settings. - - -## Keep modal actions in the footer -- When a modal has a large editable body, put preview, validation, and save controls in a fixed footer. Keep the title bar for the title and close action. - -## Prefer named API values over numeric sentinels -- When an API option means an unbounded or special mode, expose a clear named value such as `all`. -- Keep an existing numeric sentinel such as `-1` only as a compatibility alias unless the user requests a breaking change. - -## Do not auto-heal existing deployments without a request -- When a parser or label fix can apply only after container recreation, keep the change limited to new deployments and later user-initiated redeployments unless the user explicitly asks for live reconciliation. -- Do not add status lookup fallbacks that alter existing deployment behavior when the requested scope is new deployments only. - -## Trace image replacements through build stages -- When replacing a container image for development, inspect both Compose services and every development Dockerfile `FROM` stage. -- A successful Compose pull does not prove the application build is free of the old image; validate the complete build dependency chain. -- Do not replace a removed image with a floating `latest` tag. Find the newest stable release tag and pin it consistently in Compose and every Dockerfile stage. +## Trace infrastructure changes end to end +- For container image changes, inspect Compose services and every relevant Dockerfile build stage. +- Pin a stable release tag instead of using a floating `latest` tag. +- A successful image pull does not prove that the complete application build no longer uses the old image. From 065ca9c861511ae20ba3b467667cd28db5a60a79 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:50:16 +0200 Subject: [PATCH 05/11] fix(helper): prefer newer fetched helper version Use a CDN-fetched helper version when it is newer than the bundled version, while ignoring stale stored versions. --- bootstrap/helpers/shared.php | 10 +++++++- tests/Feature/HelperVersionTest.php | 38 +++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 tests/Feature/HelperVersionTest.php diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index d1f5e4016b..f40c51a335 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -4234,6 +4234,8 @@ function coolifyHelperImage(): string function getHelperVersion(): string { + $configuredHelperVersion = config('constants.coolify.helper_version'); + if (isDev()) { $devHelperVersion = InstanceSettings::query()->whereKey(0)->value('dev_helper_version'); @@ -4242,7 +4244,13 @@ function getHelperVersion(): string } } - return config('constants.coolify.helper_version'); + $fetchedHelperVersion = InstanceSettings::query()->whereKey(0)->value('helper_version'); + + if (! empty($fetchedHelperVersion) && version_compare($fetchedHelperVersion, $configuredHelperVersion, '>')) { + return $fetchedHelperVersion; + } + + return $configuredHelperVersion; } function loggy($message = null, array $context = []) diff --git a/tests/Feature/HelperVersionTest.php b/tests/Feature/HelperVersionTest.php new file mode 100644 index 0000000000..64ef8466a7 --- /dev/null +++ b/tests/Feature/HelperVersionTest.php @@ -0,0 +1,38 @@ + 0]); + + config([ + 'app.env' => 'production', + 'constants.coolify.helper_version' => '1.0.17', + 'constants.coolify.versions_url' => 'https://cdn.example.com/coolify/versions.json', + ]); +}); + +it('uses a newer helper version after fetching it from the CDN', function () { + Http::preventStrayRequests(); + Http::fake([ + 'https://cdn.example.com/coolify/versions.json' => Http::response([ + 'coolify' => ['helper' => ['version' => '1.0.18']], + ]), + ]); + + (new CheckHelperImageJob)->handle(); + + expect(InstanceSettings::findOrFail(0)->helper_version)->toBe('1.0.18') + ->and(getHelperVersion())->toBe('1.0.18'); +}); + +it('does not use a stored helper version older than the bundled version', function () { + InstanceSettings::findOrFail(0)->update(['helper_version' => '1.0.16']); + + expect(getHelperVersion())->toBe('1.0.17'); +}); From 596cb3984988d55cea5b430d967d8d737bd03f95 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 15 Sep 2026 10:14:03 +0200 Subject: [PATCH 06/11] chore(traefik): update supported image versions --- other/nightly/versions.json | 6 +++--- versions.json | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/other/nightly/versions.json b/other/nightly/versions.json index 6818286b29..8ae9763c72 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -17,14 +17,14 @@ } }, "traefik": { - "v3.7": "3.7.8", - "v3.6": "3.6.23", + "v3.7": "3.7.13", + "v3.6": "3.6.25", "v3.5": "3.5.6", "v3.4": "3.4.5", "v3.3": "3.3.7", "v3.2": "3.2.5", "v3.1": "3.1.7", "v3.0": "3.0.4", - "v2.11": "2.11.52" + "v2.11": "2.11.57" } } diff --git a/versions.json b/versions.json index 6818286b29..8ae9763c72 100644 --- a/versions.json +++ b/versions.json @@ -17,14 +17,14 @@ } }, "traefik": { - "v3.7": "3.7.8", - "v3.6": "3.6.23", + "v3.7": "3.7.13", + "v3.6": "3.6.25", "v3.5": "3.5.6", "v3.4": "3.4.5", "v3.3": "3.3.7", "v3.2": "3.2.5", "v3.1": "3.1.7", "v3.0": "3.0.4", - "v2.11": "2.11.52" + "v2.11": "2.11.57" } } From 776c56f9471c5d6ed46c5aa13f4d7c7f195edd62 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 15 Sep 2026 11:38:33 +0200 Subject: [PATCH 07/11] fix(helper): hide deployment details unless explicitly requested --- bootstrap/helpers/shared.php | 98 +++++++++++---------- tests/Unit/RunningDeploymentsOutputTest.php | 38 ++++++++ 2 files changed, 91 insertions(+), 45 deletions(-) create mode 100644 tests/Unit/RunningDeploymentsOutputTest.php diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index f40c51a335..a7a606330e 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -2400,7 +2400,7 @@ function get_public_ips() } } -function isAnyDeploymentInprogress() +function isAnyDeploymentInprogress(bool $showAll = false) { $runningJobs = ApplicationDeploymentQueue::where('horizon_job_worker', gethostname())->where('status', ApplicationDeploymentStatus::IN_PROGRESS->value)->get(); @@ -2417,34 +2417,31 @@ function isAnyDeploymentInprogress() if ($horizonJobStatus === 'unknown' || $horizonJobStatus === 'reserved') { $horizonJobIds[] = $runningJob->horizon_job_id; - // Get application and team information - $application = Application::find($runningJob->application_id); - $teamMembers = []; - $deploymentUrl = ''; + if ($showAll) { + $application = Application::find($runningJob->application_id); + $teamMembers = []; + $deploymentUrl = ''; - if ($application) { - // Get team members through the application's project - $team = $application->team(); - if ($team) { - $teamMembers = $team->members()->pluck('email')->toArray(); + if ($application) { + $team = $application->team(); + if ($team) { + $teamMembers = $team->members()->pluck('email')->toArray(); + } + + if ($runningJob->deployment_url) { + $deploymentUrl = base_url().$runningJob->deployment_url; + } } - // Construct the full deployment URL - if ($runningJob->deployment_url) { - $baseUrl = base_url(); - $deploymentUrl = $baseUrl.$runningJob->deployment_url; - } + $deploymentDetails[] = [ + 'application_name' => $runningJob->application_name ?? 'Unknown', + 'server_name' => $runningJob->server_name ?? 'Unknown', + 'deployment_url' => $deploymentUrl, + 'team_members' => $teamMembers, + 'created_at' => $runningJob->created_at->format('Y-m-d H:i:s'), + 'horizon_job_id' => $runningJob->horizon_job_id, + ]; } - - $deploymentDetails[] = [ - 'id' => $runningJob->id, - 'application_name' => $runningJob->application_name ?? 'Unknown', - 'server_name' => $runningJob->server_name ?? 'Unknown', - 'deployment_url' => $deploymentUrl, - 'team_members' => $teamMembers, - 'created_at' => $runningJob->created_at->format('Y-m-d H:i:s'), - 'horizon_job_id' => $runningJob->horizon_job_id, - ]; } } @@ -2453,30 +2450,41 @@ function isAnyDeploymentInprogress() exit(0); } - // Display enhanced deployment information - echo "\n=== Running Deployments ===\n"; - echo 'Total active deployments: '.count($horizonJobIds)."\n\n"; - - foreach ($deploymentDetails as $index => $deployment) { - echo 'Deployment #'.($index + 1).":\n"; - echo ' Application: '.$deployment['application_name']."\n"; - echo ' Server: '.$deployment['server_name']."\n"; - echo ' Started: '.$deployment['created_at']."\n"; - if ($deployment['deployment_url']) { - echo ' URL: '.$deployment['deployment_url']."\n"; - } - if (! empty($deployment['team_members'])) { - echo ' Team members: '.implode(', ', $deployment['team_members'])."\n"; - } else { - echo " Team members: No team members found\n"; - } - echo ' Horizon Job ID: '.$deployment['horizon_job_id']."\n"; - echo "\n"; - } + echo formatRunningDeploymentsOutput(count($horizonJobIds), $deploymentDetails, $showAll); exit(1); } +function formatRunningDeploymentsOutput(int $activeDeploymentCount, array $deploymentDetails = [], bool $showAll = false): string +{ + $output = "\n=== Running Deployments ===\n"; + $output .= 'Total active deployments: '.$activeDeploymentCount."\n"; + + if (! $showAll) { + return $output; + } + + $output .= "\n"; + + foreach ($deploymentDetails as $index => $deployment) { + $output .= 'Deployment #'.($index + 1).":\n"; + $output .= ' Application: '.$deployment['application_name']."\n"; + $output .= ' Server: '.$deployment['server_name']."\n"; + $output .= ' Started: '.$deployment['created_at']."\n"; + if ($deployment['deployment_url']) { + $output .= ' URL: '.$deployment['deployment_url']."\n"; + } + if (! empty($deployment['team_members'])) { + $output .= ' Team members: '.implode(', ', $deployment['team_members'])."\n"; + } else { + $output .= " Team members: No team members found\n"; + } + $output .= ' Horizon Job ID: '.$deployment['horizon_job_id']."\n\n"; + } + + return $output; +} + function isBase64Encoded($strValue) { return base64_encode(base64_decode($strValue, true)) === $strValue; diff --git a/tests/Unit/RunningDeploymentsOutputTest.php b/tests/Unit/RunningDeploymentsOutputTest.php new file mode 100644 index 0000000000..62d367514c --- /dev/null +++ b/tests/Unit/RunningDeploymentsOutputTest.php @@ -0,0 +1,38 @@ + 'secret-application', + 'server_name' => 'secret-server', + 'deployment_url' => 'https://example.com/secret-deployment', + 'team_members' => ['member@example.com'], + 'created_at' => '2026-09-15 08:52:42', + 'horizon_job_id' => 'secret-job-id', + ]]); + + expect($output) + ->toBe("\n=== Running Deployments ===\nTotal active deployments: 1\n") + ->not->toContain('secret-application') + ->not->toContain('https://example.com/secret-deployment') + ->not->toContain('member@example.com'); +}); + +it('shows all deployment details when requested', function () { + $output = formatRunningDeploymentsOutput(1, [[ + 'application_name' => 'example-application', + 'server_name' => 'example-server', + 'deployment_url' => 'https://example.com/deployment', + 'team_members' => ['member@example.com'], + 'created_at' => '2026-09-15 08:52:42', + 'horizon_job_id' => 'example-job-id', + ]], true); + + expect($output) + ->toContain('Deployment #1:') + ->toContain('Application: example-application') + ->toContain('Server: example-server') + ->toContain('Started: 2026-09-15 08:52:42') + ->toContain('URL: https://example.com/deployment') + ->toContain('Team members: member@example.com') + ->toContain('Horizon Job ID: example-job-id'); +}); From 8ab54da2ac56c5cf69c547549f501d3a5a655eda Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 15 Sep 2026 12:28:16 +0200 Subject: [PATCH 08/11] style(ui): remove description placeholders and normalize control heights --- resources/css/utilities.css | 2 +- resources/views/livewire/dashboard.blade.php | 4 ++-- resources/views/livewire/project/index.blade.php | 2 +- resources/views/livewire/server/index.blade.php | 2 +- .../livewire/server/private-key/show.blade.php | 2 +- .../shared-variables/environment/index.blade.php | 4 ++-- .../shared-variables/project/index.blade.php | 4 ++-- tests/Feature/FormControlHeightTest.php | 9 +++++---- tests/Unit/DescriptionPlaceholderViewTest.php | 16 ++++++++++++++++ 9 files changed, 31 insertions(+), 14 deletions(-) create mode 100644 tests/Unit/DescriptionPlaceholderViewTest.php diff --git a/resources/css/utilities.css b/resources/css/utilities.css index 9851180c2b..67d898da3a 100644 --- a/resources/css/utilities.css +++ b/resources/css/utilities.css @@ -59,7 +59,7 @@ /* input, select before */ @utility input-select { - @apply block h-9 px-3 py-1.5 w-full text-sm text-black rounded-md border border-neutral-200 bg-[var(--coollabs-recessed)] dark:text-fg dark:border-white/[0.08] transition-colors disabled:bg-neutral-100 disabled:text-neutral-400 dark:disabled:bg-white/[0.03] dark:disabled:text-fg-faint; + @apply block h-8 px-3 py-1.5 w-full text-sm text-black rounded-md border border-neutral-200 bg-[var(--coollabs-recessed)] dark:text-fg dark:border-white/[0.08] transition-colors disabled:bg-neutral-100 disabled:text-neutral-400 dark:disabled:bg-white/[0.03] dark:disabled:text-fg-faint; box-shadow: none; &:where(.dark, .dark *) { diff --git a/resources/views/livewire/dashboard.blade.php b/resources/views/livewire/dashboard.blade.php index f4bf96fbb7..d9eb201c5d 100644 --- a/resources/views/livewire/dashboard.blade.php +++ b/resources/views/livewire/dashboard.blade.php @@ -66,7 +66,7 @@ {{ $project->name }}
- {{ $project->description ?: 'No description' }} + {{ $project->description }}
@@ -181,7 +181,7 @@ {{ $server->name }}- {{ $server->description ?: 'No description' }} + {{ $server->description }}
@if ($serverStatusType !== 'success') diff --git a/resources/views/livewire/project/index.blade.php b/resources/views/livewire/project/index.blade.php index 4cd0e22d19..108b4024ac 100644 --- a/resources/views/livewire/project/index.blade.php +++ b/resources/views/livewire/project/index.blade.php @@ -121,7 +121,7 @@ class="truncate text-[13px]! leading-4! font-semibold! text-black dark:text-fg" x-text="project.name"> + x-text="project.description || ''"> diff --git a/resources/views/livewire/server/index.blade.php b/resources/views/livewire/server/index.blade.php index 9f5680160e..52a0c3a990 100644 --- a/resources/views/livewire/server/index.blade.php +++ b/resources/views/livewire/server/index.blade.php @@ -55,7 +55,7 @@ return [ 'uuid' => $server->uuid, 'name' => $server->name, - 'description' => $server->description ?: 'No description', + 'description' => $server->description, 'href' => route('server.show', ['server_uuid' => $server->uuid]), 'status' => $status, 'statusType' => $statusType, diff --git a/resources/views/livewire/server/private-key/show.blade.php b/resources/views/livewire/server/private-key/show.blade.php index bf938a888d..0663dd1168 100644 --- a/resources/views/livewire/server/private-key/show.blade.php +++ b/resources/views/livewire/server/private-key/show.blade.php @@ -78,7 +78,7 @@ @endif- {{ $privateKey->description ?: 'No description provided.' }} + {{ $privateKey->description }}
diff --git a/resources/views/livewire/shared-variables/environment/index.blade.php b/resources/views/livewire/shared-variables/environment/index.blade.php index 15c6dd461d..645f2e61be 100644 --- a/resources/views/livewire/shared-variables/environment/index.blade.php +++ b/resources/views/livewire/shared-variables/environment/index.blade.php @@ -33,7 +33,7 @@ href="{{ route('shared-variables.environment.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid]) }}" {{ wireNavigate() }}>{{ $environment->description ?: 'No description' }}
{{ $environment->description }}
{{ $project->description ?: 'No description' }}
+{{ $project->description }}