PICK SEA Sprint A: async sky→sea transition via WS room:sky_confirmed — TDD

- natus_save: group_send room:sky_confirmed after confirm (carries seat_role)
- consumer: sky_confirmed handler rebroadcasts to room group
- _notify_sky_confirmed() helper mirrors _notify_pick_sky_available
- sea_partial view: renders _sea_overlay.html partial for in-page injection (403 if not sky_confirmed)
- epic:sea_partial URL registered
- _natus_overlay.html: data-user-seat-role attr; _onSkyConfirmed() fetches sea partial,
  removes natus overlay + backdrop, injects sea HTML, toggles sea-open on html root;
  room:sky_confirmed WS listener calls _onSkyConfirmed only for matching seat role
- user_seat_role added to SKY_SELECT context
- FT: PickSeaAsyncTransitionTest (3 tests, ChannelsFunctionalTest) — sea overlay,
  natus gone, sea-open class — all green

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-04-28 22:16:38 -04:00
parent 379e0ab80c
commit 39e12d6a3d
5 changed files with 196 additions and 4 deletions

View File

@@ -90,5 +90,8 @@ class RoomConsumer(AsyncJsonWebsocketConsumer):
async def pick_sky_available(self, event):
await self.send_json(event)
async def sky_confirmed(self, event):
await self.send_json(event)
async def cursor_move(self, event):
await self.send_json(event)

View File

@@ -27,4 +27,5 @@ urlpatterns = [
path('room/<uuid:room_id>/tarot/deal', views.tarot_deal, name='tarot_deal'),
path('room/<uuid:room_id>/natus/preview', views.natus_preview, name='natus_preview'),
path('room/<uuid:room_id>/natus/save', views.natus_save, name='natus_save'),
path('room/<uuid:room_id>/sea/partial', views.sea_partial, name='sea_partial'),
]

View File

@@ -126,6 +126,13 @@ def _notify_pick_sky_available(room_id):
)
def _notify_sky_confirmed(room_id, seat_role):
async_to_sync(get_channel_layer().group_send)(
f'room_{room_id}',
{'type': 'sky_confirmed', 'seat_role': seat_role},
)
SLOT_ROLE_LABELS = {1: "PC", 2: "NC", 3: "EC", 4: "SC", 5: "AC", 6: "BC"}
_SIG_SEAT_ORDERING = Case(
@@ -362,6 +369,7 @@ def _role_select_context(room, user):
)
sky_confirmed = confirmed_char is not None
ctx["sky_confirmed"] = sky_confirmed
ctx["user_seat_role"] = _canonical_seat.role if _canonical_seat else ''
if sky_confirmed:
ctx["my_tray_sig"] = confirmed_char.significator
@@ -1093,5 +1101,19 @@ def natus_save(request, room_id):
char.confirmed_at = timezone.now()
char.save()
if char.is_confirmed:
_notify_sky_confirmed(room_id, seat.role)
return JsonResponse({'id': char.id, 'confirmed': char.is_confirmed})
@login_required
def sea_partial(request, room_id):
"""Return the rendered sea overlay partial for in-page injection after sky confirm."""
room = Room.objects.get(id=room_id)
ctx = _role_select_context(room, request.user)
if not ctx.get('sky_confirmed'):
return HttpResponse(status=403)
return render(request, 'apps/gameboard/_partials/_sea_overlay.html', ctx)