From 1e6fdb457ef672ae51fa00f7bb8d37b97ad16b16 Mon Sep 17 00:00:00 2001 From: boban Date: Fri, 19 Jun 2026 17:19:31 +0200 Subject: [PATCH] fix(js): never let Reverb/Echo init abort Alpine component registration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The command-palette opened itself full-screen and undismissable right after first login on production (bare-IP) installs. Root cause: app.js constructed `window.Echo = new Echo({...})` at module top level. On auth-layout pages the runtime reverb config (window.__clusev) is absent and the prod bundle bakes no VITE_REVERB_* fallback (by design — the endpoint is resolved at runtime), so key/wsHost were undefined and the constructor threw. That throw aborted app.js BEFORE the alpine:init block registered cmdk / modalTrigger / dualChart. app.js does not re-run across wire:navigate, so the dashboard inherited the broken state: Alpine could not resolve x-data="cmdk(...)", stripped x-cloak but could not evaluate x-show="open", and the palette div fell back to its default flex display — a dimmed, undismissable overlay. Dev (Vite) hid the bug because it bakes VITE_REVERB_* from the dev .env, so Echo never threw. Fix: construct Echo only when a usable runtime config is present, wrap it in try/catch so a websocket/config failure can never take down the UI, and re-run the guarded init on livewire:navigated so realtime still connects on a dashboard reached via SPA navigation from an auth page. Verified on the real prod build (rebuilt VM image): palette stays closed after first-login -> dashboard, opens on Ctrl+K and closes on ESC, Echo reaches connected state, zero console errors. Co-Authored-By: Claude Opus 4.8 (1M context) --- resources/js/app.js | 63 ++++++++++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/resources/js/app.js b/resources/js/app.js index 129251f..3b7ad81 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -4,25 +4,52 @@ import './webauthn'; // exposes window.clusevWebauthn (register/login ceremonies window.Pusher = Pusher; -// Reverb (Pusher protocol). The endpoint is injected at runtime by the layout +// Reverb (Pusher protocol). The endpoint is injected at RUNTIME by the app layout // (window.__clusev.reverb), derived from the effective panel domain — so changing the -// domain from the dashboard needs no JS rebuild. Falls back to Vite env for HMR/dev. -const rc = (typeof window !== 'undefined' && window.__clusev && window.__clusev.reverb) || {}; -const reverbScheme = rc.scheme ?? import.meta.env.VITE_REVERB_SCHEME ?? 'https'; -const reverbPort = Number(rc.port ?? import.meta.env.VITE_REVERB_PORT ?? (reverbScheme === 'https' ? 443 : 80)); -// Channels are PRIVATE: Echo POSTs to /broadcasting/auth (web group → PanelScheme + session) -// before subscribing, so we must send the CSRF token the layout exposes as . -const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content; -window.Echo = new Echo({ - broadcaster: 'reverb', - key: rc.key ?? import.meta.env.VITE_REVERB_APP_KEY, - wsHost: rc.host ?? import.meta.env.VITE_REVERB_HOST, - wsPort: reverbPort, - wssPort: reverbPort, - forceTLS: reverbScheme === 'https', - enabledTransports: ['ws', 'wss'], - auth: { headers: { 'X-CSRF-TOKEN': csrfToken } }, -}); +// domain from the dashboard needs no JS rebuild. The prod build deliberately bakes NO +// VITE_REVERB_* env (it would freeze the build-time domain); those env reads are only a +// dev/HMR convenience. +// +// CRITICAL: this MUST NOT throw at module top level. Auth pages (login / forced password +// change) use a layout that injects no __clusev, and the prod bundle has no VITE_REVERB_* +// fallback, so the config is legitimately absent there. A thrown `new Echo({key: undefined})` +// would abort this whole module BEFORE the alpine:init block below registers cmdk / +// modalTrigger / dualChart — leaving the command-palette's `x-data="cmdk(...)"` unresolved. +// Alpine then strips x-cloak but can't evaluate `x-show="open"`, so the palette renders as a +// full-screen, undismissable overlay. So: construct only when a real config is present, guard +// it, and (re)try on navigation since the config first appears on app-layout pages. +function ensureEcho() { + if (window.Echo) return; // singleton — construct once + const rc = (typeof window !== 'undefined' && window.__clusev && window.__clusev.reverb) || {}; + const key = rc.key ?? import.meta.env.VITE_REVERB_APP_KEY; + const host = rc.host ?? import.meta.env.VITE_REVERB_HOST; + if (! key || ! host) return; // no realtime config (e.g. auth pages) — skip, never throw + + const scheme = rc.scheme ?? import.meta.env.VITE_REVERB_SCHEME ?? 'https'; + const port = Number(rc.port ?? import.meta.env.VITE_REVERB_PORT ?? (scheme === 'https' ? 443 : 80)); + // Channels are PRIVATE: Echo POSTs to /broadcasting/auth (web group → PanelScheme + session) + // before subscribing, so we must send the CSRF token the layout exposes as . + const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content; + try { + window.Echo = new Echo({ + broadcaster: 'reverb', + key, + wsHost: host, + wsPort: port, + wssPort: port, + forceTLS: scheme === 'https', + enabledTransports: ['ws', 'wss'], + auth: { headers: { 'X-CSRF-TOKEN': csrfToken } }, + }); + } catch (e) { + // A realtime/websocket misconfig must never take down the rest of the UI. + console.error('[clusev] realtime (Echo/Reverb) init failed — continuing without live updates', e); + } +} +ensureEcho(); +// The reverb config first appears when an app-layout page mounts; on a SPA navigation INTO it +// (e.g. forced password change → dashboard) this module does not re-run, so try again then. +document.addEventListener('livewire:navigated', ensureEcho); // ── Modal-open detection ──────────────────────────────────────────────── // wire-elements/modal emits NO "opened" event; the only reliable, package-version-