diff --git a/backend/src/index.js b/backend/src/index.js index 6f72a8b..dfca288 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -24,7 +24,6 @@ import { joinRoom, leaveRoom, getRoomMembers, - getRoomMemberCount, isRoomMember } from './services/roomService.js'; import { updateIssueEstimate } from './services/jiraService.js'; @@ -147,19 +146,28 @@ async function emitSessionState(roomId, sessionId, tenantCloudId) { const snapshot = await getSessionSnapshot(sessionId, tenantCloudId); if (!snapshot) return; - const memberCount = await getRoomMemberCount(roomId, tenantCloudId); + const members = await getRoomMembers(roomId, tenantCloudId); + const memberKeys = new Set(members.map((m) => m.userKey)); + + // Filter to only votes from current room members + const votedUserKeys = snapshot.votedUserKeys.filter((k) => memberKeys.has(k)); io.to(`room:${roomId}`).emit('poker:vote-update', { sessionId, - voteCount: snapshot.voteCount, - votedUserKeys: snapshot.votedUserKeys, - memberCount + voteCount: votedUserKeys.length, + votedUserKeys, + memberCount: memberKeys.size }); if (snapshot.session.state === 'REVEALED' || snapshot.session.state === 'SAVED') { + // Filter revealed votes to members only + const votes = {}; + for (const [k, v] of Object.entries(snapshot.votesByUser)) { + if (memberKeys.has(k)) votes[k] = v; + } io.to(`room:${roomId}`).emit('poker:revealed', { sessionId, - votes: snapshot.votesByUser, + votes, average: snapshot.session.averageEstimate, suggestedEstimate: snapshot.session.suggestedEstimate, savedEstimate: snapshot.session.savedEstimate @@ -279,8 +287,9 @@ io.on('connection', (socket) => { } votesTotal.inc(); - const memberCount = await getRoomMemberCount(roomId, socket.user.jiraCloudId); - const reveal = await revealIfComplete(sessionId, socket.user.jiraCloudId, memberCount); + const members = await getRoomMembers(roomId, socket.user.jiraCloudId); + const memberKeys = new Set(members.map((m) => m.userKey)); + const reveal = await revealIfComplete(sessionId, socket.user.jiraCloudId, memberKeys); await emitSessionState(roomId, sessionId, socket.user.jiraCloudId); @@ -305,13 +314,22 @@ io.on('connection', (socket) => { return; } - const snapshot = await forceReveal(sessionId, socket.user.jiraCloudId); + const pre = await getSessionSnapshot(sessionId, socket.user.jiraCloudId); + if (!pre) { + socket.emit('poker:error', { error: 'Session not found.' }); + return; + } + const roomId = pre.session.roomId; + + const members = await getRoomMembers(roomId, socket.user.jiraCloudId); + const memberKeys = new Set(members.map((m) => m.userKey)); + + const snapshot = await forceReveal(sessionId, socket.user.jiraCloudId, memberKeys); if (!snapshot) { socket.emit('poker:error', { error: 'Unable to reveal votes.' }); return; } - const roomId = snapshot.session.roomId; io.to(`room:${roomId}`).emit('poker:revealed', { sessionId, votes: snapshot.votesByUser, diff --git a/backend/src/services/pokerService.js b/backend/src/services/pokerService.js index 7c511b5..8d60354 100644 --- a/backend/src/services/pokerService.js +++ b/backend/src/services/pokerService.js @@ -167,14 +167,23 @@ export async function submitVote({ sessionId, tenantCloudId, userKey, vote }) { return getSnapshot(result); } -export async function revealIfComplete(sessionId, tenantCloudId, roomMemberCount) { +export async function revealIfComplete(sessionId, tenantCloudId, roomMemberKeys) { const result = await withSessionCas(tenantCloudId, sessionId, (session) => { if (session.state !== 'VOTING') return undefined; - const allVoted = roomMemberCount > 0 && - session.votes.size === roomMemberCount; + + // Only count votes from current room members + const memberVoteCount = [...session.votes.keys()] + .filter((k) => roomMemberKeys.has(k)).length; + const allVoted = roomMemberKeys.size > 0 && + memberVoteCount === roomMemberKeys.size; if (!allVoted) return undefined; // no mutation needed + // Strip stale votes from non-members + for (const key of session.votes.keys()) { + if (!roomMemberKeys.has(key)) session.votes.delete(key); + } + const numericVotes = [...session.votes.values()] .map(Number) .filter(Number.isFinite); @@ -207,10 +216,15 @@ export async function revealIfComplete(sessionId, tenantCloudId, roomMemberCount }; } -export async function forceReveal(sessionId, tenantCloudId) { +export async function forceReveal(sessionId, tenantCloudId, roomMemberKeys) { const result = await withSessionCas(tenantCloudId, sessionId, (session) => { if (session.state !== 'VOTING') return undefined; + // Strip stale votes from non-members + for (const key of session.votes.keys()) { + if (!roomMemberKeys.has(key)) session.votes.delete(key); + } + const numericVotes = [...session.votes.values()] .map(Number) .filter(Number.isFinite); @@ -225,7 +239,7 @@ export async function forceReveal(sessionId, tenantCloudId) { return session; }); - if (!result) { + if (!result || result.state !== 'REVEALED') { const current = await getSession(tenantCloudId, sessionId); if (!current) return null; return getSnapshot(current);