import Echo from 'laravel-echo'; import Pusher from 'pusher-js'; window.Pusher = Pusher; // Reverb (Pusher protocol). The endpoint is injected at runtime by the 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 } }, }); document.addEventListener('alpine:init', () => { // Live dual-series chart (CPU + MEM). Seeds from server-rendered data, then // appends each MetricsTicked broadcast and redraws (viewBox 0 0 300 100). window.Alpine.data('dualChart', (cpuSeed = [], memSeed = [], max = 40, server = null) => ({ cpu: cpuSeed.slice(-max), mem: memSeed.slice(-max), max, server, connected: false, init() { const conn = window.Echo?.connector?.pusher?.connection; this.connected = conn?.state === 'connected'; this._onState = (s) => { this.connected = s.current === 'connected'; }; conn?.bind('state_change', this._onState); // PRIVATE channel: authorized via /broadcasting/auth (see routes/channels.php). this._channel = window.Echo?.private('metrics'); this._channel?.listen('.tick', (e) => { // only react to the server this chart is showing if (this.server && e.server && e.server !== this.server) return; this.append(this.cpu, e.cpu); this.append(this.mem, e.mem); }); }, destroy() { // Echo.leave() tears down the public, private- and presence- variants of the // name, so this correctly unsubscribes the private-metrics channel. window.Echo?.leave('metrics'); window.Echo?.connector?.pusher?.connection?.unbind('state_change', this._onState); }, append(arr, v) { const n = Number(v); if (! Number.isFinite(n)) return; arr.push(n); if (arr.length > this.max) arr.shift(); }, _coords(arr) { const n = arr.length, w = 300, h = 100, pad = 6; return arr .map((v, i) => { const x = n > 1 ? (i / (n - 1)) * w : 0; const y = h - pad - (Math.max(0, Math.min(100, v)) / 100) * (h - 2 * pad); return `${x.toFixed(1)},${y.toFixed(1)}`; }) .join(' '); }, get cpuLine() { return this._coords(this.cpu); }, get cpuArea() { return `0,100 ${this._coords(this.cpu)} 300,100`; }, get memLine() { return this._coords(this.mem); }, get memArea() { return `0,100 ${this._coords(this.mem)} 300,100`; }, })); // ── Command palette + keyboard shortcuts ──────────────────────────── // Nav + action item labels are passed in from the Blade markup (already // translated via __()), so the palette stays in sync with the active locale. // CMDK_GO maps leader-key mnemonics to routes (i=dIenste, l=Log, // e=Einstellungen, y=sYstem — d/s already taken) and stays in JS. const CMDK_GO = { d: '/', s: '/servers', i: '/services', f: '/files', l: '/audit', e: '/settings', y: '/system', v: '/versions' }; window.Alpine.data('cmdk', (nav = [], actions = []) => ({ nav, actions, open: false, help: false, query: '', active: 0, leader: false, leaderTimer: null, init() { // Reset overlays after an SPA navigation. The component is recreated on each // wire:navigate, so we MUST remove this document listener in destroy() — else // every navigation would accumulate another permanent callback. this._onNav = () => { this.open = false; this.help = false; this.leader = false; }; document.addEventListener('livewire:navigated', this._onNav); }, destroy() { document.removeEventListener('livewire:navigated', this._onNav); }, get items() { const q = this.query.trim().toLowerCase(); const all = [...this.nav, ...this.actions]; return q === '' ? all : all.filter((i) => i.label.toLowerCase().includes(q)); }, toggle(force) { this.open = force === true ? true : ! this.open; if (this.open) { this.help = false; this.query = ''; this.active = 0; this.$nextTick(() => this.$refs.input?.focus()); } }, close() { this.open = false; this.help = false; }, move(d) { const n = this.items.length; if (n) this.active = (this.active + d + n) % n; }, run(item) { item = item || this.items[this.active] || null; if (! item) return; if (item.href) { this.navigate(item.href); } else if (item.action === 'create-server') { this.close(); window.Livewire?.dispatch('openModal', { component: 'modals.create-server' }); } }, navigate(href) { this.close(); if (window.Livewire?.navigate) window.Livewire.navigate(href); else window.location.href = href; }, focusSearch() { const el = document.querySelector('[data-page-search]'); if (el) { el.focus(); return true; } return false; }, go(key) { if (CMDK_GO[key]) this.navigate(CMDK_GO[key]); }, onKey(e) { // Cmd/Ctrl-K toggles the palette even from within an input. if ((e.key === 'k' || e.key === 'K') && (e.metaKey || e.ctrlKey)) { e.preventDefault(); this.toggle(); return; } if (this.open || this.help) return; // overlay keys handled in the markup const t = e.target; if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT' || t.isContentEditable)) return; if (e.metaKey || e.ctrlKey || e.altKey) return; // Only consume "/" when a page search exists — otherwise leave the browser's quick-find. if (e.key === '/') { if (this.focusSearch()) e.preventDefault(); return; } if (e.key === '?') { e.preventDefault(); this.help = true; return; } if (this.leader) { this.leader = false; clearTimeout(this.leaderTimer); this.go(e.key.toLowerCase()); return; } if (e.key === 'g' || e.key === 'G') { this.leader = true; this.leaderTimer = setTimeout(() => { this.leader = false; }, 1200); } }, })); });