sig select: redirect a seatless multi-seat (CARTE) owner to ?seat=<owned[0]> so tray + overlay + reserve align — TDD

A CARTE owner (all 6 seats, both polarities) entering SIG_SELECT with no ?seat
saw the sig overlay + reserve URL locked to the canonical PC seat (one polarity)
while the TRAY followed `current_slot` = owned[0] (the lowest owned slot, often
the OTHER polarity). A sig reserved from that view filed against the WRONG seat
— the seat the owner thought he covered stayed empty — so that polarity never
reached 3-ready, its 12s countdown ran to 0 but the server-side `_fire` bailed at
`len(ready) < 3` and never advanced; the other polarity proceeded. Switching
pos-circles via GATE VIEW (sets ?seat) re-aligned every surface and unstuck it.

A 3-agent trace confirmed the mechanism + corrected my first guess: this is NOT a
WS problem (the cursor group only drives the cosmetic flashing numeral; the
SIG→SKY advance is a threading.Timer broadcasting to the room_<id> group every
socket joins). The stall is the misfiled reservation → 3-ready COMPLETENESS
failure, rooted in two seat resolvers disagreeing when seatless: the overlay /
sig_confirm use `_canonical_user_seat` (PC-first) while the tray / reserve use
`_viewer_current_slot` owned[0].

Fix: `room_view` redirects a seatless multi-seat owner (gate_slots.filter(gamer)
.count() > 1) in SIG_SELECT to ?seat=<current_slot>, so EVERY surface (tray,
overlay, reserve URL, WS cursor group) resolves to one seat via the already-correct
?seat path — the same realignment a GATE-VIEW switch does. SIG_SELECT-only
(SKY_SELECT already keys off selected_seat); single-seat gamers / non-owners / anon
fail the guard. Unaffected: the multi-gamer sig FTs (one seat each) + the WS-direct
CarteCursorGroupTest.

TDD: 6 ITs in CarteTrayFollowsSelectedSeatTest (redirect / ?seat-present no-redirect
/ overlay+tray agree post-redirect / single-seat / non-owner / SKY_SELECT
no-redirect); the red `'PC' != 'BC'` was the divergence itself. 381 epic-view ITs
green.

[[project-sig-select-seat-switch-open-problems]] [[feedback-ws-cursor-group-must-match-acting-seat]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-06-08 18:05:03 -04:00
parent dcfa54f522
commit a0499723d3
2 changed files with 82 additions and 1 deletions

View File

@@ -733,6 +733,30 @@ def gatekeeper(request, room_id):
def room_view(request, room_id):
room = Room.objects.get(id=room_id)
_expire_lapsed_seats(room)
# CARTE seat alignment: a multi-seat owner arriving at SIG_SELECT with no
# ?seat gets the sig overlay + reserve URL locked to the canonical PC seat,
# while the TRAY follows owned[0] (often a different polarity). A sig reserved
# from that view files against the WRONG seat → the seat the owner thought he
# covered stays empty → that polarity never reaches 3-ready → its countdown
# runs to 0 but _fire bails at `len(ready) < 3`, so it never advances. Redirect
# to ?seat=<owned[0]> so every surface (tray, overlay, reserve URL, WS cursor
# group) resolves to ONE seat via the proven ?seat path — the same realignment
# a GATE-VIEW switch performs. SIG_SELECT-only: SKY_SELECT already keys its
# overlay off selected_seat (~L639); single-seat gamers / non-owners fail the
# owner test (gate-slot ownership, matching _viewer_current_slot). Staging bug
# 2026-06-08, trace-confirmed (the misfiled-reservation chain).
if (
request.GET.get("seat") is None
and room.table_status == Room.SIG_SELECT
and request.user.is_authenticated
and room.gate_slots.filter(gamer=request.user).count() > 1
):
current_slot = _viewer_current_slot(room, request.user, None)
if current_slot is not None:
return redirect(
f"{reverse('epic:room', kwargs={'room_id': room.id})}"
f"?seat={current_slot}"
)
ctx = _role_select_context(room, request.user, request.GET.get("seat"))
ctx["room"] = room
# `page-room` drives the navbar GATE VIEW swap (mirrors my-sea's