Fix WS offline: nginx WebSocket headers + keepalive ping + drag-to-move fix

nginx: add /ws location with Upgrade/Connection headers and 1h timeout
useHotelSocket: ping every 30s to prevent idle timeout, read fresh token
  from localStorage on reconnect (handles 1h JWT expiry gracefully)
BookingCalendar: use elementFromPoint for reliable drag-to-move target

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-19 13:24:37 +03:00
parent 3602ea7318
commit 26412a04dd
2 changed files with 55 additions and 9 deletions

View File

@@ -14,6 +14,15 @@ interface Options {
onMessage: (msg: WsMessage) => void
}
function getFreshToken(): string | null {
try {
const s = localStorage.getItem('hotelsync-session')
return s ? (JSON.parse(s) as { token: string }).token : null
} catch {
return null
}
}
export function useHotelSocket({ slug, token, onMessage }: Options) {
const wsRef = useRef<WebSocket | null>(null)
const cbRef = useRef(onMessage)
@@ -30,14 +39,32 @@ export function useHotelSocket({ slug, token, onMessage }: Options) {
let destroyed = false
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
let pingTimer: ReturnType<typeof setInterval> | null = null
let attempts = 0
function startPing(ws: WebSocket) {
if (pingTimer) clearInterval(pingTimer)
// Send a ping every 30s to keep nginx / proxies from closing idle connection
pingTimer = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }))
}
}, 30_000)
}
function connect() {
if (destroyed) return
// Always read fresh token so reconnects after 1h token expiry use the refreshed JWT
const currentToken = getFreshToken() ?? token
if (!currentToken) {
console.warn('[WS] no token available')
return
}
const base = (import.meta.env.VITE_API_URL as string | undefined) ?? 'https://api.hotelsync.ru'
const wsBase = base.replace(/^https/, 'wss').replace(/^http(?!s)/, 'ws')
const url = `${wsBase}/ws?hotel=${encodeURIComponent(slug)}&token=${encodeURIComponent(token!)}`
const url = `${wsBase}/ws?hotel=${encodeURIComponent(slug)}&token=${encodeURIComponent(currentToken)}`
console.log('[WS] connecting to', url.replace(/token=[^&]+/, 'token=***'))
const ws = new WebSocket(url)
@@ -47,11 +74,17 @@ export function useHotelSocket({ slug, token, onMessage }: Options) {
if (destroyed) { ws.close(); return }
setConnected(true)
attempts = 0
startPing(ws)
console.log('[WS] connected ✓ hotel:', slug)
}
ws.onmessage = (e) => {
try { cbRef.current(JSON.parse(e.data as string) as WsMessage) } catch { /* ignore */ }
try {
const msg = JSON.parse(e.data as string) as WsMessage
// ignore pong / ping from server
if ((msg as unknown as { type: string }).type === 'pong') return
cbRef.current(msg)
} catch { /* ignore */ }
}
ws.onerror = (ev) => {
@@ -61,6 +94,7 @@ export function useHotelSocket({ slug, token, onMessage }: Options) {
ws.onclose = (ev) => {
wsRef.current = null
setConnected(false)
if (pingTimer) { clearInterval(pingTimer); pingTimer = null }
if (destroyed) return
const delay = Math.min(1000 * 2 ** attempts, 30_000)
attempts++
@@ -74,6 +108,7 @@ export function useHotelSocket({ slug, token, onMessage }: Options) {
return () => {
destroyed = true
if (reconnectTimer) clearTimeout(reconnectTimer)
if (pingTimer) clearInterval(pingTimer)
wsRef.current?.close()
wsRef.current = null
setConnected(false)