fix(js): never let Reverb/Echo init abort Alpine component registration
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) <noreply@anthropic.com>
feat/v1-foundation
parent
b1fb0e766a
commit
1e6fdb457e
|
|
@ -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 <meta name="csrf-token">.
|
||||
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 <meta name="csrf-token">.
|
||||
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-
|
||||
|
|
|
|||
Loading…
Reference in New Issue