main
735 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
86a349b64e |
wallet shop: free ($0) RWS + Fiorentine decks — FREE ITEM claim unlocks to Game Kit — TDD
- model: DeckVariant.free_in_shop flag (0015 schema); data migration 0016
seeds RWS + Minchiate Fiorentine True (Earthman stays False — it's auto-
granted at signup, not shopped)
- view: _free_decks_for decorates the free-in-shop catalog w. a per-user
.owned flag; shop_claim_free POST endpoint adds the deck to unlocked_decks
(idempotent M2M add) — the free_in_shop filter is the guard that stops the
$0 endpoint unlocking paid/auto-granted decks (404 otherwise). free_decks
wired into both the wallet view + toggle_wallet_applets HX context
- url: wallet/shop/claim (action, no trailing slash)
- template: free-deck tiles reuse the deck's own Game Kit tooltip prose
(name / card-count / description / stock-version line) + a $0 .tt-price
pinned top-right like paid tiles; .tt-micro carries .tt-free-btn (FREE
ITEM) or the same .tt-already-owned pill once owned; reuses
_deck_stack_icon.html
- js: wallet-shop.js _onFreeClick → _doClaimFree POSTs deck_slug → reload
(server-rendered owned pill, same posture as the BUY reload). No guard
portal — free = one-click. Rides the SAME delegated roots as BUY +
idempotent wiring
- css: FREE ITEM wraps to 2 lines like BUY ITEM (extend the mini-portal
.tt-buy-btn white-space:normal rule to .tt-free-btn); shop deck tiles get
the Game Kit fan-out on hover/active by adding .shop-tile-deck to the
.deck-stack-icon splay trigger list — DRY, no transform duplication
- tests: 8 ITs (shop_claim_free behaviors + free_decks context owned flag);
FT claims RWS → 'Already owned' swap → id_kit_tarot_deck appears in Game
Kit; 3 Jasmine specs F1-F3 (claim POST / no-guard / idempotent wiring);
679 dashboard+epic green, no regressions
- trap: hover-hidden microtooltip btn → .text is '' under Selenium; read
get_attribute('textContent') instead [[feedback-selenium-opacity-zero]]
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
d8377b57bc |
my-sea cards: fix rotated significator/cross blur — drop the 4-shadow contour chain on rotated image cards, keep depth — TDD
The image-card contour stroke is 4 chained `drop-shadow()`s; each re-rasterizes the already-downscaled card (408→~64px), so on a ROTATED card the compounded re-sampling reads as BLUR. It's NOT the angle — even the 5° significator blurs, while the 90° cross blurs hardest; the upright COVER (same filter, no rotation) + the unrotated preview modal stay crisp. Verified live (dpr 1): a lone depth drop-shadow on a rotated card is crisp, the 4-shadow chain is not. Fix: the rotated image cards (`.sea-sig-card` -5° + `.sea-pos-cross .sea-card-slot` 90°/270°) drop the contour chain, keeping only the depth drop-shadow → crisp. Upright cards keep the full contour. Zeroing `--img-stroke-w` wouldn't help (the blur is the chained-shadow re-rasterization, not the stroke offset). RWS's contour was a redundant double-frame over its printed border anyway, so its rotated cards lose nothing visible. Verified in Firefox: the enlarged 5° significator renders crisp (sharp title + edges) with depth + printed border intact. Code architected by Disco DeDisco <discodedisco@outlook.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7e39740f9c |
my_sea_visit nav: phase-aware NVM (hex→bud page, draw→hex) + navbar GATE VIEW → visit gate + guard reposition on resize — TDD
Spectator guard-portal + NVM/GATE VIEW routing (user-spec 2026-05-30): - The leave-the-sea guard now confirms with OK (was NVM); `_my_sea_gear.html` gains an `nvm_handler` param so a caller can swap the default leave-nav. - my_sea_visit NVM is phase-aware (`mySeaVisitNvm`): on the DRAW/spread phase it flips back to the table hex (client toggle, stays in-ecosphere → no voice guard, mirroring the owner's picker→landing); on the table hex it LEAVES to the bud's page (`billboard:bud_page`) behind the shared voice-disconnect guard. - The navbar GATE VIEW opens THIS owner's visitor gatekeeper on my_sea_visit (+ its gate page, whose page_class also carries `page-my-sea-visit`), not the viewer's own sea gate; owner pages are unchanged. - showGuard now RE-POSITIONS the open guard on resize/orientationchange (rAF-throttled) so it follows its anchor instead of stranding at its show-time coords — a cross-cutting fix (every gear-menu guard) for the portal landing off-screen after an orientation flip relocated the gear menu. Coverage: MySeaVisitNavTest (navbar→visit gate, gear NVM→mySeaVisitNvm, bud URL, OK label) + MySeaOwnerNavbarGateUnaffectedTest (owner gate untouched). Verified live in Firefox: picker NVM returns to the hex; the guard followed its anchor from 882px→54px on a simulated orientation flip, staying in-viewport. Code architected by Disco DeDisco <discodedisco@outlook.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
571d5a84ae |
voice glow: regression spec — 3-min mute auto-disconnect stops the priRd/.fa-ban path + returns to the available nudge, live — TDD
The auto-disconnect already drives this asynchronously (burger-btn `_muteAutoDisconnect`
→ VoiceRoom.leave → _teardown → _notify{inCall:false,muted:false} → voice-glow render),
and the `voiceroom:ready` subscribe fix guarantees voice-glow receives it without a
refresh. Lock the muted→not-in-call transition behind a VoiceGlowSpec case: drops
`voice-muted` (priRd + .fa-ban) + restores the channel-available `voice-glow` nudge,
no longer "live" (no pulse/eq). Jasmine green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
668105aeeb |
my-sea voice: persist mute across in-sea nav/refresh + 3-min muted auto-disconnect; fix first-connect glow/mute race — TDD
MUTE PERSISTENCE (user-spec 2026-05-30) — a voice mute used to vanish on any
in-sea navigation/refresh (the mesh tears down + auto-rejoins unmuted). Now the
mute is stamped server-side + re-applied on rejoin, with a 3-min muted →
auto-disconnect window:
- `User.voice_muted_at` (timestamp, not a bare bool, so the 3-min window anchors
here) + migration. Per-user, not per-seat: the owner has no seat row, and a
user is in ≤1 voice room at a time, so this uniformly covers owner + visitor.
- POST `/voice/mute` {muted} sets/clears it (new voice app views.py + urls.py,
mounted at `voice/` in core/urls). my_sea + my_sea_visit pass the timestamp to
`#id_voice_btn` as `data-voice-muted-at`.
- voice-mesh.js gains `setMuted(m)` (set vs. toggleMute's flip), honoured by
join's post-getUserMedia `_applyMute`. burger-btn.js: a mute toggle POSTs the
state + arms a client timer; the auto-rejoin re-applies the persisted mute +
re-arms the timer from the stored timestamp (so the 3-min spans navigations,
not resets); an elapsed window on rejoin auto-disconnects instead of rejoining;
a fresh manual join clears any stale mute. On timeout: leave voice + clear.
FIRST-CONNECT GLOW/MUTE RACE (user-reported) — `setOnStateChange` pushes the
current state immediately on subscribe, and voice-glow.js often subscribes
MID-JOIN (getUserMedia pending → inCall=false). Its `setVoiceState` only ever
DELETED `voice.dataset.inCall` (never re-set it) — wiping the join-vs-mute flag
burger-btn.js had just set, so the next click re-joined instead of muting (which
also dropped the peer + killed the equalizer). Two fixes:
- voice-glow keeps `dataset.inCall` SYMMETRIC (set on true, delete on false), so
the mid-join false is restored once the stream resolves → mute works on first
connect.
- voice-glow subscribes reliably on AUTO-REJOIN too (no click to trigger its
poll): voice-mesh.js dispatches `voiceroom:ready` on singleton creation +
voice-glow listens, so the glow is mesh-driven (peer-count equalizer) after a
refresh, not just the in-call-class fallback.
Coverage:
- ITs: VoiceMuteViewTest (login/405/invalid-json guards, stamp on true, clear on
false, re-mute restamps, missing-key=false). voice+lyric 164 green.
- Jasmine: BurgerSpec mute persistence (muteRemainingMs window, rejoin re-mute,
expired-window auto-disconnect, toggle-persists + 3-min fires, manual-join
clears); VoiceGlowSpec dataset.inCall sync (sets on in-call, clears on not,
restores after a mid-join false→true). All green.
- Live multi-party voice (mic/2-device) left to manual verification.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
de4dcd7979 |
my-sea deck glow: single (monodeck) stack matches the levity/gravity --ninUser halo — make every deck match
Bring the single/non-polarized deck stack's hover/active glow in line with the tuned --ninUser halo levity + gravity already use (0.5rem blur / 0.5rem spread / 0.3 alpha) so every deck reads identically (user-spec 2026-05-30). Added a per-deck `$_glow-single` var (parity w. `$_glow-levity`/`$_glow-gravity`, each independently tunable). The monodeck keeps its own neutral --terUser hover border; only the glow box-shadow changed. Code architected by Disco DeDisco <discodedisco@outlook.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b7d871388e |
my-sea deck-stack + spread-card glow: unify hover-reveal / click-persist + --ninUser halo — TDD
Unify the glow/FLIP interaction across the owner picker (my_sea) + the read-only spectator (my_sea_visit), then carry the same selection halo onto the spread cards + deck-stack faces. DECK STACK (user-spec 2026-05-30) — the owner revealed the FLIP only on click (persisted) but never on hover; the spectator revealed it on hover but never persisted. Now BOTH do both: - `.sea-stack-ok` reveal is a single shared rule in _card-deck.scss — opacity fades in on hover/focus (ephemeral) OR via the JS-set `.sea-deck-stack--active` class (click-persist, same class the face-glow rides). The owner's inline `display` toggling is gone (`_showOk`/`_hideOk` just flip `--active`); the spectator's hover-only override in _gameboard.scss is removed. - Interactivity stays gated on `--active`, NOT hover: hover is a purely VISUAL preview (matching the spectator's disabled FLIP). This preserves the owner's two-step deal — were the FLIP click-through on hover, a single stack-click would land on the centred FLIP + deal early (caught by the draw FT). - Spectator persist wired in my_sea_visit.html (click a stack → `--active`, click elsewhere clears); its FLIP stays `.btn-disabled` (read-only). SPREAD CARDS — the same hover-glow + active-persist now on EVERY spread card, building on the cover/cross rules. The prior `.sea-card-slot--focused` glow (0-1-0) was silently overridden by the filled-card drop-shadow ladder (up to 0-4-0) and never rendered (verified live); `!important` (consistent w. the existing `opacity:1 !important` there) makes the halo win on hover + focus. The halo is symmetric (rotation-invariant). No colour change — box-shadow only. DECK FACE HALO — the levity + gravity stack glows now mirror the card halo's tuned geometry (0.5rem blur / 0.5rem spread / 0.3 alpha), each in its own polarity colour (--ninUser / --quaUser); single keeps its own tone. Verified live in Firefox: deck FLIP persists on click + fades on hover; the card halo wins over the drop-shadow on hover/focus across crown/cover/(reversed-)cross; levity/gravity deck glows match the card halo. Draw FTs green (single-draw, hand- completion, AUTO DRAW, auto-drawn-slot reopen) — the two-step deal + card focus survive the display→opacity switch. Code architected by Disco DeDisco <discodedisco@outlook.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7e876557aa |
my-sea spectate: broadcast spread on modal-close + sequence the spectator's AUTO DRAW reveal — TDD
Two follow-ons to the spectate spread-sync, both over the `mysea_<owner>` consumer:
SPREAD ON MODAL-CLOSE — the spread only reached spectators piggy-backed on the
first `sea_draw`, so a visitor sat on a stale layout until a card landed. The
owner's SPREAD modal now broadcasts her chosen spread the moment she closes it
(backdrop / Escape / guard-OK) — before any draw:
- new hand-less `sea_spread` event: view `_notify_sea_spread` → consumer relay →
visitor `_applySpread` (re-lays-out `[data-spread]` + re-captions, no hand
touched).
- new POST `/gameboard/my-sea/spread` the modal-close handler calls, guarded by
`_lastSpread` so re-opening + closing without a change doesn't re-broadcast.
When an active row has an EMPTY hand it also persists the spread onto the row
(so a fresh spectator load lands right too) — stays within the "spread locks
at first card" policy; never overwrites a drawn hand's spread.
SEQUENCED AUTO DRAW — AUTO DRAW commits all six cards in ONE POST (navigate-away
safety) → one `sea_draw` carrying the whole hand, so the spectator saw them pop
in at once ("async as intended, but not in sequence"). The visitor's `_applyHand`
now reveals only the freshly-added entries, one per ~420ms tick (in DRAW_ORDER,
first immediately) — a lone manual-draw card still reveals instantly. Already-
shown cards (`_isShown` by slot card-id) are left untouched, so a cumulative
re-broadcast never re-animates.
Coverage:
- ITs: MySeaSpreadBroadcastViewTest — login/405/unknown-spread guards, broadcast
call, empty-hand persist, no-overwrite-of-drawn-spread, broadcast-failure
resilience.
- channels: spectate consumer relays the hand-less `sea_spread` event.
- Live-verified in Firefox: a 3-card hand fills 1 slot synchronously then the
rest after the stagger; user visually confirmed the full deal sequence + the
modal-close spread propagation.
311 gameboard ITs + 7 spectate channels green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
9678d187b4 |
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> |
||
|
|
877e0f544a |
my-sea: seated chairs settle to --secUser; owner sees all seated members; gear menu column; visit hex scales — TDD
Four my-sea / my_sea_visit fixes from user feedback. 1. Seated-chair snap-back: `.my-sea-landing .table-seat.seated .fa-chair` forced PERMANENT --terUser + --ninUser glow, out-specifying _room.scss's --secUser settle — so a seated chair eased in (the .seat-just-seated flare) then SNAPPED back to the glow. Removed it; the steady look is now the _room.scss --secUser as spec'd. The viewer's --self marker moves off the chair onto the position label so the chair can rest at --secUser. 2. Owner multi-seat: my_sea.html's landing rendered a hardcoded 1C-only seat loop, so the owner only ever saw herself even after refresh. It now renders the shared `_my_sea_seats(request.user)` ring — owner 1C + present visitors 2C-6C — the same list the spectator + broadcasts use. (Live owner-side push is a follow-on; this fixes the on-refresh case.) 3. Gear sea menu: NVM + BYE laid out in a ROW because the BYE form is display:contents + applets.js force-sets the menu to display:block on open (can't flex the menu itself). Wrap them in the shared `.menu-btns` flex container and override it to a COLUMN in portrait / ROW in landscape (DRY — same container the room/applet menus use). 4. Visit hex scale: my_sea_visit didn't load room.js, so scaleTable() never ran and the table-hex rendered unscaled (unlike the owner's my_sea). Load room.js on the visit page too. 62 gameboard ITs (gear NVM + owner-seat + visit) green. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0693a422d2 |
my-sea: second spectate broadcast — seat ring updates live on deposit / BYE — TDD
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> |
||
|
|
32836704b7 |
my_sea_visit: add --visible on live-filled spectator slots so the card lands at final opacity (no empty->filled ease race)
register's _fillSlot sets --filled (opacity:0) but not --visible — the owner adds --visible on stage-dismiss, but the spectator fills directly w. no modal, so the live card raced the empty->filled opacity transition (the long-standing my_sea ease-in-before-ease-out glitch). Add --visible in the same tick after register so the card matches the refreshed state: instant for the outer slots, the intended sea-cover/cross-appear fade for cover/cross. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
01ee8dc1fb |
my-sea: async witness — spectators see the owner's draw land live via WS, no refresh — TDD
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> |
||
|
|
a85f5b6f44 |
my-sea slot: render the 5+-char numeral squeeze (--rank-long) server-side so it survives a refresh — TDD
sea.js's _fillSlot adds .sea-card-slot--rank-long on draw (corner_rank length >= 5) to squeeze long Roman numerals (XVIII, XLVIII, ...) into the slot, but _my_sea_slot.html didn't — so a saved hand stretched the numeral back out on refresh (server render). Add the same length>=5 class server-side. Fixes both the owner picker + the spectator cross (shared partial). +3 ITs (long / short / boundary). Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
260c1c1325 |
my_sea_visit: give the visitor a top-left deck stack (owner's deck) with a hover-revealed disabled FLIP — TDD
Mirrors the owner's deck stack onto the spectator hex, DRY: - new shared _my_sea_deck_stack.html partial (mono DECK / dubbo DECKS by is_polarized) rendered by BOTH the owner picker (my_sea.html, flip_disabled= hand_complete) AND the visitor cross (flip_disabled=True). Owner markup is byte-identical, so its assertions hold. - the visitor's stack uses the OWNER's deck (everyone at @owner's sea plays the owner's deck — the visitor's own equipped deck is irrelevant), pinned top-left (--visit) across the table from the owner who deals bottom-right. - dubbodeck: the Gravity/Levity name flips above the face + upside-down to signal someone across the table is dealing. - the read-only FLIP (disabled ×) is hidden until its stack is hovered/focused, then eases in (same opacity 0->1 over 0.3s as the shared flip-btn-base reveal; inlined since _gameboard precedes _card-deck in the import order) so a permanent × doesn't clutter the stack. ITs: stack keys on the owner's deck (not the viewer's), dubbo renders 2 named stacks, FLIP is the disabled state, no stack when the owner has no deck. Owner deck-stack IT + FT stay green (identical markup). Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c3594d27ed |
my_sea_visit: share the owner picker styling (--duoUser felt + fill) when showing the draw — TDD
The bud-sea (visitor) VIEW DRAW rendered the cross but kept data-phase=landing, so it sat on the --priUser landing bg instead of the owner's --duoUser picker felt, and its #id_my_sea_visit_draw wrapper wasn't a flex container so the picker didn't fill/centre like the owner's. DRY fix (no new visit-only styling): - VIEW DRAW toggle now flips .my-sea-page data-phase landing<->picker, so the cross reuses the shared .my-sea-page[data-phase=picker] --duoUser felt rule. - .my-sea-visit-draw is display:contents, so its .my-sea-picker child becomes a direct flex item of .my-sea-page and fills/centres via the existing .my-sea-picker sizing. FT asserts the page flips to data-phase=picker on VIEW DRAW. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
02d2d565a3 |
RWS card faces: thin the contour stroke for the english deck so it doesn't double the card's own border
The image-mode contour stroke (4 cardinal drop-shadows) follows the PNG alpha. Minchiate faces are transparent-cut to their irregular outline, so the stroke cleaves to the shape; RWS faces are clean cream rectangles that carry their own printed border, so the full 0.2rem stroke reads as a redundant uniform double- frame. Rather than re-process 78 images, thin the stroke for the equipped english deck via CSS only. - _card-deck.scss: the cardinal-stroke offset is now `var(--img-stroke-w, 0.2rem)` (default unchanged for Minchiate/Earthman); `body.deck-family-english` sets it to 0.08rem (a crisp edge, not a frame). Custom props inherit, so the body class cascades into every image card on the page. - base.html: body gains `deck-family-<equipped-deck-family>` when authenticated with an equipped deck. Aesthetic polish — may be reverted (revert = drop the body class + the `--img-stroke-w` var + the english rule). Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c4e738ad16 |
my-sea voice: persist the call across in-sea reloads via auto-rejoin; pngquant the RWS card back — TDD
Voice-persistence follow-up (user-spec item 6). Every my-sea navigation is a full page reload that kills the WebSocket + peer connections; true no-reload nav would need an SPA refactor of the heavily-tested draw IIFEs. Instead we auto-rejoin: bindVoiceBtn remembers the active room in sessionStorage on join and silently re-joins it on the next my-sea page if voice is still available there (mic permission persists for the session, so no prompt). Same user- visible result (a brief reconnect, not seamless) with no risk to the draw flows. - burger-btn.js: sessionStorage 'mysea-voice-room' remember/forget helpers + window.mySeaVoiceForget; bindVoiceBtn refactored to startCall()/withVoiceRoom() and auto-rejoins on bind when the remembered room === the active btn's room. A failed join (e.g. INSECURE_CONTEXT) forgets the room so it doesn't retry. - _my_sea_gear.html: the NVM-disconnect guard confirm + BYE forget the room (and leave the mesh) — an explicit leave shouldn't auto-rejoin. - BurgerSpec: +4 auto-rejoin specs (match / different-sea / inactive / remember + forget). 438 Jasmine specs green. Also (bundled, user's parallel work): pngquant the resaved RWS deck card back (tarot-rider-waite-smith-back.png) from 733KB truecolor+a to a 264KB 8-bit palette PNG, matching its companion card faces. Dimensions preserved (the rotated 401x694). Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2cbc1bf292 |
my-sea spectator: render all present members on the hex (2C-6C), not just the viewer — TDD
The spectator hex showed only owner 1C + the viewer in 2C; other present visitors were invisible. The view now builds a list — owner 1C + each present invitee in 2C-6C by deposit order (capped at MY_SEA_MAX_VISITORS) — so every viewer sees the same absolute seating, with their own seat marked .table-seat--self (a subtle --terUser tint). - my_sea_visit: context (present/empty + token + label + is_self). - my_sea_visit.html: seat ring loops instead of a hardcoded 1C/2C. - _room.scss: .table-seat--self chair tint. - +1 IT (3 present visitors → 2C-4C seated, viewer is the --self one); the both-seated IT updated for the --self marker. 292 gameboard ITs green. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
cb7ca4b5f3 |
voice consumer: cap live connections at 6 per room (defense-in-depth) — TDD
The deposit gate already caps PRESENCE at 5 visitors (+owner = 6), so voice membership is bounded — but a present member opening multiple tabs could still oversubscribe the mesh. RoomVoiceConsumer now claims a slot on connect via an atomic cache counter (cache.incr — atomic on Redis + LocMem) and refuses past VOICE_MAX_MEMBERS=6; the slot frees on disconnect (26h TTL backstop so a leaked slot self-clears). Room-agnostic, so epic rooms inherit it. +1 channels test: 6 connections fit, the 7th is refused, a disconnect reopens a slot. 9 voice consumer channels tests green. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
92d46b3dce |
my-sea voice: volume-reactive equalizer glow + muted (--priRd/.fa-ban) state — TDD
Phase 5b of the my-sea voice batch. Resolves the Bug-B-vs-item-7 conflict per user call 2026-05-29: a click while connected MUTES (leaving stays on BYE/NVM), and the item-7 "--priRd/.fa-ban disconnected" visual maps onto the MUTED state. Replaces Phase 3's 2x-pulse stand-in with a real Web-Audio equalizer. - voice-mesh.js: an AnalyserNode taps each incoming peer stream (lazy AudioContext); `inputLevel()` returns the loudest current RMS (~0..1) across peers. Analysers torn down per-peer + on call end. - voice-glow.js: the live-mic glow now resolves to — alone → `.voice-pulse` (steady 2s cadence); others connected → `.voice-eq`, whose `--voice-level` CSS var is fed each frame from inputLevel() via a self-stopping rAF loop; muted → `.voice-muted` modifier on either (recolor only). rAF cancelled on destroy. - _burger.scss: `.voice-eq` box-shadow spread+alpha scale with `--voice-level` (no keyframe — audio drives it); `.voice-muted` recolors to --priRd (halo stays --ninUser) + flips the voice sub-btn icon to .fa-ban. Drops the unused `.voice-pulse--fast`. - VoiceGlowSpec: +4 specs (equalizer swap, muted-alone, muted-with-others, unmute clears); VoiceMeshSpec: +1 (inputLevel 0 with no analysers). 438 Jasmine specs green. Live-verify on staging (audio can't be auto-tested): the equalizer reacting to real speech + the muted/unmuted colour swap on a live call. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f0b9f02c7c |
my-sea voice: cap visitors at 5 (6 seats) + free a seat on leave — TDD
Phase 5a of the my-sea voice batch (user-spec 2026-05-29). The owner holds 1C; at most 5 visitors fill 2C–6C, which also caps the voice mesh (voice requires a deposited seat, so seat-capping caps membership). - SeaInvite: MY_SEA_MAX_VISITORS=5 + present_count(owner) / table_has_room(owner) classmethods (present = ACCEPTED + deposited + not LEFT). - my_sea_visit_insert_token: a fresh deposit into a full table is bounced (?full=1, no token spent, no seat); a visitor who BYEs frees their seat (is_present → False) for the next visitor. - my_sea_visit_gate: context → the gate shows 'TABLE FULL' + inert rails instead of INSERT TOKEN for a not-yet-present visitor. - 6 capacity ITs (count/room, full-table bounce, leave-frees-seat, gate flag, already-seated not blocked). 291 gameboard ITs green. Remaining Phase 5 (live-verify / needs a spec call): disconnect visuals (--priRd/.fa-ban, item 7) + the true Web-Audio equalizer (item 5) + consumer- level voice-member enforcement + multi-seat (3C–6C) spectator viz. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
da97c623c9 |
voice: fail loud on insecure-context join (mic blocked over HTTP) instead of silent reject — TDD
getUserMedia is only exposed in a secure context (HTTPS, or the localhost exemption). Reached over plain HTTP on a LAN IP — the dev server from a phone at http://192.168.x.x:8000 — iOS/Android leave navigator.mediaDevices undefined, so join() fetched TURN creds (a confusing 200) then silently rejected deep in a .then() with no mic prompt. Desktop works because 127.0.0.1 IS a secure context. (Not a regression — voice never worked on the HTTP dev server from mobile.) - voice-mesh.js: _micSupported() seam + an early INSECURE_CONTEXT reject in join() before any network, so the failure is fast + diagnosable. - burger-btn.js: bindVoiceBtn catches the rejected join, rolls back the optimistic .in-call/dataset.inCall (so the next click retries), and surfaces a Brief — 'Voice needs HTTPS (or localhost) — your browser blocked the mic here.' — instead of failing invisibly. - VoiceMeshSpec: +2 specs (join rejects INSECURE_CONTEXT; btn rolls back + Briefs on reject). Jasmine green. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6799749ede |
my-sea voice: guard before NVM disconnects voice + harden mute (honor pre-stream mute) — TDD
Phase 4 of the my-sea voice batch (user-spec 2026-05-29). ── Voice-disconnect guard (item 6, the achievable slice) ── Every my-sea navigation is a full page reload, which tears the WebRTC mesh down — so voice can't literally persist across a reload without an SPA-style no-reload nav (a separate, larger refactor, deferred). What ships now: the gear-menu NVM warns before dropping the call. - _my_sea_gear.html: the NVM routes through an inline, dependency-free `mySeaGuardedNav(event, url)`. When voice is LIVE (VoiceRoom.localStream) AND the target leaves my_sea (`url` has no `/my-sea`), it pops the shared guard portal — "Leave the Sea? You'll disconnect from voice." — before navigating; confirm proceeds, dismiss stays. NVMs that stay within my_sea, or any nav with no live mic, go straight through. Covers all NVMs (owner my_sea, the gatekeeper, the spectator) since they all include this partial. ── Bug B: desktop mute (mute robustness) ── - voice-mesh.js: extracted `_applyMute()` and call it from join (post- getUserMedia) as well as toggleMute. On desktop the first join pops a mic- permission prompt; a mute toggled while that prompt is open used to be lost because the stream didn't exist yet — re-applying after it resolves makes the mute stick. Teardown resets `muted=false` so a rejoin starts clean. - voice-glow.js: setVoiceState now syncs the voice btn's own flags (.in-call / .muted / dataset.inCall) to the mesh truth, so a rejoin starts clean and the glow's DOM fallback can't get stuck "live". Tests: +5 VoiceMeshSpec mute specs (incl. the pre-stream-mute Bug-B case); +2 NVM-guard FTs (warn when live / pass through when not); 3 gear-NVM ITs updated for the mySeaGuardedNav markup. 286 gameboard ITs + 433 Jasmine specs green. Note: true cross-view voice persistence (no-reload within-my_sea nav) + the desktop-mute live confirmation remain to verify on staging w. 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> |
||
|
|
b021d8017c |
my-sea voice: voice-btn glow/pulse state machine (sea-precedence, pulse-while-alone, 2x on 2nd party) — TDD
Phase 3 of the my-sea voice batch (user-spec 2026-05-29). A --quaUser/--ninUser
glow + pulse machine for the burger btn + its voice sub-btn, driven by the
voice sub-btn availability (voice_active) + the live mesh state.
- voice-mesh.js: VoiceRoom gains a state-change hook — setOnStateChange(cb) +
peerCount() + _notify({inCall, peerCount, muted}), fired on join, every peer
add/drop, mute toggle, and teardown. No behaviour change without a subscriber
(VoiceMeshSpec stays green).
- voice-glow.js (new): the glow machine. PRE-JOIN nudge — burger glows when the
fan is closed (sea draw-nudge keeps burger precedence; voice reclaims it once
the sea glow clears), voice sub-btn glows when the fan opens. LIVE — the glow
PULSES on whichever surface shows (voice sub-btn fan-open, burger fan-closed):
base 2s cadence while alone, doubled (.voice-pulse--fast) once a 2nd party
connects (equalizer stand-in; a true volume-reactive equalizer is a live-only
enhancement). Class writes are reconciled (idempotent) so the burger-class
MutationObserver doesn't feed back on itself.
- _burger.scss: .voice-glow + @keyframes voice-pulse + .voice-pulse(--fast).
- loaded on my_sea.html + my_sea_visit.html (after burger-btn.js).
- VoiceGlowSpec.js (18 specs) + registered in SpecRunner; MySeaSeatsSpec flare
window updated 1.5s → 2s (Phase 2 bump). 428 Jasmine specs green.
Live-verify on staging: the actual glow colours/cadence + the equalizer
upgrade (item 5) and disconnect states (item 7) land in later phases.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
7bd8e3256a |
my-sea spectator: render owner's draw as the identical interactive cross stage; owner seated in 1C when paid OR drawn — TDD
Phase 1 + 2 of the my-sea spectator/voice batch (user-spec 2026-05-29). ── Phase 1: spectator VIEW DRAW parity ── The visitor's VIEW DRAW rendered _my_sea_readonly_draw.html — a flat `.my-sea-scroll` strip that, out of its applet context, blew a single card up to fill the viewport. It now renders the SAME `.my-sea-cross` picker + `_sea_stage` modal the owner sees, populated from the owner's draw, read-only but fully interactive (click card → magnified stage, hover, SPIN, FYI). No FLIP / DEL / AUTO DRAW / deck-stacks / spread combobox — the visitor watches. - `_saved_by_position(saved_hand)` extracted as a shared helper (owner picker + spectator render build the IDENTICAL cross); my_sea refactored onto it. - my_sea_visit context gains `saved_by_position`, `label_by_position`, `default_spread`, and the OWNER's `sea_deck_data` (so sea.js resolves each clicked slot's full card face for the stage). - new `_my_sea_visit_cross.html` mirrors the owner cross + includes `_sea_stage` under `#id_sea_overlay`; my_sea_visit.html embeds the owner deck JSON + loads stage-card.js + sea.js + a trimmed seed IIFE (reconstructs SeaDeal's `_seaHand` from the filled slots so each card is clickable into the stage). - deletes the obsolete `_my_sea_readonly_draw.html`. ── Phase 2: owner 1C seating ── The owner is "seated" in 1C whenever committed to a draw cycle — paid for one (deposit reserved / paid-through credit) OR partially/completely drawn — not only once a card lands. Previously a paid-but-undrawn owner (the PAID DRAW landing) and the visitor's view of her showed the semi-opaque `.fa-ban` default. Seat 1C now carries persistent `.seated` + `.fa-circle-check` (sync on refresh; the one-shot flare just settles into it). - my_sea: new `seat1_seated = hand_non_empty or show_paid_draw`; my_sea.html seat 1C keys on it (class + data-seat-token + status icon). - my_sea_visit: `seat1_present = owner drawn OR owner paid` so the visitor sees the owner seated on the spectator hex under the same conditions. - seat flare bumped 1.5s → 2s (my-sea-seats.js GLOW_MS + _room.scss keyframe). Tests: +2 spectator-cross ITs, +1 spectator-cross FT (Phase 1); +4 owner-seat ITs, +2 visitor both-seated/owner-seated ITs, +1 owner-seating FT (Phase 2). 286 gameboard ITs/UTs green. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1ac380dfc5 |
my-buds async add: insert new row before .applet-list-buffer, not after — keeps spacer last
`_appendBudEntry` queried `.bud-entry-buffer` (a class that doesn't exist — the shell renders `.applet-list-buffer`), so the lookup missed and the row fell through to appendChild, landing BELOW the trailing spacer <li> and leaving a visible gap between the list and the new bud. Query the real class so the new row inserts before the spacer. FT now asserts the buffer stays last-child. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
af8452f22d |
my-buds async add: render full row (anchor + the <Title> + data-tt-* attrs) so the appended entry's tooltip isn't empty — TDD
Adding a bud appended a stripped `@handle`-only <li> — no `applet-list-entry` class, no bud-page anchor, no ` the <Title>` span, no data-tt-* attrs — so the new row rendered without its title and its tooltip came up empty next to the server-rendered rows above it. - add_bud (billboard/views.py) — bud payload now carries `at_handle` (server- computed; the client can't replicate at_handle's truncate_email fallback for username-less buds) + `title` (active_title_display). IT asserts both. - _bud_add_panel.html `_appendBudEntry` — rebuilt to mirror _my_buds_item.html exactly: both classes, data-tt-title/description/email/shoptalk, the bud-name anchor into /billboard/buds/<id>/, and the trailing ` the <Title>`. New FT pins the regression: appended row carries the attrs + anchor + title and its portal populates non-empty on row-lock. AddBudViewTest + append FT green. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3bf35ad539 |
remove dead my-sea invite accept/decline endpoints — acceptance is now implicit on bud-page sea-btn visit (accept-on-GET)
The @mailman Post's OK/BYE block + _invite_actions.html were dropped by the
bud-landing-page sprint; with accept-on-GET on my_sea_visit shipped in
|
||
|
|
f5ee83be0a |
bud page sea-btn cascade: live-invite window + accept-on-GET + glow handoff; my-buds tooltip clamp + row hover/lock — TDD
Fixes the Bill Bud invite cascade so the sea sub-btn actually lights + leads to the bud's my_sea, and gives the My Buds row tooltip viewport clamping + hover/lock styling. SeaInvite.invitee_access_open (gameboard/models.py): new invitee-facing access window — a non-terminal invite (PENDING/ACCEPTED) within 24h of being proffered OR within 24h of the invitee's last gate token deposit. Re-arms on each deposit; DECLINED/LEFT/EXPIRED stay shut. Distinct from is_expired (which only models the PENDING lapse). 8 UTs. bud_page (billboard/views.py): sea_btn_active / sea_first_draw_pending now key on invitee_access_open across PENDING + ACCEPTED, not PENDING-only. Old design darkened the btn the instant the user accepted, so they could never reach the bud's sea from here post-accept — that was the red .fa-ban the user saw. ITs updated: accepted-within-window now lights; added stale-accepted-dark + recent-deposit-relights cases. my_sea_visit (gameboard/views.py): accept-on-GET — a still-pending, non-expired invite from the owner to the visitor is accepted implicitly on arrival (the sea-btn cascade + @mailman post-attribution anchor both land here, so the click IS the acceptance). Previously PENDING → 403, so the cascade dead-ended. ITs: pending-invitee now auto-accepts (200); expired-pending still 403s; stranger still 403s. bud.html: burger → sea_btn glow-handoff machine (the my_sea.html cascade minus the spread-modal stage) so the glow rides the affordance chain to the click target; active sea click clears glow, preserves .active, navigates. my-buds-tooltip.js: clamp the position:fixed #id_tooltip_portal to the viewport on row-lock — same 1rem-inset shape as game-kit.js / sky-wheel.js / wallet.js (measure after .active, clamp left, prefer above / flip below). Reset on clear. _billboard.scss: .bud-entry hover + .row-locked highlight (rows aren't .row-3col so the existing rule missed them) — fill --secUser, flip the --terUser handle to --quiUser, trailing title to readable --priUser. 520 billboard+gameboard ITs/UTs green; affected sea-btn-cascade FT green. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d87f26003b |
CI: wrap test-two-browser-FTs commands in _retry_failed.sh
Pipeline #351 hit a NoSuchWindowException / browsing-context-discarded flake on the LAST channels FT (test_first_done_polarity_sees_other_group_settling_message) — typical cumulative-Firefox-memory-pressure failure on a multi-browser test run as the 22nd in its bucket. Test passes locally and in isolation; no code regression. The other two FT stages (test-FTs-room, test-FTs-non-room) already route through `_retry_failed.sh`, which parses Django's FAIL:/ERROR: lines from stdout and re-runs only the failed labels. Wrapping the three two-browser-FTs commands (two-browser / sequential / channels tags) in the same script gives the channels suite the same flake recovery without slowing the happy path (first-run-green short-circuits to exit 0). Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
b563e96f82 |
RWS deck: flip has_card_images=True to light up image-mode rendering — TDD
The 79 resized + pngquant'd RWS card-face PNGs at cards-faces/english/rider-waite-smith/ are now in place (commit
|
||
|
|
1e1a0a5ab8 |
deck images: resize Minchiate + RWS to 700px height + re-pngquant; drop orphan MySeaInviteAcceptanceLogTest — TDD
Card art was loading slowly enough on fast networks that animations were firing on empty <img> objects. Photoshop pass dropped both decks to 700px height (~50% linear from Minchiate's prior 1024px and ~57% from RWS's prior 1646px), then pngquant `--quality=65-85 --speed=1 --strip --skip-if-larger` reclaimed the per-pixel bit-rate cost of the fresh PSD save: Minchiate: 36.5 MB → 19.1 MB (avg 381 KB → 199 KB) [-48%] RWS: 76.3 MB → 15.8 MB (avg 989 KB → 205 KB) [-79%] Combined: 112.8 MB → 35 MB across 177 cards [-69%] RWS dropped the most because it was previously at 960×1646 vs Minchiate's 615×1024 — the higher source resolution had more room to give up. 700px height @ ~420px width is still 2× Retina coverage for the Sea Stage modal (the largest display surface at ~350px wide on desktop). No code change needed — `TarotCard.image_url` resolves the same filenames; the swap is purely on-disk. Drop `MySeaInviteAcceptanceLogTest` (whole class, single test_invitee_sees_invite_line_with_ok_bye method) from functional_tests/test_game_my_sea.py — orphaned by the bud landing page sprint that removed `.invite-ok-btn` / `.invite-bye-btn` / `.invite-actions` from the @mailman invite Line unconditionally. The salvageable "invites you to" prose assertion is now covered by functional_tests/test_bill_mailman_invite_post.py::MailmanPostStructureTest plus apps/billboard/tests/integrated/test_mail.py::LogSeaInviteTest::test_prose_interpolates_owner_handle_and_default_possessive. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
6cc11924e3 |
bud landing page: /billboard/buds/<id>/ + my_buds tooltip portal + @mailman post-attribution anchor — TDD
Replaces the @mailman invite Line's inline OK/BYE block w. a dedicated per-bud surface. Three new FTs (test_bill_bud_page, test_bill_my_buds_tooltip, test_bill_mailman_invite_post — landed red 2026-05-27 PM) drive: per-bud landing page rendering 4-btn apparatus + shoptalk textarea + invite-cascade glow handoff; my-buds row tooltip portal w. .tt-title/.tt-description/.tt-email/.tt-shoptalk/.tt-milestone slots; mailman Brief surfacing on any authenticated page-load via context processor + base.html JSON-script.
Models: new `BudshipNote(user, bud, shoptalk[CharField max=160], edited_at)` w. unique_together — per-relation personal note about a bud, never visible to the bud. Lazy-created on first shoptalk save so absence of a row reads as 'never edited' (drives .tt-milestone slot presence).
URLs (billboard): `buds/<uuid:bud_id>/` (bud_page), `buds/<uuid:bud_id>/shoptalk` (save_bud_shoptalk), `buds/<uuid:bud_id>/delete` (delete_bud).
Views: bud_page auto-adds the bud on first visit (mirrors share_post implicit-add); resolves `pending_invite` as non-expired PENDING SeaInvite(owner=bud, invitee=request.user) → drives `sea_btn_active` + `sea_first_draw_pending` flags that _burger.html already reads on my_sea + room. my_buds enriches each bud w. `.shoptalk_text` + `.milestone_dt` so the row template can render data-tt-* attrs without an extra template tag.
mail.py: INVITE_TEMPLATE now interpolates `owner_id` into an `<a class="post-attribution" href="/billboard/buds/{owner_id}/">{handle}</a>` wrapper around the owner's handle. post.html's existing safe-filter branch (gated on author username == 'mailman') passes it through unescaped. Removed the {% if line.sea_invite %} include path — _invite_actions.html left in place for archival.
Templates: new bud.html (header + shoptalk form + apparatus + gear + burger fan + sea_btn nav inline JS); new _bud_gear.html (NVM→my_buds, DEL→guard portal "Delete this bud?" → POST delete_bud); new _bud_tooltip.html (portal w. .tt-* slots); _my_buds_item.html wraps `@handle` in an anchor to bud_page + carries data-tt-* attrs + " the {{ active_title_display }}"; my_buds.html includes the tooltip portal + loads my-buds-tooltip.js.
JS: new my-buds-tooltip.js binds row clicks → .row-locked + populates #id_tooltip_portal from data-tt-* attrs; anchor clicks pass through to navigate; .tt-milestone is removed from DOM (not just emptied) when never-edited so the FT can distinguish absent vs cleared-after-edit.
SCSS: extend landscape gear-btn rule + #id_*_menu rule w. `.bud-page` + `#id_bud_menu` (otherwise gear-btn collided w. bud-btn in landscape on bud.html). Bump active burger sub-btn z-index to 1 so click hit-test picks the active sub-btn during the 0.25s fan arc-out animation (otherwise a later-in-DOM inactive btn obscured the active target during transition).
Cross-page Brief surface: new `mail_brief_payload` context processor injects the user's oldest unread MAIL_ACCEPTANCE Brief into every authenticated response; base.html renders the JSON-script + auto-fires Brief.showBanner. Mark-read still rides view_post's existing GET unread-flip — no new endpoint.
Pre-existing MySeaInvitePostRenderTest (test_sea_invite_views.py) inverted to match the new contract: the .invite-actions sweep is unconditional (PENDING / ACCEPTED / DECLINED all carry prose only); pinned the post-attribution anchor + bud-page href in its place.
1518 ITs green (1475 app ITs + 43 sprint ITs), 23 sprint FTs green (5 my_buds tooltip + 13 bud page + 5 mailman invite post). Jasmine specs from sprint plan deferred — FT coverage of burger-glow / row-lock / portal-populate paths suffices and the textarea blur-POST flow isn't implemented in this sprint (form is server-action only, save-on-blur AJAX is a follow-on).
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
c41cf7ed36 |
coturn: activate [coturn] inventory host (turn.earthmanrpg.me + v4/v6)
Uncomment + fill the [coturn] group so the play has a host to target (empty group was the 'no hosts matched' / 'no hosts to target' error). Secret stays vault-only — deliberately omitted from the host line (host_vars override group_vars). Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
68239ac5d4 |
coturn: wire COTURN_* into app env template (gamearray.env.j2)
COTURN_SHARED_SECRET={{ coturn_secret }} (vault) + literal host/realm. Only the shared secret is sensitive; it must equal the coturn droplet's static-auth-secret. Host/realm are public.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
c9a61e5614 |
coturn: optional dual-stack TURN via guarded coturn_public_ip6
Set coturn_public_ip6 in inventory to advertise IPv6 relay candidates (2nd external-ip) AND emit matching v6 denied-peer-ip ranges (::1, fe80::/10, fc00::/7) for SSRF parity with the v4 lockdown. Unset → byte-identical pure-IPv4 config as before, so it's zero-risk opt-in. Droplet now has IPv6 on; this makes the conf dual-stack-ready. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
41217d5438 |
my-sea voice Phase C: WebRTC mesh signaling app + TURN endpoint + voice-btn wiring + coturn infra — TDD
Phase C (final) of the my-sea invite → spectator → voice blueprint. Self- hosted WebRTC mesh voice, built room-general but wired for my-sea only; epic 6-seat rooms reuse the same consumer later (key on Room.id). Media never touches the server — only signaling is relayed. Built from the blueprint's distilled spec (disco-voice-mesh.pdf unreadable in-env: no poppler/pypdf). - C1: new apps/voice/ — RoomVoiceConsumer (AsyncJsonWebsocketConsumer): signaling-only relay (room group voice.<room_id> + per-peer peer.<uuid>; hello→present handshake, offer/answer/ice routed by target/source, left on disconnect). room_id is a STRING kwarg (mysea-<owner_id> now). _can_join gates: mysea → owner OR present invitee (token deposited, not left); epic UUID → seated gamer (later). routing.py ws/voice/<str:room_id>/; asgi.py aggregates epic + voice urlpatterns under AuthMiddlewareStack. voice-mesh.js: VoiceRoom client (getUserMedia AEC/NS/AGC, mesh RTCPeerConnection, newcomer-offers handshake, tuneOpus SDP munge = inbandfec+dtx+40kbps cap, mute via getAudioTracks().enabled), lazy-loaded. - C2: apps/api VoiceTURNCredentialsAPI at /api/voice/turn-credentials/ — coturn use-auth-secret REST scheme: username=<expiry>:<user_id>, credential=base64(HMAC-SHA1(username, COTURN_SHARED_SECRET)) + stun/turn iceServers + ttl. Authenticated-only. 4 ITs (HMAC shape, auth gate). - C3: settings COTURN_SHARED_SECRET / COTURN_TURN_HOST / COTURN_REALM / COTURN_TTL env block. - C4: #id_voice_btn wiring — _burger.html renders .active + data-room-id when voice_active; burger-btn.js bindVoiceBtn (active click → lazy-load voice-mesh.js → join / toggle-mute; inactive → existing 2-pulse flash). my_sea (owner) + my_sea_visit (spectator) views compute voice_active (open 24h window) + voice_room_id=mysea-<owner_id>; spectator page now includes the burger. 4 voice-context ITs. - C5: infra/coturn.conf.j2 (use-auth-secret, the external-ip footgun, relay port range, TLS 5349, peer-IP lockdown) + infra/coturn-playbook.yaml (dedicated droplet, PySwiss-style split: install coturn, template conf, ufw 3478/5349/49152-65535, systemd enable) + [coturn] inventory placeholder. *** Manual ops step: provision the droplet + fill inventory before voice works on staging/prod; CI/local need none of it. *** - C6: 8 channels ITs (@tag channels) — connect/auth/_can_join gate (owner, present invitee, stranger, not-present, anon) + hello/present handshake + offer routing + left-on-disconnect. Scope-injected; TransactionTestCase. - JS: VoiceMeshSpec.js (tuneOpus) + voice-mesh.js registered in SpecRunner. 1440 IT/UT green; voice channels IT + full Jasmine + voice-btn FT green. Voice infra is code-complete — provision the coturn droplet to go live. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d0c39b51b6 |
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> |
||
|
|
fb8563eed2 |
my-sea bud-invite Phase A: SeaInvite model + @mailman log + OK/BYE accept/decline — TDD
Phase A of the my-sea invite → @mailman → spectator → voice blueprint (magical-dancing-quasar.md). Pure Django; no new infra this phase (the coturn droplet lands in Phase C5). Mirrors the @taxman ledger shape throughout. - A1: SeaInvite model (gameboard) — single source of truth for a my-sea invite (owner / invitee / status / timestamps + OneToOne FK to its @mailman Line). is_expired / voice_active / is_present / expires_at properties; 12 UTs. created_at uses default=timezone.now (MySeaDraw precedent) for testable 24h expiry; a token deposit makes the invite non-expiring per spec. - A2: reserved @mailman system user — get_or_create_mailman + "mailman" added to RESERVED_USERNAMES + seed migration lyric/0015. Email domain confirmed w. user as mailman@earthmanrpg.local (matches adman/taxman). - A3: billboard KIND_MAIL_ACCEPTANCE on Post + Brief; extends the post_save unsolicited-line guard (_SYSTEM_AUTHOR_POST_KINDS) + migration billboard/0009. - A4: apps/billboard/mail.py log_sea_invite — appends one interactive Line + invitee Brief on the invitee's "Acceptances & rejections" Post, links the Line back onto the SeaInvite; "Listen!—@owner invites you to {poss} drawing table" prose via at_handle + resolve_pronouns. Unregistered invitee no-ops. - A5: post.html renders OK .btn-confirm / BYE .btn-abandon (PENDING) or a status badge (ACCEPTED / DECLINED / LEFT / EXPIRED) from line.sea_invite.status via new _partials/_invite_actions.html; 'mailman' added to the system-author |safe + read-only-input + bud-panel-suppression branches. - A6: real my_sea_invite (replaces the coming-soon stub) — resolves recipient, dedups outstanding PENDING/ACCEPTED, creates SeaInvite + logs the @mailman line; new my_sea_invite_accept / my_sea_invite_decline endpoints (invitee-only, redirect back to the invite-log Post; accept links invitee FK + stamps accepted_at). 16 ITs. - A7: updated MySeaBudBtnInviteTest (stub→real invite) + new MySeaInviteAcceptanceLogTest FT (invitee opens their log Post, sees the line + OK/BYE). Both green. 457 IT/UT green. Phase B (invitee spectator seat-2 + visitor token gate) + Phase C (WebRTC mesh voice + coturn droplet) to follow. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1c799d35ca |
room-stage FTs: realign 4 fails to new my-sea NVM + spread-modal + landscape kit-bag UX — TDD
CI #346 test-FTs-room had 4 consistent fails (failed on both the first run AND the retry, so real, not flakes). All 4 are test-side — the shipped features are correct; the FTs lagged behind deliberate UX changes + a race they never needed to depend on. test_game_my_sea.py - test_nvm_navigates_back_to_gameboard → renamed test_nvm_navigates_back_to_ my_sea_hex; asserts /gameboard/my-sea/$ now. NVM on the gatekeeper navigates to the table hex, not out to /gameboard/ (changed |
||
|
|
c30b63cd5d |
burger Sea sub-btn: first-draw --priYl glow handoff (phase 3/3) — TDD
Final slice of the Sea sub-btn rollout (phase 1 = .active wiring 3ae85b9; phase 2 = modal extraction + CONT DRAW
|
||
|
|
a39053d3f6 |
CI #345 fixes: bud-kit mutual exclusion test → portrait viewport; carte sign-gate-brief wait → wait_for_slow
Two CI #345 failures addressed: ## test_bud_active_fades_kit_btn (test_core_bud_btn.py) Real regression — earlier sprint scoped `html.bud-open #id_kit_btn { opacity: 0 }` to `@media (orientation: portrait)` (in `_bud.scss`) because in landscape kit_btn sits at the TOP of the right sidebar + bud_panel slides across the BOTTOM, no visual conflict. The default CI landscape viewport (1366x900) rendered the fade rule inert → the kit_btn stayed at opacity 1 → assertEqual(opacity, 0.0) failed. Fix: `BudKitMutualExclusionTest.setUp` now resizes to portrait (800x1200) so the fade rule actually fires. Both `test_bud_active_fades_kit_btn` + `test_kit_active_fades_bud_btn` now exercise the rule in the orientation where it lives. ## test_carte_blanche_equip_and_multi_slot_gatekeeper (test_trinket_carte_blanche.py) CI flake — the test waits up to 10s (`wait_for` default) for `.my-sea-sign-gate-brief` to appear after navigating to /gameboard/. Under CI contention the Brief's DOM-ready handler can land past 10s; CI #345 hit a NoSuchElement timeout. The screendump confirmed the Brief WAS in the DOM, just past the wait window. Fix: bump that one wait to `wait_for_slow` (60s ceiling). Same pattern used elsewhere (Jasmine spec runner, sig select countdown). ## Verification Both fixed tests green locally. No model / view / template touches. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
6fbeed78d8 |
burger Sea sub-btn: spread-form modal + relocated deck stacks + mid-draw CONT DRAW escape — TDD (phase 2/3)
Second slice of the Sea sub-btn rollout (phase 1 = .active wiring in 3ae85b9; phase 3 = --priYl glow handoff to come).
## Spread modal (#id_sea_spread_modal)
`templates/apps/gameboard/my_sea.html` — `.sea-form-col` (spread combobox + AUTO DRAW + DEL btns) now lives inside `<div id="id_sea_spread_modal" class="my-sea-spread-modal" hidden>`. Hidden by default; opens on #id_sea_btn click (per the inline `<script>` block). The deck stacks (`.sea-stacks` + `.sea-stacks-label`) are extracted to a new `.my-sea-stacks-wrap` sibling that stays visible on the page so the FLIP affordance remains usable while the modal is closed.
## Modal SCSS
`static_src/scss/_gameboard.scss`:
- `.my-sea-spread-modal` — position:fixed inset:0; z-index:320 (above all corner btns); pointer-events:none w. children opting back in. `&[hidden]` makes it explicit that the closed state stays display:none.
- `.my-sea-spread-modal__backdrop` — full-viewport semi-transparent w. backdrop-filter blur; pointer-events:auto so click-outside closes.
- `.my-sea-spread-modal__panel` — opaque card, border + shadow, max-width:90vw. `.sea-form-col` inside drops its fixed 16rem width + uses min-width.
- `.my-sea-stacks-wrap` — position:absolute bottom:4.5rem right:1rem (clear of bud/burger); z-index:5 (above .my-sea-cross, below modal).
## Modal JS
`templates/apps/gameboard/my_sea.html` — new inline `<script>` at the bottom owns:
- `#id_sea_btn` click → opens modal (guarded on `.active` so the burger-btn.js inactive-flash handler still runs for inactive sub-btns).
- Escape key → close.
- Backdrop click → close.
- `#id_guard_portal .guard-yes` click (delegated) → close. This is the key UX detail user-spec'd: AUTO DRAW + DEL both open a guard portal that positions itself against the visible action/del btn — closing the modal on the btn click itself would dump the portal at (0,0). Closing on guard OK instead means the portal positions correctly + the modal closes only when the action is confirmed. NVM (`.guard-no`) leaves the modal up so the user can retry.
Also `apps/epic/static/apps/epic/burger-btn.js` — delegated fan click now closes the burger fan when an ACTIVE sub-btn is clicked (was: no-op for active). The per-page sub-btn handler runs in target phase BEFORE the fan-bubble handler, so the action kicks off w. a clean visual.
Client-side `.active` sync: server renders sea_btn inactive on the landing page (show_picker=False there). The DRAW SEA → picker transition is client-side (no re-render), so the IIFE in my_sea.html now flips `seaBtn.classList.add('active')` at the same SEAT_ANIM_MS moment data-phase swaps to 'picker'. Sea_btn stays `.active` for the rest of the picker phase, INCLUDING after hand_complete — DEL + GATE VIEW both live inside the modal + the user needs them accessible (earlier iteration deactivated on hand_complete; reverted on user feedback).
## Mid-draw NVM → CONT DRAW (apps/gameboard/views.py + my_sea.html)
Earlier iteration's gear-menu NVM nav-backed to `/gameboard/my-sea/` mid-draw, but the server's `hand_non_empty` branch re-rendered the picker on the next GET — looping the user right back into the spread. New `?phase=landing` escape hatch:
- `views.py`: `force_landing = request.GET.get('phase') == 'landing'`; `show_picker = (hand_non_empty or (phase_param and show_paid_draw)) and not force_landing`. New context var `show_cont_draw = force_landing and active_draw and not active_draw.is_hand_complete`.
- `my_sea.html` landing: new `{% if show_cont_draw %} CONT DRAW {% elif show_paid_draw %} ... ` branch on the table-center action-btn. CONT DRAW is an `<a href="{% url 'my_sea' %}">` (plain navigation, no `?phase=landing` so the next GET falls through to the hand_non_empty picker branch).
- `my_sea.html` gear include: picker phase passes `nvm_url={% url 'my_sea' %}?phase=landing` so the gear NVM exits to the landing-with-CONT-DRAW state instead of looping back into the picker.
## Tests
`apps/gameboard/tests/integrated/test_views.py` — 4 ITs added/updated:
- `test_sea_btn_stays_active_when_hand_complete` (replaced the prior "returns to inactive" assertion — sea_btn now stays active per user spec).
- `test_gear_nvm_navs_to_my_sea_landing_on_picker_phase` (updated URL to include `?phase=landing`).
- `test_force_landing_renders_cont_draw_btn_mid_draw` (NEW) — verifies CONT DRAW renders + FREE DRAW absent.
- `test_force_landing_hides_cont_draw_when_no_active_draw` (NEW) — regression guard: sig'd user w. no draw still sees FREE DRAW.
- `test_force_landing_hides_cont_draw_when_hand_complete` (NEW) — CONT DRAW absent for completed hands.
`functional_tests/test_game_my_sea.py` — new module-level `_open_spread_modal(test)` helper + applied to 7 tests that touch in-modal selectors (combobox / action_btn / del):
- `MySeaSpreadFormTest.test_picking_spread_swaps_data_spread_and_position_visibility`
- `MySeaSpreadFormTest.test_per_spread_position_labels_render_and_update`
- `MySeaCardDrawTest.test_action_btn_transitions_to_gate_view_on_hand_complete` (also: close modal before manual FLIP draws, use innerText for hidden-element text checks)
- `MySeaCardDrawTest.test_auto_drawn_slots_can_reopen_stage_modal_on_click`
- `MySeaCardDrawTest.test_form_col_renders_decks_lock_hand_del_and_reversal_pct`
- `MySeaLockHandTest.test_del_click_opens_shared_guard_portal`
- `MySeaLockHandTest.test_del_confirm_clears_hand_and_returns_to_gate_view_landing`
JS .click() is used inside `_open_spread_modal` (not Selenium .click) because the fan sub-btns spend ~0.25s mid-animation stacked at the burger centre — Selenium would hit a click-intercept by whichever sub-btn is z-topmost during the transform.
## Verification
All 1370 IT+UT green (+5 new MySeaViewTest, +1 from earlier phase 1 carry-over). 7 updated FTs green when re-run together.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
3ae85b962b |
burger sea sub-btn: wire .active = show_picker and not hand_complete (phase 1/3) — TDD
First slice of the "Sea sub-btn opens the spread modal" feature push.
## Server (apps/gameboard/views.py)
`my_sea` view now passes `sea_btn_active = show_picker and not hand_complete` in context. Picker phase w. cards still to draw → True; landing / sign-gate / hand-complete states → False. The condition mirrors the AUTO-DRAW-vs-GATE-VIEW state machine — sea_btn lights up exactly when AUTO DRAW is the live action btn, returns to inactive at the same moment AUTO DRAW becomes GATE VIEW + the deck FLIP gains .btn-disabled.
## Template (_partials/_burger.html)
`#id_sea_btn` conditionally renders the `.active` class:
```
<button id="id_sea_btn" type="button" class="burger-fan-btn{% if sea_btn_active %} active{% endif %}" ...>
```
Per the burger sub-btn CSS that landed in
|
||
|
|
5cade51d03 |
my-sea gear NVM: gatekeeper + picker nav-back to table hex instead of ejecting to gameboard — TDD
User-spec'd 2026-05-26: the gear menu's NVM was always nav-backing to /gameboard/ from every my-sea state, which ejects the user one step further than they want when bailing on the gatekeeper or the spread/crucifix picker. Both should nav-back to the my-sea TABLE HEX (the landing) — one step back, not all the way out.
## templates/apps/gameboard/_partials/_my_sea_gear.html
Now takes an optional `nvm_url` context variable; falls back to `{% url 'gameboard' %}` when unset. The onclick handler reads `{{ nvm_url }}` so callers can override per-page.
## templates/apps/gameboard/my_sea.html
```
{% if show_picker %}{% url 'my_sea' as nvm_url %}{% endif %}
{% include "apps/gameboard/_partials/_my_sea_gear.html" %}
```
Picker phase → nvm_url = my_sea landing (table hex). Sign-gate + landing phases fall through to the default (gameboard) — landing's NVM = "back out of my-sea entirely", which is the existing behaviour.
## templates/apps/gameboard/my_sea_gate.html
```
{% url 'my_sea' as nvm_url %}
{% include "apps/gameboard/_partials/_my_sea_gear.html" %}
```
Gatekeeper unconditionally nav-backs to the my-sea landing.
## Tests
`apps/gameboard/tests/integrated/test_views.py`:
- `MySeaViewTest.test_gear_nvm_navs_to_gameboard_on_landing_phase` — landing NVM still goes to /gameboard/ (regression guard).
- `MySeaViewTest.test_gear_nvm_navs_to_my_sea_landing_on_picker_phase` — picker NVM goes to /gameboard/my-sea/ (seeds a non-empty hand to trigger show_picker per views.py:277's `hand_non_empty or (phase_param and show_paid_draw)` logic).
- `MySeaGateViewTest.test_gear_nvm_navs_to_my_sea_landing_not_gameboard` — gatekeeper NVM goes to /gameboard/my-sea/ (and explicitly NOT /gameboard/).
## Verification
All 1364 IT+UT green. No FTs assert the gear-menu NVM target URL (per pre-change grep) so no test fixes required there.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
ca960d1d43 |
my_sea.html: persist burger + bud btns across all 3 phases — TDD
Bud + burger now render unconditionally on /gameboard/my-sea/ alongside the always-on gear-btn (was sprint-6c) + base-html kit-btn. Persists through sign-gate, landing, and picker phases so the user can reach the cross-cutting menu / invite affordance from every state. ## templates/apps/gameboard/my_sea.html 3 new includes/scripts placed at the bottom of `<div class="my-sea-page">` alongside the existing `_my_sea_gear.html`: - `_my_sea_bud_panel.html` — bud btn + slide-out (POSTs to `my_sea_invite` stub, same shape as on my_sea_gate.html) - `_burger.html` — burger btn + fan of 5 sub-btns - `<script src="...burger-btn.js">` — the delegated click + flash handler All unconditional (outside the if/else nesting that branches on user_has_sig / show_picker), so they survive every phase transition. ## apps/gameboard/tests/integrated/test_views.py 3 new ITs on MySeaViewTest — one per phase: - `test_my_sea_renders_bud_btn_and_burger_in_sign_gate_phase` (no significator) - `test_my_sea_renders_bud_btn_and_burger_in_landing_phase` (significator set, no draw) - `test_my_sea_renders_bud_btn_and_burger_in_picker_phase` (significator + empty MySeaDraw row + ?phase=picker) Each asserts both `id="id_bud_btn"` + `id="id_burger_btn"`. Sign-gate phase also asserts burger-btn.js loaded. ## Verification All 215 gameboard IT+UT green (+3 new). No JS / model touches; pure template additions. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
894d65fd6b |
burger sub-btns: opacity-0.6 inactive default + 2-pulse --priRd flash w. fa-ban swap on click — TDD
Adds the active/inactive distinction to the 5 burger fan sub-btns. Default state is INACTIVE (opacity 0.6, real icon visible); active conditions get wired one-by-one in later sprints as each surface matures.
## Markup (templates/apps/gameboard/_partials/_burger.html)
Each sub-btn now renders BOTH icons:
- `<i class="fa-solid fa-<real> burger-fan-icon--on">` (sky/earth/sea/voice/text)
- `<i class="fa-solid fa-ban burger-fan-icon--off">`
CSS keeps the real icon visible by default in both .active + inactive states. The fa-ban only surfaces during the .flash-inactive pulse below (icon swap is tied to the pulse class, not to inactive state per se — user-spec'd).
## SCSS (static_src/scss/_burger.scss)
- `#id_burger_btn.active ~ ... .burger-fan-btn.active { opacity: 1 }` — active sub-btn fully visible.
- `#id_burger_btn.active ~ ... .burger-fan-btn:not(.active) { opacity: 0.6 }` — inactive default.
- `.burger-fan-icon--on / --off` stacked absolute-position so the swap doesn't shift the layout box.
- `.burger-fan-btn.flash-inactive` — adds --priRd border + glow (box-shadow modeled on sig-select's SAVE SIG countdown but lighter), AND swaps to fa-ban via `.burger-fan-icon--on { display: none } / --off { display: inline-block }`.
The `#id_burger_btn` itself (the trigger btn) is explicitly NOT subject to inactive/active opacity treatment — only the sub-btns.
## JS (apps/epic/static/apps/epic/burger-btn.js)
Delegated click handler on `#id_burger_fan`: any `.burger-fan-btn` click that DOESN'T carry `.active` runs `_flashInactive(subBtn)` — 2 pulses, 180ms ON / 120ms OFF (tighter than sig-select's 600ms cadence per user spec). Active sub-btns will route to their per-feature handlers in later sprints; for now they no-op.
## Tests
- `apps/epic/tests/integrated/test_views.py::RoomBurgerBtnRenderTest::test_each_sub_btn_renders_dual_icon_for_inactive_flash_swap` — asserts `burger-fan-icon--on` + `--off` appear 5 times each (one per sub-btn). fa-ban itself isn't counted directly — `_table_positions.html` also renders fa-ban for non-starter seats — but the burger-fan-icon classes are unique to the fan.
- `static_src/tests/BurgerSpec.js` — 5 new specs under `describe("inactive sub-btn flash")`:
- adds .flash-inactive on click
- removes after ~180ms (first ON window)
- re-adds after ~480ms (second ON window during the 2nd pulse)
- settles back to default after ~800ms (full 2-pulse cycle)
- does NOT flash when sub-btn carries .active
Uses `jasmine.clock()` for deterministic timing. Mirror-copied to `static/tests/BurgerSpec.js` for the Jasmine FT runner.
## Verification
1358 IT+UT green. Jasmine FT runs all specs (incl. the 5 new flash specs) green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
6809681e5a |
my_sea_gate burger; _bud_apparatus shared shell; CI #344 tray-anchor fix — TDD
Continuation of the burger sprint into the my-sea gatekeeper + a couple companion cleanups the visual + CI runs surfaced.
## my_sea_gate.html burger
`templates/apps/gameboard/_partials/_room_burger.html` → `_burger.html` (git mv) — now lives at a non-room-scoped path since it's reused across templates. Updated room.html's include path.
`templates/apps/gameboard/my_sea_gate.html` — includes `_burger.html` + loads `burger-btn.js`. Burger renders unconditionally on the my-sea gatekeeper, same affordance as the room gatekeeper.
`apps/gameboard/tests/integrated/test_views.py::MySeaGateViewTest` — new `test_gate_view_renders_burger_btn_and_fan` IT asserts burger_btn + burger_fan + 5 sub-btns w. correct ids + burger-btn.js loaded on `/gameboard/my-sea/gate/`.
## Burger fade rule reinstated
`static_src/scss/_burger.scss` — `html.bud-open #id_burger_btn { opacity: 0; pointer-events: none; }` reinstated, scoped to landscape only. User-confirmed: even after the z-index drop the burger needs to disappear when bud_panel is open in landscape (panel + bud_ok races vs. burger pointer-events). Portrait keeps burger visible since the panel sits BELOW the burger w. no overlap.
## "friend@example.com" → "bud@example.com" placeholder
Renamed in 4 bud-panel templates + the FT asserting it:
- `templates/apps/gameboard/_partials/_my_sea_bud_panel.html`
- `templates/apps/billboard/_partials/_bud_panel.html`
- `templates/apps/billboard/_partials/_bud_invite_panel.html`
- `templates/apps/billboard/_partials/_bud_add_panel.html`
- `functional_tests/test_core_sharing.py`
"bud" matches the broader naming convention (bud_btn, my-buds, share-w.-a-bud, etc.) — `friend` was an outlier.
## _bud_apparatus.html shared shell refactor
New `templates/apps/billboard/_partials/_bud_apparatus.html` — single shared markup partial for the four bud-btn use cases. Contains btn + panel + input + OK + (optional) suggestions div + bud-btn.js script. Each of the 4 specific partials becomes a thin wrapper that `{% include %}`s the shell + renders its own `<script>bindBudBtn({...})</script>` block w. per-use-case submitUrl / onSuccess / duplicateTargetSelector.
Context vars accepted by the shell:
- `aria_label` — string for #id_bud_btn aria-label
- `sharer_name` — optional; renders `data-sharer-name=` on #id_bud_panel (post-share only)
- `include_suggestions` — bool; renders suggestions div + autocomplete script (false on my_buds where the pool == request.user.buds == nothing useful to suggest)
Per-call wrappers are now ~10-50 lines instead of 30-120 lines of duplicate markup. Behaviour is identical; only DRY-ed up.
## CI #344 tray-anchor regression fix
`apps/epic/static/apps/epic/tray.js` — `_computeBounds` in landscape used `id_gear_btn || id_kit_btn` as the bottom anchor (wrap height = anchor.top). After the burger sprint relocated kit_btn to the TOP of the right sidebar (top:0.5rem), kit_btn.top ≈ 8px → wrap.height collapsed to 8px → tray couldn't slide → `test_dragging_tray_btn_down_opens_tray_in_landscape` failed.
Fix: anchor fallback chain is now `id_burger_btn || id_bud_btn` (the new bottom-anchored btns) → `window.innerHeight - 3.5rem` reserved fallback (for pages that have neither). Burger renders unconditionally on room.html so the SIG_SELECT tray test now finds its anchor + lays out the wrap correctly.
## Verification
- IT+UT 1357 green (+1 from MySeaGateViewTest burger).
- Jasmine specs green.
- `test_dragging_tray_btn_down_opens_tray_in_landscape` green (was the CI #344 failure).
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
03feaee9f2 |
burger z-index drop 318→314 to match .gear-btn; FT base dismiss_brief_if_present helper — TDD
Follow-up to
|