my-sea: async witness — spectators see the owner's draw land live via WS, no refresh — TDD
The bud watching @owner's sea now sees each card appear in real time instead of having to refresh. Follows the epic RoomConsumer broadcast pattern (view -> group_send -> consumer handler -> send_json), keyed on the owner (mysea_<owner>) since my-sea has no Room. - apps/gameboard/consumers.py: MySeaSpectateConsumer — read-only WS; membership gate matches voice (owner OR present invitee: ACCEPTED + deposited + not left). Relays a `sea_draw` event carrying the owner's full hand. - apps/gameboard/routing.py + core/asgi.py: ws/my-sea-spectate/<owner_id>/. - gameboard/views.py: _notify_sea_draw(owner_id, hand) — best-effort, guarded group_send so a down/missing channel layer can't break the solo draw. Fired from my_sea_lock (both the create + the mid-draw-upsert branch) and from my_sea_delete (empty hand -> clears the spectators' cross). - my_sea_visit.html: a WS listener fills the cross live — SeaDeal.register(card, '.sea-pos-'+pos, isLevity) reuses _fillSlot (incl. the --rank-long squeeze) + seeds the slot clickable into the stage; a DEL re-empties cleared slots. Capped reconnect for transient blips. Tests: 5 channels ITs (owner/present-invitee connect + receive; unauth / stranger / accepted-not-present rejected); +2 view ITs (lock broadcasts owner+ hand; lock still 200s when the broadcast raises). Client fill needs live two-party verification on staging (Redis up). Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -450,6 +450,25 @@ def _tax_brief_payload(user, slug_marker, dismissed_at, dismiss_url):
|
||||
return payload
|
||||
|
||||
|
||||
def _notify_sea_draw(owner_id, hand):
|
||||
"""Best-effort push of the owner's current hand to watching invitees (the
|
||||
my-sea spectate WS, `mysea_<owner_id>`) so they witness each card land
|
||||
without refreshing. Guarded — a missing/down channel layer must never break
|
||||
the solo draw, since the spectate is an enhancement, not a hard dependency."""
|
||||
try:
|
||||
from asgiref.sync import async_to_sync
|
||||
from channels.layers import get_channel_layer
|
||||
layer = get_channel_layer()
|
||||
if layer is None:
|
||||
return
|
||||
async_to_sync(layer.group_send)(
|
||||
f"mysea_{owner_id}",
|
||||
{"type": "sea_draw", "hand": hand},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@login_required(login_url="/")
|
||||
@require_POST
|
||||
def my_sea_lock(request):
|
||||
@@ -512,6 +531,7 @@ def my_sea_lock(request):
|
||||
existing.paid_through_at = None
|
||||
update_fields.append("paid_through_at")
|
||||
existing.save(update_fields=update_fields)
|
||||
_notify_sea_draw(request.user.id, existing.hand) # live to spectators
|
||||
return JsonResponse({
|
||||
"ok": True,
|
||||
"next_free_draw_at": (
|
||||
@@ -549,6 +569,7 @@ def my_sea_lock(request):
|
||||
significator_id=sig_id,
|
||||
significator_reversed=request.user.significator_reversed,
|
||||
)
|
||||
_notify_sea_draw(request.user.id, draw.hand) # live to spectators
|
||||
# Append the @taxman ledger entry + spawn the Brief. Response carries
|
||||
# the Brief payload so the picker IIFE can surface the banner in-place
|
||||
# w.o. a page reload — same affordance the prior in-template
|
||||
@@ -584,6 +605,7 @@ def my_sea_delete(request):
|
||||
if draw is not None:
|
||||
draw.hand = []
|
||||
draw.save(update_fields=["hand"])
|
||||
_notify_sea_draw(request.user.id, []) # clear the spectators' cross
|
||||
return HttpResponse(status=204)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user