From 30511194054399b991cd6100f4631d1191fac45d Mon Sep 17 00:00:00 2001 From: Jan Willem Mannaerts Date: Tue, 3 Mar 2026 13:54:26 +0100 Subject: [PATCH] Add poker:leave on unmount and poker:kick to remove ghost participants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Frontend now emits poker:leave when PokerRoom unmounts, preventing ghost participants. Also adds poker:kick socket event so any session participant can remove a stale user — shows a small X button next to each participant. Fixes deadlocked sessions where a disconnected user blocks reveal (votes.size can never equal participants.size). Co-Authored-By: Claude Opus 4.6 --- backend/src/index.js | 22 ++++++++++++++++++++ frontend/src/components/PokerRoom.jsx | 30 ++++++++++++++++++++------- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/backend/src/index.js b/backend/src/index.js index 3736d98..c17429d 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -343,6 +343,28 @@ io.on('connection', (socket) => { } }); + throttled('poker:kick', async ({ sessionId, userKey }) => { + try { + if (!sessionId || !userKey) { + socket.emit('poker:error', { error: 'sessionId and userKey are required.' }); + return; + } + if (!await canAccessSession(sessionId, socket.user.jiraCloudId)) { + socket.emit('poker:error', { error: 'Session not found.' }); + return; + } + await leaveSession({ + sessionId, + tenantCloudId: socket.user.jiraCloudId, + userKey + }); + await emitSessionState(sessionId, socket.user.jiraCloudId); + } catch (error) { + console.error('[socket] poker:kick failed:', error); + socket.emit('poker:error', { error: safeError(error) }); + } + }); + throttled('poker:leave', async ({ sessionId }) => { try { if (!sessionId) return; diff --git a/frontend/src/components/PokerRoom.jsx b/frontend/src/components/PokerRoom.jsx index ea095c5..c3d7070 100644 --- a/frontend/src/components/PokerRoom.jsx +++ b/frontend/src/components/PokerRoom.jsx @@ -80,6 +80,7 @@ export default function PokerRoom({ session, issue, user, onSaved }) { } return () => { + socket.emit('poker:leave', { sessionId: session.id }); socket.off('poker:participants', onParticipants); socket.off('poker:vote-update', onVoteUpdate); socket.off('poker:revealed', onRevealed); @@ -98,6 +99,10 @@ export default function PokerRoom({ session, issue, user, onSaved }) { }); } + function handleKick(userKey) { + socket.emit('poker:kick', { sessionId: session.id, userKey }); + } + function handleSave() { const estimate = Number(manualEstimate || suggestedEstimate); if (!Number.isFinite(estimate)) { @@ -229,13 +234,24 @@ export default function PokerRoom({ session, issue, user, onSaved }) { {isCurrentUser && (you)} -
- {revealed && vote !== null && vote !== undefined ? ( - {vote} - ) : hasVoted ? ( - - ) : ( - ... +
+ + {revealed && vote !== null && vote !== undefined ? ( + {vote} + ) : hasVoted ? ( + + ) : ( + ... + )} + + {!isCurrentUser && ( + )}