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
|
// hotel slug → set of connected streams
|
||||||
const hotelRooms = new Map<string, Set<SocketStream>>()
|
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) => {
|
const ws: FastifyPluginAsync = async (fastify) => {
|
||||||
await fastify.register(fastifyWebsocket)
|
await fastify.register(fastifyWebsocket)
|
||||||
|
|
||||||
@@ -19,8 +21,21 @@ const ws: FastifyPluginAsync = async (fastify) => {
|
|||||||
if (!hotelRooms.has(hotel)) hotelRooms.set(hotel, new Set())
|
if (!hotelRooms.has(hotel)) hotelRooms.set(hotel, new Set())
|
||||||
hotelRooms.get(hotel)!.add(connection)
|
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) => {
|
connection.socket.on('message', (raw: RawData) => {
|
||||||
const data = raw.toString()
|
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)
|
const peers = hotelRooms.get(hotel)
|
||||||
if (!peers) return
|
if (!peers) return
|
||||||
peers.forEach(peer => {
|
peers.forEach(peer => {
|
||||||
@@ -31,6 +46,7 @@ const ws: FastifyPluginAsync = async (fastify) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
connection.socket.on('close', () => {
|
connection.socket.on('close', () => {
|
||||||
|
clearInterval(pingTimer)
|
||||||
hotelRooms.get(hotel)?.delete(connection)
|
hotelRooms.get(hotel)?.delete(connection)
|
||||||
if (hotelRooms.get(hotel)?.size === 0) hotelRooms.delete(hotel)
|
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
|
// Drag-to-move booking state
|
||||||
const [movingBooking, setMovingBooking] = useState<Booking | null>(null)
|
const [movingBooking, setMovingBooking] = useState<Booking | null>(null)
|
||||||
const [moveTargetRoomId, setMoveTargetRoomId] = useState<string | 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 movingBookingRef = useRef<Booking | null>(null)
|
||||||
const moveTargetRoomIdRef = useRef<string | null>(null)
|
|
||||||
const didDragRef = useRef(false)
|
const didDragRef = useRef(false)
|
||||||
movingBookingRef.current = movingBooking
|
movingBookingRef.current = movingBooking
|
||||||
moveTargetRoomIdRef.current = moveTargetRoomId
|
|
||||||
|
|
||||||
// Compact mode (default from Settings → Appearance)
|
// Compact mode (default from Settings → Appearance)
|
||||||
const [compact, setCompact] = useState(
|
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) => {
|
const handleCellMouseDown = useCallback((roomId: string, dayIdx: number, e: React.MouseEvent) => {
|
||||||
if (e.button !== 0) return
|
if (e.button !== 0) return
|
||||||
if (pendingMoveBooking.current || movingBookingRef.current) return
|
if (movingBookingRef.current) return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setDragStart({ roomId, dayIdx })
|
setDragStart({ roomId, dayIdx })
|
||||||
setDragEnd(dayIdx)
|
setDragEnd(dayIdx)
|
||||||
@@ -152,50 +148,7 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd
|
|||||||
setDragEnd(dayIdx)
|
setDragEnd(dayIdx)
|
||||||
}, [dragStart])
|
}, [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 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
|
if (!dragStart || dragEnd === null) return
|
||||||
const minDay = Math.min(dragStart.dayIdx, dragEnd)
|
const minDay = Math.min(dragStart.dayIdx, dragEnd)
|
||||||
const maxDay = Math.max(dragStart.dayIdx, dragEnd)
|
const maxDay = Math.max(dragStart.dayIdx, dragEnd)
|
||||||
@@ -225,7 +178,7 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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 */}
|
{/* 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-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">
|
<div className="flex items-center gap-1.5">
|
||||||
@@ -523,8 +476,39 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd
|
|||||||
if (e.button !== 0) return
|
if (e.button !== 0) return
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
pendingMoveBooking.current = booking
|
const startX = e.clientX
|
||||||
moveStartPos.current = { x: e.clientX, y: e.clientY }
|
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) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|||||||
Reference in New Issue
Block a user