room GATE VIEW: keep all six position circles solid — never fade with role-assigned

The gatekeeper's GATE VIEW is the canonical "who holds which position"
surface, but it reused _table_positions.html, which paints the
role-assigned fade-out class (opacity:0 / scale .5) server-side from
pos.role_assigned (slot <= assigned_count). So as gamers got seated
during Role Select, the gate-view circles vanished one-by-one in
lockstep with the table-hex — and by the time SCAN SIGS appeared (all
six roles assigned) the gate showed an empty hex. The disappear-as-
seated animation is meant as a TABLE-HEX-only cue.

Fix: _table_positions.html now suppresses role-assigned when its new
persist_circles flag is set; room_gate.html includes the partial with
persist_circles=True. room.html passes no flag (→ falsy), so the
table-hex keeps the fade animation untouched. No JS reads role-assigned
in the gate view (role-select.js isn't loaded there), so the server-side
guard is sufficient.

TDD: PositionTooltipRenderTest.test_gate_view_circles_never_fade_when_roles_assigned
— assigns all six roles, asserts the gate view keeps six .gate-slot
circles with NO role-assigned. Verified live via Claudezilla on a
SIG_SELECT setup_sig_session room. 47 render/gate ITs green
(RoleSelectRenderingTest still asserts the table-hex DOES fade).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-06-03 01:26:08 -04:00
parent c4279c5515
commit 148fcac7af
3 changed files with 26 additions and 2 deletions

View File

@@ -714,6 +714,22 @@ class PositionTooltipRenderTest(TestCase):
self.assertNotIn(self.gamers[1].email, content) # g2@test.io
self.assertNotIn(self.viewer.email, content) # disco@test.io
def test_gate_view_circles_never_fade_when_roles_assigned(self):
# The gatekeeper's GATE VIEW is the canonical "who holds which
# position" surface: it must ALWAYS show all six circles, even after
# gamers get seated and the main table-hex has faded their circles away
# one-by-one (the role-assigned animation). So when every role is
# assigned — the moment SCAN SIGS appears on the hex — the gate view
# keeps all six circles solid, with NO role-assigned (fade-out) class.
roles = ["PC", "NC", "EC", "SC", "AC", "BC"]
for i, gamer in enumerate(self.gamers, start=1):
TableSeat.objects.create(
room=self.room, gamer=gamer, slot_number=i, role=roles[i - 1],
)
content = self._gate_content()
self.assertEqual(content.count('class="gate-slot'), 6)
self.assertNotIn("role-assigned", content)
class PositionTooltipCarteRenderTest(TestCase):
"""CARTE-solo render contract: a single gamer owns all six slots — their

View File

@@ -6,9 +6,13 @@
{# appended AFTER `role-assigned` so the `gate-slot filled role-assigned` #}
{# substring (RoleSelectRenderingTest) stays intact, and `class` stays first #}
{# (before data-slot) for the class-attr regex IT. #}
{# `persist_circles` (passed True by room_gate.html) suppresses the #}
{# role-assigned fade-out so the GATE VIEW always keeps all six circles — #}
{# the disappear-as-seated animation is a table-hex-only cue. Undefined #}
{# (→ falsy) on the room.html include, which keeps the animation. #}
<div class="position-strip">
{% for pos in gate_positions %}
<div class="gate-slot{% if pos.slot.status == 'EMPTY' %} empty{% elif pos.slot.status == 'FILLED' %} filled{% elif pos.slot.status == 'RESERVED' %} reserved{% endif %}{% if pos.role_assigned %} role-assigned{% endif %}{% if pos.state_class %} {{ pos.state_class }}{% endif %}"
<div class="gate-slot{% if pos.slot.status == 'EMPTY' %} empty{% elif pos.slot.status == 'FILLED' %} filled{% elif pos.slot.status == 'RESERVED' %} reserved{% endif %}{% if pos.role_assigned and not persist_circles %} role-assigned{% endif %}{% if pos.state_class %} {{ pos.state_class }}{% endif %}"
data-slot="{{ pos.slot.slot_number }}"{% if pos.slot.gamer %} data-user-id="{{ pos.slot.gamer.id }}" data-tt-title="{{ pos.slot.gamer|at_handle }}" data-tt-description="the {{ pos.slot.gamer.active_title_display }}" data-tt-shoptalk="{{ pos.shoptalk }}" data-tt-tokens="{{ pos.tokens }}" data-tt-token-types="{{ pos.token_types|join:'|' }}"{% if pos.expiry %} data-tt-expiry="expires {{ pos.expiry|relative_ts }}"{% endif %}{% if pos.sign_rank %} data-tt-sign-rank="{{ pos.sign_rank }}" data-tt-sign-suit="{{ pos.sign_suit_icon }}"{% endif %}{% endif %}>
<span class="slot-number">{{ pos.slot.slot_number }}</span>
<span class="slot-gamer">{% if pos.slot.gamer %}{{ pos.slot.gamer|at_handle }}{% else %}empty{% endif %}</span>

View File

@@ -83,7 +83,11 @@
{# Position circles + their hover tooltips — the gate-view rendered no #}
{# circles before this sprint (the headline gap). Reuses the shared #}
{# _table_positions partial fed by the merged _gate_context. #}
{% include "apps/gameboard/_partials/_table_positions.html" %}
{# persist_circles=True keeps ALL six circles solid here: the gatekeeper #}
{# is the canonical "who holds which position" surface, so it must never #}
{# fade a circle away as roles get assigned (the role-assigned animation #}
{# is the TABLE-HEX cue only). They stay up to + including SCAN SIGS. #}
{% include "apps/gameboard/_partials/_table_positions.html" with persist_circles=True %}
{% include "apps/gameboard/_partials/_position_tooltip.html" %}
{# NVM nav-backs one step to the table hex (not out to /gameboard/). #}