fix: manual my-sea draws persist on refresh + reloaded slots stay clickable — root cause was SeaDeal stamping slot.dataset.posKey w. selector form (".sea-pos-cover") while my-sea's inline _collectHandFromDom + template's _my_sea_slot.html use raw names ("cover"). Key mismatch silently dropped manual draws from the lock POST → server rejected empty hand → no row → refresh showed empty state. AUTO DRAW worked only because it assembled fullHand w. raw posNames directly, bypassing the broken collector. TDD — 2 new FTs pin the contract:
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful

- test_manual_draw_persists_on_refresh
- test_reloaded_slot_can_reopen_stage_modal_on_click

Changes:
- sea.js: stamp `dataset.posKey` w. raw name (strip `.sea-pos-` prefix); `_seaHand` keyed by raw; `_viewingPos` is raw too (`_hideStage` prefixes when querySelector'ing); new `SeaDeal.seedHand(handByPosName)` public method for init-time DOM-walk seeding.
- my_sea.html inline init: walk server-rendered filled slots, look up each card by `data-card-id` from the embedded deck JSON, reconstruct per-instance `reversed` + polarity from the slot's classes, hand the map to `SeaDeal.seedHand`. Without this, reloaded slots short-circuit the overlay click handler on `if (!_seaHand[pos]) return;`.

The gameroom-side SeaDeal callers in `_sea_overlay.html` continue to pass selector form (SeaDeal accepts either — `_posName` helper strips prefix tolerantly).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-20 15:08:49 -04:00
parent bb44aa326a
commit 97a6da28a5
3 changed files with 147 additions and 9 deletions

View File

@@ -6,8 +6,24 @@ var SeaDeal = (function () {
var fyiPanel, fyiTitle, fyiType, fyiEffect, fyiIndex, fyiPrev, fyiNext;
var _userPolarity = 'levity';
var _seaHand = {}; // posSelector → {card, isLevity}
// posName (raw, e.g. "cover") → {card, isLevity}. The KEY here MUST
// match what `_collectHandFromDom` in `my_sea.html` reads (which is
// `slot.dataset.posKey`, stamped raw by both the template + the
// inline `_fillSlot` shim). Storing the SELECTOR form ".sea-pos-
// cover" instead silently broke persistence (manual draws never
// POSTed) — fixed 2026-05-21.
var _seaHand = {};
// Raw position name (e.g. "cover") of the currently-showing stage.
// `_hideStage` prefixes `.sea-pos-` when querySelector'ing the cell.
var _viewingPos = null;
// Internal helper — strip the `.sea-pos-` prefix off a selector to
// get the raw position name. Tolerant of inputs that are already
// raw (no prefix → returned as-is) so callers don't need to know
// which form they have.
function _posName(posSelector) {
return (posSelector || '').replace(/^\.sea-pos-/, '');
}
var _infoData = [];
var _infoIdx = 0;
var _infoOpen = false;
@@ -72,7 +88,12 @@ var SeaDeal = (function () {
// XLIII / XLVIII) need horizontal squeezing to fit the slot — see SCSS.
if ((card.corner_rank || '').length >= 5) slot.classList.add('sea-card-slot--rank-long');
slot.dataset.cardId = String(card.id);
slot.dataset.posKey = posSelector;
// Raw position name (e.g. "cover"), NOT the selector form. The
// my-sea inline `_collectHandFromDom` reads this + looks up by
// raw name from `_currentOrder()`; the template's empty-slot
// partial also stamps `data-pos-key="{{ position }}"` raw.
// Standardize so both code paths agree (2026-05-21 fix).
slot.dataset.posKey = _posName(posSelector);
slot.innerHTML =
'<span class="fan-corner-rank">' + card.corner_rank + '</span>' +
(card.suit_icon ? '<i class="fa-solid ' + card.suit_icon + '"></i>' : '');
@@ -90,7 +111,7 @@ var SeaDeal = (function () {
function _hideStage() {
// Reveal the deposited card in its slot (opacity 0 → 0.6 transition)
if (_viewingPos) {
var cell = overlay.querySelector(_viewingPos);
var cell = overlay.querySelector('.sea-pos-' + _viewingPos);
if (cell) {
var slot = cell.querySelector('.sea-card-slot--filled');
if (slot) slot.classList.add('sea-card-slot--visible');
@@ -105,8 +126,9 @@ var SeaDeal = (function () {
// ── Public API ─────────────────────────────────────────────────────────────
function openStage(card, posSelector, isLevity) {
_viewingPos = posSelector;
_seaHand[posSelector] = { card: card, isLevity: isLevity };
var posName = _posName(posSelector);
_viewingPos = posName;
_seaHand[posName] = { card: card, isLevity: isLevity };
_populate(card, isLevity);
_fillSlot(posSelector, card, isLevity);
_showStage(isLevity);
@@ -115,16 +137,30 @@ var SeaDeal = (function () {
// Like `openStage` but DOESN'T show the stage modal — used by AUTO
// DRAW (my_sea.html) to place cards quietly while keeping them
// clickable later. The overlay click handler reads `_seaHand[pos]`;
// without an entry here, click silently no-ops (the user-reported
// without an entry here, click silently no-ops (user-reported
// bug fixed 2026-05-21). Routing slot fill thru SeaDeal's internal
// `_fillSlot` (instead of the inline shim) also ensures
// `dataset.posKey` stays consistent w. the manual-FLIP path
// (selector form like ".sea-pos-cover", not raw "cover").
// `dataset.posKey` stays consistent w. the manual-FLIP path.
function register(card, posSelector, isLevity) {
_seaHand[posSelector] = { card: card, isLevity: isLevity };
_seaHand[_posName(posSelector)] = { card: card, isLevity: isLevity };
_fillSlot(posSelector, card, isLevity);
}
// Seed `_seaHand` from outside SeaDeal — used by my-sea's inline
// IIFE on init to make server-rendered (saved-hand) slots re-
// clickable after a refresh. Caller walks the filled DOM slots,
// looks up each card by `data-card-id` against the embedded deck
// JSON, and passes a `{posName: {card, isLevity}}` map here. Does
// NOT touch slot DOM — the slots are already filled by the server
// template; we just need the in-memory lookup to resolve the
// overlay click handler. Idempotent.
function seedHand(handByPosName) {
if (!handByPosName) return;
Object.keys(handByPosName).forEach(function (posName) {
_seaHand[posName] = handByPosName[posName];
});
}
// ── Init ──────────────────────────────────────────────────────────────────
function init() {
@@ -253,6 +289,7 @@ var SeaDeal = (function () {
return {
openStage: openStage,
register: register,
seedHand: seedHand,
resetHand: resetHand,
reinit: init, // call after overlay is injected into the DOM
_testInit: function () {