my-sea spectate: live spread-sync + owner seat-ring push + visit caption fix — TDD
Three fixes to the my-sea spectator (bud-sea), all flowing over the existing `mysea_<owner>` spectate consumer: VISIT CAPTIONS (.sea-pos-label) — two bugs left every CROWN/COVER/… caption blank on my_sea_visit: - empty-hand: `label_by_position` was built from `latest_draw_slots`, which returns [] when the owner's hand is empty (only a significator placed) — so an owner mid-setup showed no captions, while her OWN my_sea (whose JS seeds labels from the POSITION_LABELS constant) showed them. Now the view pulls captions straight from POSITION_LABELS[spread], drawn-cards-independent. - `--seciUser` typo (used once, never defined) → invalid colour dropped → the labels inherited the body colour, contrasting on some palettes but blending into the felt on others (read as "missing"). → `--secUser`. SPREAD-SYNC — the owner's live draw pushed only the hand, not the spread, so a post-DEL spread switch landed the new cards into the OLD spread's cells (the asymmetry the user hit: owner on desire-obstacle-solution, visitor still laid out as escape-velocity). The spread now rides each `sea_draw` broadcast; `_applySpread` re-sets `data-spread` (CSS keys cell visibility off it), re-captions from a server-sourced POSITION_LABELS json_script, + clears stale fills before `_applyHand` repopulates against the right layout. OWNER-SIDE LIVE SEAT PUSH — the owner's my_sea now subscribes to her own spectate WS for `sea_seats`, so visitors arriving (deposit → 2C-6C) / leaving (BYE) appear without a refresh, same broadcast the spectators get. The visit page's inline `_renderSeats` is hoisted into my-sea-seats.js as the shared `mySeaRenderSeats(seats, myToken)` (+ `mySeaConnectSeatRing`); each page passes its own self-token (owner page passes '' — her 1C isn't --self server-side). Coverage: - ITs: MySeaVisitEmptyHandLabelsTest (captions present + rendered for an empty hand); MySeaLockHandViewTest broadcast test asserts the spread arg; spectate consumer test asserts the hand+spread relay (channels). - Jasmine: 6 new MySeaSeatsSpec cases for mySeaRenderSeats (per-seat rebuild, --self by token, owner-page no-self, no-duplicate re-render, one-shot flare). - Live-verified in Firefox: captions paint khaki on the brown palette; a desire-obstacle-solution sync flips data-spread + relabels Solution/Obstacle/ Desire + hides leave/cover/lay. [[feedback-jsonfield-exclude-sqlite-null]] not implicated; spread map is a plain dict lookup. 304 gameboard ITs + Jasmine green. Code architected by Disco DeDisco <discodedisco@outlook.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -947,6 +947,18 @@
|
||||
{# the spectator page. Exposes window.playSeatGlow + auto-plays on #}
|
||||
{# load for any server-rendered .seated[data-seat-token] seat. #}
|
||||
<script src="{% static 'apps/gameboard/my-sea-seats.js' %}"></script>
|
||||
{# Live seat ring — subscribe the OWNER's page to her own spectate WS #}
|
||||
{# (`mysea_<id>`) so visitors arriving (deposit → 2C-6C) / leaving #}
|
||||
{# (BYE) appear without a refresh, the same `sea_seats` broadcast the #}
|
||||
{# spectators receive. Empty self-token: the owner's 1C isn't `--self` #}
|
||||
{# server-side, so the live re-render shouldn't add it either. #}
|
||||
<script>
|
||||
(function () {
|
||||
if (window.mySeaConnectSeatRing) {
|
||||
window.mySeaConnectSeatRing('{{ request.user.id }}', '');
|
||||
}
|
||||
}());
|
||||
</script>
|
||||
<script>
|
||||
(function () {
|
||||
var page = document.querySelector('.my-sea-page');
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
{# SPIN, FYI) off the owner's draw payload — see _my_sea_visit_cross.html. #}
|
||||
<div id="id_my_sea_visit_draw" class="my-sea-visit-draw" style="display:none">
|
||||
{{ sea_deck_data|json_script:"id_my_sea_deck" }}
|
||||
{{ position_labels|json_script:"id_my_sea_position_labels" }}
|
||||
{% include "apps/gameboard/_partials/_my_sea_visit_cross.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -147,37 +148,45 @@
|
||||
try { deck = JSON.parse((deckEl && deckEl.textContent) || '{}'); } catch (e) {}
|
||||
var byId = {};
|
||||
(deck.levity || []).concat(deck.gravity || []).forEach(function (c) { byId[c.id] = c; });
|
||||
// Per-spread caption map (server-sourced from POSITION_LABELS) — used to
|
||||
// re-caption the cross live when the owner switches spreads.
|
||||
var POSITION_LABELS = {};
|
||||
var plEl = document.getElementById('id_my_sea_position_labels');
|
||||
try { POSITION_LABELS = JSON.parse((plEl && plEl.textContent) || '{}'); } catch (e) {}
|
||||
// This viewer's own seat token — so a live seat re-render can re-mark
|
||||
// their --self chair without the server knowing who's watching.
|
||||
var MY_SEAT_TOKEN = 'visit-{{ sea_invite.id }}';
|
||||
|
||||
// Re-render the table-seat ring when presence changes (a deposit takes
|
||||
// a seat / a BYE frees one), so other watchers see members come + go
|
||||
// without a refresh. Mirrors the server seat loop in markup.
|
||||
function _renderSeats(seats) {
|
||||
var scene = document.querySelector('.room-table-scene');
|
||||
if (!scene || !seats) return;
|
||||
scene.querySelectorAll('.table-seat').forEach(function (s) { s.remove(); });
|
||||
seats.forEach(function (seat) {
|
||||
var isSelf = seat.token && seat.token === MY_SEAT_TOKEN;
|
||||
var div = document.createElement('div');
|
||||
div.className = 'table-seat' + (seat.present ? ' seated' : '')
|
||||
+ (isSelf ? ' table-seat--self' : '');
|
||||
div.setAttribute('data-slot', seat.n);
|
||||
if (seat.present && seat.token) div.setAttribute('data-seat-token', seat.token);
|
||||
div.innerHTML =
|
||||
'<i class="fa-solid fa-chair"></i>' +
|
||||
'<span class="seat-position-label">' + seat.label + '</span>' +
|
||||
'<i class="position-status-icon fa-solid ' +
|
||||
(seat.present ? 'fa-circle-check' : 'fa-ban') + '"></i>';
|
||||
scene.appendChild(div);
|
||||
// Re-sync the cross to the owner's CURRENT spread (carried on each
|
||||
// `sea_draw`). A post-DEL spread switch changes both which cells are
|
||||
// active (CSS keys off `[data-spread]`) AND their captions — without
|
||||
// this, the owner's new cards land into the OLD spread's cells, the
|
||||
// asymmetry the user reported 2026-05-30. Clears any stale fill so the
|
||||
// subsequent _applyHand repopulates against the right layout.
|
||||
function _applySpread(spread) {
|
||||
var cross = document.querySelector('.my-sea-cross');
|
||||
if (!cross || !spread || cross.getAttribute('data-spread') === spread) return;
|
||||
cross.setAttribute('data-spread', spread);
|
||||
var labels = POSITION_LABELS[spread] || {};
|
||||
cross.querySelectorAll('.sea-pos-label').forEach(function (el) {
|
||||
el.textContent = labels[el.dataset.position] || '';
|
||||
});
|
||||
// Re-fire the one-shot "just seated" glow for fresh occupancies
|
||||
// (localStorage-gated per token, so it only flares once).
|
||||
if (window.playSeatGlow) {
|
||||
scene.querySelectorAll('.table-seat.seated[data-seat-token]')
|
||||
.forEach(window.playSeatGlow);
|
||||
}
|
||||
// Wipe filled slots — the active position set just changed.
|
||||
cross.querySelectorAll('.sea-card-slot.sea-card-slot--filled').forEach(function (slot) {
|
||||
var crossing = slot.classList.contains('sea-card-slot--crossing');
|
||||
slot.className = 'sea-card-slot sea-card-slot--empty' + (crossing ? ' sea-card-slot--crossing' : '');
|
||||
slot.innerHTML = '';
|
||||
slot.removeAttribute('data-card-id');
|
||||
slot.removeAttribute('data-pos-key');
|
||||
});
|
||||
}
|
||||
window._mySeaApplySpread = _applySpread; // test seam
|
||||
|
||||
// Seat-ring re-render is shared with the owner's my_sea (DRY) — see
|
||||
// `mySeaRenderSeats` in my-sea-seats.js. Pass this viewer's own token so
|
||||
// their deposited chair re-marks `--self` on a live re-render.
|
||||
function _renderSeats(seats) {
|
||||
if (window.mySeaRenderSeats) window.mySeaRenderSeats(seats, MY_SEAT_TOKEN);
|
||||
}
|
||||
window._mySeaRenderSeats = _renderSeats; // test seam
|
||||
|
||||
@@ -230,7 +239,7 @@
|
||||
var msg;
|
||||
try { msg = JSON.parse(ev.data); } catch (e) { return; }
|
||||
if (!msg) return;
|
||||
if (msg.type === 'sea_draw') _applyHand(msg.hand);
|
||||
if (msg.type === 'sea_draw') { _applySpread(msg.spread); _applyHand(msg.hand); }
|
||||
else if (msg.type === 'sea_seats') _renderSeats(msg.seats);
|
||||
};
|
||||
// Brief capped reconnect for transient blips (no infinite loop if
|
||||
|
||||
Reference in New Issue
Block a user