From c1c234da5fb8d2889ac266ab6f608a886e3bb51f Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 13 Nov 2025 14:39:55 +0100 Subject: [PATCH 01/11] fix(server): wrap complex piped commands in bash -c for sudo execution Fixes Docker installation failures on non-root servers by properly handling complex shell commands with pipes and operators. Previously, the sudo parser would insert sudo throughout command chains, breaking pipe structures like 'curl URL | sh || curl URL2 | sh'. The fix detects complex piped commands (containing '| sh', '| bash', or pipes combined with && or || operators) and wraps them in 'sudo bash -c' instead of inserting sudo mid-command. This preserves the command structure and prevents syntax errors. Changes: - Detect complex piped commands in parseCommandsByLineForSudo - Wrap complex commands in 'sudo bash -c' with proper quote escaping - Preserve original behavior for simple commands - Add 27 comprehensive unit tests covering all scenarios Fixes #7116 --- bootstrap/helpers/sudo.php | 25 +- tests/Unit/ParseCommandsByLineForSudoTest.php | 310 ++++++++++++++++++ 2 files changed, 332 insertions(+), 3 deletions(-) create mode 100644 tests/Unit/ParseCommandsByLineForSudoTest.php diff --git a/bootstrap/helpers/sudo.php b/bootstrap/helpers/sudo.php index ba252c64f..f7336beeb 100644 --- a/bootstrap/helpers/sudo.php +++ b/bootstrap/helpers/sudo.php @@ -58,16 +58,35 @@ function parseCommandsByLineForSudo(Collection $commands, Server $server): array $commands = $commands->map(function ($line) { $line = str($line); + + // Detect complex piped commands that should be wrapped in bash -c + $isComplexPipeCommand = ( + $line->contains(' | sh') || + $line->contains(' | bash') || + ($line->contains(' | ') && ($line->contains('||') || $line->contains('&&'))) + ); + + // If it's a complex pipe command and starts with sudo, wrap it in bash -c + if ($isComplexPipeCommand && $line->startsWith('sudo ')) { + $commandWithoutSudo = $line->after('sudo ')->value(); + // Escape single quotes for bash -c by replacing ' with '\'' + $escapedCommand = str_replace("'", "'\\''", $commandWithoutSudo); + + return "sudo bash -c '$escapedCommand'"; + } + + // For non-complex commands, apply the original logic if (str($line)->contains('$(')) { $line = $line->replace('$(', '$(sudo '); } - if (str($line)->contains('||')) { + if (! $isComplexPipeCommand && str($line)->contains('||')) { $line = $line->replace('||', '|| sudo'); } - if (str($line)->contains('&&')) { + if (! $isComplexPipeCommand && str($line)->contains('&&')) { $line = $line->replace('&&', '&& sudo'); } - if (str($line)->contains(' | ')) { + // Don't insert sudo into pipes for complex commands + if (! $isComplexPipeCommand && str($line)->contains(' | ')) { $line = $line->replace(' | ', ' | sudo '); } diff --git a/tests/Unit/ParseCommandsByLineForSudoTest.php b/tests/Unit/ParseCommandsByLineForSudoTest.php new file mode 100644 index 000000000..0f9fda83c --- /dev/null +++ b/tests/Unit/ParseCommandsByLineForSudoTest.php @@ -0,0 +1,310 @@ +server = Mockery::mock(Server::class)->makePartial(); + $this->server->shouldReceive('getAttribute')->with('user')->andReturn('ubuntu'); + $this->server->shouldReceive('setAttribute')->andReturnSelf(); + $this->server->user = 'ubuntu'; +}); + +afterEach(function () { + Mockery::close(); +}); + +test('wraps complex Docker install command with pipes in bash -c', function () { + $commands = collect([ + 'curl https://releases.rancher.com/install-docker/27.3.sh | sh || curl https://get.docker.com | sh', + ]); + + $result = parseCommandsByLineForSudo($commands, $this->server); + + expect($result[0])->toBe("sudo bash -c 'curl https://releases.rancher.com/install-docker/27.3.sh | sh || curl https://get.docker.com | sh'"); +}); + +test('wraps complex Docker install command with multiple fallbacks', function () { + $commands = collect([ + 'curl --max-time 300 https://releases.rancher.com/install-docker/27.3.sh | sh || curl https://get.docker.com | sh -s -- --version 27.3', + ]); + + $result = parseCommandsByLineForSudo($commands, $this->server); + + expect($result[0])->toBe("sudo bash -c 'curl --max-time 300 https://releases.rancher.com/install-docker/27.3.sh | sh || curl https://get.docker.com | sh -s -- --version 27.3'"); +}); + +test('wraps command with pipe to bash in bash -c', function () { + $commands = collect([ + 'curl https://example.com/script.sh | bash', + ]); + + $result = parseCommandsByLineForSudo($commands, $this->server); + + expect($result[0])->toBe("sudo bash -c 'curl https://example.com/script.sh | bash'"); +}); + +test('wraps complex command with pipes and && operators', function () { + $commands = collect([ + 'curl https://example.com | sh && echo "done"', + ]); + + $result = parseCommandsByLineForSudo($commands, $this->server); + + expect($result[0])->toBe("sudo bash -c 'curl https://example.com | sh && echo \"done\"'"); +}); + +test('escapes single quotes in complex piped commands', function () { + $commands = collect([ + "curl https://example.com | sh -c 'echo \"test\"'", + ]); + + $result = parseCommandsByLineForSudo($commands, $this->server); + + expect($result[0])->toBe("sudo bash -c 'curl https://example.com | sh -c '\\''echo \"test\"'\\'''"); +}); + +test('handles simple command without pipes or operators', function () { + $commands = collect([ + 'apt-get update', + ]); + + $result = parseCommandsByLineForSudo($commands, $this->server); + + expect($result[0])->toBe('sudo apt-get update'); +}); + +test('handles command with double ampersand operator but no pipes', function () { + $commands = collect([ + 'mkdir -p /foo && chown ubuntu /foo', + ]); + + $result = parseCommandsByLineForSudo($commands, $this->server); + + expect($result[0])->toBe('sudo mkdir -p /foo && sudo chown ubuntu /foo'); +}); + +test('handles command with double pipe operator but no pipes', function () { + $commands = collect([ + 'command -v docker || echo "not found"', + ]); + + $result = parseCommandsByLineForSudo($commands, $this->server); + + // 'command' is exempted from sudo, but echo gets sudo after || + expect($result[0])->toBe('command -v docker || sudo echo "not found"'); +}); + +test('handles command with simple pipe but no operators', function () { + $commands = collect([ + 'cat file | grep pattern', + ]); + + $result = parseCommandsByLineForSudo($commands, $this->server); + + expect($result[0])->toBe('sudo cat file | sudo grep pattern'); +}); + +test('handles command with subshell $(...)', function () { + $commands = collect([ + 'echo $(whoami)', + ]); + + $result = parseCommandsByLineForSudo($commands, $this->server); + + // 'echo' is exempted from sudo at the start + expect($result[0])->toBe('echo $(sudo whoami)'); +}); + +test('skips sudo for cd commands', function () { + $commands = collect([ + 'cd /var/www', + ]); + + $result = parseCommandsByLineForSudo($commands, $this->server); + + expect($result[0])->toBe('cd /var/www'); +}); + +test('skips sudo for echo commands', function () { + $commands = collect([ + 'echo "test"', + ]); + + $result = parseCommandsByLineForSudo($commands, $this->server); + + expect($result[0])->toBe('echo "test"'); +}); + +test('skips sudo for command commands', function () { + $commands = collect([ + 'command -v docker', + ]); + + $result = parseCommandsByLineForSudo($commands, $this->server); + + expect($result[0])->toBe('command -v docker'); +}); + +test('skips sudo for true commands', function () { + $commands = collect([ + 'true', + ]); + + $result = parseCommandsByLineForSudo($commands, $this->server); + + expect($result[0])->toBe('true'); +}); + +test('handles if statements by adding sudo to condition', function () { + $commands = collect([ + 'if command -v docker', + ]); + + $result = parseCommandsByLineForSudo($commands, $this->server); + + expect($result[0])->toBe('if sudo command -v docker'); +}); + +test('skips sudo for fi statements', function () { + $commands = collect([ + 'fi', + ]); + + $result = parseCommandsByLineForSudo($commands, $this->server); + + expect($result[0])->toBe('fi'); +}); + +test('adds ownership changes for Coolify data paths', function () { + $commands = collect([ + 'mkdir -p /data/coolify/logs', + ]); + + $result = parseCommandsByLineForSudo($commands, $this->server); + + // Note: The && operator adds another sudo, creating double sudo for chown/chmod + // This is existing behavior that may need refactoring but isn't part of this bug fix + expect($result[0])->toBe('sudo mkdir -p /data/coolify/logs && sudo sudo chown -R ubuntu:ubuntu /data/coolify/logs && sudo sudo chmod -R o-rwx /data/coolify/logs'); +}); + +test('adds ownership changes for Coolify tmp paths', function () { + $commands = collect([ + 'mkdir -p /tmp/coolify/cache', + ]); + + $result = parseCommandsByLineForSudo($commands, $this->server); + + // Note: The && operator adds another sudo, creating double sudo for chown/chmod + // This is existing behavior that may need refactoring but isn't part of this bug fix + expect($result[0])->toBe('sudo mkdir -p /tmp/coolify/cache && sudo sudo chown -R ubuntu:ubuntu /tmp/coolify/cache && sudo sudo chmod -R o-rwx /tmp/coolify/cache'); +}); + +test('does not add ownership changes for system paths', function () { + $commands = collect([ + 'mkdir -p /var/log', + ]); + + $result = parseCommandsByLineForSudo($commands, $this->server); + + expect($result[0])->toBe('sudo mkdir -p /var/log'); +}); + +test('handles multiple commands in sequence', function () { + $commands = collect([ + 'apt-get update', + 'apt-get install -y docker', + 'curl https://get.docker.com | sh', + ]); + + $result = parseCommandsByLineForSudo($commands, $this->server); + + expect($result)->toHaveCount(3); + expect($result[0])->toBe('sudo apt-get update'); + expect($result[1])->toBe('sudo apt-get install -y docker'); + expect($result[2])->toBe("sudo bash -c 'curl https://get.docker.com | sh'"); +}); + +test('handles empty command list', function () { + $commands = collect([]); + + $result = parseCommandsByLineForSudo($commands, $this->server); + + expect($result)->toBeArray(); + expect($result)->toHaveCount(0); +}); + +test('handles real-world Docker installation command from InstallDocker action', function () { + $version = '27.3'; + $commands = collect([ + "curl --max-time 300 --retry 3 https://releases.rancher.com/install-docker/{$version}.sh | sh || curl --max-time 300 --retry 3 https://get.docker.com | sh -s -- --version {$version}", + ]); + + $result = parseCommandsByLineForSudo($commands, $this->server); + + expect($result[0])->toStartWith("sudo bash -c '"); + expect($result[0])->toEndWith("'"); + expect($result[0])->toContain('curl --max-time 300'); + expect($result[0])->toContain('| sh'); + expect($result[0])->toContain('||'); + expect($result[0])->not->toContain('| sudo sh'); +}); + +test('preserves command structure in wrapped bash -c', function () { + $commands = collect([ + 'curl https://example.com | sh || curl https://backup.com | sh', + ]); + + $result = parseCommandsByLineForSudo($commands, $this->server); + + // The command should be wrapped without breaking the pipe and fallback structure + expect($result[0])->toBe("sudo bash -c 'curl https://example.com | sh || curl https://backup.com | sh'"); + + // Verify it doesn't contain broken patterns like "| sudo sh" + expect($result[0])->not->toContain('| sudo sh'); + expect($result[0])->not->toContain('|| sudo curl'); +}); + +test('handles command with mixed operators and subshells', function () { + $commands = collect([ + 'docker ps || echo $(date)', + ]); + + $result = parseCommandsByLineForSudo($commands, $this->server); + + // This should use the original logic since it's not a complex pipe command + expect($result[0])->toBe('sudo docker ps || sudo echo $(sudo date)'); +}); + +test('handles whitespace-only commands gracefully', function () { + $commands = collect([ + ' ', + '', + ]); + + $result = parseCommandsByLineForSudo($commands, $this->server); + + expect($result)->toHaveCount(2); +}); + +test('detects pipe to sh with additional arguments', function () { + $commands = collect([ + 'curl https://example.com | sh -s -- --arg1 --arg2', + ]); + + $result = parseCommandsByLineForSudo($commands, $this->server); + + expect($result[0])->toBe("sudo bash -c 'curl https://example.com | sh -s -- --arg1 --arg2'"); +}); + +test('handles command chains with both && and || operators with pipes', function () { + $commands = collect([ + 'curl https://first.com | sh && echo "success" || curl https://backup.com | sh', + ]); + + $result = parseCommandsByLineForSudo($commands, $this->server); + + expect($result[0])->toStartWith("sudo bash -c '"); + expect($result[0])->toEndWith("'"); + expect($result[0])->not->toContain('| sudo'); +}); From 9656855cefdeacc61d75fe184991b794dfe8566c Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 13 Nov 2025 14:51:47 +0100 Subject: [PATCH 02/11] fix(proxy): downgrade Traefik image version from v3.6 to v3.5 in default proxy configuration --- bootstrap/helpers/proxy.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php index b8274a3b3..b8332cba2 100644 --- a/bootstrap/helpers/proxy.php +++ b/bootstrap/helpers/proxy.php @@ -212,7 +212,7 @@ function generateDefaultProxyConfiguration(Server $server, array $custom_command 'services' => [ 'traefik' => [ 'container_name' => 'coolify-proxy', - 'image' => 'traefik:v3.6', + 'image' => 'traefik:v3.5', 'restart' => RESTART_MODE, 'extra_hosts' => [ 'host.docker.internal:host-gateway', From 0df2620744fa148ed97853a0b94161f802cbe8cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 Nov 2025 14:37:48 +0000 Subject: [PATCH 03/11] chore(deps): bump symfony/http-foundation from 7.3.2 to 7.3.7 Bumps [symfony/http-foundation](https://github.com/symfony/http-foundation) from 7.3.2 to 7.3.7. - [Release notes](https://github.com/symfony/http-foundation/releases) - [Changelog](https://github.com/symfony/http-foundation/blob/7.3/CHANGELOG.md) - [Commits](https://github.com/symfony/http-foundation/compare/v7.3.2...v7.3.7) --- updated-dependencies: - dependency-name: symfony/http-foundation dependency-version: 7.3.7 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- composer.lock | 72 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/composer.lock b/composer.lock index 5ffeb7d39..b2923a240 100644 --- a/composer.lock +++ b/composer.lock @@ -9514,16 +9514,16 @@ }, { "name": "symfony/http-foundation", - "version": "v7.3.2", + "version": "v7.3.7", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "6877c122b3a6cc3695849622720054f6e6fa5fa6" + "reference": "db488a62f98f7a81d5746f05eea63a74e55bb7c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/6877c122b3a6cc3695849622720054f6e6fa5fa6", - "reference": "6877c122b3a6cc3695849622720054f6e6fa5fa6", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/db488a62f98f7a81d5746f05eea63a74e55bb7c4", + "reference": "db488a62f98f7a81d5746f05eea63a74e55bb7c4", "shasum": "" }, "require": { @@ -9573,7 +9573,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.3.2" + "source": "https://github.com/symfony/http-foundation/tree/v7.3.7" }, "funding": [ { @@ -9593,7 +9593,7 @@ "type": "tidelift" } ], - "time": "2025-07-10T08:47:49+00:00" + "time": "2025-11-08T16:41:12+00:00" }, { "name": "symfony/http-kernel", @@ -9799,16 +9799,16 @@ }, { "name": "symfony/mime", - "version": "v7.3.2", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "e0a0f859148daf1edf6c60b398eb40bfc96697d1" + "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/e0a0f859148daf1edf6c60b398eb40bfc96697d1", - "reference": "e0a0f859148daf1edf6c60b398eb40bfc96697d1", + "url": "https://api.github.com/repos/symfony/mime/zipball/b1b828f69cbaf887fa835a091869e55df91d0e35", + "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35", "shasum": "" }, "require": { @@ -9863,7 +9863,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.3.2" + "source": "https://github.com/symfony/mime/tree/v7.3.4" }, "funding": [ { @@ -9883,7 +9883,7 @@ "type": "tidelift" } ], - "time": "2025-07-15T13:41:35+00:00" + "time": "2025-09-16T08:38:17+00:00" }, { "name": "symfony/options-resolver", @@ -10195,7 +10195,7 @@ }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", @@ -10258,7 +10258,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" }, "funding": [ { @@ -10269,6 +10269,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -10278,7 +10282,7 @@ }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -10339,7 +10343,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" }, "funding": [ { @@ -10350,6 +10354,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -10359,7 +10367,7 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", @@ -10420,7 +10428,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" }, "funding": [ { @@ -10431,6 +10439,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -10440,7 +10452,7 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", @@ -10500,7 +10512,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" }, "funding": [ { @@ -10511,6 +10523,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -10520,16 +10536,16 @@ }, { "name": "symfony/polyfill-php83", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", "shasum": "" }, "require": { @@ -10576,7 +10592,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" }, "funding": [ { @@ -10587,12 +10603,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-07-08T02:45:35+00:00" }, { "name": "symfony/polyfill-uuid", From f731ec74e6ff4b4f94bde993a3d73499149863f9 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 14 Nov 2025 09:31:07 +0100 Subject: [PATCH 04/11] feat(proxy): upgrade Traefik image to v3.6 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upgrade default Traefik proxy configuration from v3.5 to v3.6, with Coolify version bump to beta.444. 🤖 Generated with Claude Code Co-Authored-By: Claude --- bootstrap/helpers/proxy.php | 2 +- config/constants.php | 2 +- versions.json | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php index b8332cba2..b8274a3b3 100644 --- a/bootstrap/helpers/proxy.php +++ b/bootstrap/helpers/proxy.php @@ -212,7 +212,7 @@ function generateDefaultProxyConfiguration(Server $server, array $custom_command 'services' => [ 'traefik' => [ 'container_name' => 'coolify-proxy', - 'image' => 'traefik:v3.5', + 'image' => 'traefik:v3.6', 'restart' => RESTART_MODE, 'extra_hosts' => [ 'host.docker.internal:host-gateway', diff --git a/config/constants.php b/config/constants.php index 770e00ffe..d28f313ee 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.443', + 'version' => '4.0.0-beta.444', 'helper_version' => '1.0.12', 'realtime_version' => '1.0.10', 'self_hosted' => env('SELF_HOSTED', true), diff --git a/versions.json b/versions.json index 0d9519bf8..30f8f5b44 100644 --- a/versions.json +++ b/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.443" + "version": "4.0.0-beta.444" }, "nightly": { - "version": "4.0.0-beta.444" + "version": "4.0.0-beta.445" }, "helper": { "version": "1.0.11" From 4c7d0594694af26d26ea8586525e34ee6b6d35b7 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 14 Nov 2025 09:44:27 +0100 Subject: [PATCH 05/11] Reduce width of Edit Domains modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reduced the minWidth of the Edit Domains modal from 36rem to 24rem to provide a more compact and focused user experience. The modal was previously too wide for the simple domain input form. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../views/livewire/project/service/configuration.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/project/service/configuration.blade.php b/resources/views/livewire/project/service/configuration.blade.php index 452808282..4b7008984 100644 --- a/resources/views/livewire/project/service/configuration.blade.php +++ b/resources/views/livewire/project/service/configuration.blade.php @@ -66,7 +66,7 @@ @if ($application->fqdn) {{ Str::limit($application->fqdn, 60) }} @can('update', $service) - + Date: Fri, 14 Nov 2025 09:46:05 +0100 Subject: [PATCH 06/11] Fix modal width: add maxWidth constraint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous fix reduced minWidth but modal still expanded to fit content. Now: - Added maxWidth prop to modal-input component (default 48rem) - Set Edit Domains modal to minWidth=32rem, maxWidth=40rem - This constrains the modal to a reasonable size range 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- resources/views/components/modal-input.blade.php | 3 ++- .../views/livewire/project/service/configuration.blade.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/resources/views/components/modal-input.blade.php b/resources/views/components/modal-input.blade.php index b031740ca..d451f452d 100644 --- a/resources/views/components/modal-input.blade.php +++ b/resources/views/components/modal-input.blade.php @@ -8,6 +8,7 @@ 'content' => null, 'closeOutside' => true, 'minWidth' => '36rem', + 'maxWidth' => '48rem', 'isFullWidth' => false, ])
x-transition:leave="ease-in duration-100" x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100" x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95" - class="relative w-full border rounded-sm drop-shadow-sm min-w-full lg:min-w-[{{ $minWidth }}] max-w-fit max-h-[calc(100vh-2rem)] bg-white border-neutral-200 dark:bg-base dark:border-coolgray-300 flex flex-col"> + class="relative w-full border rounded-sm drop-shadow-sm min-w-full lg:min-w-[{{ $minWidth }}] lg:max-w-[{{ $maxWidth }}] max-h-[calc(100vh-2rem)] bg-white border-neutral-200 dark:bg-base dark:border-coolgray-300 flex flex-col">

{{ $title }}

x-transition:leave="ease-in duration-100" x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100" x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95" - class="relative w-full border rounded-sm drop-shadow-sm min-w-full lg:min-w-[{{ $minWidth }}] lg:max-w-[{{ $maxWidth }}] max-h-[calc(100vh-2rem)] bg-white border-neutral-200 dark:bg-base dark:border-coolgray-300 flex flex-col"> + style="max-height: calc(100vh - 2rem); @media (min-width: 1024px) { min-width: {{ $minWidth }}; max-width: {{ $maxWidth }}; }" + class="relative w-full border rounded-sm drop-shadow-sm min-w-full bg-white border-neutral-200 dark:bg-base dark:border-coolgray-300 flex flex-col">

{{ $title }}