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:
@@ -152,13 +152,24 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd
|
||||
}, [dragStart])
|
||||
|
||||
const handleMouseMove = useCallback((e: React.MouseEvent) => {
|
||||
if (!pendingMoveBooking.current || movingBookingRef.current) return
|
||||
// Phase 1: detect drag start
|
||||
if (pendingMoveBooking.current && !movingBookingRef.current) {
|
||||
const dx = Math.abs(e.clientX - (moveStartPos.current?.x ?? 0))
|
||||
const dy = Math.abs(e.clientY - (moveStartPos.current?.y ?? 0))
|
||||
if (dx > 5 || dy > 5) {
|
||||
setMovingBooking(pendingMoveBooking.current)
|
||||
setMoveTargetRoomId(pendingMoveBooking.current.roomId)
|
||||
}
|
||||
return
|
||||
}
|
||||
// Phase 2: track which room row the cursor is over
|
||||
if (movingBookingRef.current) {
|
||||
const el = document.elementFromPoint(e.clientX, e.clientY)
|
||||
const rowEl = el?.closest('[data-room-id]') as HTMLElement | null
|
||||
if (rowEl?.dataset.roomId) {
|
||||
setMoveTargetRoomId(rowEl.dataset.roomId)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
@@ -429,12 +440,12 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd
|
||||
rows.push(
|
||||
<div
|
||||
key={room.id}
|
||||
data-room-id={room.id}
|
||||
className={cn(
|
||||
'flex border-b border-slate-200 dark:border-slate-700 group hover:bg-slate-50/50 dark:hover:bg-slate-800/30',
|
||||
movingBooking && moveTargetRoomId === room.id && 'bg-brand-50/60 dark:bg-brand-900/20',
|
||||
)}
|
||||
style={{ height: rowHeight }}
|
||||
onMouseEnter={() => { if (movingBookingRef.current) setMoveTargetRoomId(room.id) }}
|
||||
>
|
||||
{/* Room label */}
|
||||
<div
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user