v1.1.2
- Real-time updates (Laravel Reverb): live operator queue, live ticket chat/detail updates for operator and client, periodic fallback refresh with a visible countdown as a backstop for dropped websocket connections. - SLA automation rules (Admin > Automatyzacja SLA): act on a ticket after N minutes of customer silence (change priority/status/team/assignee), evaluated every 15 minutes, reusing TicketService's own setters so automated changes get the same history/notification/broadcast a manual change would. - New notification: every operator on a matching team gets notified when a new ticket lands in one of their subcategories. - BookStack knowledge-base sidebar now also shown on the client's own ticket view (previously operator-only); suggestions everywhere now load in after first paint instead of blocking it. - Client ticket view: shows assigned operator + team; page widened to match the operator's. - Notification bell shows unread only; read notifications disappear instead of just dimming. - Stats dashboard: sectioned layout, new breakdowns (by subcategory, CSAT by team/operator, top clients, client x subcategory cross-tab). - Mobile: nav dropdowns (theme/notifications/profile) now expand full width instead of overflowing off-screen below 640px. - Fixed two bugs that silently disabled all real-time updates (missing CSRF header on Echo's private-channel auth; a script-load-order race that could miss the livewire:init event) and the mariadb healthcheck (world-writable credentials file on this stack's NFS mount). - Assorted test-suite fixes (roles virtual attribute needs the roles table seeded; a few missing seeds/wrong assertions found along the way). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
84
src/resources/js/echo.js
Normal file
84
src/resources/js/echo.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import Echo from 'laravel-echo';
|
||||
|
||||
import Pusher from 'pusher-js';
|
||||
window.Pusher = Pusher;
|
||||
|
||||
// Private channel subscriptions POST to /broadcasting/auth, which sits
|
||||
// behind the app's normal CSRF middleware like any other POST route —
|
||||
// without this header every private-channel auth request 419s silently
|
||||
// (pusher-js swallows it as a subscription error), so nothing broadcast
|
||||
// ever reaches the browser even though the socket connection itself works.
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
|
||||
window.Echo = new Echo({
|
||||
broadcaster: 'reverb',
|
||||
key: import.meta.env.VITE_REVERB_APP_KEY,
|
||||
wsHost: import.meta.env.VITE_REVERB_HOST,
|
||||
wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
|
||||
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
|
||||
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
|
||||
enabledTransports: ['ws', 'wss'],
|
||||
auth: {
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': csrfToken,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Bridges Reverb broadcast events into plain Livewire events rather than
|
||||
* using the `#[On('echo-private:...')]` attribute directly on components —
|
||||
* this indirection is deliberately version-agnostic and easy to verify from
|
||||
* the browser console regardless of Livewire's internals.
|
||||
*
|
||||
* This file is loaded via @vite as `type="module"`, which the HTML spec
|
||||
* defers until after the document is parsed — meaning any plain
|
||||
* (non-deferred) <script> earlier in the page, including Livewire's own
|
||||
* bootstrap script from @livewireScripts, has ALREADY run by the time this
|
||||
* executes. So `window.Livewire` is already available here; there's no
|
||||
* reason to wait for the 'livewire:init' event. Waiting for it was actually
|
||||
* a bug: Livewire dispatches that event synchronously as part of its own
|
||||
* (earlier-running) script, so a listener registered this late permanently
|
||||
* missed it — silently disabling this whole subscription, every time.
|
||||
*/
|
||||
if (window.currentUserId) {
|
||||
window.Echo.private('operator.queue')
|
||||
.listen('.TicketQueueChanged', (e) => {
|
||||
if (e.actorId !== window.currentUserId) {
|
||||
// Only ticketId is passed through — Livewire calls #[On] methods
|
||||
// with the payload as named arguments, so keeping this to a
|
||||
// single well-known key avoids every listener having to declare
|
||||
// (and ignore) every field this event might ever carry.
|
||||
Livewire.dispatch('queue-changed', { ticketId: e.ticketId });
|
||||
}
|
||||
})
|
||||
.error((error) => console.error('operator.queue subscription error', error));
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to a single ticket's channel — called by the Blade view of
|
||||
* whichever TicketShow component (operator or client) is currently mounted,
|
||||
* since the channel name needs the ticket id that only the page knows.
|
||||
*/
|
||||
window.subscribeToTicketChannel = function (ticketId) {
|
||||
window.Echo.private('ticket.' + ticketId)
|
||||
.listen('.TicketMessagePosted', (e) => {
|
||||
if (e.actorId !== window.currentUserId) {
|
||||
Livewire.dispatch('ticket-message-posted', { ticketId: e.ticketId });
|
||||
}
|
||||
})
|
||||
.listen('.TicketQueueChanged', (e) => {
|
||||
if (e.actorId !== window.currentUserId) {
|
||||
Livewire.dispatch('queue-changed', { ticketId: e.ticketId });
|
||||
}
|
||||
})
|
||||
.error((error) => console.error('ticket.' + ticketId + ' subscription error', error));
|
||||
};
|
||||
|
||||
// The @script block in ticket-show.blade.php calls subscribeToTicketChannel()
|
||||
// as soon as Livewire processes that component — which can happen either
|
||||
// before or after this deferred module has run, depending on exactly when
|
||||
// Livewire gets to it. If it ran first, it queued the ticket id here instead
|
||||
// of finding the function undefined; flush that queue now that we're ready.
|
||||
(window.__pendingTicketChannelIds || []).forEach((id) => window.subscribeToTicketChannel(id));
|
||||
window.__pendingTicketChannelIds = null;
|
||||
Reference in New Issue
Block a user