From 3da2d5e0ab55817f4a580ffd27024e6076cda687 Mon Sep 17 00:00:00 2001 From: HotelSync Date: Thu, 19 Mar 2026 13:44:12 +0300 Subject: [PATCH] Fix drag-to-move and WS keepalive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit drag-to-move: use window-level mousemove/mouseup listeners for reliable drag detection — React synthetic events on container were unreliable WS keepalive: server sends ping JSON every 25s so nginx proxy_read_timeout never fires; client ignores pings (not broadcast to peers anymore) Co-Authored-By: Claude Sonnet 4.6 --- backend/src/routes/ws.ts | 16 ++++ src/components/calendar/BookingCalendar.tsx | 86 +++++++++------------ 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/backend/src/routes/ws.ts b/backend/src/routes/ws.ts index 456a060..e752260 100644 --- a/backend/src/routes/ws.ts +++ b/backend/src/routes/ws.ts @@ -6,6 +6,8 @@ import type { RawData } from 'ws' // hotel slug → set of connected streams const hotelRooms = new Map>() +const PING_INTERVAL_MS = 25_000 // ping every 25s — keeps nginx proxy_read_timeout alive + const ws: FastifyPluginAsync = async (fastify) => { await fastify.register(fastifyWebsocket) @@ -19,8 +21,21 @@ const ws: FastifyPluginAsync = async (fastify) => { if (!hotelRooms.has(hotel)) hotelRooms.set(hotel, new Set()) hotelRooms.get(hotel)!.add(connection) + // Send periodic ping so nginx proxy_read_timeout never fires + const pingTimer = setInterval(() => { + if (connection.socket.readyState === 1) { + connection.socket.send(JSON.stringify({ type: 'ping' })) + } + }, PING_INTERVAL_MS) + connection.socket.on('message', (raw: RawData) => { const data = raw.toString() + // Ignore client pings — no need to broadcast them + try { + const msg = JSON.parse(data) as { type?: string } + if (msg.type === 'ping') return + } catch { /* not JSON, fall through */ } + const peers = hotelRooms.get(hotel) if (!peers) return peers.forEach(peer => { @@ -31,6 +46,7 @@ const ws: FastifyPluginAsync = async (fastify) => { }) connection.socket.on('close', () => { + clearInterval(pingTimer) hotelRooms.get(hotel)?.delete(connection) if (hotelRooms.get(hotel)?.size === 0) hotelRooms.delete(hotel) }) diff --git a/src/components/calendar/BookingCalendar.tsx b/src/components/calendar/BookingCalendar.tsx index 28989c2..bf4975e 100644 --- a/src/components/calendar/BookingCalendar.tsx +++ b/src/components/calendar/BookingCalendar.tsx @@ -83,13 +83,9 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd // Drag-to-move booking state const [movingBooking, setMovingBooking] = useState(null) const [moveTargetRoomId, setMoveTargetRoomId] = useState(null) - const pendingMoveBooking = useRef(null) - const moveStartPos = useRef<{ x: number; y: number } | null>(null) const movingBookingRef = useRef(null) - const moveTargetRoomIdRef = useRef(null) const didDragRef = useRef(false) movingBookingRef.current = movingBooking - moveTargetRoomIdRef.current = moveTargetRoomId // Compact mode (default from Settings → Appearance) const [compact, setCompact] = useState( @@ -141,7 +137,7 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd const handleCellMouseDown = useCallback((roomId: string, dayIdx: number, e: React.MouseEvent) => { if (e.button !== 0) return - if (pendingMoveBooking.current || movingBookingRef.current) return + if (movingBookingRef.current) return e.preventDefault() setDragStart({ roomId, dayIdx }) setDragEnd(dayIdx) @@ -152,50 +148,7 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd setDragEnd(dayIdx) }, [dragStart]) - const handleMouseMove = useCallback((e: React.MouseEvent) => { - // 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(() => { - const mb = movingBookingRef.current - const targetRoom = moveTargetRoomIdRef.current - - if (mb) { - // Was dragging a booking to another room - if (targetRoom && targetRoom !== mb.roomId) { - didDragRef.current = true - onBookingUpdate(mb.id, { roomId: targetRoom }) - } - setMovingBooking(null) - setMoveTargetRoomId(null) - pendingMoveBooking.current = null - moveStartPos.current = null - return - } - - // No drag — clear pending refs; onClick on booking block will open the panel - if (pendingMoveBooking.current) { - pendingMoveBooking.current = null - moveStartPos.current = null - } - if (!dragStart || dragEnd === null) return const minDay = Math.min(dragStart.dayIdx, dragEnd) const maxDay = Math.max(dragStart.dayIdx, dragEnd) @@ -225,7 +178,7 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd } return ( -
+
{/* Toolbar */}
@@ -523,8 +476,39 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd if (e.button !== 0) return e.stopPropagation() e.preventDefault() - pendingMoveBooking.current = booking - moveStartPos.current = { x: e.clientX, y: e.clientY } + const startX = e.clientX + const startY = e.clientY + let dragging = false + const onMove = (ev: MouseEvent) => { + if (!dragging) { + if (Math.abs(ev.clientX - startX) > 5 || Math.abs(ev.clientY - startY) > 5) { + dragging = true + setMovingBooking(booking) + setMoveTargetRoomId(booking.roomId) + } + return + } + const el = document.elementFromPoint(ev.clientX, ev.clientY) + const row = el?.closest('[data-room-id]') as HTMLElement | null + if (row?.dataset.roomId) setMoveTargetRoomId(row.dataset.roomId) + } + const onUp = (ev: MouseEvent) => { + window.removeEventListener('mousemove', onMove) + window.removeEventListener('mouseup', onUp) + if (dragging) { + const el = document.elementFromPoint(ev.clientX, ev.clientY) + const row = el?.closest('[data-room-id]') as HTMLElement | null + const targetRoomId = row?.dataset.roomId + if (targetRoomId && targetRoomId !== booking.roomId) { + didDragRef.current = true + onBookingUpdate(booking.id, { roomId: targetRoomId }) + } + setMovingBooking(null) + setMoveTargetRoomId(null) + } + } + window.addEventListener('mousemove', onMove) + window.addEventListener('mouseup', onUp) }} onClick={(e) => { e.stopPropagation()