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

@@ -0,0 +1,103 @@
#!/usr/bin/env python
import importlib
import inspect
import os
import sys
from enum import Enum, IntEnum, StrEnum
# Adjust these for your project
PY_MODULE = "draft.constants" # where your enums live
OUTPUT_PATH = "frontend/src/apps/draft/constants.js"
# Optionally allow running from any cwd
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = os.path.abspath(os.path.join(PROJECT_ROOT, ".."))
if PROJECT_ROOT not in sys.path:
sys.path.insert(0, PROJECT_ROOT)
def js_quote(s: str) -> str:
return s.replace("\\", "\\\\").replace('"', '\\"')
def titleize(name: str) -> str:
# e.g., "DETERMINE_ORDER" -> "Determine Order"
return name.replace("_", " ").title()
def emit_header():
return "// AUTO-GENERATED. Do not edit by hand.\n" \
"// Run: python scripts/generate_js_constants.py\n\n"
def emit_str_enum(name: str, enum_cls) -> str:
"""
Emit a JS object for StrEnum:
export const DraftMessage = { KEY: "value", ... };
"""
lines = [f"export const {name} = {{"] # ESM export
for member in enum_cls:
lines.append(f' {member.name}: "{js_quote(member.value)}",')
lines.append("};\n")
return "\n".join(lines)
def emit_int_enum(name: str, enum_cls) -> str:
"""
Emit a JS object + labels + ordered list for IntEnum:
export const DraftPhase = { KEY: number, ... };
export const DraftPhaseLabel = { [number]: "Pretty", ... };
export const DraftPhasesOrdered = [numbers...];
"""
lines = [f"export const {name} = {{"] # ESM export
items = list(enum_cls)
# object map
for member in items:
lines.append(f" {member.name}: {int(member.value)},")
lines.append("};\n")
# label map (use .pretty_name if you added it; else derive from name or __str__)
lines.append(f"export const {name}Label = {{")
for member in items:
if hasattr(member, "pretty_name"):
label = getattr(member, "pretty_name")
else:
# fall back: __str__ if you overload it, else Title Case of name
label = str(member)
if label == f"{enum_cls.__name__}.{member.name}":
label = titleize(member.name)
lines.append(f' [{name}.{member.name}]: "{js_quote(label)}",')
lines.append("};\n")
# ordered list
ordered = sorted(items, key=lambda m: int(m.value))
ordered_vals = ", ".join(f"{name}.{m.name}" for m in ordered)
lines.append(f"export const {name}sOrdered = [{ordered_vals}];\n")
return "\n".join(lines)
def main():
mod = importlib.import_module(PY_MODULE)
out = [emit_header()]
# Pick which enums to export. You can filter here if you dont want all.
for name, obj in inspect.getmembers(mod):
ignore_classes = [Enum, IntEnum, StrEnum]
if inspect.isclass(obj) and issubclass(obj, Enum) and not obj in ignore_classes:
# Skip helper classes that arent actual Enums
if name.startswith("_"):
continue
if issubclass(obj, IntEnum):
out.append(emit_int_enum(name, obj))
else:
out.append(emit_str_enum(name, obj))
os.makedirs(os.path.dirname(OUTPUT_PATH), exist_ok=True)
with open(OUTPUT_PATH, "w", encoding="utf-8") as f:
f.write("\n".join(out))
print(f"✅ Wrote {OUTPUT_PATH}")
if __name__ == "__main__":
main()