Refactor draft messaging to unified enum-based protocol

- Replaced scattered message strings with `DraftMessage` `StrEnum` and
  numeric `DraftPhase` `IntEnum` for clear, centralized definitions.
- Added Python→JS constants sync via `scripts/generate_js_constants.py`
  to ensure backend/frontend parity.
- Refactored WebSocket consumers to use `broadcast.*` and
  `direct.message` handlers with `_dispatch_broadcast` for consistent
  event delivery.
- Enhanced `DraftStateManager` to store `draft_index` and explicitly
  manage `connected_participants`.
- Added colored logging config in settings for improved debugging.
- Frontend: split UI into `ParticipantList` and `DraftMoviePool`,
  extracted message handlers (`handleDraftStatusMessages`,
  `handleUserIdentifyMessages`), and updated components to use new
  message/phase enums.
This commit is contained in:
2025-08-10 13:16:07 -05:00
parent 24700071ed
commit 28c98afc32
11 changed files with 509 additions and 341 deletions

View File

@@ -1,53 +1,13 @@
// DraftAdmin.jsx
import React, { useEffect, useState } from "react";
import { useWebSocket } from "../WebSocketContext.jsx";
import { WebSocketStatus } from "../common/WebSocketStatus.jsx";
import { DraftMessage, DraftPhases, DraftPhase } from '../constants.js';
import { fetchDraftDetails } from "../common/utils.js"
const ParticipantList = ({ socket, participants, draftOrder }) => {
const [connectedParticipants, setConnectedParticipants] = useState([])
useEffect(() => {
const handleMessage = async ({ data }) => {
const message = JSON.parse(data)
const { type, payload } = message
console.log('socket changed', message)
if (payload?.connected_participants) {
const { connected_participants } = payload
setConnectedParticipants(connected_participants)
}
}
socket.addEventListener("message", handleMessage)
return () => {
socket.removeEventListener("message", handleMessage)
}
}, [socket])
const ListTag = draftOrder.length > 0 ? "ol" : "ul"
console.log
const listItems = draftOrder.length > 0 ? draftOrder.map(d => participants.find(p => p.username == d)) : participants
import { ParticipantList } from "../common/ParticipantList.jsx";
import { DraftMessage, DraftPhase, DraftPhaseLabel, DraftPhasesOrdered } from '../constants.js';
import { fetchDraftDetails, isEmptyObject, handleDraftStatusMessages, handleUserIdentifyMessages } from "../common/utils.js"
import { DraftMoviePool } from "../common/DraftMoviePool.jsx"
return (
<div className="participant-list-container">
<label>Particpants</label>
<ListTag className="participant-list">
{listItems.map((p, i) => (
<li key={i}>
<span>{p?.full_name}</span>
<div
className={
`ms-2 stop-light ${connectedParticipants.includes(p?.username) ? "success" : "danger"}`
}
></div>
</li>
))}
</ListTag>
</div>
)
}
const DraftPhaseDisplay = ({ draftPhase, nextPhaseHandler, prevPhaseHandler }) => {
return (
@@ -57,14 +17,14 @@ const DraftPhaseDisplay = ({ draftPhase, nextPhaseHandler, prevPhaseHandler }) =
<div className="change-phase"><button onClick={prevPhaseHandler}><i className="bi bi-chevron-left"></i></button></div>
<ol>
{
DraftPhases.map((p) => (
DraftPhasesOrdered.map((p) => (
<li key={p} className={p === draftPhase ? "current-phase" : ""}>
<span>{p}</span>
<span>{DraftPhaseLabel[p]}</span>
</li>
))
}
</ol>
<div className="change-phase"><button onClick={nextPhaseHandler}><i className="bi bi-chevron-right"></i></button></div>
<div className="change-phase"><button onClick={nextPhaseHandler}><i className="bi bi-chevron-right"></i></button></div>
</div>
</div>
)
@@ -72,86 +32,84 @@ const DraftPhaseDisplay = ({ draftPhase, nextPhaseHandler, prevPhaseHandler }) =
export const DraftAdmin = ({ draftSessionId }) => {
const socket = useWebSocket();
const [connectedParticipants, setConnectedParticipants] = useState([]);
const [draftDetails, setDraftDetails] = useState();
const [participants, setParticipants] = React.useState([]);
const [draftPhase, setDraftPhase] = useState();
const [draftOrder, setDraftOrder] = useState([]);
console.log(socket)
const [draftState, setDraftState] = useState({})
const [currentUser, setCurrentUser] = useState(null);
useEffect(() => {
fetchDraftDetails(draftSessionId)
.then((data) => {
console.log("Fetched draft data", data)
setParticipants(data.participants)
setDraftDetails(data)
})
}, [])
useEffect(() => {
useEffect(()=>{
if (!socket) return;
const openHandler = (event)=>{
console.log('Websocket Opened')
}
const closeHandler = (event)=>{
console.log('Websocket Closed')
}
socket.addEventListener('open', openHandler );
socket.addEventListener('close', closeHandler );
return ()=>{
socket.removeEventListener('open', openHandler );
socket.removeEventListener('close', closeHandler );
}
}, [socket])
useEffect(() => {
if (!socket) return;
const handleMessage = (event) => {
const message = JSON.parse(event.data)
const { type, payload } = message;
console.log(type, event)
if (!payload) return
if (type == DraftMessage.REQUEST.JOIN_PARTICIPANT) {
console.log('join request', data)
}
if (payload.phase) {
console.log('phase_change')
setDraftPhase(payload.phase)
}
if (payload.draft_order) {
console.log('draft_order', payload.draft_order)
setDraftOrder(payload.draft_order)
}
}
socket.addEventListener('message', handleMessage);
socket.onclose = (event) => {
console.log('Websocket Closed')
socket = null;
}
const draftStatusMessageHandler = (event) => handleDraftStatusMessages(event, setDraftState)
const userIdentifyMessageHandler = (event) => handleUserIdentifyMessages(event, setCurrentUser)
socket.addEventListener('message', draftStatusMessageHandler );
socket.addEventListener('message', userIdentifyMessageHandler );
return () => {
socket.removeEventListener('message', handleMessage)
socket.close();
socket.removeEventListener('message', draftStatusMessageHandler)
socket.removeEventListener('message', userIdentifyMessageHandler );
};
}, [socket]);
const handlePhaseChange = (target) => {
let destination
const origin = draftPhase
if (target == "next") {
console.log(DraftPhase)
console.log("phase to be changed", origin, target, DraftPhase.WAITING)
if (origin == "waiting"){
destination = DraftPhase.DETERMINE_ORDER
} else if (origin == "determine_order"){
destination = DraftPhase.NOMINATION
}
}
else if (target=="previous") {
const origin = draftState.phase
const originPhaseIndex = DraftPhasesOrdered.findIndex(i => i == origin)
console.log('origin phase index', originPhaseIndex)
if (target == "next" && originPhaseIndex < DraftPhasesOrdered.length) {
destination = DraftPhasesOrdered[originPhaseIndex + 1]
}
if (!destination) {return}
else if (target == "previous" && originPhaseIndex > 0) {
destination = DraftPhasesOrdered[originPhaseIndex - 1]
}
console.log(destination)
socket.send(
JSON.stringify(
{ type: DraftMessage.REQUEST.PHASE_CHANGE, origin, destination }
{ type: DraftMessage.PHASE_CHANGE_REQUEST, origin, destination }
)
)
}
const handleStartDraft = () => {
}
const handleAdvanceDraft = () => {
socket.send(
JSON.stringify(
{ type: DraftMessage.DRAFT_INDEX_ADVANCE_REQUEST }
)
)
}
const handleRequestDraftSummary = () => {
socket.send(
JSON.stringify(
{ type: DraftMessage.REQUEST.DRAFT_STATUS }
{ type: DraftMessage.STATUS_SYNC_REQUEST }
)
)
}
@@ -169,12 +127,14 @@ export const DraftAdmin = ({ draftSessionId }) => {
</div>
<ParticipantList
socket={socket}
participants={participants}
draftOrder={draftOrder}
draftState={draftState}
draftDetails={draftDetails}
isAdmin={true}
/>
<button onClick={handleAdvanceDraft} className="btn btn-primary">Advance Draft</button>
<DraftMoviePool draftDetails={draftDetails}></DraftMoviePool>
<DraftPhaseDisplay draftPhase={draftPhase} nextPhaseHandler={ ()=>{handlePhaseChange('next')}} prevPhaseHandler= {() => {handlePhaseChange('previous')}}></DraftPhaseDisplay>
<DraftPhaseDisplay draftPhase={draftState.phase} nextPhaseHandler={() => { handlePhaseChange('next') }} prevPhaseHandler={() => { handlePhaseChange('previous') }}></DraftPhaseDisplay>
</div>
);
};