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

@@ -0,0 +1,54 @@
// Jasmine spec for my-sea-seats.js — the one-shot "seated" glow module
// (Phase B of the my-sea invite/voice sprint). Verifies the localStorage-
// gated first-view behaviour + the timed flare-class removal.
describe('my-sea-seats one-shot seated glow', function () {
var seat;
beforeEach(function () {
window.localStorage.clear();
seat = document.createElement('div');
seat.className = 'table-seat seated';
document.body.appendChild(seat);
});
afterEach(function () {
if (seat && seat.parentNode) seat.parentNode.removeChild(seat);
window.localStorage.clear();
});
it('exposes playSeatGlow globally', function () {
expect(typeof window.playSeatGlow).toBe('function');
});
it('adds the seat-just-seated flare class', function () {
window.playSeatGlow(seat);
expect(seat.classList.contains('seat-just-seated')).toBe(true);
});
it('marks a tokened seat seen in localStorage', function () {
seat.setAttribute('data-seat-token', 'visit-42');
window.playSeatGlow(seat);
expect(window.localStorage.getItem('mysea-seat-seen:visit-42')).toBe('1');
});
it('does not replay the flare for an already-seen token', function () {
seat.setAttribute('data-seat-token', 'visit-42');
window.localStorage.setItem('mysea-seat-seen:visit-42', '1');
window.playSeatGlow(seat);
expect(seat.classList.contains('seat-just-seated')).toBe(false);
});
it('always animates a tokenless seat (no persistence)', function () {
window.playSeatGlow(seat);
expect(seat.classList.contains('seat-just-seated')).toBe(true);
});
it('removes the flare class after the ~1.5s glow window', function () {
jasmine.clock().install();
window.playSeatGlow(seat);
expect(seat.classList.contains('seat-just-seated')).toBe(true);
jasmine.clock().tick(1600);
expect(seat.classList.contains('seat-just-seated')).toBe(false);
jasmine.clock().uninstall();
});
});

View File

@@ -31,6 +31,7 @@
<script src="RowLockSpec.js"></script>
<script src="WalletShopSpec.js"></script>
<script src="BurgerSpec.js"></script>
<script src="MySeaSeatsSpec.js"></script>
<!-- src files -->
<script src="/static/apps/applets/row-lock.js"></script>
<script src="/static/apps/dashboard/dashboard.js"></script>
@@ -45,6 +46,7 @@
<script src="/static/apps/epic/sea.js"></script>
<script src="/static/apps/epic/burger-btn.js"></script>
<script src="/static/apps/gameboard/game-kit.js"></script>
<script src="/static/apps/gameboard/my-sea-seats.js"></script>
<script src="/static/apps/gameboard/d3.min.js"></script>
<script src="/static/apps/gameboard/sky-wheel.js"></script>
<!-- Jasmine env config (optional) -->