appendMessage() publishes ticket.created and message.created for the
same first message of a new ticket. TicketToasts and DesktopNotifications
listened to both independently, so one new customer ticket produced two
toasts/two chimes/two OS notifications. message.created now carries
ticketIsNew so both subscribers can skip it when ticket.created already
covered the same event.
Sound stopped again after the previous fix — likely Chrome's power-saving
auto-suspend re-suspending an already-unlocked AudioContext after a
stretch of no audio activity, which is a separate mechanism from the
autoplay-gesture policy the earlier fix addressed. The unlock listeners
were {once: true}, so they'd already fired and detached themselves,
leaving nothing to re-resume the context once the browser suspended it
again. Now the listeners stay attached for the whole session (a resume()
call on an already-running context is a harmless no-op, so this costs
nothing), and playChime() actually awaits resume() before scheduling the
oscillators instead of assuming it completes synchronously.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012o9j9RezxbZVKQMrB7oRLY
Toast now works (confirmed after the event-bus fix), but the sound never
played. Cause: a fresh AudioContext created outside a real user gesture
(click/keypress) starts in "suspended" state per browser autoplay
policy — an SSE message arriving is a network callback, not a gesture,
so every chime was silently creating a context that never actually
produced sound (start()/stop() don't throw on a suspended context, so
nothing surfaced this in testing without watching for it specifically).
Now a single AudioContext is created once and reused, resumed on the
page's first pointerdown/keydown — after that one real interaction it
stays running for the rest of the session, so every subsequent chime
actually plays.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012o9j9RezxbZVKQMrB7oRLY
Four requested improvements:
1. Logo + wordmark now link to /dashboard.
2. Header reordered: nav links (Заявки/Статистика) centered, right side
now reads notifications -> theme -> settings -> account name -> logout,
matching the requested order.
3. New TicketToasts component: in-page toast + a short Web Audio chime for
new tickets and customer messages, shown regardless of tab focus.
Complements the existing DesktopNotifications, which only fires the
native OS notification while the tab is backgrounded. Also added a 30s
polling refresh on the dashboard as a plain safety net under the
existing SSE push, in case an event is dropped by whatever's in front
of this app on the network.
4. Agents now see only tickets assigned to them plus unassigned ones (not
the whole board) — listTickets() takes an optional visibility filter,
applied in both the dashboard page and the /api/tickets search route,
and mirrored client-side so a live SSE update can't leak a
just-reassigned-away ticket onto their board. Settings gear and the
Статистика nav link are now hidden entirely for agents rather than
just showing fewer items inside. Also closed a real gap this surfaced:
/stats had no server-side role check at all (only the nav link was
hidden), so any agent could load it directly — added the same
session.user.role !== "admin" guard the other admin-only settings
pages already have.
Verified end-to-end with a temporary Playwright pass (installed and fully
removed afterward): logo click navigation, header icon order, agent view
missing the gear/Статистика, direct-URL access to /stats and
/settings/accounts still redirecting non-admins, and ticket visibility
correctly disappearing from an agent's board the moment it's reassigned
to someone else.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012o9j9RezxbZVKQMrB7oRLY