Fix drag-to-move and WS keepalive

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 <noreply@anthropic.com>
This commit is contained in:
2026-03-19 13:44:12 +03:00
parent 14c5327870
commit 3da2d5e0ab
2 changed files with 51 additions and 51 deletions

View File

@@ -6,6 +6,8 @@ import type { RawData } from 'ws'
// hotel slug → set of connected streams
const hotelRooms = new Map<string, Set<SocketStream>>()
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)
})

View File

@@ -83,13 +83,9 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd
// Drag-to-move booking state
const [movingBooking, setMovingBooking] = useState<Booking | null>(null)
const [moveTargetRoomId, setMoveTargetRoomId] = useState<string | null>(null)
const pendingMoveBooking = useRef<Booking | null>(null)
const moveStartPos = useRef<{ x: number; y: number } | null>(null)
const movingBookingRef = useRef<Booking | null>(null)
const moveTargetRoomIdRef = useRef<string | null>(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 (
<div className="flex flex-col h-full" onMouseUp={handleMouseUp} onMouseLeave={handleMouseUp} onMouseMove={handleMouseMove}>
<div className="flex flex-col h-full" onMouseUp={handleMouseUp} onMouseLeave={handleMouseUp}>
{/* Toolbar */}
<div className="flex items-center gap-3 px-4 py-3 border-b border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 shrink-0 flex-wrap gap-y-2">
<div className="flex items-center gap-1.5">
@@ -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()