User scope (per design conv this session): split the room's sig-select responsibility off into a standalone billboard-context "My Significator" applet — branded "Game Sign" on the surface. Same 18-card pile as room sig-select (16 middle arcana + Major 0 & 1 filtered by Note unlocks); polarity collapses to a single FLIP choice (the FLIP btn in the picker carousel toggles User.significator_reversed). Selection persists globally on the User model + propagates to the billboard's Game Sign applet ; **naming convention locked**: "significator" stays at storage (User.significator FK + User.significator_reversed) + room sig-select context (DRY w. existing template/JS); "Sign" / "Game Sign" is the billboard-surface branding (file my_sign.html, URL /billboard/my-sign/, URL names my_sign + save_sign, applet name "Game Sign", page wordmark "Game Sign", btn label SAVE SIGN). Action URLs don't carry a trailing slash per project convention (/billboard/my-sign/save vs the page's /billboard/my-sign/) ; **schema**: User gains 2 fields — `significator: FK → epic.TarotCard (nullable, on_delete=SET_NULL)` + `significator_reversed: BooleanField(default=False)`. Migration lyric/0006_user_significator_user_significator_reversed.py auto-generated; reversible. Applet seed in applets/0009_seed_my_sig_applet.py adds the row (slug='my-sign', name='Game Sign', context='billboard', default_visible=True, grid_cols=4, grid_rows=6), idempotent update_or_create, reversible unseed() ; **picker page** (my_sign.html): solo lift of `_sig_select_overlay.html` — sig-stage-card scaffold + sig-stat-block + 18-card grid + SAVE SIGN form. Stripped: countdown / WebSocket / polarity / multi-user / reservations. Empty-state branch covers no-equipped-deck (link back to Game Kit; full Brief-redirect + Earthman-Backup fallback deferred to a follow-up sub-sprint). Minimal inline JS: click .sig-card → mark .sig-focused + set hidden card_id + enable SAVE SIGN; FLIP btn toggles .is-reversed + the hidden reversed input. Stage-card preview (name/qualifier population + keyword swap on FLIP) deferred — Sprint 4a follow-up will lift stage-card.js's populator into a non-room context ; **applet partial** (_applet-my-sign.html): renders user.significator's corner-rank + suit-icon + name_title if set; `.my-sign-applet-empty` "No sign chosen yet." otherwise. Header `<h2><a href="{% url 'billboard:my_sign' %}">Game Sign</a></h2>` links to the picker ; **helper refactor** (epic/models.py): extracted `_sig_unique_cards_for_deck(deck_variant)` from `_sig_unique_cards(room)`. New public `personal_sig_cards(user)` parallels `levity_sig_cards / gravity_sig_cards` but pulls from `user.equipped_deck` instead of `room.deck_variant`. Same Note-unlock filtering. No behavior change to existing room callers (3-line wrapper preserves the room signature) ; **TDD trail** — user called out mid-sprint that I'd skipped FTs; pivoted to FT-first. test_bill_my_sign.py (new, 3 FTs): T1 picker renders w. wordmark + target card present in grid; T2 click card → SAVE SIGN enables → POST persists → applet shows the card; T3 fresh user → applet renders empty-state. Initial reds — (a) setUp's `personal_sig_cards(user)` returned [] because StaticLiveServerTestCase → TransactionTestCase flushes migration-seeded DeckVariant + TarotCard between tests; fixed w. `serialized_rollback = True` on the test class (per [[feedback_transactiontestcase_flush]]); (b) h2 wordmark assertion against `MYSIGNIFICATOR` failed against the renamed "Game Sign" + the letter-splitter spreading chars across <span> children — switched to whitespace-stripped substring check `GAMESIGN`; (c) `.fan-corner-rank` text is CSS-hidden so Selenium returns "" — replaced corner-rank assertions w. data-card-id selectors (already-proven reliable from the parent .sig-card lookup) ; ITs (+12, in apps.billboard.tests.integrated.test_views): MySignViewTest (6 — login redirect, 200 + template, 16-card pile, save persists, invalid card_id → 403, GET save redirects); BillboardAppletMySignTest (3 — applet rendered, empty-state w/o sig, card+reversed class w. sig). PersonalSigCardsTest in apps.epic.tests.integrated.test_models (3 — happy path 16 cards, no-equipped-deck → [], schizo Note unlocks Major 1) ; pre-existing change picked up by the commit: my_sea.html branding "Game Sea" (user-modified mid-session; was "My Sea" in Sprint 3 — divergence captured in MEMORY.md follow-up) ; 1020 IT/UT green (+12) in 46s; 3 FTs green in 24s. Sprint 4a unblocks Sprint 4b (My Sea gating w. --terUser link to /billboard/my-sign/) + Sprint 4c (FT helper for mocking the sig choice across other FTs)
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
User caught a missed surface on iPhone after the May-18b rename pass (1ccb045): the post.html `.post-attribution` spans still rendered "@disco the Ard!" instead of "@disco the Baltimorean" — six callsites across the post header (author / invitee / shared-self / created-by) plus `_my_buds_applet_item.html`'s bud row body. Same shape on display: navbar DON greeting is the only surface that should keep "Ayo, Ard!", per the May-18b architectural decision ; root cause: `User.active_title_display` at lyric/models.py:152 returned `self.active_title.display_title` ("Ard!" for Baltimorean) instead of `self.active_title.display_name` ("Baltimorean"). The Sprint-18b rename pass swapped the inline `attr_combo` + Brief.title to use `display_name`, but missed this property which is the indirection layer for `.post-attribution` callsites. Navbar uses `{{ user.active_title.display_title }}` directly (no helper-property indirection) so it stays at "Ard!" — that's the intended single Ard! surface ; fix: one-line swap in `active_title_display` from `display_title` to `display_name`. For stargazer / schizo / nomad these two are equal (the Note model's `display_name` property at drama/models.py:262 falls through to `display_title` unless the slug has an override in `_NOTE_DISPLAY[slug]["display_name"]`) — Baltimorean is the only current override w. `{"display_name": "Baltimorean"}`. So this is no-op for every non-Baltimorean Note ; TDD trail: +3 UTs in apps.lyric.tests.integrated.test_models.UserModelTest: `test_active_title_display_returns_earthman_when_no_note_donned` (smoke), `test_active_title_display_uses_display_name_not_display_title` (pins the Baltimorean override path — went red 'Ard! != Baltimorean' before the fix), `test_active_title_display_falls_through_to_display_title_for_non_overridden_slugs` (pins the no-op path for stargazer). Red → green confirmed. Surfaces auto-affected: post.html post-attribution × 5 callsites + `_my_buds_applet_item.html` bud row body (all use `{{ user.active_title_display }}`) ; 1008 IT/UT green in 46s (+3 from 1005)
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
User roadmap step (Sprint 3 of cluster): scaffold the My Sea applet on the gameboard + the standalone /gameboard/my-sea/ page where later sprints will host the gatekeeper / sig-select / sea-select reskin for solo-user draws. Shell-only — no draw flow yet; latest-draw rendering, mid-progress save, daily quota land in Sprints 4-9 ; **migration**: `applets/migrations/0008_seed_my_sea_applet.py` — RunPython that `update_or_create`s Applet(`slug='my-sea'`, name='My Sea', context='gameboard', default_visible=True, grid_cols=12, grid_rows=4). 12×4 wide horizontal banner so the Celtic Cross spread's 10 cards can render left-to-right in the applet aperture, scrollable like My Palette (per user spec). Reverse migration (`unseed`) deletes the row so the migration is reversible for staging rollbacks ; **applet partial**: `templates/apps/gameboard/_partials/_applet-my-sea.html` — same `{% applet_context %}` auto-discovery shape every other applet uses (`<section id="id_applet_my_sea" style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};">`). Header is a `<h2><a href="{% url 'my_sea' %}">My Sea</a></h2>` link (gold via global `body a` rule); body is a `.my-sea-scroll` container that either renders `.my-sea-card` cells from a `latest_draw_cards` context (TBD in Sprint 4-7) or a `.my-sea-empty` placeholder line "No draws yet." for fresh users ; **standalone page**: new `gameboard/views.py:my_sea` view + url at `/gameboard/my-sea/` (URL name `my_sea`) rendering `apps/gameboard/my_sea.html` — `{% extends "core/base.html" %}` shell w. letter-spread `<span>My</span><span>Sea</span>` h2 wordmark + `.my-sea-page__empty` placeholder paragraph "Your sea is calm. Draws will appear here." `page_class` doubled to `page-gameboard page-my-sea` so the body inherits the gameboard's landscape aperture treatment AND any future my-sea-specific styles can target a single class. Login-required like the rest of gameboard ; **tests (+6 ITs)**: GameboardViewTest gains 3 — `test_gameboard_shows_my_sea_applet` (cssselect pins #id_applet_my_sea), `test_my_sea_applet_renders_empty_state_for_new_user` (asserts ".my-sea-empty" text + no ".my-sea-card" rows), `test_my_sea_applet_header_links_to_my_sea_page` (h2 a href == reverse('my_sea')); new MySeaViewTest class — `test_my_sea_requires_login` (redirect to /?next=...), `test_my_sea_renders_200`, `test_my_sea_uses_gameboard_page_class` (page-gameboard + page-my-sea both in body class). Existing GameboardViewTest setUp already does `get_or_create` per-applet so no fixture change needed for the migration-driven my-sea row ; 1005 IT/UT green (+6 from 999) in 45s; visual verified in Claudezilla at iPhone-14 portrait — applet renders w. rotated "MY SEA" vertical label + "No draws yet." body; /gameboard/my-sea/ standalone page renders w. letter-spread wordmark + placeholder ; **next**: Sprint 4 — My Sea sig-select phase (single-significator pick for solo user, w. the parameterized hex CSS from Sprint 1 hosting the chair-less or single-chair variant)
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
User ask: ensure trinket FTs (test_trinket_carte_blanche.py, test_trinket_coin_on_a_string.py, test_trinket_backstage_pass.py) run in the room-stage CI bucket so they exercise the same room-template surface as test_game_room_* — relevant when a sprint touches the table hex SCSS / chair geometry / gatekeeper flow, since trinket FTs create rooms + walk thru the gate. Previously they fell into test-FTs-non-room by elimination (the non-room glob was `grep -v 'test_game_room_'`) ; main.yaml updates: non-room step's grep is now `-vE 'test_(game_room|trinket)_'` to exclude both clusters; room step's ls now globs BOTH `test_game_room_*.py` AND `test_trinket_*.py` ; companion memory tweak in feedback_ft_naming_prefix.md documents the new convention so future trinket FTs end up in the right bucket without re-asking ; pipeline behavior unchanged otherwise — both steps still depend_on test-two-browser-FTs only, parallel re-enable still blocked by the shared-sqlite issue from 2026-05-12 (cf. memo note)
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
User reported on iPhone: after depositing a COIN at a game's gatekeeper, the Kit Bag's Trinket slot still shows the COIN — even though the tooltip correctly carries the room attribution ("Ready 2026-05-25 / Billingsworth"). Expected behavior matches CARTE: the deposited token disappears from the Kit Bag Trinket slot because it's committed elsewhere & can't be re-used as the active trinket until released. PASS preserved — auto-admits w.o ever going thru the deposit path so it stays equipped ; **the real bug**: `debit_token` in epic/models.py's COIN branch set `current_room` + `next_ready_at` but never cleared `user.equipped_trinket`. CARTE's `drop_token` view (epic/views.py:440-442) explicitly unequips at deposit time via `user.equipped_trinket = None; user.save(update_fields=["equipped_trinket"])`; COIN had no parity. Fix: same 4-line unequip stanza now lives inside the COIN branch of `debit_token`, guarded by `if user.equipped_trinket_id == token.pk` so a fresh-purchased COIN deposit (not the equipped one) doesn't accidentally clear another trinket. PASS untouched — falls thru `debit_token` w.o entering any branch & never reaches this path; CARTE untouched too (its branch is `pass`, unequip happens at `drop_token` time before debit_token is even called) ; **the FT false-positive**: yesterday's Sprint 2 commit (d2491c5) shipped `test_coin_deposit_unequips_from_kit_bag_and_fills_one_slot` w. selector `#id_kit_bag_dialog .kit-bag-placeholder`. That selector was matching the **Dice** section's placeholder (Dice feature isn't built — `_kit_bag_panel.html` L23-29 renders `.kit-bag-placeholder` unconditionally), masking the bug whether or not the Trinket section was empty. Tightened to `.kit-bag-section--trinket .kit-bag-placeholder` w. comment explaining why a bare selector is unsafe ; template change in `_kit_bag_panel.html` L31: Trinket section gains a `kit-bag-section--trinket` modifier class so the FT (and any future selector that needs to single out the trinket section vs the deck/dice/tokens siblings) has an anchor. Mirrors the existing `kit-bag-section--tokens` class at L70 ; TDD trail: (1) tightened selector + reran → red on `NoSuchElement` (no `.kit-bag-section--trinket .kit-bag-placeholder` because COIN still equipped post-deposit, so trinket section renders the token card not the placeholder); (2) added unequip stanza to debit_token; (3) reran → green. 10 trinket FTs in 99s; 999 IT/UT in 46s — no regressions ; **generalizable trap**: when an FT waits for an element via a CSS selector, scope the selector to the section/container that uniquely identifies the assertion target — a class like `.kit-bag-placeholder` that's reused across multiple sections will silently pass even when the section you care about is in the wrong state. This is the second false-positive trap in two days (cf. d2491c5's wrong-selector trap where `.token-slot.claimed` was Carte-specific); pattern's worth noting
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
User-driven roadmap step (Sprint 2 of My Sea cluster): give the COIN trinket parity w. CARTE's deposit-aware Game Kit surface — when COIN is deposited in a room (Token.current_room FK set via debit_token in epic/models.py:135), its Game Kit token should expose the room name on `data-current-room-name` so the mini-portal can render "In-Use: <room name>" on hover, and both DON / DOFF btns should go btn-disabled (token is committed elsewhere, neither equip nor un-equip is valid). Mirrors the 3-state branch CARTE has in the same template (deposited / equipped / unequipped); PASS not in scope — auto-admits w.o deposit ; template change in _applet-game-kit.html — line 40 (COIN's div) gains `data-current-room-name="{{ coin.current_room.name|default:'' }}"` + an extra `{% if coin.current_room %}…{% elif coin.pk == equipped_trinket_id %}…{% else %}…{% endif %}` branch that fronts the existing 2-way w. a deposited-state arm (both btns "×" btn-disabled). View-side wiring already in place — coin context var is the user's COIN token incl. its `current_room` FK; no Python change needed ; TDD trail — test_trinket_coin_on_a_string.py (new, 3 FTs): T1 hover equipped COIN → mini portal "Equipped" + main portal tooltip prose (Coin-on-a-String / Admit 1 Entry / "…and another after that…" / no expiry); T2 deposit flow — rails-click → slot 1 RESERVED → click `.btn-confirm` inside the reserved gate-slot (NOT `.drop-token-btn` which is Carte's carte_active path) → slot fills + COIN admits only 1 entry (slot 2 has no follow-up btn cf. Carte's 6) + kit-bag Trinkets section empty (COIN unequipped on deposit); T3 navigate back to /gameboard/ → COIN's `#id_kit_coin_on_a_string` has `data-current-room-name="Commitment Room"` + both DON & DOFF btns inside `.tt` are btn-disabled ; initial red run hit a Carte-specific selector trap — `.token-slot.claimed` (the Carte machine UI from `user_filled_slot or carte_active` branch in _gatekeeper.html L23) doesn't fire for COIN, which lands on `.token-slot.pending` (user_reserved_slot branch); diagnosed via screendump grep — slot 1 carried class "gate-slot reserved" + token-slot was "pending"; FT rewritten to wait for `.gate-slot[data-slot='1'].reserved` → click `.btn-confirm` (the OK btn rendered for the reserving user in _table_positions.html L7-15) → wait for `.filled`. T1+T2 then green; T3 stayed red on `data-current-room-name` AttributeError (None != "Commitment Room") which is the actual bug the template fix addresses ; test_trinket_backstage_pass.py (new, 4 skeleton FTs): T1 staff-user signal contract — `gamer.equipped_trinket_id == pass_token.pk` post-signal; T2 tooltip renders title/description/shoptalk/expiry (Backstage Pass / Admit All Entry / "'Entry fee'? …" / no expiry); T3 equipped PASS mini portal says "Equipped"; T4 PASS btn apparatus — DON × btn-disabled, DOFF active w. label "DOFF" (symmetric to COIN's equipped state cf. test_gameboard.py:207-220). DEPOSIT FLOW DEFERRED to future sprint w. TODO comment block — PASS magically auto-admits any gate w.o going through the `.token-rails` deposit path that CARTE / COIN share, so no `data-current-room-name` parity work applies; user explicitly chose "Auto-admits, never deposited — keep current behavior" for this sprint ; 10 trinket FTs green in 93s (carte 4 + coin 3 + pass 3 — wait, pass has 4: 4); full IT/UT 999 green in 46s — no regressions; coin context already passing `coin.current_room` correctly thru _game_kit_context (no Python change). Sprint 2 of [[project_my_sea_applet]] cluster — next: My Sea applet shell (Sprint 3+)
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
Belt-and-suspenders for the iOS Safari auto-zoom-on-input quirk: Mobile Safari zooms the viewport when an `<input>`/`<textarea>`/`<select>` is focused & its computed font-size < 16px, and never zooms back out on blur. Two layers ; PRIMARY — SCSS prevention: new `input, textarea, select, [contenteditable] { font-size: unquote("max(16px, 1em)") }` in core.scss (Sass can't reconcile px/em units in compile-time max() so unquote() passes the CSS max() through verbatim — modern browsers handle natively). 1em inherits parent, max() floors at 16. ALSO floored `.form-control-lg` in _base.scss — was `font-size: 1.125rem`, which at rem=14 (small portrait, clamp(14px, 2.4vmin, 22px) hits its floor) computes to 15.75px → **0.25px** under iOS's 16px threshold → the "ever so slightly" zoom on New Game + New Post applets the user reported (both use `.form-control.form-control-lg`, specificity 0,2,0 beats my element-level 0,0,1 rule). Floor: `unquote("max(16px, 1.125rem)")` ; SECONDARY — JS fallback in base.html: rewritten from `setAttribute('content', ...)` toggle to full meta-element remove+re-add, which modern iOS handles more reliably than attribute mutations on the existing meta. Triggers on document-level `focusout` (bubbles natively, no capture-phase needed) for `input/textarea/select`; injects fresh viewport meta w. `maximum-scale=1.0, user-scalable=no` for 300ms (iOS reads as zoom violation → snaps to 1:1), then swaps back to the cached base content so pinch-zoom remains available elsewhere ; user observed horizontal scrollbar appearing when the page zoomed — that's the symptom the user actually cared about (broken layout, not aesthetic zoom). w. SCSS floor in place the zoom shouldn't trigger to begin with; the JS is purely for inputs that slip through (future custom controls, shadow DOM, etc.) ; iOS-specific behavior — Selenium+Firefox doesn't replicate the auto-zoom so no FT layer added. Verified by user manual iPhone test (post-fix retest pending after force-refresh)
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
iOS Safari auto-zooms when a user taps an `<input>`/`<textarea>`/`<select>` whose font-size is < 16px, and does NOT auto-zoom back out on blur — the page stays zoomed even after the field loses focus. New ~10-line IIFE at base.html slots next to the existing h2-letter-splitter at the bottom of <body>: caches the page's `<meta name="viewport">` content, listens (document-level, bubbling `focusout`) for inputs leaving focus, then briefly appends `, maximum-scale=1.0` before reverting 100ms later — iOS reads the tightened constraint as a "zoom violation" and snaps the viewport back to 1:1, after which the revert frees the user to pinch-zoom manually anywhere else on the page ; chose `focusout` over `blur`+capture-phase since focusout bubbles natively (cleaner); skips if `.matches` isn't available (defensive for older browsers); skips silently if no viewport meta is present (defensive) ; no test layer — iOS-specific behavior that's awkward to FT (would need a real iOS Safari runner; Selenium+Firefox doesn't replicate the auto-zoom). Verified no conflict w. other focusout listeners (grep: only vendor JS — d3 / htmx / jquery / select2 — none of which listen at document scope on inputs/textareas/selects). Side-track addition between Sprint 1 (table hex layout fbe6c12... well, 7165974) and Sprint 2 (My Sea applet kickoff) per user ask
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
User report: hex felt smaller than the aperture even at portrait (off-centered, room to spare on top + bottom), and chair labels overlapped the hex edges at landscape — progressively worse as the hex grew at larger viewports. Three contributors stacked: (1) `.room-shell { max-height: 80vh }` capped the shell at 80% of viewport height even when the .room-page aperture had more room — at 1789×1031 this donated 228px (1053→825) of aperture height to dead margin; (2) scene design 360×300 was wider than tall (1.2 aspect) but landscape aperture is narrower than tall (~1.4), so the height cap bottlenecked scene-scale at min(aperture_w/360, aperture_h/300) instead of letting the hex grow; (3) chair font-size scales w. rem (clamp(14,2.4vmin,22)) but chair position scales w. --table-scale — at large viewports rem maxes at 22 so labels widen and push chair icons further from box-center toward the hex (visual "creep") ; fix: remove the 80vh cap (`max-height: 80vh` → `height: 100%` on .room-shell L340) so the shell stretches to fill the .room-page aperture; bump hex from 160×231 to 200×231 (regular pointy-top w. width = height × √3/2 = 200 * 1.1547 — comment in _room.scss updated); apothem of 200-wide pointy-top regular hex is 100px exact (200/√3 × √3/2), so `$pos-d` 110px → 140px gives 40px design-units of radial chair clearance (was 30); derived `$pos-d-x: round(140*0.5) = 70`, `$pos-d-y: round(140*0.866) = 121` for slot 2/3/5/6 diagonal anchors at 60° from horizontal (matches existing geometry approach); scene design height 300 → 320 to leave enough vertical headroom at large landscape that the rem-driven (font-size 1.6rem × scale) chair icons + labels don't clip the aperture top/bottom edges — at 1789×1111 w. scene_H=300 the AC/BC label tops sat AT aperture top (y=-21 vs aperture y=-22), bumping to 320 drops scale from 4.05 → 3.54 and leaves 76px of headroom; SCENE_H in room.js bumped to 320 to match (Math.min(w/SCENE_W, h/SCENE_H) sets --table-scale CSS var via transform: scale on .room-table-scene) ; visual verification via Claudezilla across three viewports (no test layer per user preference — layout regression coverage via spot-check on next room render) — iPhone-14 portrait 566×875: hex 243×281 → 314×363 (+29% wider, fills 55% of aperture width vs 44% before); mid landscape 1149×781: hex 333×385 → 493×569 (+48% wider, 56% vs 38% before); large landscape 1789×1111: hex 440×509 → 708×818 (+61% wider, 48% vs 30% before — the most dramatic improvement, matching user's "progressively worse the larger the hex grows" observation). Chair clearance now uniform 40 design-units radially across all scales; AC/BC labels stay 76px inside aperture top at the largest viewport ; dead `$seat-r`/`$seat-r-x`/`$seat-r-y` consts at L357-359 left in place (unused elsewhere in codebase but out of scope for this layout fix) ; full IT/UT 999 green in 46s — no regressions; .table-hex / .table-hex-border / .room-table-scene / .table-seat positioning consts are the only refs to these dimensions across SCSS & JS so no cascade beyond room layout. Unblocks Sprint 2+ (My Sea applet will share the same hex CSS, parameterized, per user's intent for future friend-invite up-to-6-person rooms)
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
CAST SKY btn click handler in sig-select.js init() was bound to `Tray.open()` — wrong on two counts: (1) the tray was already played during the `polarity_room_done` → `Tray.placeSig` sequence (sig stage card slides into the tray cell before the overlay dismisses), so re-opening it on CAST SKY click pops the tray a second time; (2) Sky Select never opens — `_sky_overlay.html` is only `{% include %}`d server-side when `room.table_status == "SKY_SELECT"`, so during SIG_SELECT the partial + its `openSky` handler aren't in the DOM and `Tray.open()` is the only thing the click does. Bug surfaced symmetrically in both polarity rooms regardless of which finished first ; fix: replace `Tray.open()` w. `window.location.reload()` so the server re-renders the room w. table_status=SKY_SELECT — which surfaces the sky overlay partial + the `openSky` handler bound at _sky_overlay.html:192-193. Same pattern as `_onSkyConfirmed` in the sky partial (location.reload after sky save) ; testability hook mirrors `RoleSelect.setReload` (role-select.js:236): `var _reload = function () { window.location.reload(); };` at module scope, listener calls `_reload()` (closure looks up the var at click time so reassignment works), `setReload(fn)` exposed on the module's test API. SigSelectSpec.js adds `describe("CAST SKY click (post pick_sky_available)")` w. 2 specs — reload spy hit on click + `Tray.open` spy NOT hit on click; the negative assertion catches the original bug, the positive verifies the fix's intent. Existing 363 specs untouched ; Jasmine FT green in 8.6s; full IT/UT 999 green in 44s ; collectstatic mirror at src/static/apps/epic/sig-select.js refreshed in same commit so the served JS carries the fix (Django serves from STATIC_ROOT, not from app static dirs, in StaticLiveServerTestCase)
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
pipeline fix — `_post_gear.html` (commit 6a7464e) gated the NVM target on `{% url 'billboard:my_posts' user_id=request.user.id %}`, which exploded w. NoReverseMatch when an anonymous user (Percival ch.18 anonymous-post lab — ownerless `Post.objects.create()`) hit view_post (which has no @login_required); whole gear-include now wrapped in `{% if request.user.is_authenticated %}` since anonymous viewers can't DEL/BYE/back-to-my-posts anyway; AnonymousPostViewerTest pins the 200-render + gear-absence contract so future ownerless-post regressions surface in ITs (pipeline run #298 fixed) — TDD
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 <noreply@anthropic.com>
.woodpecker/main.yaml — CI test-FTs step splits into parallel siblings test-FTs-non-room (22 files via `ls functional_tests/test_*.py | grep -v 'test_game_room_'`) + test-FTs-room (9 files via `ls functional_tests/test_game_room_*.py`); room cluster is the heaviest (~70% of the pre-split ~40-min wall-clock) and now runs concurrently w. the rest instead of in series; DAG explicit via depends_on on every step (Woodpecker mixes default-sequential w. depends_on awkwardly, so each step pins its prerequisite); collectstatic stays in test-two-browser-FTs only — the shared workspace propagates assets to both parallel FT steps, no race + no duplication; screendumps + build-and-push fan back in (depends_on both parallel steps); deploy-staging + deploy-prod depend on build-and-push
smoke-import: 31/31 FT modules green after the rename pass
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- billboard.views.view_post adds viewer_is_owner + other_recipients context vars. is_real_invitee = (auth AND post has owner AND viewer != owner). Anon viewers + ownerless-post legacy path fall through to owner-style rendering (which renders empty gracefully via the at_handle / display_name AnonymousUser guards).
- other_recipients = post.shared_with.exclude(viewer) when invitee; .all() otherwise.
- post.html .post-header branches:
• viewer_is_owner: existing prose ("just me, @owner …" / "shared between {recipients} & me, @owner …").
• sole invitee: "shared with me, @viewer the {viewer.title}" + "created by @owner the {owner.title}".
• multi invitee: "shared with {other_recipients}" + "& me, @viewer the {viewer.title}" + "created by @owner the {owner.title}".
- lyric_extras at_handle + display_name: guard against AnonymousUser (no .email attribute) — return "" rather than crash. Preserves the Percival ch. 18 anon-views-ownerless-post path.
- 12 new ITs in test_post_invitee_view (context vars: viewer_is_owner, other_recipients exclude/include; template prose: sole + multi invitee phrasing, owner unchanged).
- 878 IT regression + 8 post-html FT regression green (1 Marionette flake on multi-run that passes in isolation).
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Brief schema (billboard/0007): post FK becomes nullable + new room FK to epic.Room + KIND_GAME_INVITE enum value. to_banner_dict resolves post_url to reverse('epic:gatekeeper', room.id) when post is null and room is set.
- epic.invite_gamer view refactor:
• Accepts `recipient` (matches bud-panel field; legacy `invitee_email` still works for full backwards compat).
• Resolves via apps.billboard.views._resolve_recipient (email if "@" present, else username).
• RoomInvite stores the resolved User's email (or raw input if unregistered).
• Auto-adds inviter ↔ recipient to each others' buds (symmetric per Phase 2 spec) when recipient is a registered User.
• Spawns a Brief w. owner=request.user, kind=GAME_INVITE, room=room, post=null.
• Accept: application/json → {brief, recipient_display}; otherwise redirects to gatekeeper as before.
• Self-invite + blank recipient: 200 w. brief=null, no RoomInvite, no buds touch.
- _gatekeeper.html: gate-invite-panel block (lines 62-71) removed.
- new templates/apps/billboard/_partials/_bud_invite_panel.html: clone of _bud_panel.html w. data-invite-url + autocomplete from request.user.buds. JS posts to invite_gamer + Brief.showBanner. room.html includes it owner-only when not table_status and gate_status != RENEWAL_DUE.
- room.html scripts block now loads apps/dashboard/note.js so window.Brief is defined for the slide-down banner.
- Tests: new test_invite_gamer.py (14 ITs) covering ajax + legacy form-submit paths, recipient resolution, RoomInvite creation, Brief w. room FK + GAME_INVITE kind, symmetric buds auto-add, unregistered/self/blank silent no-op cases. New test_gatekeeper_bud_btn.py FT (9 tests) covers presence (owner-only), absence of legacy #id_invite_email, async invite flow end-to-end (RoomInvite, Brief, banner, panel close, username resolve, buds auto-add).
- test_brief.test_brief_owner_post_required relaxed to test_brief_owner_required (post is now nullable).
- test_room_gatekeeper.test_second_gamer_drops_token_into_open_slot updated to drive the bud-btn flow (drops the #id_invite_email/#id_invite_btn references).
- 866 ITs (+14) + 9 gatekeeper FTs + 28 existing room-gatekeeper FTs green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- _game-kit.scss #id_kit_btn landscape rule now uses `right: calc((var(--sidebar-w) - 3rem) / 2)` — same formula as gear-btn (_applets.scss) and bud-btn (_bud.scss). All three 3rem-wide circular btns now share the same horizontal-centre math against the fluid sidebar.
- Drops `@media (orientation: landscape) and (min-width: 1800px) { right: 2.5rem }` which was a leftover from the old doubled-8rem-sidebar regime; the rem clamp ceiling now caps the sidebar without per-breakpoint overrides.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- portrait h2 spans get padding-inline-end / padding-inline-start (0.4em each) at the 45/55 boundary; box-sizing: border-box keeps the flex basis honest. Solves the "B I L L B O A R D" run-together where the L of BILL touches the B of BOARD.
- landscape h2 drops the `display: block` override + the > span resets that nuked text-align: justify and flex. Now inherits the portrait flex 45/55 + per-span justify + padding-inline. With writing-mode: vertical-rl, the flex axis runs vertically (45% bottom for BILL post-rotate, 55% top for POST/BOARD/etc.); padding-inline-end resolves to the bottom edge of the first span = natural gap between the two rotated words.
- Explicit h2 height: 80vh in landscape so the flex 45/55 percentages have a defined basis to resolve against (block height isn't auto-derived in writing-mode: vertical-rl).
- 8 layout/navbar FTs still green; assertions are categorical (position: fixed, etc.) not exact-px.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- html { font-size: clamp(14px, 2.4vmin, 22px) } — single sliding scale; everything in rem (sidebar widths, h2 font-size, paddings) scales together. Phone rotation swaps width/height but vmin stays the same → 1rem stays the same → navbar/footer/h2 hold their size between portrait + landscape.
- :root --sidebar-w: 5rem (replaces the locally-scoped $sidebar-w SCSS var that lived inside @media blocks); --h2-col-w: 3rem for the rotated wordmark column in landscape. var(--sidebar-w) + var(--h2-col-w) are the only knobs that move the layout.
- Landscape container: margin-left = calc(var(--sidebar-w) + var(--h2-col-w)); margin-right = var(--sidebar-w). Applets are now clipped INSIDE the h2 column, so the rotated "BILLPOST" / "DASHBOARD" wordmark never has content bleeding behind it (the original complaint).
- h2 markup refactor across 13 templates: <span>BILL</span><span>POST</span> instead of <span>BILL</span>POST. Portrait styling: display: flex; first span flex 0 0 45% + --quaUser colour; second span flex 0 0 55% + --secUser inherited. Per-span text-align: justify + text-justify: inter-character keeps the inter-letter spacing within each span. Landscape resets the flex (single rotated wordmark, not split).
- Drop the four h2 font-size jumps (min-height: 400/500/800px) — single font-size: 3rem now scales fluidly via root rem. Drop the @media (orientation: landscape) and (max-width: 1100px) h1 override (rem-fluid handles cramped widths). Drop the entire @media (orientation: landscape) and (min-width: 1800px) sidebar-doubling block in _base.scss / _applets.scss / _bud.scss — the rem clamp ceiling already caps the size.
- _bud.scss + _applets.scss: bud-btn / bud-panel / bud-suggestions / gear-btn / applet menus all switch to var(--sidebar-w)-based positioning; landscape rules are single (no per-breakpoint duplication).
- Per-spec tradeoff: non-.btn-primary buttons (BYE / NVM / OK / kit-btn / etc.) inherit rem-fluid like everything else and will scale slightly w. viewport. User explicitly OK'd this — they don't need to stay px-fixed.
- 852 ITs + 24 layout/navbar/bud FTs green; existing geometry assertions are relative or categorical (not exact-px) so the rem clamp doesn't surface failures at the 800x1200 FT viewport.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tonal shift to lean into the bureaucratic-flavour of the @adman entity. Going-forward only; existing super-schizo / super-nomad Lines in DB keep the old prose.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- new lyric_extras.at_handle filter: '@{username}' if user.username, else truncate_email(user.email). Companion to display_name (which has no @-prefix). Used by post.html line author col + self/shared self lines.
- post.html updates: line author span renders {{ line.author|at_handle }}; .post-shared-recipients chips render {{ r|at_handle }} + .post-attribution; .post-shared-self wraps "{handle} the {title}" in <span class="post-attribution">. The 'just me' / '& me' prose stays plain (only the handle+title combo is coloured).
- Note.grant_if_new prose wraps both the @-handle (or bare email fallback) AND the title in <span class="post-attribution">. Standard format wraps the combo "{handle} the {title}" together; admin format wraps each independently since the prose splits them ("recognizes @disco for ... customary title of Schizoid Man"). Existing Lines unchanged — going-forward styling only.
- SCSS: .post-attribution { color: rgba(var(--quaUser), 1); } scoped at .post-page so it lights up in both .post-header descendants and #id_post_table descendants. .post-line-author also switches from opacity-based dim to the same --quaUser key (drops opacity 0.75 since the colour change reads as the de-emphasis on its own).
- 852 ITs still green — line.text inclusions ("Stargazer", "alice@test.io" etc.) still substring-match through the wrapping spans.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>