diff --git a/src/apps/epic/tests/integrated/test_views.py b/src/apps/epic/tests/integrated/test_views.py
index 34c0538..c13969f 100644
--- a/src/apps/epic/tests/integrated/test_views.py
+++ b/src/apps/epic/tests/integrated/test_views.py
@@ -884,6 +884,41 @@ class CarteTrayFollowsSelectedSeatTest(TestCase):
response = self.client.get(self.room_url + "?seat=6")
self.assertIsNone(response.context["my_tray_sig"])
+ def test_gate_view_button_carries_acting_seat(self):
+ # Regression (gate-view pos-1 lockout): the GATE VIEW buttons linked
+ # to room_gate with NO ?seat, so the gate view fell back to owned[0]
+ # (pos 1) → pos 1 was always me-current + never got a switch href:
+ # the one seat you could never return to. The acting ?seat must ride
+ # the gate-view nav so the gate view's current matches the table.
+ gate_url = reverse("epic:room_gate", kwargs={"room_id": self.room.id})
+ response = self.client.get(self.room_url + "?seat=4")
+ self.assertEqual(response.context["current_slot"], 4)
+ self.assertContains(response, f"{gate_url}?seat=4")
+
+ def test_gate_view_button_default_targets_lowest_owned(self):
+ # No ?seat on the table → the gate button carries the lowest owned
+ # slot (current_slot), keeping the table + gate views in agreement.
+ gate_url = reverse("epic:room_gate", kwargs={"room_id": self.room.id})
+ response = self.client.get(self.room_url)
+ self.assertEqual(response.context["current_slot"], 1)
+ self.assertContains(response, f"{gate_url}?seat=1")
+
+ def test_gate_page_exposes_and_re_carries_acting_seat(self):
+ # The gate page's own navbar GATE VIEW re-click must keep the acting
+ # seat → _gate_context exposes current_slot too, and pos 1 is now
+ # me-also (switchable) while pos 4 is me-current — no lockout.
+ gate_url = reverse("epic:room_gate", kwargs={"room_id": self.room.id})
+ response = self.client.get(gate_url + "?seat=4")
+ self.assertEqual(response.context["current_slot"], 4)
+ self.assertContains(response, f"{gate_url}?seat=4")
+ by_slot = {
+ p["slot"].slot_number: p
+ for p in response.context["gate_positions"]
+ }
+ self.assertEqual(by_slot[4]["state_class"], "tt-pos-me-current")
+ self.assertTrue(by_slot[1]["is_me_also"])
+ self.assertEqual(by_slot[1]["state_class"], "tt-pos-me-also")
+
class PickRolesViewTest(TestCase):
def setUp(self):
diff --git a/src/apps/epic/views.py b/src/apps/epic/views.py
index c691db9..10a0ef3 100644
--- a/src/apps/epic/views.py
+++ b/src/apps/epic/views.py
@@ -336,6 +336,7 @@ def _expire_lapsed_seats(room):
def _gate_context(room, user, seat_param=None):
_expire_reserved_slots(room)
+ current_slot = _viewer_current_slot(room, user, seat_param)
slots = room.gate_slots.order_by("slot_number")
pending_slot = slots.filter(status=GateSlot.RESERVED).first()
user_reserved_slot = None
@@ -390,9 +391,10 @@ def _gate_context(room, user, seat_param=None):
"carte_slots_claimed": carte_slots_claimed,
"carte_nvm_slot_number": carte_nvm_slot_number,
"carte_next_slot_number": carte_next_slot_number,
- "gate_positions": _gate_positions(
- room, user, _viewer_current_slot(room, user, seat_param)
- ),
+ # Acting seat — exposed so the gate page's own GATE VIEW re-click
+ # re-carries ?seat (keeps the seat-switch context across reloads).
+ "current_slot": current_slot,
+ "gate_positions": _gate_positions(room, user, current_slot),
"starter_roles": [],
}
@@ -486,6 +488,11 @@ def _role_select_context(room, user, seat_param=None):
role_select_deck_id = seat_w_deck.deck_variant_id
ctx = {
"card_stack_state": card_stack_state,
+ # The viewer's acting seat (?seat slot, else lowest owned). Threaded
+ # onto the GATE VIEW nav so the gate view's current matches the table
+ # — without it the gate view falls back to owned[0] (pos 1), locking
+ # pos 1 as a never-switchable me-current circle.
+ "current_slot": current_slot,
"equipped_deck_id": role_select_deck_id,
"starter_roles": starter_roles,
"assigned_seats": assigned_seats,
diff --git a/src/templates/apps/gameboard/room.html b/src/templates/apps/gameboard/room.html
index 54b1529..f4ca32c 100644
--- a/src/templates/apps/gameboard/room.html
+++ b/src/templates/apps/gameboard/room.html
@@ -80,7 +80,7 @@
{# family]] #}
+ onclick="window.location.href='{% url 'epic:room_gate' room.id %}{% if current_slot %}?seat={{ current_slot }}{% endif %}'">GATE
VIEW
{% else %}
{% if room.table_status == "SKY_SELECT" %}
{% if sky_confirmed %}
diff --git a/src/templates/core/_partials/_navbar.html b/src/templates/core/_partials/_navbar.html
index 0960238..4704bef 100644
--- a/src/templates/core/_partials/_navbar.html
+++ b/src/templates/core/_partials/_navbar.html
@@ -45,7 +45,7 @@
id="id_navbar_gate_view_btn"
class="btn btn-primary"
type="button"
- onclick="window.location.href='{% if 'page-room' in page_class %}{% url 'epic:room_gate' room.id %}{% elif 'page-my-sea-visit' in page_class %}{% url 'my_sea_visit_gate' owner.id %}{% else %}{% url 'my_sea_gate' %}{% endif %}'"
+ onclick="window.location.href='{% if 'page-room' in page_class %}{% url 'epic:room_gate' room.id %}{% if current_slot %}?seat={{ current_slot }}{% endif %}{% elif 'page-my-sea-visit' in page_class %}{% url 'my_sea_visit_gate' owner.id %}{% else %}{% url 'my_sea_gate' %}{% endif %}'"
>
GATE
VIEW