fix(realtime): force-WS option and polish connection popup UI

Add PUSHER_FORCE_WS so Echo/Pusher can prefer plain WebSocket
transports, redesign the real-time connection warning popup, and
give copy-button inputs durable right padding in settings forms.
This commit is contained in:
Andras Bacsai
2026-08-04 19:36:36 +02:00
parent 07933640f2
commit dbc6f5e08c
12 changed files with 188 additions and 52 deletions
+2
View File
@@ -42,6 +42,8 @@ return [
'host' => env('PUSHER_HOST'),
'port' => env('PUSHER_PORT'),
'app_key' => env('PUSHER_APP_KEY'),
'scheme' => env('PUSHER_SCHEME', 'http'),
'force_ws' => filter_var(env('PUSHER_FORCE_WS', false), FILTER_VALIDATE_BOOLEAN),
],
'migration' => [
+1
View File
@@ -21,6 +21,7 @@ services:
PUSHER_HOST: "${PUSHER_HOST:-}"
PUSHER_PORT: "${PUSHER_PORT:-}"
PUSHER_SCHEME: "${PUSHER_SCHEME:-http}"
PUSHER_FORCE_WS: "${PUSHER_FORCE_WS:-false}"
PUSHER_APP_ID: "${PUSHER_APP_ID:-coolify}"
PUSHER_APP_KEY: "${PUSHER_APP_KEY:-coolify}"
PUSHER_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}"
+1
View File
@@ -29,6 +29,7 @@ services:
PUSHER_HOST: "${PUSHER_HOST:-}"
PUSHER_PORT: "${PUSHER_PORT:-}"
PUSHER_SCHEME: "${PUSHER_SCHEME:-http}"
PUSHER_FORCE_WS: "${PUSHER_FORCE_WS:-false}"
PUSHER_APP_ID: "${PUSHER_APP_ID:-coolify}"
PUSHER_APP_KEY: "${PUSHER_APP_KEY:-coolify}"
PUSHER_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}"
+9 -2
View File
@@ -364,6 +364,11 @@ tr td:first-child {
padding-right: 2.5rem;
}
/* Same durable clearance for copy-affordance inputs (utility pr-* loses to settings CSS). */
.input.input-with-copy-button {
padding-right: 2.5rem;
}
.lds-heart {
animation: lds-heart 1.2s infinite cubic-bezier(0.215, 0.61, 0.355, 1);
}
@@ -1075,11 +1080,13 @@ body.terminal-is-fullscreen .terminal-fullscreen-shell [data-terminal-mobile-too
box-shadow: none !important;
}
/* Keep password toggle clear of the value inside denser settings inputs */
/* Keep password toggle / copy button clear of the value inside denser settings inputs */
.application-settings-workspace .input.input-with-password-toggle,
.application-settings-workspace .input[type="password"],
.application-settings-workspace .input.input-with-copy-button,
.application-settings-form .input.input-with-password-toggle,
.application-settings-form .input[type="password"] {
.application-settings-form .input[type="password"],
.application-settings-form .input.input-with-copy-button {
padding-right: 2.5rem;
}
@@ -90,7 +90,7 @@
<x-reicon name="eye"
x-show="! expandedRows['{{ $changeKey }}']"
class="size-3.5" />
<x-reicon name="eye-off"
<x-reicon name="eye-off2"
x-show="expandedRows['{{ $changeKey }}']"
x-cloak class="size-3.5" />
</button>
@@ -6,21 +6,22 @@
@endif
<div class="relative">
<input type="text" value="{{ $text }}"
class="input pr-11 bg-white dark:bg-coolgray-100 dark:read-only:bg-coolgray-100 dark:read-only:text-white"
class="input input-with-copy-button bg-white dark:bg-coolgray-100 dark:read-only:bg-coolgray-100 dark:read-only:text-white"
readonly
@keydown.prevent @paste.prevent @cut.prevent @drop.prevent
@focus="$event.target.select()">
<button
x-show="isSecure"
type="button"
@click.prevent="copied = true; navigator.clipboard.writeText({{ Js::from($text) }}); setTimeout(() => copied = false, 1000)"
class="absolute right-2 top-1/2 -translate-y-1/2 rounded-sm p-1.5 text-neutral-500 transition-colors hover:text-neutral-700 focus-visible:ring-2 focus-visible:ring-coollabs focus-visible:ring-offset-2 dark:text-neutral-400 dark:hover:text-white dark:focus-visible:ring-warning dark:focus-visible:ring-offset-base"
class="copy-button flex absolute inset-y-0 right-0 z-10 items-center pr-2 cursor-pointer text-neutral-500 transition-colors hover:text-black focus-visible:ring-2 focus-visible:ring-coollabs focus-visible:ring-offset-2 dark:text-neutral-400 dark:hover:text-white dark:focus-visible:ring-warning dark:focus-visible:ring-offset-base"
title="Copy to clipboard"
aria-label="Copy to clipboard">
<svg x-show="!copied" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg x-show="!copied" class="size-[18px]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<svg x-show="copied" class="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg x-show="copied" class="size-[18px] text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</button>
+10 -4
View File
@@ -171,7 +171,15 @@
}
}
@auth
@php
$pusherForceWs = (bool) config('constants.pusher.force_ws');
@endphp
window.Pusher = Pusher;
@if ($pusherForceWs)
if (window.Pusher && window.Pusher.Runtime) {
window.Pusher.Runtime.getProtocol = function () { return 'http:'; };
}
@endif
const EchoConstructor = typeof Echo === 'function' ? Echo : Echo.default;
window.Echo = new EchoConstructor({
broadcaster: 'pusher',
@@ -181,13 +189,11 @@
wsPort: "{{ getRealtime() }}",
wssPort: "{{ getRealtime() }}",
forceTLS: false,
encrypted: true,
encrypted: @json($pusherForceWs ? false : true),
enableStats: false,
enableLogging: true,
enabledTransports: ['ws', 'wss'],
disableStats: true,
// Add auto reconnection settings
enabledTransports: ['ws', 'wss'],
enabledTransports: @json($pusherForceWs ? ['ws'] : ['ws', 'wss']),
disabledTransports: ['sockjs', 'xhr_streaming', 'xhr_polling'],
// Attempt to reconnect on connection lost
autoReconnect: true,
@@ -67,21 +67,51 @@
<span x-show="popups.realtime === true">
@if (!isCloud())
<x-popup>
<x-slot:title>
<span class="font-bold text-left text-red-500">WARNING: </span> Cannot connect to real-time service
</x-slot:title>
<x-slot:description>
<div>This will cause unusual problems on the
UI! <br><br>
Please ensure that you have opened the
<a class="underline" href='https://coolify.io/docs/knowledge-base/server/firewall'
target='_blank'>required ports</a> or get
help on <a class="underline" href='https://coollabs.io/discord' target='_blank'>Discord</a>.
<x-slot:customActions>
<div
class="relative mx-auto flex w-full max-w-2xl flex-col gap-5 overflow-hidden rounded-2xl border border-red-200 bg-white p-5 shadow-modal sm:p-6 dark:border-red-500/20 dark:bg-surface">
<button type="button" aria-label="Dismiss real-time connection warning"
class="absolute top-3 right-3 flex size-7 items-center justify-center rounded-full text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-black dark:text-fg-faint dark:hover:bg-white/[0.07] dark:hover:text-fg"
@click="bannerVisible=false;disableRealtime()">
<x-reicon name="x" class="size-3.5" />
</button>
<div class="flex items-start gap-4 pr-8">
<div
class="hidden size-12 shrink-0 items-center justify-center rounded-xl border border-red-200 bg-red-50 text-red-600 sm:flex dark:border-red-500/20 dark:bg-red-500/10 dark:text-red-400">
<x-reicon name="alert-triangle" class="size-6" />
</div>
<div class="min-w-0">
<h2 class="text-[15px]! leading-5! font-semibold! text-black dark:text-fg">
Cannot connect to real-time service
</h2>
<p class="mt-1 text-[12px] leading-5 text-neutral-500 dark:text-fg-dim">
This will cause unusual problems on the UI. Open the
<a class="font-medium text-coollabs underline decoration-coollabs/30 underline-offset-2 transition-colors hover:text-coollabs-100 dark:text-warning dark:decoration-warning/30 dark:hover:text-warning/90"
href="https://coolify.io/docs/knowledge-base/server/firewall"
target="_blank" rel="noopener noreferrer">required ports</a>
or get help on
<a class="font-medium text-coollabs underline decoration-coollabs/30 underline-offset-2 transition-colors hover:text-coollabs-100 dark:text-warning dark:decoration-warning/30 dark:hover:text-warning/90"
href="https://coollabs.io/discord" target="_blank"
rel="noopener noreferrer">Discord</a>.
</p>
</div>
</div>
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-end">
<a target="_blank" rel="noopener noreferrer"
href="https://coolify.io/docs/knowledge-base/server/firewall"
class="button h-9 justify-center sm:min-w-28">
View docs
</a>
<button type="button"
class="button h-9 justify-center bg-red-600! text-white! ring-1 ring-red-600/25 hover:bg-red-700! sm:min-w-40 dark:bg-red-500! dark:ring-red-500/30 dark:hover:bg-red-400!"
@click="bannerVisible=false;disableRealtime()">
Acknowledge &amp; disable
</button>
</div>
</div>
</x-slot:description>
<x-slot:button-text @click="disableRealtime()">
Acknowledge & Disable This Popup
</x-slot:button-text>
</x-slot:customActions>
</x-popup>
@endif
</span>
@@ -192,28 +222,47 @@
@if (!currentTeam()->isAnyNotificationEnabled())
<span x-show="popups.notification">
<x-popup>
<x-slot:title>
No notifications enabled.
</x-slot:title>
<x-slot:icon>
<svg xmlns="http://www.w3.org/2000/svg" class="text-red-500 stroke-current w-14 h-14 shrink-0"
fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</x-slot:icon>
<x-slot:description>
It is
highly recommended to enable at least
one
notification channel to receive important alerts.<br>Visit <a
href="{{ route('notifications.email') }}" {{ wireNavigate() }} class="underline dark:text-white">/notification</a> to
enable notifications.</span>
</x-slot:description>
<x-slot:button-text @click="disableNotification()">
Accept and Close
</x-slot:button-text>
</x-popup>
<x-slot:customActions>
<div
class="relative mx-auto flex w-full max-w-2xl flex-col gap-5 overflow-hidden rounded-2xl border border-neutral-200 bg-white p-5 shadow-modal sm:p-6 dark:border-white/[0.1] dark:bg-surface">
<button type="button" aria-label="Dismiss notifications reminder"
class="absolute top-3 right-3 flex size-7 items-center justify-center rounded-full text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-black dark:text-fg-faint dark:hover:bg-white/[0.07] dark:hover:text-fg"
@click="bannerVisible=false;disableNotification()">
<x-reicon name="x" class="size-3.5" />
</button>
<div class="flex items-start gap-4 pr-8">
<div
class="hidden size-12 shrink-0 items-center justify-center rounded-xl border border-amber-200 bg-amber-50 text-amber-700 sm:flex dark:border-warning/20 dark:bg-warning/10 dark:text-warning">
<x-reicon name="alert-triangle" class="size-6" />
</div>
<div class="min-w-0">
<h2 class="text-[15px]! leading-5! font-semibold! text-black dark:text-fg">
No notifications enabled
</h2>
<p class="mt-1 text-[12px] leading-5 text-neutral-500 dark:text-fg-dim">
Enable at least one notification channel so you receive important alerts.
Visit
<a href="{{ route('notifications.email') }}" {{ wireNavigate() }}
class="font-medium text-coollabs underline decoration-coollabs/30 underline-offset-2 transition-colors hover:text-coollabs-100 dark:text-warning dark:decoration-warning/30 dark:hover:text-warning/90">notifications</a>
to get started.
</p>
</div>
</div>
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-end">
<a href="{{ route('notifications.email') }}" {{ wireNavigate() }}
class="button h-9 justify-center sm:min-w-28">
Open notifications
</a>
<button type="button" class="button h-9 justify-center sm:min-w-32"
@click="bannerVisible=false;disableNotification()">
Accept and close
</button>
</div>
</div>
</x-slot:customActions>
</x-popup>
</span>
@endif
<script>
+10 -2
View File
@@ -12,10 +12,18 @@
<link rel="icon" href="{{ asset('coolify-logo.svg') }}" type="image/svg+xml" />
@endenv
@auth
@php
$pusherForceWs = (bool) config('constants.pusher.force_ws');
@endphp
<script type="text/javascript" src="{{ URL::asset('js/echo.js') }}"></script>
<script type="text/javascript" src="{{ URL::asset('js/pusher.js') }}"></script>
<script>
window.Pusher = Pusher;
@if ($pusherForceWs)
if (window.Pusher && window.Pusher.Runtime) {
window.Pusher.Runtime.getProtocol = function () { return 'http:'; };
}
@endif
const EchoConstructor = typeof Echo === 'function' ? Echo : Echo.default;
window.Echo = new EchoConstructor({
broadcaster: 'pusher',
@@ -25,10 +33,10 @@
wsPort: "{{ getRealtime() }}",
wssPort: "{{ getRealtime() }}",
forceTLS: false,
encrypted: true,
encrypted: @json($pusherForceWs ? false : true),
enableStats: false,
enableLogging: @json(app()->environment('local')),
enabledTransports: ['ws', 'wss'],
enabledTransports: @json($pusherForceWs ? ['ws'] : ['ws', 'wss']),
disabledTransports: ['sockjs', 'xhr_streaming', 'xhr_polling'],
});
</script>
+27
View File
@@ -0,0 +1,27 @@
<?php
/**
* Layout popups (realtime warning, sponsorship, notifications) use the redesigned UI shell.
*/
test('realtime connection warning uses the redesigned popup shell', function () {
$view = file_get_contents(resource_path('views/livewire/layout-popups.blade.php'));
expect($view)
->toContain('Cannot connect to real-time service')
->toContain('Acknowledge &amp; disable')
->toContain('customActions')
->toContain('rounded-2xl border border-red-200')
->toContain('name="alert-triangle"')
->not->toContain('WARNING: </span> Cannot connect to real-time service')
->not->toContain('Acknowledge & Disable This Popup');
});
test('notification reminder uses the redesigned popup shell', function () {
$view = file_get_contents(resource_path('views/livewire/layout-popups.blade.php'));
expect($view)
->toContain('No notifications enabled')
->toContain('Accept and close')
->toContain('Open notifications')
->not->toContain('Accept and Close');
});
@@ -107,6 +107,30 @@ it('registers the eye-off2 reicon used when password value is visible', function
->toContain('M2.53033 1.46967');
});
it('uses the same eye-off2 icon in configuration changes expand toggle', function () {
$html = view('components.deployment.configuration-diff', [
'diff' => [
'changes' => [
[
'key' => 'env.SECRET',
'section_label' => 'Environment',
'label' => 'SECRET',
'expandable' => true,
'old_display_value' => 'old-***',
'new_display_value' => 'new-***',
'old_full_value' => 'old-secret-value',
'new_full_value' => 'new-secret-value',
],
],
],
])->render();
// eye-off2 path (password inputs); not the older eye-off glyph.
expect($html)
->toContain('M2.53033 1.46967')
->not->toContain('M22.2954 6.31083');
});
it('renders env var password input before visibility toggle in tab order', function () {
$html = Blade::render('<x-forms.env-var-input type="password" id="secret" />');
@@ -38,9 +38,19 @@ it('renders copy fields as visible readonly controls with an accessible copy act
expect($html)
->toContain('label class="flex gap-1 items-center mb-1 text-sm font-medium text-black dark:text-white"')
->toContain('readonly')
->toContain('class="input pr-11 bg-white dark:bg-coolgray-100 dark:read-only:bg-coolgray-100 dark:read-only:text-white"')
->toContain('input-with-copy-button')
->toContain('copy-button')
->toContain('aria-label="Copy to clipboard"')
->toContain('title="Copy to clipboard"')
->toContain('rounded-sm p-1.5 text-neutral-500 transition-colors hover:text-neutral-700 focus-visible:ring-2 focus-visible:ring-coollabs focus-visible:ring-offset-2 dark:text-neutral-400 dark:hover:text-white dark:focus-visible:ring-warning dark:focus-visible:ring-offset-base')
->toContain('class="w-5 h-5 text-green-500"');
->toContain('class="size-[18px] text-green-500"');
});
it('keeps copy button padding above settings-workspace input overrides', function () {
$css = file_get_contents(resource_path('css/app.css'));
expect($css)
->toContain('.input.input-with-copy-button')
->toContain('.application-settings-workspace .input.input-with-copy-button')
->toContain('.application-settings-form .input.input-with-copy-button')
->toContain('padding-right: 2.5rem');
});