From 9d64b30612af2d80bf32ff46a09d9d9bdb5f3b64 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:26:57 +0200 Subject: [PATCH] fix(ui): improve listbox hover state and Traefik redirects Add an opaque hover background for listbox triggers and preserve single-escaped Traefik redirect capture groups when generating application labels. --- bootstrap/helpers/docker.php | 11 ++++++++--- resources/css/app.css | 4 ++++ tests/Feature/ListboxTriggerTruncationTest.php | 7 +++++++ tests/Unit/TraefikServiceNameSegmentTest.php | 12 ++++++++++++ 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 3be2ae008..d4466d389 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -563,7 +563,7 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, return $labels->sort(); } -function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, bool $generate_unique_uuid = false, ?string $image = null, string $redirect_direction = 'both', bool $is_http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null, ?Collection $noindex_domains = null) +function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, bool $generate_unique_uuid = false, ?string $image = null, string $redirect_direction = 'both', bool $is_http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null, ?Collection $noindex_domains = null, bool $escape_redirect_replacement_for_compose = true) { $labels = collect([]); $labels->push('traefik.enable=true'); @@ -646,14 +646,15 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $to_www_name = "{$loop}-{$uuid}-to-www"; $to_non_www_name = "{$loop}-{$uuid}-to-non-www"; + $redirect_capture_prefix = $escape_redirect_replacement_for_compose ? '$$' : '$'; $redirect_to_non_www = [ "traefik.http.middlewares.{$to_non_www_name}.redirectregex.regex=^(http|https)://www\.(.+)", - "traefik.http.middlewares.{$to_non_www_name}.redirectregex.replacement=\$\${1}://\$\${2}", + "traefik.http.middlewares.{$to_non_www_name}.redirectregex.replacement={$redirect_capture_prefix}{1}://{$redirect_capture_prefix}{2}", "traefik.http.middlewares.{$to_non_www_name}.redirectregex.permanent=false", ]; $redirect_to_www = [ "traefik.http.middlewares.{$to_www_name}.redirectregex.regex=^(http|https)://(?:www\.)?(.+)", - "traefik.http.middlewares.{$to_www_name}.redirectregex.replacement=\$\${1}://www.\$\${2}", + "traefik.http.middlewares.{$to_www_name}.redirectregex.replacement={$redirect_capture_prefix}{1}://www.{$redirect_capture_prefix}{2}", "traefik.http.middlewares.{$to_www_name}.redirectregex.permanent=false", ]; if ($schema === 'https') { @@ -876,6 +877,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview http_basic_auth_username: $application->http_basic_auth_username, http_basic_auth_password: $application->http_basic_auth_password, noindex_domains: $noindexDomains, + escape_redirect_replacement_for_compose: false, )); break; } @@ -892,6 +894,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview http_basic_auth_username: $application->http_basic_auth_username, http_basic_auth_password: $application->http_basic_auth_password, noindex_domains: $noindexDomains, + escape_redirect_replacement_for_compose: false, )); $labels = $labels->merge(fqdnLabelsForCaddy( network: $application->destination->network, @@ -932,6 +935,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview http_basic_auth_username: $application->http_basic_auth_username, http_basic_auth_password: $application->http_basic_auth_password, noindex_domains: $noindexDomains, + escape_redirect_replacement_for_compose: false, )); break; case ProxyTypes::CADDY->value: @@ -962,6 +966,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview http_basic_auth_username: $application->http_basic_auth_username, http_basic_auth_password: $application->http_basic_auth_password, noindex_domains: $noindexDomains, + escape_redirect_replacement_for_compose: false, )); $labels = $labels->merge(fqdnLabelsForCaddy( network: $application->destination->network, diff --git a/resources/css/app.css b/resources/css/app.css index 95e0207ce..566f29031 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -1903,6 +1903,10 @@ html[data-theme="custom"] textarea:disabled { color: var(--color-fg); } +.listbox-trigger:hover { + background: var(--coollabs-fill); +} + .listbox-trigger:disabled { cursor: not-allowed; opacity: 0.5; diff --git a/tests/Feature/ListboxTriggerTruncationTest.php b/tests/Feature/ListboxTriggerTruncationTest.php index a64a97dd8..a35ff8cbe 100644 --- a/tests/Feature/ListboxTriggerTruncationTest.php +++ b/tests/Feature/ListboxTriggerTruncationTest.php @@ -22,6 +22,13 @@ test('listbox trigger height matches shared inputs', function () { ->toMatch('/\.application-settings-workspace \.listbox-trigger[^}]*height: 2rem;/s'); }); +test('listbox trigger uses an opaque background on hover', function () { + $css = file_get_contents(resource_path('css/app.css')); + + expect($css) + ->toMatch('/\.listbox-trigger:hover \{[^}]*background: var\(--coollabs-fill\);/s'); +}); + test('listbox component uses shared trigger label truncation', function () { $html = Blade::render(<<<'BLADE' toContain("https-0-{$uuid}-another-service-{$hash}.rule="); }); + +test('application labels keep redirect capture groups single escaped before compose generation', function () { + $labels = fqdnLabelsForTraefik( + uuid: 'application-uuid', + domains: collect(['https://example.com']), + redirect_direction: 'www', + escape_redirect_replacement_for_compose: false, + ); + + expect($labels) + ->toContain('traefik.http.middlewares.0-application-uuid-to-www.redirectregex.replacement=${1}://www.${2}'); +});