Sig Select gate-view: CONT GAME/NVM keep the acting seat; restore the live countdown numeral on load — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful

Two Sig-Select seat-switch follow-ups from the 2026-06-05 countdown sprint.

(1) CONT GAME + gear NVM pos-1 shuttle.
The GATE VIEW (room_gate.html) CONT GAME button and the gear-menu NVM both
linked to epic:room with NO ?seat, so a CARTE multi-seat gamer acting at pos 4
got shuttled back to owned[0] (pos 1) instead of staying on his acting seat —
the same gap bedc489 fixed for the GATE VIEW nav buttons. room_gate now
computes a single table_url (epic:room + ?seat=<current_slot> when seated) and
feeds it to both the CONT GAME onclick and the NVM include. The table view
already reads ?seat, so the gamer lands on the seat he was viewing. Added
`reverse` import to epic/views.py. 2 ITs in CarteTrayFollowsSelectedSeatTest
(cont-game + nvm carry the acting seat; default targets lowest owned); updated
RoomGateViewTest.test_nvm_returns_to_room_hex to expect the seat-carrying href.

(2) Live countdown numeral not restored on a fresh seat view mid-countdown.
countdown_start is a ONE-SHOT WS broadcast: a gamer (esp. a CARTE owner
switching to an already-ready seat) who loads the view after it fired saw a
static WAIT NVM, never the 12s flashing numeral — the redirect still fired, so
it was a visual-restore-on-load gap. The cache entry now stores the absolute
deadline alongside the timer token ({token, deadline} dict, was a bare token
string); _fire's token guard reads either shape defensively so a stale string
from an older deploy can't crash the callback. New tasks.countdown_remaining()
derives the seconds-left from the deadline (None when no countdown / elapsed).
The room view seeds ctx["countdown_remaining"] for the acting polarity;
_sig_select_overlay.html carries data-countdown-remaining; sig-select.js's
_replayReservations restores _showCountdown(remaining) on load when a count is
live, else falls back to WAIT NVM. 4 unit tests (CountdownRemainingTest), 2 ITs
(SigSelectRenderingTest), 3 Jasmine specs (SigSelectSpec countdown-restore).

922 epic+gameboard ITs + 19 task UTs + Jasmine SpecRunner all green.
Trap [[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-05 12:25:54 -04:00
parent e10f0f3939
commit faaa4ecfb0
9 changed files with 257 additions and 13 deletions

View File

@@ -10,6 +10,7 @@ from django.contrib.auth.decorators import login_required
from django.db import transaction
from django.http import HttpResponse, HttpResponseForbidden, JsonResponse
from django.shortcuts import redirect, render
from django.urls import reverse
from django.template.loader import render_to_string
from django.utils import timezone
@@ -610,8 +611,15 @@ def _role_select_context(room, user, seat_param=None):
str(res.card_id): res.role
for res in room.sig_reservations.filter(polarity=polarity_const)
}
# Seconds left on a live polarity countdown, so a gamer landing on
# this seat view mid-countdown restores the flashing numeral instead
# of a static WAIT NVM (countdown_start is a one-shot WS broadcast).
# None when no countdown is running → template renders 0.
from apps.epic.tasks import countdown_remaining
ctx["countdown_remaining"] = countdown_remaining(room.id, polarity_const)
else:
reservations = {}
ctx["countdown_remaining"] = None
ctx["sig_reservations_json"] = json.dumps(reservations)
if user_polarity == 'levity':
@@ -789,8 +797,16 @@ def room_gate(request, room_id):
# partial's slot conditionals read. The renewal modal's own keys override
# below.
ctx = _gate_context(room, request.user, request.GET.get("seat"))
# CONT GAME + the gear NVM both return to the table hex — but they must land
# the gamer on his ACTING seat (the ?seat-selected pos-circle), not owned[0].
# Without the ?seat a CARTE multi-seat gamer acting at pos 4 got shuttled
# back to pos 1. Mirrors the GATE VIEW nav buttons (bedc489).
table_url = reverse("epic:room", args=[room.id])
if ctx.get("current_slot"):
table_url += f"?seat={ctx['current_slot']}"
ctx.update({
"room": room,
"table_url": table_url,
"cost_current": user_slot.cost_current if user_slot else True,
"deposited_count": room.gate_slots.filter(status=GateSlot.FILLED).count(),
"page_class": "page-gameboard page-room page-room-gate",