The gameroom DRAW SEA phase now writes drama provenance, mirroring sig/sky. When a gamer's 6-card Celtic Cross COMPLETES, a SEA_DRAWN Scroll log publishes their affinity with the card sitting in their Role-correlated position.
- epic/views.py: ROLE_POSITION_MAP — the user's sixfold index (PC->crown, NC->leave, EC->loom, SC->cover, AC->cross, BC->lay; roles rotate each round, so a seat's CURRENT role drives it) + SEA_POSITION_LABELS (each spread's display label for a position KEY; Waite-Smith's Behind/Before/Beneath + Escape-Velocity's Leave/Loom/Lay both key to the same index). sea_save publishes SEA_DRAWN on the <6->6 completing transition only (a reload that re-POSTs the full hand can't double-publish); a re-draw first redacts the standing relinquishment, then publishes anew. sea_delete redacts the published affinity (the strikethrough) + records SEA_RELINQUISHED in its wake (the redact-pair). _sea_affinity_for mirrors SIG_READY's polarity-qualified name_title + corner abbrev; _redact_standing_sea_event tests 'not retracted' in PYTHON (the SQLite JSONField exclude-NULL trap).
- drama/models.py: SEA_DRAWN + SEA_RELINQUISHED verbs + to_prose ('draws {poss} Celtic Cross, finding affinity with the {card}{abbrev} in the {Position}.' / 'relinquishes {poss} affinity with the {card}.'); 'The ' stripped so a levity/gravity qualifier butts the proper name. The generic struck/retracted property renders the strikethrough + data-label=redact in _scroll.html unchanged.
TDD: 4 drama prose ITs (affinity statement, spread-label passthrough, relinquishment, struck-when-retracted) + 7 epic ITs (publish-on-complete, position=crown for PC, none-before-complete, no-double-publish-on-reload, DEL redacts+relinquishes, re-draw redacts-the-relinquishment+republishes, DEL-noop-when-nothing-published). 459 drama + epic-view ITs green.
- bundled (parallel work): rootvars.scss --sixUser/--sepUser/--octUser slot reassignments across the forest/khaki/blade palettes (tuning the new reelhouse h2 bgs) + a new --terMrb.
[[project-sea-select-scroll-provenance]] [[feedback-jsonfield-exclude-sqlite-null]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Voice (Phase C, room path) — the my_sea voice mesh now extends to the epic game room. room_view sets voice_active for a SEATED gamer (a TableSeat with their gamer) while table_status is in {ROLE_SELECT, SIG_SELECT, SKY_SELECT} — from ROLE_SELECT onset (all tokens committed, seats pre-created by pick_roles) continuously through SKY_SELECT, which hosts the in-page DRAW SEA felt; dark at IN_GAME. voice_room_id = the room UUID (the WebRTC mesh key), voice_muted_at = the persisted mute so an in-arc nav/refresh rejoins muted (mirrors my_sea). The burger fan's shared #id_voice_btn lights .active + carries data-room-id; room.html now includes voice-glow.js (the glow/pulse machine, coexisting w. the sea-btn glow handoff).
No consumer/JS change needed: RoomVoiceConsumer._can_join already gates an epic room on TableSeat membership, the ws/voice/<str:room_id> route serves both mysea-… + bare-UUID keys, and burger-btn.js/voice-mesh.js read data-room-id generically. TDD: RoomVoiceActivationTest (9 ITs — active across the arc, dark at IN_GAME, dark for a non-seated viewer, room-id = UUID, muted-at passthrough) + RoomVoiceConsumerEpicGateTest (2 channels ITs — seated gamer admitted + receives welcome; non-seated refused). 390 epic-view + 11 voice channels ITs green.
- _room.scss: POST composer kept as a flex row — the OK btn sits inline beside the 'Enter a post line' input (the felt/pill chrome stays salvaged in YARN, but the input<->OK row layout is retained; follow-up to 577ef30).
- bundled (parallel work): _room.scss reelhouse h2 font --priUser -> --secUser (SCROLL/POST/PULSE); rootvars.scss chroma-hue primaries brightened (yellow/lime/cyan/indigo/violet/fuschia/magenta).
[[project-my-sea-invite-voice-blueprint]] [[project-character-creation-spec]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The token + position-circle expiry tooltips hardcoded 'expires' and never
flipped to 'expired' once the time passed — a seat whose `cost_current_until`
(the 7d cost clock) lapsed still read 'expires 11:30 p.m.' (staging 2026-06-08).
- New `expiry_phrase(dt)` filter (lyric_extras): 'expires <when>' for a FUTURE
datetime, 'expired <when>' for a PAST one — the verb carries the tense so the
underlying `relative_ts` stays direction-agnostic. Wired into
`Token.tooltip_expiry` + the position-circle `data-tt-expiry` (position-tooltip.js
copies it verbatim, so no JS change).
- `relative_ts` gains a <60min → 'N min' bucket (buckets: 60min / 24h / 7d /
12mo / >12mo). Per user-spec it stays SHARED, so scroll.html's provenance feed
(+ post.html / my-games row-ts) now reads 'N min' for very recent events too.
TDD: relative_ts <60min past+future + the 1h boundary; expiry_phrase
none/future/past/wraps-relative_ts; billboard post-line test updated (3h→clock-
time, + new just-posted→'N min'). 727 lyric+billboard+gameboard ITs green.
Bundled (parallel work): rootvars.scss chroma-hue primaries brightened
(--priRd/Or/Gn/Tk/Bl/Id + --terGn).
[[project-position-circle-tooltips]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A CARTE owner (all 6 seats, both polarities) entering SIG_SELECT with no ?seat
saw the sig overlay + reserve URL locked to the canonical PC seat (one polarity)
while the TRAY followed `current_slot` = owned[0] (the lowest owned slot, often
the OTHER polarity). A sig reserved from that view filed against the WRONG seat
— the seat the owner thought he covered stayed empty — so that polarity never
reached 3-ready, its 12s countdown ran to 0 but the server-side `_fire` bailed at
`len(ready) < 3` and never advanced; the other polarity proceeded. Switching
pos-circles via GATE VIEW (sets ?seat) re-aligned every surface and unstuck it.
A 3-agent trace confirmed the mechanism + corrected my first guess: this is NOT a
WS problem (the cursor group only drives the cosmetic flashing numeral; the
SIG→SKY advance is a threading.Timer broadcasting to the room_<id> group every
socket joins). The stall is the misfiled reservation → 3-ready COMPLETENESS
failure, rooted in two seat resolvers disagreeing when seatless: the overlay /
sig_confirm use `_canonical_user_seat` (PC-first) while the tray / reserve use
`_viewer_current_slot` owned[0].
Fix: `room_view` redirects a seatless multi-seat owner (gate_slots.filter(gamer)
.count() > 1) in SIG_SELECT to ?seat=<current_slot>, so EVERY surface (tray,
overlay, reserve URL, WS cursor group) resolves to one seat via the already-correct
?seat path — the same realignment a GATE-VIEW switch does. SIG_SELECT-only
(SKY_SELECT already keys off selected_seat); single-seat gamers / non-owners / anon
fail the guard. Unaffected: the multi-gamer sig FTs (one seat each) + the WS-direct
CarteCursorGroupTest.
TDD: 6 ITs in CarteTrayFollowsSelectedSeatTest (redirect / ?seat-present no-redirect
/ overlay+tray agree post-redirect / single-seat / non-owner / SKY_SELECT
no-redirect); the red `'PC' != 'BC'` was the divergence itself. 381 epic-view ITs
green.
[[project-sig-select-seat-switch-open-problems]] [[feedback-ws-cursor-group-must-match-acting-seat]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A deposited trinket binds `Token.current_room` (the 'In-Use: <room>' Game Kit
label), set on deposit + cleared only on MANUAL return. Once a seat's token cost
lapsed (7d → GATE VIEW returns to prompt re-deposit) nothing freed the binding —
`cost_current` is a render-time prop, so no 7d mutation — leaving e.g. a CARTE
used a week prior stuck 'In-Use' (staging, 2026-06-08).
Fix — a uniform, type-agnostic in-use clock:
- New `Token.in_use_since`, stamped when `current_room` is set (`debit_token`
COIN, `drop_token` CARTE). `release_lapsed_trinkets(tokens)` frees
`current_room` + `in_use_since` + `slots_claimed` once held >= the room's
`renewal_period` (7d). The SEAT is untouched — renewal grace (to 2xspan) +
auto-BYE stay `_expire_lapsed_seats`'s job (a separate, later threshold).
- PASS/BAND never bind (reusable keys) -> no-op; COIN + CARTE covered by the one
rule. Fires on the gameboard / `_game_kit_context` render (the gamer sees it
freed immediately) + the cron backstop (`expire_lapsed_room_seats`).
- Migration 0018 backfills `in_use_since` from the earliest backing-slot
`filled_at` so existing staging bindings release on accurate timing (old free
on next sweep, recent keep grace) instead of every legacy NULL releasing early.
Retired COIN's bespoke ROOM cooldown: `debit_token` no longer sets
`next_ready_at` + `return_token` no longer clears it. `next_ready_at` is now
My-Sea-exclusive (the 24h DRAW cooldown, no room context) — the two clocks no
longer share a meaning + can't clash (user-spec 2026-06-08).
TDD: ReleaseLapsedTrinketsTest (6) + cron release + 2 gameboard-immediacy tests +
updated return/tooltip/bind assertions; 1124 lyric+epic+gameboard ITs green.
[[feedback-equip-slot-gates-trinket-use]] [[feedback-my-sea-cooldown-design]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pure rename, no behavior change — the deck-stack reveal btn kept the name `.sea-stack-ok` from when it said OK; it has rendered FLIP for a while. Renamed across all 8 references: _card-deck.scss (reveal + pointer-events rules) + a _gameboard.scss comment; the 3 templates that emit or query it (_sea_overlay.html, _my_sea_deck_stack.html, my_sea.html inline JS); and the 3 tests that select it (test_game_room_select_sea FT, test_game_my_sea FT, test_sea_visit IT rendered-HTML asserts). Verified 47 green across all three surfaces (visit IT + gameroom PickSeaDeal stack FT + my_sea CardDraw FT).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
test_clicking_stack_shows_ok_btn is the only PickSeaDealTest that pairs _choose_spread w. an is_displayed() assert (the six-draws test checks the class instead). Once a spread is OK'd the felt scroll-snaps onto two pages + the deck stacks sit on the page the post-OK rAF scroll-to-cross targets — but headless Firefox DROPS that delayed scroll, leaving the clicked stack's revealed FLIP btn off the visible page → is_displayed False. scrollIntoView the btn first so the assert reflects the `--active` reveal, not the scroll position. Surfaced in CI build 374's channels stage (a flake that 373 survived on retry; my bd9155c preview edits only shifted the timing).
[[feedback-headless-delayed-scroll-dropped]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- the `.sea-stage--gravity/levity .sea-stage-card.is-flipped-to-back::after` 0.3 fill differentiates the two halves of a monodeck CLONED into a two-toned dubbodeck — only meaningful in Sea Select. my_sea / my_sea_visit draw a single monodeck that is never cloned, so the tint was just noise on their FLIP'd card-back
- the shared `_sea_stage.html` now takes a `sea_stage_dubbo` flag; only `_sea_overlay.html` (the gameroom Sea Select felt) passes it → `.sea-stage--dubbo` on #id_sea_stage. The two tint rules gate on `.sea-stage--dubbo.sea-stage--gravity/levity`; non-dubbo stages fall through to the empty, fill-less base `::after` (no visible overlay)
[[project-deck-segment-model]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- rename the spread-preview's `.sea-pos-*` cells to `.sea-prev-pos-*` (template + aliased Celtic-cross geometry under `.sea-cross--preview` in _gameboard.scss + the SeaDealSpec fixture) so a bare `.sea-pos-*` matches ONLY the live `.my-sea-cross`; fixes test_picker_renders_sao_default_position_subset (was 2≠1 — the preview duplicated every cross cell)
- spread dropdown: cap `.sea-select-list` w. max-height + overflow-y:auto, & JS-click the option in the `_pick` FT helper, so the last option (escape-velocity) can't land below the un-scrollable picker-modal fold in landscape (ElementNotInteractable, seen in CI build 373)
- _retry_failed.sh: anchor the FAIL/ERROR label sed to the FIRST paren group so parameterized subTest failures retry the real method label, not a bogus `position='…'` module (ModuleNotFoundError)
[[project-my-sea-roadmap]] [[feedback-collectstatic-before-ft]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 06-07 modal→felt scroll-snap refactor dropped LOCK HAND (→ AUTO DRAW +
auto-completion on the 6th draw) and moved the deck stacks into the cross-col
(`.sea-page--room:not(.sea-spread-chosen) .sea-cross-col{display:none}`), but the
channels `PickSeaDealTest` was never updated → 3 reds in the two-browser/channels
CI stage:
• test_lock_hand_btn_present_and_disabled + test_lock_hand_enables_after_six_draws
— looked up the removed `id_sea_lock_hand` (NoSuchElement).
• test_clicking_stack_shows_ok_btn — the stack OK is hidden until a spread is OK'd.
Fix: deleted the disabled-LOCK-HAND test; rewrote the six-draws test to assert the
new synchronous completion (the deck-stack FLIP btns gain `.btn-disabled` the
instant the 6th card lands, before the 3s felt cascade — SEED MAP is IT-covered);
added a `_choose_spread()` helper (clicks `id_sea_confirm_spread`) so the
stack-interaction tests run against the revealed cross page. Pre-existing 06-07
staleness — NOT tonight's glow/swap/applet commits (verified: `_load_sea_overlay`
opens the sea felt with no sky felt open, so the new swap guard is a no-op).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Applet titles + gear-menu toggle labels (Applet.name, via data migration 0014)
both retitled — a deliberate departure from the "My X" applet convention for
these two surfaces. Slugs (my-sky / my-sea) stay put.
Recolor (applet shells only — the standalone pages are tomorrow's todo):
• SkyDrive (uranium ramp): --sixU light-green title font, --priU deep-green
shell bg; hover keeps the palette --ninUser highlight, glow takes the --sixU
tint. Reverses to --priU font / --sixU bg on *-light palettes.
• Sea of Cards (neptunium ramp): --sixNp light-teal font, --priNp deep-teal shell;
the conjunction "of" is a lowercase-italic span against the h2 uppercase
transform. Reverses to --priNp / --sixNp on *-light palettes.
Mechanism: the shared applet title-link rule now reads --applet-title-fg (with a
--terUser fallback so every other applet is untouched); each applet sets that +
--applet-shell-bg, and a body[class*="-light"] override swaps the pair. No
specificity war.
Tests: data migration 0014 (reversible); FT/IT applet seeds + the My Sky heading
assertion updated to the new names; 531 dashboard/gameboard/applets ITs green.
[[feedback-applet-vs-page-naming-convention]] [[feedback-scss-id-context-specificity-trap]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Symmetry (user-spec): Sky Select leaves the burger sea btn lit while the sky
felt is up; Sea Select now mirrors it — openSea disables ONLY id_text_btn, no
longer id_sky_btn, so a revisited Sea Select keeps the completed sky one click
away. An adversarial pass flagged that the two felts are equal-z (z-index:5)
siblings and neither open path drops the other's open-class, so leaving the sky
btn lit and clicking it from inside the sea felt would DOUBLE-OPEN (sea paints
over sky → confusing no-op) — the same latent stack already reachable in the
sky->sea direction on a both-complete reload. Fixed in BOTH directions: openSea
now closes the sky felt first + openSky closes the sea felt first (each exposes
its close on window: closeSeaFelt / closeSkyFelt), so clicking the other phase's
lit btn performs a clean SWAP. The text-btn disable/restore chain stays correct
across the swap (closeSky restores text, openSea re-disables + recaches it).
Glow: the glow-handoff pulse ease-OUT now runs ~2.8s (was ~1.4s) — moved the
cycle to 3.2s with the bright peak at 12.5%, keeping the ease-IN swell at ~0.4s
(user asked for a longer linger).
[[feedback-felt-aperture-fill-covers-felt]] [[feedback-inline-partial-script-defer-for-later-partial]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
room.html includes _sea_overlay.html (~L77) BEFORE _burger.html (~L167, which
holds #id_burger_btn + #id_sea_btn), so the overlay's two inline scripts
captured those btns at parse time → null → both bindings silently no-op'd:
the sea btn (active post-completion) did nothing on click + the burger stayed
stuck --priId because its glow-handoff transfer listener never bound. Defer both
the sea-btn REOPEN binding (_bindSeaReopen) & the burger→sea_btn→.sea-select
GLOW chain (_bindGlowHandoff) to DOMContentLoaded so the burger fan exists first.
Also make the glow-handoff halo PULSE (quick ease-in swell, slow ease-out decay
via per-segment timing fns + a lopsided 22/78 keystop split) instead of a flat
glow — the burger, then the sea btn after the handoff click, keep cueing
"click me to reopen your sea".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- The option chunks' width tracked the select label length (jumped between the
two spreads). Fix the `.sea-form-col` to 19rem (a touch wider) so all three
chunks share a standard width.
- Split the spread label onto two lines around the comma + add the "Rider-"
prefix now there's room: "Celtic Cross," / "Rider-Waite-Smith" and "Celtic
Cross," / "Escape Velocity". combobox.js now writes the current label via
`innerHTML` (not textContent) so the `<br>` survives a selection — plain-text
options (sky / my_sea) are unaffected (their innerHTML == textContent).
- ITs updated for the new gameroom labels (my_sea's single-line names untouched).
953 epic+gameboard ITs + Jasmine green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- A disabled OK/DEL × inside a --priUser option chunk blended into it (the global
`.btn-disabled` bg is also --priUser → no visible circle). Drop the disabled
btns in `.sea-options-col` to the felt --duoUser so they read as a distinct
disabled circle, like the deck-stack FLIP ×.
- AUTO DRAW now eases the felt back UP to the cross even when the user already
OK'd the spread + scrolled DOWN to the options page — so he watches the cards
land one-by-one. `_chooseSpread(slideIn)`: the OK reveal pins to the options
(slide-in from above); AUTO DRAW (already chosen) skips the pin + just eases up
to the cross. `_scrollToCross` now eases from the current scroll position.
- 12 PickSeaUnifiedFeltTest render ITs green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Restyle the spread-options page (post the scroll-snap refactor):
- OK `.btn-confirm` moves UP beside the `.sea-select` combobox (a new
`.sea-select-row`), off the AUTO DRAW / DEL action row.
- OK gains `.btn-disabled` + × the moment the first card is drawn — inverse to
DEL (which loses them then), simultaneous with the combobox locking. So
`_chooseSpread` (OK) no longer locks; the lock + both btn states flip together
at the first draw via `_setHasDrawn` + `_lockSpread`. Server-renders OK
disabled/× when `saved_by_position`.
- The three chunks (spread/select/OK, the mini preview, AUTO DRAW/DEL) each get
the same --priUser rounded rectangle as the GAME POST lines / composer
(`_base.scss` `.form-control`): --priUser fill + half-alpha --secUser border +
rounded + padding. The `.sea-form-col`/`-main` go transparent flex columns so
the felt shows between the chunks.
- IT: OK enabled / DEL disabled when fresh; flips once a card is drawn.
953 epic+gameboard ITs green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Gaussian spread modal couldn't hang off the burger #id_sea_btn anymore (that
button now also opens the felt). Mirror Sky Select's form→wheel scroll-snap
instead: the felt starts with the spread OPTIONS on the --duoUser felt; clicking
OK confirms the spread → the options shunt DOWN and the spread CROSS takes page 1
(scroll down to find the options again). No modal, no corner NVM.
- `_sea_overlay.html` restructured into `.sea-options-col` (the .sea-select
combobox + mini preview + OK .btn-confirm + AUTO DRAW + DEL — NO deck stacks)
and `.sea-cross-col` (the real .my-sea-cross + the Gravity/Levity deck stacks +
the portaled stage). `#id_sea_overlay` is a `display:contents` passthrough so
the two cols are the scroll-snap sections.
- OK (`#id_sea_confirm_spread`) → `_chooseSpread()`: adds `sea-spread-chosen` to
the felt → SCSS engages `scroll-snap-type:y mandatory`, the cross-col gets
`order:-1` (page 1), options shunt to page 2; locks the combobox; eases the
scroller to the cross. AUTO DRAW also confirms first. A reload of an in-progress
sea renders `sea-spread-chosen` (cross revealed) server-side.
- SCSS (`_sky.scss`): the sea felt is now a column scroller; `.sea-cross-col`
`display:none` pre-confirm; the `sea-spread-chosen` scroll-snap block mirrors
`body.sky-saved`. The options `.sea-form-col` goes transparent/content-sized
(blends onto the felt, not the modal's --priUser card).
- Sea sub-btn: no longer activated by openSea; it's the POST-COMPLETION reopen
affordance (cascade activates it + `sea_btn_active = hand_complete` ctx flag),
an active click → `window.openSeaFelt()` (review the saved spread), like the
sky btn. Removed the sea_btn open-modal IIFE + the corner NVM.
- IT: options-on-felt (combobox + OK + AUTO DRAW + DEL + preview) w. NO modal /
NVM. 952 epic+gameboard ITs + Jasmine + PickSeaAsyncTransitionTest(3) green.
my_sea.html keeps its modal (untouched) — the gameroom intentionally diverges.
[[project-character-creation-spec]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror CAST SKY's post-save cascade for the sea phase. When the 6-card spread
completes (live FLIP of the 6th card / AUTO DRAW finishing): linger ~3s on the
felt → the felt eases OUT (`.sea-page--cascade-out`, revealing the table-hex) →
DRAW SEA gives way to SEED MAP + the sea glow fires on the burger (handoff →
sea_btn) → +3s → SEED MAP eases IN. Same shape as CAST SKY → sky-btn glow →
DRAW SEA.
- `_room_hex_center.html`: SEED MAP joins the hex-phase-stack; DRAW SEA goes
--out once `hand_complete`, SEED MAP --out until then (a reload of a complete
sea lands on SEED MAP server-side = the cascade's end-state). SEED MAP → the
Voronoi map (roadmap step 21) is a stub — it only needs to APPEAR here
- `_sea_overlay.html`: `_setComplete(on, live)` runs `_startSeaCascade()` on the
LIVE completion (FLIP / AUTO DRAW pass `live=true`; init does not, so a reload
doesn't re-animate). The completion-glow IIFE no longer self-starts on the
data-state transition — the cascade adds `glow-handoff` to the burger; the IIFE
keeps only the burger → sea_btn → .sea-select handoff
- `.sea-page--cascade-out` SCSS (mirrors `.sky-page--cascade-out`)
- ITs: SEED MAP --out pre-completion (DRAW SEA in); SEED MAP in + DRAW SEA --out
when hand_complete. 952 epic+gameboard ITs + PickSeaAsyncTransitionTest(3) green
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The felt cross's CENTER significator was hardcoded to the corner-rank + suit-icon
text thumbnail, so an image deck (RWS / Minchiate) showed a bare "0"+icon card
instead of its face. Mirror my_sea: render the `.sig-stage-card-img` (image mode)
when the sig's deck has card images, else fall through to the text thumbnail. The
tray sig stays the simple thumbnail (user-spec). The sig's `deck_variant` is the
card's OWN deck — the Sig Select pick is drawn from the Role Select contributed
deck (`_room_deck_variant`), so this is the correct image source (no equipped_deck
bug, unlike the earlier FLIP back-img).
- IT: an image-deck significator renders the center `sea-sig-card sig-stage-card--image`
- 949+ epic ITs green
; FLIP tint tweak (parallel edit): flipped-back overlay alpha 0.6 → 0.3
[[project-deck-segment-model]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A Carte Blanche gamer owns all 6 seats but could only cast sky / draw sea for ONE:
the sky/sea state IS per-seat (Character.seat), but the code resolved to the fixed
canonical (PC-first) seat, ignoring the ?seat switched to via the GATE VIEW pos-
circles — so the tray switched but the sky wheel / sea spread stuck on the
canonical seat + saves wrote back to it. Mirrors Sig Select's existing ?seat path.
- generalize `_acting_sig_seat` → `_acting_seat` (logic is sig-agnostic; 3 callers)
- `_role_select_context` SKY_SELECT branch keys off `selected_seat` (the ?seat-aware
seat, already computed) instead of `_canonical_seat`: user_polarity,
confirmed_char, user_seat_role, my_tray_sig, saved_by_position, saved_sea_spread,
sea_default_spread, hand_complete, sea_back_image_url
- sky_save / sky_delete / sea_save / sea_delete / sea_deck resolve the acting seat
via `_acting_seat(…, request.GET.get("seat"))`; sea_partial threads seat_param
- the sky + sea felts carry `?seat={{ current_slot }}` on their save/delete/deck/
sea_partial action URLs so the POSTs target the switched-to seat
- single-seat flow unchanged (no ?seat → canonical fallback)
- ITs: CARTE owner — ?seat switches the displayed spread; sea_save/sky_save target
the switched seat leaving the canonical seat's Character intact; felt URLs carry
?seat. 949 epic+gameboard ITs green.
; FLIP tint tweak (parallel edit): drop the polarity border, bump the flipped-back
overlay --quiUser/--terUser to 0.6 alpha
[[project-sig-select-seat-switch-open-problems]] [[project-deck-segment-model]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The sea-stage FLIP no-op'd in the gameroom because `_sea_stage.html` rendered
the back-img from `request.user.equipped_deck.back_image_url` — but
`select_role` NULLS OUT the user's equipped_deck once it's contributed to the
room's seats (views.py:1148), so in the Sea Select phase the deck was None →
no back-img element → sea.js's FLIP handler short-circuits on the missing
`.sig-stage-card-back-img` sibling. (my_sea worked: solo, no contribution.)
- `_sea_stage.html` now renders the back-img from a `sea_back_image_url` ctx var
instead of `request.user.equipped_deck`
- gameroom: `_role_select_context` sets it from the SEAT's contributed deck
(`_canonical_seat.deck_variant`, when it has card images)
- my_sea: the my_sea view sets it from the user's own equipped deck
- ITs: image seat-deck renders `.sig-stage-card-back-img`; text seat-deck omits it
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- fix: drawn spread slots silently no-op'd on click AFTER a refresh — SeaDeal's
in-memory `_seaHand` is only populated by openStage/register during the live
session, so a reload left it empty + the overlay click handler short-circuited
(`if (!_seaHand[pos]) return`). `_sea_overlay.html` now re-seeds `_seaHand`
from the server-rendered saved slots once the deck fetch resolves (cards
looked up by `data-card-id`; reversed/polarity DOM-sourced) — the same fix
my_sea already carries
- FLIP card-back: the sea stage now renders the deck back-img for ANY image-
equipped deck w. a back image (dropped the `not is_polarized` gate — it
omitted the back for the room's polarized Gravity/Levity draw, so FLIP
no-op'd). The back-art is identical across polarities, so `_card-deck.scss`
tints the FLIPped card by the drawn polarity: gravity --quiUser fill @ 0.3 +
--quaUser border; levity --terUser fill @ 0.3 + --ninUser border (scoped to
`.sea-stage--*`, so my_sign / applet stages are untouched)
- IT: an image-deck sea stage renders `.sig-stage-card-back-img`
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hollow out the gameroom DRAW SEA dark modal into the two surfaces my_sea
uses: a --duoUser felt (.sea-page--room) filling the hex pane where the
Celtic Cross deals, + a Gaussian spread modal (#id_sea_spread_modal: the
.sea-select combobox w. the two Celtic Cross 6-card opts ONLY, AUTO DRAW/DEL,
a mini preview, + a corner #id_sea_cancel NVM). Opened by DRAW SEA
(html.sea-open); the room gear's NVM (room-menu-sea) returns to the hex;
#id_text_btn + #id_sky_btn go inert while the felt is open.
- persist: epic:sea_save / sea_delete upsert the seat's Character.celtic_cross
(none of my_sea's MySeaDraw quota/Brief machinery); room ctx adds
saved_by_position + saved_sea_spread + sea_default_spread + hand_complete so
a reload re-renders the filled cross. celtic_cross field already existed (no
migration)
- mini spread preview (_sea_spread_preview.html) in BOTH the gameroom + my_sea
modals — shape only, NEVER dealt to: SeaDeal scopes its slot queries to
.sea-cross:not(.sea-cross--preview)
- always TWO deck stacks (Gravity + Levity) in the room Sea Select — the gamer
draws from either populated half (sea_deck split), even a CARTE monodeck;
unlike my_sea / Sig Select's polarization collapse
- glow coordination: the sky-saved glow is muted in the sky/sea phases
(html.sky-open / sea-open / sea-entered); sea glow color --priYl -> --priId
(distinct from sky's --priTk); the sea glow-machine fires at hand-COMPLETION
(mirrors Sky Select), not during drawing
- guard copy "Auto deal cards?" -> "Auto Draw cards?" (match the AUTO DRAW btn)
- fix: drop the stale `html.sea-open #id_aperture_fill { opacity:1 }` — it
painted the opaque z-90 fill over the z-5 felt so the spread flashed then
vanished (same trap as the CAST SKY felt); removed the dead .sea-backdrop /
.sea-overlay / .sea-modal-* SCSS
- tests: epic PickSeaPersistTest (7) + PickSeaUnifiedFeltTest (6) ITs; SeaDeal
preview-scoping + BurgerSpec sky-glow-mute Jasmine specs; my_sea sig-card
ITs scoped to .my-sea-cross (the preview adds a 2nd .sea-sig-card)
[[feedback-felt-aperture-fill-covers-felt]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The .room-table-scene → .table-hex-border → .table-hex → .table-center + seats
ring was byte-identical across room.html, my_sea.html, my_sea_visit.html &
billboard/my_sign.html. Lift the skeleton into one cross-app partial so every
surface — and the upcoming Sea Select felt rebuild — shares a single source.
- core/_partials/_table_hex.html: the 4-div skeleton. The varying center +
seats are passed as partial NAMES (`hex_center` / `hex_seats`) since Django
{% include %} has no slot; `{% include hex_center %}` resolves the string var
+ inherits the full page context (no `only`), so each fragment sees its page's
vars unchanged → identical render.
- Per-surface center fragments (verbatim moves): _room_hex_center,
_my_sea_hex_center, _my_sea_visit_hex_center (gameboard/_partials),
_my_sign_hex_center (billboard/_partials).
- Seats fragments: _room_hex_seats (gate_positions); _table_seats — SHARED by
my_sea + my_sea_visit (`seats` loop; the `--self` modifier is inert on my_sea);
_my_sign_hex_seats (single chair).
- The page-specific outer wrappers stay put (room's .room-shell + ROLE_SELECT
SCAN SIGS form; .my-sea-landing / .my-sign-landing; my_sign's
{% if not current_significator %} gate).
The "felt" is deliberately NOT extracted — it's a --duoUser bg toggled by
phase/stage classes (CSS), already DRY; each phase's felt content is bespoke.
Markup-only, no behaviour change. Verified: 1170 epic+gameboard+billboard render
ITs green (the table-hex / table-seat / center-btn assertions are the gate) +
MySeaDrawSeaLandingTest FTs green (live hex render + FREE DRAW seats
.table-seat[data-slot="1"] through the shared seats partial).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Running the corequisite channels FTs surfaced a real gap the cascade introduced:
a sky confirm that did NOT come through the felt's own save (a direct POST, or
another browser of the same seat) left _lastChartData null, so _onSkyConfirmed's
_activateSavedState returned early → no transition at all (the old code reloaded).
- _sky_overlay.html: _onSkyConfirmed now reloads when _lastChartData is absent
(the pre-cascade behaviour, preserved for non-felt-save confirm paths); the
felt-save path still eases to DRAW SEA via the cascade (no reload).
- test_game_room_select_sea.py: the async-transition assertion updated for the
phase-stack — CAST SKY is now present-but-`--out` (hidden in the shared grid
cell), not removed from the DOM, so assert the `hex-phase-btn--out` class on
CAST SKY + its ABSENCE on DRAW SEA rather than `find_elements(...) == []`.
Corequisite FTs run green: select_sea async-transition (3) + deal (9) channels;
dash my_sky async-save + aperture-snap (3) — the shared body.sky-saved apparatus
is untouched by the room-scoped felt SCSS. select_sky FTs already green. The sig
SRG7 reveal FT is unaffected (it waits for the SIG_SELECT hidden CAST SKY, which
that branch still renders; it never clicks, so the _reload change isn't reached).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Post-save choreography + the polish asks from this session.
- Post-save cascade (_sky_overlay.html): after SAVE the gamer lingers on the
wheel ~3s, then the felt eases OUT (fade, .sky-page--cascade-out) to reveal the
table-hex; the burger fires its --priTk glow + #id_sky_btn goes active (both
ride the cascade now, not the save instant); 3s later the DRAW SEA btn eases IN
and the sea overlay is injected so it's live with NO reload.
- Hex phase-stack (room.html + _room.scss): CAST SKY + DRAW SEA share one grid
cell (.hex-phase-stack) so they cross-fade in place (.hex-phase-btn--out); the
server seeds --out on the inactive one, the cascade swaps them. A confirmed
reload lands DRAW SEA visible / CAST SKY out, same as the cascade end.
- Seamless sea injection: _injectSeaOverlay fetches sea_partial (the URL already
existed, unused) + re-creates its <script>s so the overlay's own init (openSea
+ SeaDeal.reinit) runs — DRAW SEA opens with no reload. (Bridge until Sea Select
is itself hollowed into a felt.)
- Burger → Sky-btn handoff (burger-btn.js + _burger.scss): _pulseGlow generalized;
once saved, the next burger-OPEN pulses #id_sky_btn --priTk ("now click me to
reopen"). Jasmine specs added (BurgerSpec).
- #id_text_btn disabled while the felt is up (openSky/closeSky) — its swipe
machine would otherwise half-load the scroll-locked reelhouse.
- Reload-into-open (sig-select.js + _sky_overlay.html): the SIG_SELECT→SKY_SELECT
CAST SKY click crosses a server-render boundary (felt/phase-stack/sea-inject
only exist in SKY_SELECT), so it still reloads — but drops a sessionStorage
flag first so the felt OPENS on arrival. Kills the old click→reload→click-again
double-take (the "first click reloads" report). DRAW SEA can inject in-place
(stays within SKY_SELECT); CAST SKY can't, so this is the seamless equivalent.
Tests: BurgerSpec handoff specs + full Jasmine green; 32 render ITs + 930
epic+gameboard green. Cascade timing live-verified by the user.
[[feedback-scss-import-order-specificity]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two live-only stacking/layout bugs in the in-room felt (caught via Claudezilla
DOM inspection — neither is reachable by IT/Jasmine):
- .sky-page--room (0,1,0) lost the source-order tie to the LATER base
`.sky-page { position: relative }` (also 0,1,0), so the felt stayed
position:relative + flex:1 and collapsed to width 0 as a flex child of the
hex-pane — the form vanished onto a 0-wide column. Chained to
`.sky-page.sky-page--room` (0,2,0) so it wins regardless of order.
[[feedback-scss-import-order-specificity]]
- Dropped `html.sky-open { #id_aperture_fill { opacity: 1 } }` — that modal-era
backdrop is a full-cover --duoUser div at z-90; the old dark modal sat above
it (z-120), but the felt sits at z-5, so the fill painted an opaque green
sheet OVER the felt + form. Its opacity transition is why the form "flashed
then vanished after <0.5s". The felt is its own --duoUser surface + covers the
hex on its own, so the fill stays transparent now.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the root-level dark Gaussian CAST SKY modal with the my_sky / sky.html
apparatus, mirroring the Sig Select unify (71c0069). Sky data stays seat-bound
(Character.seat), never a pos-circle.
- room.html: the sky overlay moves INTO .room-hex-pane on --duoUser felt
(has-sky-stage), my_sea-style; rendered through the confirmed state too so
the burger can reopen the saved wheel. Sky tooltips stay at root.
- _sky_overlay.html: drops .sky-backdrop / .sky-modal-wrap / .sky-modal / header
+ the in-felt NVM; reuses the shared .sky-page form/wheel (.sky-page--room).
No live preview — the wheel only paints after SAVE SKY (my_sky parity); SAVE
adds body.sky-saved → the felt flips to scroll-snap (form shunts to page 2,
ease to the wheel on page 1). saved_sky_json primes the reopen draw. Inert
STUB hook for the post-character-creation form lock (roadmap step 21).
- _sky.scss: in-room felt fill + open/close (html.sky-open); hides the position
strip while the felt is up for a clean homogeneous surface.
- _room.scss: html.sky-open pins .room-aperture.is-scrollable (overflow hidden,
snap none) so the ATLAS/SCROLL/YARN/POST/PULSE reelhouse is unreachable while
casting; restored the instant the felt closes.
- _room_gear.html + room-views.js: NVM moves into a new .room-menu-sky gear pane
(→ epic:room, which re-renders DRAW SEA if saved else CAST SKY); syncGear()
shows it while sky-open.
- _burger.html + _burger.scss + burger-btn.js: the Sky sub-btn goes .active once
saved (sky_btn_active = sky_confirmed) — concurrent w. a thrice --priTk burger
pulse (.sky-saved-glow, rhymes w. .flash-inactive); an active click reopens
the wheel via window.openSkyFelt.
- epic/views.py: sky_btn_active + saved_sky_json ctx off the seat's confirmed
Character; the acting gamer's WS auto-reload is dropped (SAVE reveals the
wheel in place; the gear NVM does the nav to DRAW SEA).
- Tests: PickSkyUnifiedFeltTest + PickSeaRenderingTest ITs (930 epic+gameboard
green); BurgerSpec sky-glow/reopen Jasmine (full suite green); PickSky
LocalStorageTest + PickSkyDelTest FTs reworked to the post-save flow.
[[project-deck-segment-model]] [[feedback-scss-import-order-specificity]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On local Windows + SQLite, DiscoverRunner's teardown ends in
os.remove(test_db.sqlite3), which raises PermissionError [WinError 32] when
another handle still holds the file (a concurrent run, a lingering connection,
AV / Search indexer) — crashing an otherwise-green run at the very end.
RobustCompressorTestRunner.teardown_databases now retries super() up to 10x with
a 0.1s sleep, then leaves the stale file for the next run to overwrite rather
than fail. Mirrors the _robust_save PermissionError retry already in the runner.
CI-neutral: CI is Postgres on Linux — teardown is DROP DATABASE (no file remove),
and Linux unlinks open files without error — so the loop never triggers there.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
a6ce207 moved the 12s polarity confirm to a Celery task (apply_async countdown).
That requires a running Celery worker to execute it — but local dev runs only
uvicorn (the dev-server skill starts no worker; the original tasks.py docstring
chose threading.Timer precisely "so no separate Celery worker is needed in
development"). So locally the confirm was queued and never ran: the countdown hit
0, no significators saved, and a refresh stayed in SIG_SELECT (no skip to the
table hex). A regression in the core flow.
Restore tasks.py + test_tasks.py to the faaa4ec threading.Timer version (still
in-process, with the {token, deadline} cache + countdown_remaining restore-on-
load intact) and drop the now-unneeded CELERY_BROKER_URL='memory://' test
override.
Kept from a6ce207: the room.js WebSocket auto-reconnect — that is the actual fix
for the dropped-socket delivery bug (the SigSelectSpec bisection proved the
client restarts the numeral fine on re-received events; the failure was delivery,
which a dead socket with no reconnect explains). Celery was a misdiagnosis of an
in-process broadcast that works fine for a single-process dev/staging server.
23 task UTs + CARTE sig ITs green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A diagnostic that replays the reported "cancel → re-ready doesn't restart the
flashing numeral" sequence purely through the client handlers (countdown_start →
countdown_cancel → countdown_start). It passes: _showCountdown re-renders the
numeral every time the event is received, so the client logic is sound.
This bisects the live bug to WS *delivery* (the re-ready countdown_start not
reaching the browser), not client state — consistent with a dropped room socket
that never reconnected (every subsequent live event lost until a refresh, which
matches the symptom exactly). The auto-reconnect added in a6ce207 is the
relevant mitigation; kept as a permanent guard on the client restart path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The flaky tray→thumbnail→hex animation after the 12s countdown.
Root cause: the confirm ran in a threading.Timer thread inside the web process,
and _fire broadcast polarity_room_done / pick_sky_available via
async_to_sync(group_send) from an ephemeral per-call event loop. With the Redis
channel layer that publish is unreliable across loops (the production analog of
the "broadcast must originate in daphne" test trap), so the live events reached
the client only sporadically — the server-side state (sig assignment,
SKY_SELECT) still committed, which is why a refresh always showed the concluded
hex but the animation usually didn't play.
Fix (chosen: migrate to Celery — the path the tasks.py docstring already called
for): _fire becomes the @shared_task confirm_polarity_room, enqueued by
schedule_polarity_confirm via apply_async(countdown=seconds). The worker is a
stable long-lived process whose channel-layer singleton is never shared with a
serving loop, so its group_send reaches daphne reliably; it also survives
web-worker restarts. No task revocation needed — cancellation/supersession ride
the existing cache token guard (cancel just deletes the token; a stale queued
task no-ops). Dropped threading.Timer + the _timers registry.
Test settings get CELERY_BROKER_URL='memory://' so apply_async queues without a
live Redis and without running the task (no worker) — mirrors the old timer that
was scheduled but never fired inside a sub-12s test. NOT eager: eager would
ignore the countdown and assign significators synchronously during the ready
POST. test_tasks rewritten: confirm_polarity_room called directly (task body),
schedule asserts the enqueue + countdown + fresh-token supersession; the
broadcast itself stays IT-uncoverable under InMemory (known channels limit).
Also: room.js now auto-reconnects the room WebSocket with capped exponential
backoff (1s→30s, reset on open, halted on beforeunload). A dropped socket (proxy
idle-timeout, blip, server restart) previously stayed dead until a manual
refresh, silently losing every live event — an independent reliability gap that
compounded the "sporadic" feel.
602 epic ITs + 18 task UTs green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 12s countdown numeral set its size with an inline style.fontSize='2em',
but .btn-primary carries `font-size: 0.625rem !important` inside its small
landscape/short-portrait down-size media query — and an !important declaration
beats an inline style. So on phones / short viewports the numeral stayed
button-sized instead of doubling.
Fix: sig-select.js now toggles a `.sig-take-sig-btn--counting` class instead of
the inline font-size (in _showCountdown / _hideCountdown / the unready path),
and a new `.sig-stage .sig-take-sig-btn.sig-take-sig-btn--counting` rule in
_card-deck.scss re-asserts `font-size: 2em !important` at (0,3,0) specificity —
strictly beating the (0,2,0) btn-primary media-query !important at all queries.
em stays parent-relative so the doubling tracks the stage font across sizes.
2 Jasmine specs (class present + no inline override on show; class cleared on
countdown_cancel) added to SigSelectSpec; SpecRunner green; SCSS compiles.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two Sig-Select seat-switch follow-ups from the 2026-06-05 countdown sprint.
(1) CONT GAME + gear NVM pos-1 shuttle.
The GATE VIEW (room_gate.html) CONT GAME button and the gear-menu NVM both
linked to epic:room with NO ?seat, so a CARTE multi-seat gamer acting at pos 4
got shuttled back to owned[0] (pos 1) instead of staying on his acting seat —
the same gap bedc489 fixed for the GATE VIEW nav buttons. room_gate now
computes a single table_url (epic:room + ?seat=<current_slot> when seated) and
feeds it to both the CONT GAME onclick and the NVM include. The table view
already reads ?seat, so the gamer lands on the seat he was viewing. Added
`reverse` import to epic/views.py. 2 ITs in CarteTrayFollowsSelectedSeatTest
(cont-game + nvm carry the acting seat; default targets lowest owned); updated
RoomGateViewTest.test_nvm_returns_to_room_hex to expect the seat-carrying href.
(2) Live countdown numeral not restored on a fresh seat view mid-countdown.
countdown_start is a ONE-SHOT WS broadcast: a gamer (esp. a CARTE owner
switching to an already-ready seat) who loads the view after it fired saw a
static WAIT NVM, never the 12s flashing numeral — the redirect still fired, so
it was a visual-restore-on-load gap. The cache entry now stores the absolute
deadline alongside the timer token ({token, deadline} dict, was a bare token
string); _fire's token guard reads either shape defensively so a stale string
from an older deploy can't crash the callback. New tasks.countdown_remaining()
derives the seconds-left from the deadline (None when no countdown / elapsed).
The room view seeds ctx["countdown_remaining"] for the acting polarity;
_sig_select_overlay.html carries data-countdown-remaining; sig-select.js's
_replayReservations restores _showCountdown(remaining) on load when a count is
live, else falls back to WAIT NVM. 4 unit tests (CountdownRemainingTest), 2 ITs
(SigSelectRenderingTest), 3 Jasmine specs (SigSelectSpec countdown-restore).
922 epic+gameboard ITs + 19 task UTs + Jasmine SpecRunner all green.
Trap [[feedback-ws-cursor-group-must-match-acting-seat]].
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 12s flashing #id_take_sig_btn never appeared for a solo CARTE gamer: the
countdown is broadcast only to cursors_<room>_<polarity>, but RoomConsumer
subscribed the WS to a single cursor group chosen from an arbitrary .first()
seat and ignored the acting ?seat. A CARTE owner holds seats in BOTH polarities,
so whenever he was completing the polarity his WS wasn't subscribed to, the
countdown_start event silently missed him (the sigs still committed server-side
via the timer — hence 'works but no visual').
Fix: carry the acting ?seat on the WS URL (room.js) and resolve the cursor
group from it — the ?seat owned-slot override, else the role-canonical seat
(PC-first), matching the overlay's seat resolution. Single-seat gamers are
unaffected (one seat → canonical == .first()).
New channels test CarteCursorGroupTest: acting a gravity seat (?seat) receives
the gravity countdown; no-seat receives the canonical levity countdown. Full
epic channels suite (7) green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A CARTE gamer owning multiple seats was capped at one SigReservation per
room by the (room, gamer) unique constraint, and sig_reserve had a solo
immediate-commit shortcut that wrote seat.significator on OK and never ran a
countdown. Together they 409'd the second sig: after OK-ing one seat, switching
to another and clicking OK was blocked — a gamer could only ever pick one sig.
The countdown/confirm machinery (sig_confirm + the threading-timer task) was
already per-seat-row based — it iterates every ready reservation in a polarity
and seats each. So the fix is to let one gamer hold one reservation PER SEAT:
- Constraint (room, gamer) -> (room, gamer, seat) (migration 0018). The
(room, card, polarity) constraint still enforces a distinct sig per seat.
- Demolish the solo immediate-commit shortcut in sig_reserve — it predated the
countdown mechanism. Reserve now always just creates a provisional row.
- Scope the reserve guard, release, and idempotency lookups to the acting seat
via a new _acting_sig_seat(room, user, ?seat) helper; add a guard against
re-using a card already held for another of the gamer's seats.
- sig_ready resolves the acting seat the same way (?seat) and looks up the
per-seat reservation; data-ready-url + user_ready now carry/reflect the seat.
Now a solo CARTE tester reserves + readies all 3 sigs per polarity and each
room fires its own 12s countdown, then both confirm -> SKY_SELECT.
Tests: new SigReserveCarteMultiSeatTest (no-NVM per-seat reserve; same-seat 409
retained; 3-levity-ready fires one countdown); rewrote the model constraint
test + the CARTE FT to the new behaviour. Multi-gamer path unchanged (one row
per gamer) — epic 595 + gameboard 319 + epic channels 5 + jasmine all green;
full 1663-test suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The GATE VIEW buttons (navbar + room.html lapsed-cost) linked to room_gate
with no ?seat, so _viewer_current_slot fell back to owned[0] (pos 1). On the
gate view a CARTE multi-seat gamer acting at pos 2+ saw pos 1 wrongly flagged
me-current AND href-less — the one circle you could never switch back to.
Fix (option a): _role_select_context + _gate_context now both expose
current_slot; both GATE VIEW buttons append ?seat={{ current_slot }} on
page-room. The gate view's current now matches the table → pos 1 becomes
me-also (switch href) when acting elsewhere, the occupied seat correctly
carries no href. _gate_context computes current_slot once and reuses it for
gate_positions.
3 ITs in CarteTrayFollowsSelectedSeatTest (button carries acting seat;
default targets lowest owned; gate page re-carries seat + pos 1 is me-also).
911 epic+gameboard ITs green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A multi-seat (CARTE) gamer clicking a pos-circle switched the sig overlay
(via the ?seat=N override) but the tray stayed pinned to the role-canonical
PC seat — my_tray_role read assigned_seats[0] (role-sorted, always PC) and
my_tray_sig read _canonical_user_seat. So every circle put the PC icon in
the tray regardless of which one was clicked.
Re-point both tray keys to the seat occupying current_slot (the acting
seat). Single-seat gamers are unaffected — their lone slot IS current_slot,
so selected_seat == their canonical seat.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
RoomScrollLiveRefreshTest could never pass: ChannelsLiveServerTestCase runs
daphne in a SEPARATE PROCESS, and the test settings force the in-memory channel
layer, so a group_send issued from the test process (via record() → on_commit)
can't reach the consumer in the daphne process — the scroll_update nudge is
undeliverable no matter how correct the feature is. (Probed it: WS open, the
scroll-status endpoint returns the new row, but the browser logs 0 nudges.)
Production uses Redis, which is cross-process, so the live refresh works there.
Every link is already covered by ITs, so the E2E hop is the only gap:
record() schedules the broadcast on_commit (test_record_broadcasts_scroll_update_
on_commit), RoomConsumer relays it (test_receives_scroll_update_broadcast), and
the re-fetched feed partial renders the latest events (ScrollStatusViewTest).
Replaced the FT with a NOTE documenting why there's no FT + where the coverage
lives. Unblocks the channels CI stage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The qualifier (Elevated/Enlightened/Graven) now renders on the frozen stat-block
stat-face, not just the hover-only text card-face (which image-mode decks hide).
populateStatExtras takes opts.polarity and fills new .stat-face-qualifier--above
/--below slots, mirroring the card-face placement: non-major above the title,
major below it (title gets a trailing comma). Qualifier shares the title's style
per request ("same style as 'Jack of Crowns' below it").
The 3 theme FTs were asserting the qualifier on hover, but the stat-block is
display:none until a card is OK'd (.sig-stage--frozen) — only the card preview
shows on hover. Repointed them to select→OK→freeze, then read the stat-block.
Threaded polarity through the sig + my_sign callers; added 4 Jasmine specs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Earthman deck now renders the sig stage in image-mode (has_card_images
defaults True), so .fan-card-face — where .sig-qualifier-above/below live — is
display:none and Selenium reads "". The Elevated/Enlightened/Graven qualifier
belongs on the always-visible stat-block now (the card-face instance goes away
once every deck gets images). Skipped pending the repoint + green-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
End-to-end coverage for this session's two shipped features.
SigStageUnifiedTest (FunctionalTest, no WS — both pass locally):
- the sig stage renders INSIDE .room-hex-pane on green --duoUser felt
(.has-sig-stage), the overlay is a descendant of the hex pane, the dark
.sig-backdrop is gone, and the overlay bg is not a translucent-black wash;
- OK'ing a card freezes the stage and reveals the DRY _stat_face.html —
.stat-face-title + .stat-chip-rank populate (the old reduced block had
neither; proves the populateStatExtras wiring).
RoomScrollLiveRefreshTest (ChannelsFunctionalTest, @tag channels):
- with the room open, a server-side record() of a new GameEvent grows the
feed (#id_drama_scroll .drama-event 1 → 2) WITHOUT a reload, via the
record() on_commit broadcast → RoomConsumer.scroll_update relay →
room-scroll.js re-fetch+swap. Validated in the CI channels stage (needs a
cross-process channel layer); the plumbing is already green via the
consumer-relay + record-hook + scroll_status ITs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The room's scroll-of-events feed only updated on refresh — a gamer
watching the SCROLL view never saw a co-player's deposit / role pick /
sig appear. Now every recorded GameEvent nudges all open room sockets to
re-fetch the feed.
- drama.models.record() broadcasts a `scroll_update` to the `room_<id>`
group via transaction.on_commit — so the live re-fetch sees the
committed row, and a rolled-back TestCase never fires it (zero overhead
/ channel-layer traffic for the plain IT suite). _broadcast_scroll_update
is fully guarded: a missing/unreachable channel layer must NEVER break
event recording (falls back to refresh-to-update). One central hook
covers every event writer, current + future.
- RoomConsumer gains a `scroll_update` relay handler (same one-liner shape
as gate_update / turn_changed).
- New `scroll_status` view + url (epic:scroll_status,
room/<id>/scroll/status) renders JUST core/_partials/_scroll.html with
the same events/viewer/scroll_position context as room_view's inline
paint, so the swapped feed is identical.
- room-scroll.js listens for `room:scroll_update`, fetches the partial,
swaps #id_drama_scroll, then re-applies the saved Frame/Redact filter +
restarts the buffer dots on the fresh nodes. URL comes from
.room-page[data-scroll-status-url]. Refactored the dots + filter into
re-runnable helpers; existing behavior (title reel IO, filter form,
localStorage) preserved.
TDD:
- drama RecordBroadcast ITs: record() schedules the broadcast on commit
(captureOnCommitCallbacks execute=True) and NOT before commit.
- RoomConsumer relays scroll_update (InMemory layer, WebsocketCommunicator).
- ScrollStatusViewTest: endpoint renders the feed section, reflects the
latest events, is the bare partial (no navbar/aperture chrome).
544 drama+epic ITs green — the on_commit hook is inert under TestCase, so
no existing event-writer test regressed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The in-room SIG_SELECT stage diverged from the polished GAME SIGN page:
a fixed dark-Gaussian modal over the hex, a stale label-only stat-block,
and no card imagery. This brings it in line with my_sign / my_sea.
A — Stat-block DRY: _sig_select_overlay.html now renders the shared
core/_partials/_stat_face.html (rank-chip + title + arcana + keywords)
instead of a reduced label-only copy; sig-select.js's updateStage() now
calls StageCard.populateStatExtras (the missing call that left those
fields blank). data-arcana-key added per card for title color-keying.
B — Per-card stage image: the stage card gains a .sig-stage-card-img
slot + data-image-url per thumbnail, so an image-equipped seat deck
(RWS / Minchiate) shows real card art on the preview. Thumbnails stay
glyph-only (rank + suit) at every deck — only the stage shows the image.
Keyed off each card's OWN deck_variant, so it auto-upgrades to mixed art
when the dubbodeck assembly lands. No backend change (cards already
carry a deck_variant via _room_deck_variant).
C — Felt-in-aperture: the stage renders INSIDE .room-hex-pane on edge-to-
edge green --duoUser felt (my_sea-style), replacing the hex content; the
old .sig-backdrop blur is gone. .sig-overlay absolute-fills the pane
(.room-hex-pane.has-sig-stage = positioning context); dismissing it
reveals the hex + waiting message behind. Scroll-down still reaches the
reelhouse carousel (untouched scroll pane).
Polishes:
- Image-mode bg escape: the levity 0,3,0 polarity rule
(.sig-overlay/.my-sign-page[data-polarity="levity"] .sig-stage-card)
hard-set a --secUser background that re-clothed image cards behind the
transparent PNG. Added the &.sig-stage-card--image { background:
transparent; border:0; overflow:visible } escape (parity w. the base +
my-sea rules). Latent my_sign bug too. Monodeck-era assumption.
- FLIP .btn-reveal: non-polarized image decks get a FLIP that turns the
preview to the deck card-back (my_sign parity) — back-img + reused
.my-sign-flip-btn (shared positioning/hide/counter-position rules
already cover .sig-stage-card) + a frozen-gated reveal scoped to
.sig-overlay + sig-select.js _flipToBack (500ms Y-rotate, midpoint
swap). SPIN now sets data-spinning so the btn hides mid-rotate.
- Reserved thumbs-up / hover cursors portal to a body-root fixed
container, so they hung over the reelhouse on scroll. sig-select.js now
toggles .cursors-hidden off the aperture scrollTop: instant hide the
moment the scroll leaves the hex, 0.5s opacity ease-in on the full
return. Tray intentionally kept.
TDD: SigSelectUnifiedStageTest (6 ITs) — DRY stat-face present, per-card
data-image-url + data-arcana-key, .sig-stage-card-img slot, image deck
non-empty face URL / text deck empty, has-sig-stage felt + overlay inside
the hex pane. 319 epic test_views ITs green; user-verified live on an RWS
room (no rect, FLIP works, thumb timing). Jasmine for the JS wiring +
the dubbodeck cross-deck assembly (per-seat segment cards, CARTE-solo
both-polarity case, per-card backs) are the tracked follow-on.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The gatekeeper's GATE VIEW is the canonical "who holds which position"
surface, but it reused _table_positions.html, which paints the
role-assigned fade-out class (opacity:0 / scale .5) server-side from
pos.role_assigned (slot <= assigned_count). So as gamers got seated
during Role Select, the gate-view circles vanished one-by-one in
lockstep with the table-hex — and by the time SCAN SIGS appeared (all
six roles assigned) the gate showed an empty hex. The disappear-as-
seated animation is meant as a TABLE-HEX-only cue.
Fix: _table_positions.html now suppresses role-assigned when its new
persist_circles flag is set; room_gate.html includes the partial with
persist_circles=True. room.html passes no flag (→ falsy), so the
table-hex keeps the fade animation untouched. No JS reads role-assigned
in the gate view (role-select.js isn't loaded there), so the server-side
guard is sufficient.
TDD: PositionTooltipRenderTest.test_gate_view_circles_never_fade_when_roles_assigned
— assigns all six roles, asserts the gate view keeps six .gate-slot
circles with NO role-assigned. Verified live via Claudezilla on a
SIG_SELECT setup_sig_session room. 47 render/gate ITs green
(RoleSelectRenderingTest still asserts the table-hex DOES fade).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- In landscape the wordmark is `writing-mode: vertical-rl` + `rotate(180deg)`. The rotation runs every reel child's translate in the rotated frame (inverting motion vs portrait), and the vertical-rl forces the flex MAIN axis onto the vertical, so the five view-word cells stack vertically.
- Vertical reel (hex⇄views): negate the translateY so ROOM exits UP + the view reel rises from BELOW (was dropping ROOM below the rising view).
- Lateral reel (view-to-view): the cells stack vertically, so the portrait `translateX` slid that stack sideways off the slot — only one word rendered (desktop showed PULSE, mobile ATLAS; `row`/`column`/`order`/`row-reverse` all collapse). Traverse with `translateY` instead: the words now slide up-down ALONG the rotated wordmark and each view lands its own word, consistently across desktop + mobile. (True left-right needs a track writing-mode override + reopens the rotate inversion — deferred; up-down is the working result.)
- `overflow: hidden` on the view reel clips it to its one-slot box so the neighbouring cell can't bleed over ROOM (the lateral reel now shares the vertical axis with the hex⇄views reel).
- Pure SCSS, verified visually across desktop + mobile landscape; portrait untouched (all rules inside the landscape media query).
[[feedback-vertical-rl-flex-axis]] [[project-room-game-views-carousel]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- The reelhouse POST card now takes the billboard New Post applet's look: a --duoUser green felt bg with the rotated room-name `> h2` given the SAME green-tinted strip — three translucent 0,0,0/0.125 layers over the felt, NO opaque base, so the felt shows through (matches #id_applet_new_post, which lands there via a %applet-box > h2 specificity quirk; we do it on purpose).
- Each .post-line is restyled to LOOK LIKE the "Enter a post line" composer input below it (mirrors .form-control, _base.scss): a --priUser fill (0.8 alpha), a 0.1rem --secUser border at the same border-radius, full width, an up/down margin + content-driven (dynamic) height — so the thread reads as a stack of input-style pills on the felt. #id_post_table left/right padding zeroed so pills span the full card content width (= the composer row).
- OK button wrapped in .applet-btn-panel (--priUser fill + faint --terUser border) so the green .btn-confirm reads against the felt, mirroring the New Post composer.
- All scoped to .room-view--post — post.html (.post-page), the billboard New Post applet, and MY POSTS (.applet-list-entry) stay untouched (verified live: no .post-line bleed).
- Verified: GameViewsCarouselTest.test_post_view_is_room_thread_with_working_composer + test_atlas_aggregates_provenance_and_posts green (composer still works with the OK-panel wrap); billboard + GAME POST visually confirmed via Claudezilla.
[[feedback-scss-id-context-specificity-trap]] — %applet-box > h2's background-color inherits the applets-container ID via the @extend chain, out-specifying a plain #id_applet > h2 override, so an "opaque base" mask silently renders translucent (the felt bleeds through). New Post hit this by accident; GAME POST replicates it deliberately.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Bug (staging, touch only): a finger-swipe onto ATLAS landed blank ("The atlas gathers . . .") though SCROLL + POST had content; opening the ATLAS gear & clicking OK (no option change) made it appear. Desktop never reproduced.
- Cause: the ATLAS feed is a client-side merge of the live SCROLL + POST DOM. buildAtlasFeed ran in placeView (initial land) + goToView (icon-click / horizontal wheel) only. A native swipe is scroll-snap → it reaches the carousel solely through the IntersectionObserver, which called setActiveView('atlas') w. NO build. The gear-OK was the only other path that re-ran the merge.
- Fix: centralise the build in setActiveView — the single chokepoint placeView, goToView, AND the IO all share — so every activation path (incl. swipe) rebuilds; placeView/goToView no longer call buildAtlasFeed directly.
- Removed the empty-state (template + JS): ATLAS is never reached before SCROLL's game-creation "Welcome to <game>!" event, so rows is never bare; an empty render beats a stale placeholder lingering.
- Jasmine: new spec stubs window.IntersectionObserver, fires a synthetic atlas-intersect entry, asserts #id_room_atlas fills from the SCROLL/POST DOM w.o. any goToView or gear-OK (headless can't fire a real intersection — see the swipe-machine note). 473 specs green; the 3 ATLAS carousel FTs green (icon-click path + empty-state removal).
[[feedback-client-view-rebuild-on-io-swipe-path]] [[project-room-game-views-carousel]] [[feedback-headless-delayed-scroll-dropped]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Unifies the Post applet across post.html and the room game-views POST chat:
- Extract _post_recipient.html (the @handle chip: post-recipient + post-attribution) + _post_header.html (title + 'shared between … & me' / 'just me' prose). post.html's owner branch + the reelhouse POST view both render via _post_header; the invitee branch stays bespoke but reuses the chip.
- Async-chip styling bug (post.html): the bud-invite append built a bare <span class=post-recipient> with the raw display name, so the recipient rendered without the --quaUser key + the @ until a refresh. Now billboard:share_post returns recipient_chip_html (the server-rendered _post_recipient.html) and the bud panel splices THAT in — identical classes + @handle. Also fixed the 'just me'→'& me' flip to mutate only the leading text node so the self line's own .post-attribution span survives.
- Reelhouse POST chat: gains the full .post-header — title hardcoded to 'Gamer Introduction' (dynamic template later) + recipients = the gamers OCCUPYING SEATS (room.table_seats, deduped, viewer excluded), NOT gate-slot/token depositors. And room_post ACCESS now requires a TableSeat, not a filled gate slot: a depositor who never took a seat can retract + leave, so they must not have R/W access to the private chat.
Tests: header IT (seatmate listed, transient gate-slot-only depositor not — scoped to the recipients paragraph since the position strip carries every gate-slot handle elsewhere); room_post seat-access ITs (seated OK; non-seated + gate-slot-only → 403); share_post recipient_chip_html IT; carousel FT setUp now seats disco/amigo/bud (pal/dude/bro stay transient). All green: 255 ITs, 11 carousel FTs, 29 bud-btn/composer FTs.
[[project-room-game-views-carousel]] [[feedback-at-handle-for-usernames]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>