Files
python-tdd/src/static/tests
Disco DeDisco 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>
2026-05-27 13:57:09 -04:00
..
.bud-duplicate-flash: auto-ease-out 3s after FYI + palette swap — note.js's Brief.showDuplicateBanner FYI handler now setTimeout(() => target.classList.remove('bud-duplicate-flash'), 3000) after the .add(); the existing transition: color 600ms ease, text-shadow 600ms ease rule on the class already covered the ease-in (default → flash), so the same rule now also covers the ease-out (flash → default) when the class drops — net behaviour: tap FYI → flash peaks → flash visibly fades back to the default text styling over ~600ms after a 3s hold, instead of persisting til page refresh; palette keys swapped per user steer — color: var(--terUser); text-shadow: var(--ninUser)color: var(--ninUser); text-shadow: var(--terUser), so the highlight reads as a lighter handle w. a gold glow rather than a gold handle w. a light glow, matching the duplicate-guard spec the user re-aligned on; affects all three flash targets uniformly (.bud-entry .bud-name on /billboard/my-buds/, .post-recipient on post.html share-flow, .gate-slot.filled on the gatekeeper invite-flow) since they all flow through the same _bud.scss .bud-duplicate-flash selector + the same Brief.showDuplicateBanner JS handler; new Jasmine spec D7b in NoteSpec.js uses jasmine.clock().install() + clock().tick(3001) to fast-forward past the dismiss window + assert the class is gone (existing D7 still pins the immediate-after-FYI peak state); existing FTs (test_bill_my_buds.test_re_add_existing_bud_shows_already_present_brief… + test_core_bud_btn duplicate-guard FTs) still green because they assert immediately after the FYI click (well inside the 3s hold) — TDD
2026-05-13 00:43:03 -04:00
applet rows: hover + click-lock highlight on every .applet-list-entry.row-3col (My Posts / My Buds / My Notes / My Scrolls / My Games) — bg shifts to --secUser, title to --quiUser (overriding the inherited --terUser link color + stripping the text-shadow the global .applet-list-entry a:hover rule had been baking in), body + ts cells come up from their dimmed 0.6 / 0.5 opacity to full --priUser so the dim middle/right cols pop against the --secUser fill; new apps/applets/static/apps/applets/row-lock.js IIFE module owns the touch-persistence state machine (single _lockedRow ref, .row-locked class toggle): clicking a row not currently locked → locks (clearing any prior lock); clicking the locked row again → unlocks; clicking another row → moves the lock to the new row; clicking anywhere not inside a .row-3col → clears the lock — mirrors the note-page notes-locked click-lock state machine but lighter (no DON/DOFF, no greeting swap, no fetch), one document-level click listener bound once via _bound re-entry guard so beforeEach _init() calls in specs don't pile up handlers; loaded globally via base.html next to applets.js since the rows render on both /billboard/ + /gameboard/; padding-inline 0.5rem + border-radius 0.25rem on the row container shrinks the highlight to a chip shape so hovered rows don't bleed all the way to the applet box edge; 6 Jasmine specs in RowLockSpec.js cover the four state-machine transitions + the "child element of row still locks the parent row" affordance (since the user can tap the body cell text, not just the title link) + the "only one row carries .row-locked at a time" invariant; SpecRunner.html updated (both static_src + the static/ runtime mirror the FT reads from per the project's static-src→static copy discipline) — TDD
2026-05-13 00:27:39 -04:00
feat: wallet Shop polish — microtooltip extraction, Shop-first ordering, DRY tooltip styling, writs rebalance, "no expiry" on all items. Visual-pass tweaks landing atop the 5-chunk Shop rollout (commits 8e476f5d28cf7b). **Microtooltip extraction**: .tt-microbutton-portal (Chunk 4's wrap-inside-.tt) replaced w. a sibling .tt-micro div on each .shop-tile. wallet.js's initWalletTooltips clones BOTH into separate portals on hover — .tt#id_tooltip_portal (main card), .tt-micro#id_mini_tooltip_portal (small italic pill at bottom-right of main, mirroring Game Kit's Equipped/Unequipped/In-Use mini portal). Hover persistence covers both portals + the source tile w. a 200ms grace timer cancelled by mouseenter on any of the 3 zones. Capped items (BAND-owned) render NO btn at all — just "Already owned" microtext (mirrors Game Kit's status-only "Equipped" pill rather than the disabled-× pattern that lived in Chunk 4). **Tooltip-pin on guard open**: WalletTooltips.pin() / .unpin() exposed on window; wallet-shop.js's BUY click calls pin() before showGuard() + both onConfirm / onDismiss callbacks call unpin() → the item tooltip stays visible behind the guard's "Buy {name} for ${price}?" prompt instead of orphaning. **Shop-first applet ordering**: new Applet.display_order field (default 100, lower = earlier; PK tie-break preserves legacy insertion-order for the existing 3 applets); seed migration sets wallet-shop.display_order=10 so Shop renders atop Balances/Tokens/Payment. applet_context() updated to .order_by("display_order", "pk"). New WalletAppletOrderTest (2 ITs) pins Shop-first DOM order + view-context list. **DRY tooltip styling**: shop tooltip now uses the same 4-slot .tt-title / .tt-description / .tt-shoptalk / .tt-expiry classes as the Tokens row. New ShopItem.shoptalk field for the italic flavor line (band-1 = "Unlimited free entry (BYOB)" split out of description; tithes blank). New ShopItem.tooltip_expiry() method returns "no expiry" — eternal-stock convention (all current items; seasonal listings could override later). **Writs rebalance**: locked 2026-05-22 — tithe-1 144→12 writs, tithe-5 750→60 writs. Description text updated in lockstep ("1 Tithe Token + 12 Writs" / "5 Tithe Tokens + 60 Writs"). **Badge tweak**: ×N badge shrunk 2rem → 1.5rem + nudged further off-tile (top: -0.7rem, right: -1rem) so most of the underlying icon stays visible. **SCSS**: .tt-micro hidden in source DOM (portal-only); #id_mini_tooltip_portal mostly mirrors gameboard's mini at _gameboard.scss:140 but allows BUY-btn label to wrap onto multiple lines (white-space: normal on .tt-buy-btn); .tt-already-owned styled w. --secUser italic at 0.85rem to match Game Kit pills. **Migrations** — 5 new: lyric/0010_repricing_tithe_writs (writs + description), lyric/0011_shopitem_shoptalk (schema), lyric/0012_seed_shop_shoptalk (band split), applets/0012_applet_display_order (schema), applets/0013_wallet_shop_display_order (Shop atop). All idempotent. **TDD** — 5 new ITs across test_shop_models.py (shoptalk default + per-item assertions, tooltip_expiry method, updated tithe writs values, WalletAppletOrderTest), 1 new FT (test_shop_buy_guard_portal_pins_item_tooltip — programmatically dispatches mouseenter/mouseleave to exercise the pin/unpin race), 3 new Jasmine specs (T6 pin-on-click, T7 unpin-on-confirm, T8 unpin-on-dismiss). Existing FT band-owned assertion switched to .tt-micro (no .tt-buy-btn present), Jasmine T2 rewritten to assert no btn renders. **3 traps caught** mid-build: (a) multi-line {# #} comment leaked into DOM again (cf [[feedback-django-comments-single-line-only]]) — pinned the trap; (b) spyOn(window, 'fetch') Jasmine double-spy collision (cf trapped previously); (c) async pollution where afterEach restores window.Stripe=undefined before _doBuy's continuation hits it — fixed by per-test never-resolving fetch mock. 1211 IT/UT + 9 wallet FTs green; Jasmine SpecRunner verified visually (FT hangs Selenium-side on spec count). Pipeline will sweep all FTs
2026-05-22 02:21:10 -04:00