my-sea spectator Phase B: seat-2C occupancy + visitor token gate + one-shot seated glow + gear BYE — TDD

Phase B of the my-sea invite → spectator → voice blueprint. An ACCEPTED
invitee can watch the owner's my-sea read-only, deposit a token to occupy
seat 2C (opening a 24h voice window for Phase C), and BYE out. Owner's
my_sea.html is left structurally intact — the spectator gets a dedicated,
simpler my_sea_visit.html; the read-only draw reuses the existing
`latest_draw_slots` payload (no picker surgery).

- B1: my_sea_visit(owner_id) spectator view — 403 unless an ACCEPTED
  SeaInvite(owner, request.user); owner bounced to their own my_sea. Context
  forces owner-only controls off (sea_btn_active=False, read_only=True);
  renders the table hex (1C owner / 2C visitor) + owner draw read-only.
- B2: visitor gate — my_sea_visit_gate reuses my_sea_gate.html w. a
  spectator branch (titles the OWNER's Sea, INSERT posts to the visitor
  endpoint, bud-panel suppressed, gear NVM→visit + BYE). Single-step
  my_sea_visit_insert_token selects+debits the visitor's token (same
  priority chain) and records token_deposited_at + a 24h voice_until on the
  SeaInvite → seat 2C present. Center btn flips GATE VIEW → VIEW DRAW.
- B3: spectator gear BYE — my_sea_visit_leave sets status=LEFT, left_at,
  clears voice_until (frees 2C, ends voice), redirects /gameboard/.
  _my_sea_gear.html gains a `leave_url`-gated BYE below NVM (owner pages
  pass no leave_url, so unchanged).
- B-seat: one-shot "seated" glow per user-spec 2026-05-27 — new shared
  apps/gameboard/my-sea-seats.js: on first view (localStorage-gated by a
  per-occupancy data-seat-token) an occupied seat flares --terUser +
  --ninUser glow ~1.5s then settles to full-opacity --secUser (.fa-ban
  already swapped to .fa-circle-check). _room.scss adds .seated /
  .seat-just-seated + the my-sea-seat-flare keyframes (mirrors the room's
  .active→.role-confirmed handoff). Wired on BOTH the spectator page (load)
  and the owner page (load + on the FREE DRAW seat-1 transition).
  MySeaSeatsSpec.js Jasmine spec covers the gating + timed class removal.
- B5: MySeaSpectatorFlowTest FT — accept → visit → GATE VIEW → deposit →
  VIEW DRAW + seat 2C seated.

URLs: my-sea/visit/<uuid:owner_id>/ (+ /gate/, /insert, /leave). 470 IT/UT
green; spectator FT + full Jasmine suite green. Phase C (WebRTC mesh voice
+ coturn droplet) next — the 24h voice_until window set here drives it.

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-27 13:35:00 -04:00
parent fb8563eed2
commit d0c39b51b6
15 changed files with 740 additions and 9 deletions

View File

@@ -6,6 +6,7 @@ Look!-formatted Brief-style line w. FYI (→ /billboard/my-sign/) + NVM
(→ /gameboard/) instead of the draw UX. The My Sea applet on /gameboard/
mirrors the gate hint in its empty-state slot.
"""
from django.utils import timezone
from selenium.webdriver.common.by import By
from .base import FunctionalTest
@@ -1891,6 +1892,61 @@ class MySeaInviteAcceptanceLogTest(FunctionalTest):
self.assertEqual(bye.text.upper(), "BYE")
class MySeaSpectatorFlowTest(FunctionalTest):
"""Phase B of [[my-sea-invite-voice-blueprint]] — an ACCEPTED invitee
visits the owner's my-sea, deposits a token at the visitor gate, and
thereby occupies seat 2C (the center btn flips GATE VIEW → VIEW DRAW)."""
def setUp(self):
super().setUp()
self.browser.set_window_size(800, 1200) # portrait — center-hex clicks
_seed_gameboard_applets()
from apps.gameboard.models import MySeaDraw, SeaInvite
self.owner = User.objects.create(email="owner@test.io", username="discoman")
# Owner has drawn → seat 1C is occupied + there's a draw to view.
MySeaDraw.objects.create(
user=self.owner, spread="situation-action-outcome",
significator_id=1,
hand=[{"position": "lay", "card_id": 1, "reversed": False,
"polarity": "gravity"}],
)
self.email = "bud@test.io"
self.bud = User.objects.create(email=self.email, username="budster")
self.invite = SeaInvite.objects.create(
owner=self.owner, invitee=self.bud, invitee_email=self.email,
status=SeaInvite.ACCEPTED, accepted_at=timezone.now(),
)
def test_spectator_deposits_token_to_occupy_seat_2c(self):
from django.urls import reverse
self.create_pre_authenticated_session(self.email)
self.browser.get(
self.live_server_url + reverse("my_sea_visit", args=[self.owner.id])
)
# Before deposit: GATE VIEW shown, seat 2C not yet seated.
gate_btn = self.wait_for(
lambda: self.browser.find_element(By.ID, "id_my_sea_gate_view_btn")
)
seat2 = self.browser.find_element(
By.CSS_SELECTOR, ".table-seat[data-slot='2']"
)
self.assertNotIn("seated", seat2.get_attribute("class"))
# GATE VIEW → visitor gate → INSERT TOKEN (single-step deposit).
gate_btn.click()
insert = self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-rails")
)
insert.click()
# Back on the visit page: VIEW DRAW now shown, seat 2C seated.
self.wait_for(
lambda: self.browser.find_element(By.ID, "id_my_sea_view_draw_btn")
)
seat2 = self.browser.find_element(
By.CSS_SELECTOR, ".table-seat[data-slot='2']"
)
self.assertIn("seated", seat2.get_attribute("class"))
class MySeaGearBtnTest(FunctionalTest):
"""Sprint 6 iter 6c — `.gear-btn` on every my-sea page state
(landing / picker / gatekeeper). Opens a NVM-only menu (DEL/BYE