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>
Extends the async-witness WS so a visitor joining (deposit) or leaving (BYE)
pushes the seat ring to the other watchers — they see members come + go without
a refresh, same channel as the live draw.
- views.py: `_my_sea_seats(owner)` extracted (owner 1C + present invitees 2C-6C
by deposit order, sans per-viewer is_self) — used by BOTH the my_sea_visit
render (layers is_self on) AND a new guarded `_notify_sea_seats(owner_id)`
broadcast. Fired from my_sea_visit_insert_token (seat taken) +
my_sea_visit_leave (seat freed).
- consumers.py: MySeaSpectateConsumer gains a `sea_seats` handler.
- my_sea_visit.html: the WS client re-renders the `.table-seat` ring from a
`sea_seats` message, re-marking the viewer's own --self chair via the embedded
seat token + re-firing the one-shot seated glow (localStorage-gated).
Tests: +1 channels relay IT (sea_seats received) + 2 view ITs (deposit / BYE
each broadcast the ring). Existing multi-seat ITs stay green on the refactored
helper. Client re-render needs live 3-party verification on staging.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The bud watching @owner's sea now sees each card appear in real time instead of
having to refresh. Follows the epic RoomConsumer broadcast pattern (view ->
group_send -> consumer handler -> send_json), keyed on the owner (mysea_<owner>)
since my-sea has no Room.
- apps/gameboard/consumers.py: MySeaSpectateConsumer — read-only WS; membership
gate matches voice (owner OR present invitee: ACCEPTED + deposited + not
left). Relays a `sea_draw` event carrying the owner's full hand.
- apps/gameboard/routing.py + core/asgi.py: ws/my-sea-spectate/<owner_id>/.
- gameboard/views.py: _notify_sea_draw(owner_id, hand) — best-effort, guarded
group_send so a down/missing channel layer can't break the solo draw. Fired
from my_sea_lock (both the create + the mid-draw-upsert branch) and from
my_sea_delete (empty hand -> clears the spectators' cross).
- my_sea_visit.html: a WS listener fills the cross live — SeaDeal.register(card,
'.sea-pos-'+pos, isLevity) reuses _fillSlot (incl. the --rank-long squeeze) +
seeds the slot clickable into the stage; a DEL re-empties cleared slots.
Capped reconnect for transient blips.
Tests: 5 channels ITs (owner/present-invitee connect + receive; unauth /
stranger / accepted-not-present rejected); +2 view ITs (lock broadcasts owner+
hand; lock still 200s when the broadcast raises). Client fill needs live
two-party verification on staging (Redis up).
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.8 (1M context) <noreply@anthropic.com>