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:
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user