Commit Graph

616 Commits

Author SHA1 Message Date
Disco DeDisco
4963237420 my-sea portrait form-col split: SPREAD field + action btns LEFT, DECKS RIGHT — fits the form on a phone-portrait viewport without DECKS pushing the AUTO DRAW / DEL row off-screen. CSS grid w. display: contents on .sea-form-main flattens the intermediate wrapper so its children participate directly in the grid
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 11:58:41 -04:00
Disco DeDisco
191dad5365 my-sea hex-btn state-machine FT pin: extend test_landing_renders_hex_with_free_draw_btn to assert PAID DRAW + GATE VIEW are absent for fresh users — closes the mutual-exclusion gap (the other two states already pin the same invariant from their own directions; this adds the FREE-DRAW side). Docstring spells out the 3-way state machine for future readers
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 11:56:09 -04:00
Disco DeDisco
611ca9b5b4 my-sea polish v2: portrait .my-sea-picker stacks form col BELOW the cross (mirrors gameroom SEA SELECT modal) + sync MySeaGatekeeperPageTest.test_paid_draw_commits_token_and_redirects_to_picker w. iter-6c row-delete + ?phase=picker semantics (was pinning iter-6a behavior; pipeline #319 caught it)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 11:52:17 -04:00
Disco DeDisco
db443b7533 Revert universal .btn-disabled → × pseudo-element overlay (iter-4c); restore case-by-case &times; rendering convention. My Sea DEL btn now swaps DEL× in lockstep w. its .btn-disabled toggle (matches game-kit tooltip + DON/DOFF pattern). User-spec 2026-05-20.
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed
The iter-4c bundle added a universal `&::before { content: "\00d7"; ... }`
overlay on every `.btn-disabled` button + hid native text via
`> * { visibility: hidden }` + `color: transparent`. Visually flattened
every disabled state across the app (DEL, FLIP, DON/DOFF, palette
swatches, etc.) onto a single × glyph — user-rejected: "ruined the old
UX appearance".

Revert restores `_button-pad.scss` to its pre-iter-4c shape:
`color: rgba(--secUser, 0.25)` dims native text in place; no overlay,
no inner-content hiding. Templates that want a × on disabled buttons
render it explicitly in their own markup (game-kit tooltip `<button
class="btn-equip btn-disabled">×</button>`, my_notes DON/DOFF, etc.).

My Sea DEL btn picks up the case-by-case convention: template renders
`{% if hand_complete %}DEL{% else %}&times;{% endif %}`; the picker's
`_setComplete(on)` JS handler swaps `delBtn.innerHTML` between `DEL`
and `×` in lockstep w. the `.btn-disabled` class toggle so visual +
label always agree post-hand-completion.

FT `test_form_col_renders_decks_lock_hand_del_and_reversal_pct` now
asserts `delbtn.text == "×"` instead of relying on the (now-removed)
pseudo-element comment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 09:56:19 -04:00
Disco DeDisco
4417b8c972 My Sea iter 6c: bud-btn invite stub + #id_my_sea_menu gear (NVM-only, %applet-menu-styled, on both /gameboard/my-sea/ and the gatekeeper) + PAID DRAW now deletes the row and redirects to ?phase=picker so the user drops straight into picking cards instead of looping back to GATE VIEW — Sprint 5 iter 6c of My Sea roadmap — TDD
Bundled fix for the PAID-DRAW-loops-to-GATE-VIEW bug surfaced 2026-05-20 in
live testing: previously the view reset `created_at = now()` + cleared the
hand, but the row's continued existence meant `quota_spent=True` on the
next render → landing rendered GATE VIEW → user clicked it → back to
gatekeeper → loop.

Now PAID DRAW does `active_draw.delete()` after debiting the token + then
redirects to `/gameboard/my-sea/?phase=picker`. The my_sea view honors
`?phase=picker` (only when no active_draw exists — can't bypass
post-DEL GATE VIEW) by forcing `show_picker=True` so the user lands in
the picker ready to draw. First card draw creates a fresh row w. fresh
`created_at`, starting the new 24h quota cycle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 09:47:47 -04:00
Disco DeDisco
1e37fe1475 My Sea iter 6b: navbar GATE VIEW swap on page-my-sea + landing PAID DRAW state + seat-1 server-render + auto-token IT trap in gatekeeper FT — Sprint 5 iter 6b of My Sea roadmap — TDD
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed
Second of three Sprint 6 commits per [[sprint-my-sea-iter-6-plan]]. Wires the always-reachable navbar gate-entry, completes the landing center-btn 3-way state machine (FREE DRAW / GATE VIEW / PAID DRAW), and lifts seat-1's `.seated` state from JS-only to server-rendered (reload-stable).

## Navbar GATE VIEW swap

`templates/core/_partials/_navbar.html` — when `'page-my-sea' in page_class`, CONT GAME swaps for `#id_navbar_gate_view_btn` (`.btn-primary`, plain `<button>` w. inline onclick navigation). Reaches the gatekeeper at any quota state — no confirm guard (non-destructive nav).

**Typeface trap caught (user 2026-05-20 visual report)**: first cut used `<a>` for GATE VIEW, which UA-renders serif while `<button>` stays sans-serif (`.btn` doesn't reset `font-family`). Same fix pattern as iter-4c's in-hex GATE VIEW: always use `<button>`. Second cut used a form-wrapped `<button>` w. `display:contents`; the form was correctly invisible in layout but broke the landscape `> #id_cont_game { order: -1 }` direct-child SCSS pin (form became the direct child, not the button). Final cut: plain `<button>` w. `onclick="window.location.href=..."`, no form, no anchor — direct flex child of `.container-fluid` so the SCSS pin matches.

`_base.scss` — paired `> #id_navbar_gate_view_btn` alongside `> #id_cont_game` in both portrait (line 93) + landscape (line 309) rules so GATE VIEW occupies the same top-center navbar slot CONT GAME does (above brand, `order: -1`).

## Landing center-btn 3-way state machine

`my_sea` view gains `deposit_reserved` (active_draw has deposit_token_id) + `hand_non_empty` context vars.

`my_sea.html` landing branches:
- `deposit_reserved` → **PAID DRAW** form (POSTs to `my_sea_paid_draw`); fastest path back to picker w. one click — no gatekeeper round-trip.
- `quota_spent and not deposit_reserved` → **GATE VIEW** (existing iter-4c btn, navigates to gatekeeper).
- else → **FREE DRAW** (existing iter-1 btn).

Three branches are mutually exclusive — FT asserts only one of `#id_my_sea_paid_draw_btn` / `#id_my_sea_gate_view_btn` / `#id_draw_sea_btn` renders at a time.

## Seat-1 server-render

`my_sea.html` table-seat 1 now picks up `.seated` + `.fa-circle-check` (instead of `.fa-ban`) when `hand_non_empty`. Other 5 seats stay banned (placeholders for the future friend-invite feature; only owner ever occupies seat 1 in solo my-sea). Reloads no longer lose the chair-styling state — existing JS animation (FREE DRAW click → flip seat to seated) still fires on first draw.

In practice today the landing only renders when hand IS empty (show_picker hides landing once hand has cards), so the `.seated` branch isn't actually visible in iter 6b. Defensive code for future surfaces (any hex render w. hand non-empty) per [[sprint-my-sea-iter-6-plan]] §Seat-1 persistence.

## FT delta

**Replaced** `MySeaGatekeeperPageTest.test_gatekeeper_renders_six_chair_seats_with_seat1_seated` w. `test_gatekeeper_renders_no_hex_modal_only`. The iter-6a FT skeleton was written before the user's "no hex on gatekeeper" spec (2026-05-20) — seats now live ONLY on the my-sea picker page; the gatekeeper is a transient `.gate-modal` overlay w. no hex / chair-seats.

**Trap caught**: `MySeaGatekeeperPageTest.test_paid_draw_commits_token_and_redirects_to_picker` was passing in iter 6a only because it didn't actually exist in CI then; running it locally exposed the IT-trap pattern: User post_save signal auto-creates COIN + FREE tokens (`apps.lyric.models:309`), so `_select_my_sea_token` picks the auto-COIN (PASS > **COIN** > FREE > TITHE) instead of the manually-seeded FREE. Test asserted FREE count drops by 1 → fails because COIN was actually debited (sets cooldown, doesn't delete the token). Same trap as the iter-6a IT memo; fix is identical: `self.gamer.tokens.all().delete()` after User.create + then seed only the token the test cares about.

## Tests

- 4 MySeaGatekeeperPageTest (iter 6a, now passing) + 1 MySeaLandingPaidDrawTest + 1 MySeaNavbarGateViewTest + 2 MySeaSeatOnePersistenceTest = 8 FTs green in 84s.
- All 7 `test_core_navbar` FTs (NavbarByeTest + NavbarContGameTest) still green — landscape order rule extension is additive; CONT GAME path unchanged.
- 153/153 gameboard ITs green.

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-20 02:50:54 -04:00
Disco DeDisco
d2c34d44d3 My Sea iter 6a follow-up: gatekeeper layout mirrors room exactly — .gate-title-panel w. "@<handle>'s Sea" + .gate-top-row w. .gate-main-panel (token slot) + .gate-roles-panel (PAID DRAW square), all on shared --priUser panel chrome — TDD
Per user spec 2026-05-20: my-sea gatekeeper should look exactly like the room gatekeeper, with the PAID DRAW button living in its own `--priUser` square panel beside the token-slot rectangle (mirroring room's PICK ROLES placement). Earlier iter-6a draft had the PAID DRAW button rendered as a standalone btn below the token slot; now it sits in `.gate-roles-panel` next to `.gate-main-panel`. Title panel reads "@<handle>'s Sea" via the existing `at_handle` filter — falls back to email prefix for handle-less users (parity w. navbar identity rendering).

No SCSS changes — all three `.gate-*-panel` rules already exist in `_room.scss` lines 98-135 and apply universally to anything under `.gate-modal`. 153 gameboard ITs still green.

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 02:31:51 -04:00
Disco DeDisco
3fc5491372 My Sea iter 6a: gatekeeper page + INSERT/REFUND/PAID DRAW endpoints + MySeaDraw deposit fields + _select_my_sea_token / debit_my_sea_token helpers (CARTE blocked, COIN 24h cooldown not 7-day) + Sprint 6 FT skeleton — Sprint 5 iter 6a of My Sea roadmap — TDD
First of three Sprint 6 commits per [[sprint-my-sea-iter-6-plan]]. Replaces the iter-4c 404 stub at `/gameboard/my-sea/gate/` w. a real token-deposit-to-redraw UI. Iter 6b will wire the navbar GATE VIEW swap + landing PAID DRAW state + seat-1 persistence; iter 6c will land the bud-btn stub.

## Server

`MySeaDraw` gains two fields: `deposit_token_id` (int, nullable) + `deposit_reserved_at` (datetime, nullable). Migration 0002. The row plays triple duty now: hand storage + 24h quota tracker + deposit reservation slot.

`_select_my_sea_token(user)` mirrors `apps.epic.models.select_token` priority (PASS > COIN > FREE > TITHE) w. two adaptations:
- CARTE excluded outright (door-spell trinket, not valid for my-sea draws).
- COIN cooldown-respecting: filters out COINs w. `next_ready_at > now`. Standard `select_token` doesn't apply this filter — room logic unchanged.

`debit_my_sea_token(user, token)` is the my-sea variant of `apps.epic.models.debit_token`:
- CARTE → ValueError (defensive; caller validates upstream).
- COIN: `next_ready_at = now + 24h` (not 7-day room cycle) + unequip from kit if equipped.
- PASS: no consumption (auto-admit, unlimited redraws).
- FREE / TITHE: deleted.

`my_sea_gate` view replaces the 404 stub. Renders the gatekeeper template w. branching on `deposit_reserved` (token reserved on row vs not).

`my_sea_insert_token` POST: picks a token via `_select_my_sea_token` + sets `deposit_token_id + deposit_reserved_at`. Creates the row if missing (so a fresh user can deposit without first using their free draw). Idempotent w.r.t. an already-reserved deposit.

`my_sea_refund_token` POST: clears deposit fields. Token isn't consumed at INSERT (refund-aware design), so this is purely a row update — no inventory side effects.

`my_sea_paid_draw` POST: commits via `debit_my_sea_token` + resets row (hand=[], created_at=now, deposit fields cleared). Redirects to `/gameboard/my-sea/` for a fresh quota cycle.

## Template + UX

`apps/gameboard/my_sea_gate.html` (new) — per user spec 2026-05-20, the gatekeeper is a darkened-modal-over-`--duoUser` bg matching the room gatekeeper's chrome (`.gate-backdrop` + `.gate-overlay` + `.gate-modal`). No hex / chair-seats — those live on the my-sea picker page itself; the gatekeeper is a transient in-flight UI for token deposit.

Coin-slot rails (mirrors room's `.token-slot`):
- Pre-deposit: form-wrapped `.token-rails` button → POSTs to `my_sea_insert_token`. Coin-panel labels read INSERT TOKEN TO PLAY.
- Post-deposit: rails inert (no form); `.token-return-btn` form → POSTs to `my_sea_refund_token`. Coin-panel labels swap to PUSH TO RETURN.
- Post-deposit: PAID DRAW btn (`#id_my_sea_paid_draw_btn`, `.btn-primary`) → POSTs to `my_sea_paid_draw`. Mirrors the room's PICK ROLES btn shape.

SCSS minimal — page bg `rgba(--duoUser, 1)` on `.my-sea-page[data-phase="gate"]`; everything else reuses the room gatekeeper's existing rules.

## FT skeleton

Per user TDD directive (2026-05-20: "Also via TDD so if we run out we're adhering to FT-described behavior"), wrote the FULL Sprint 6 FT skeleton up front (covers iter 6a + 6b + 6c). Five new FT classes in `test_game_my_sea.py`:

- `MySeaGatekeeperPageTest` (5 tests) — iter 6a; pre-deposit / INSERT / REFUND / PAID DRAW paths.
- `MySeaLandingPaidDrawTest` (1 test) — iter 6b; landing renders PAID DRAW btn when deposit reserved (red until iter 6b lands).
- `MySeaNavbarGateViewTest` (1 test) — iter 6b; navbar GATE VIEW swap (red until iter 6b).
- `MySeaSeatOnePersistenceTest` (2 tests) — iter 6b; seat 1 banned for fresh user + empty-hand active draw (red until iter 6b).
- `MySeaBudBtnStubTest` (2 tests) — iter 6c; panel opens + OK shows coming-soon Brief (red until iter 6c).

## ITs (iter 6a — 22 new + 153 total green)

- `MySeaGateViewTest` (4) — view branching pre/post deposit.
- `MySeaInsertTokenViewTest` (4) — row creation, existing row, idempotency, GET=405.
- `MySeaRefundTokenViewTest` (3) — clears fields, no token consumption, idempotent.
- `MySeaPaidDrawViewTest` (6) — FREE consumed, COIN cooldown + unequip, PASS no-op, hand reset, created_at reset, redirect.
- `SelectMySeaTokenTest` (3) — CARTE excluded, COIN cooldown excluded, PASS priority for staff.
- `DebitMySeaTokenTest` (4) — CARTE ValueError, FREE/TITHE consumed, PASS preserved.

## Trap caught

Existing User `post_save` signal auto-creates COIN + FREE tokens (`apps.lyric.models:309`). Sprint 6 ITs that assert "user has only the token I seeded" must `self.user.tokens.all().delete()` after User.create. Without it, `_select_my_sea_token` returns the auto-COIN instead of None for the CARTE-excluded test. Worth a future feedback memory if it bites again.

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 02:29:08 -04:00
Disco DeDisco
7b7e80520a My Sea iter 4c: drop LOCK HAND → AUTO DRAW + GATE VIEW; quota committed at first card draw (irrevocable); DEL clears hand but preserves row as quota tracker; per-placement /lock POST upsert; lazy stale-row cleanup; sig polarity + .btn-disabled → ×; landing aperture bg revert to --priUser — Sprint 5 iter 4c of My Sea roadmap — TDD
Major refactor of the iter-4b skeleton ahead of Sprint 6's token costs. Iter 4b's LOCK HAND model let users freely DEL + LOCK in a loop, bypassing the 1/day quota; iter 4c closes that loophole by committing quota at first-card-draw (manual via FLIP OR auto via AUTO DRAW) + preserving the MySeaDraw row through DEL so the 24h clock keeps running.

## Server

`MySeaDraw` now plays double-duty: hand storage AND 24h quota tracker.
- `HAND_SIZE_BY_SPREAD` module dict maps each spread slug to its expected hand size (mirrors DRAW_ORDER in JS).
- `is_hand_complete` / `is_hand_empty` props drive view branching + template button states.
- `delete_stale()` classmethod hard-deletes rows older than FREE_DRAW_COOLDOWN_HOURS. Called lazily from `active_draw_for` on every view access (rides user traffic; no scheduler needed) + via the new `delete_stale_my_sea_draws` management command (cron backstop).
- `active_draw_for` prunes user's stale rows before lookup — auto-cleanup at the 24h mark per user spec ("sink 'em all at the 24hr mark and reinstate the FREE DRAW btn").

`my_sea_lock` is now a true upsert:
- First POST creates the row (quota commit).
- Subsequent POSTs UPDATE the existing row's hand (per-placement cadence — server stays current so navigate-away mid-draw still persists).
- Spread-mismatch (attempted spread switch within quota window) → 409.
- Empty/malformed hand → 400.
- Response carries `{ok, next_free_draw_at, hand_complete}` for JS state transitions.

`my_sea_delete` no longer deletes the row — clears the `hand` JSON only. `created_at` preserved so landing renders GATE VIEW (not FREE DRAW) until the row expires. Idempotent.

`my_sea_gate` new stub view — returns 404 for now; lets the template wire up GATE VIEW button URLs in advance. Sprint 6 will replace this w. the gatekeeper token-deposit UX.

`my_sea` view branches:
1. No sig → sign-gate
2. Active draw + non-empty hand (mid or complete) → picker phase w. saved hand
3. Active draw + empty hand (post-DEL) → landing phase w. GATE VIEW btn
4. No active draw → landing phase w. FREE DRAW btn

## Template + UX

- Picker form col: removed LOCK HAND. Replaced w. `#id_sea_action_btn` — same DOM node, label + behavior keyed on `data-state`:
  - `auto-draw` → label "AUTO DRAW"; click opens shared guard portal ("Auto deal cards?"); OK → fill remaining slots client-side + single-POST commit to server (per user spec: "commit all six draws in the same POST" so navigate-away mid-animation still persists).
  - `gate-view` → label "GATE VIEW"; click navigates to /gameboard/my-sea/gate/ (Sprint 6).
  - JS transitions auto-draw → gate-view automatically when the hand fills (via FLIP or AUTO DRAW completion).
- DEL btn: server-renders `.btn-disabled` pre-completion (per spec, the 1/day quota commits at first-card-draw — can't be refunded by an early DEL). JS removes `.btn-disabled` on hand completion. Post-completion click opens the shared guard portal; CONFIRM POSTs the delete endpoint (which clears hand server-side) + reloads to GATE VIEW landing.
- Deck stacks remain click-responsive post-completion so the user sees the disabled-FLIP feedback (signalling "no more draws"); the FLIP click is gated on `_locked` flag.
- Landing: primary nav btn is FREE DRAW (no active draw) or GATE VIEW (active draw exists w. empty hand). Both render as `<button>` (not `<a>`) so the typography matches across states — `<a>`'s UA-default serif typeface was bleeding into GATE VIEW under iter 4b polish.

## Other polish bundled

- **Sig polarity rendered in picker** — added `.my-sea-page[data-polarity]` to the existing `.sig-overlay[data-polarity]` + `.my-sign-page[data-polarity]` selector list in `_card-deck.scss`. Template wires `data-polarity` on the page wrapper based on `significator_reversed`. Previously the picker's center sig card was always gravity-themed regardless of the user's actual sig polarity.
- **`.btn-disabled` → × overlay** — universal CSS rule: any `.btn-disabled` button reads as × regardless of its native inner text/icons (DEL → ×, FLIP → ×, etc.). Hides inner content via `visibility: hidden` on children + paints × via `::before` pseudo-element. Templates that already render `&times;` explicitly (don/doff toggle pairs) get the pseudo overlay on top of their hidden inner ×; no double-× regression.
- **Landing aperture bg → `--priUser`** — explicit override on `.my-sea-page[data-phase="landing"]` so any bf-cache / stale-CSS state can't leak the picker-phase `--duoUser` green bg onto a landing render. Per user spec (2026-05-20): "Keep --duoUser on the hex, not on the aperture bg."
- **Dynamic combobox state** — `aria-selected` + `.sea-select-current` visible label both branch on `default_spread` (previously hardcoded SAO). Matters when the saved spread is non-SAO (e.g., Celtic Cross resumed mid-draw).

## Test coverage

- ITs (1100 IT/UT green in 57s):
  - `MySeaDrawModelTest` — `is_hand_complete`, `is_hand_empty`, `delete_stale`, lazy cleanup in `active_draw_for`.
  - `MySeaLockHandViewTest` — upsert same-row (rewrote 409 test), spread-mismatch 409, hand_complete flag in response.
  - `MySeaDeleteDrawViewTest` — clears hand but preserves row (rewrote "deletes row" test).
  - `MySeaViewWithSavedDrawTest` — picker w. complete hand renders GATE VIEW state.
  - `MySeaViewWithEmptyHandTest` (new) — empty-hand post-DEL renders landing w. GATE VIEW btn, no FREE DRAW.
  - `MySeaViewWithPartialHandTest` (new) — partial-hand renders picker w. AUTO DRAW + DEL btn-disabled.
  - `MySeaGateStubViewTest` (new) — 404 stub + login required.
- FTs (35 my_sea FTs green in 5m):
  - Iter-4b `test_del_confirm_clears_saved_draw_and_returns_to_landing` rewrote → `test_del_confirm_clears_hand_and_returns_to_gate_view_landing` (row preserved, landing renders GATE VIEW).
  - Iter-4a `test_lock_hand_enables_when_sao_hand_is_complete` → `test_action_btn_transitions_to_gate_view_on_hand_complete`.
  - Iter-4a `test_del_click_resets_hand_and_disables_lock_hand` → `test_del_btn_is_disabled_until_hand_complete`.
  - Iter-4a `test_lock_hand_click_disables_further_interaction` → `test_hand_completion_locks_picker_state` (no LOCK HAND click; transition is automatic).
  - Iter-4a `test_first_draw_locks_spread_combobox` trimmed — DEL no longer unlocks (DEL is `.btn-disabled` pre-completion).
  - Iter-4a `test_form_col_renders_decks_lock_hand_del_and_reversal_pct` → action btn + DEL btn-disabled assertions.

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 01:34:03 -04:00
Disco DeDisco
6f901fd9ce My Sea iter 4b polish v2: drop FYI from locked-draw Brief; dynamic aria-selected per default_spread; defensive cross data-spread sync on init (guards bf-cache drift causing all 6 slots to render post-DEL+reload) — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
(1) FYI removal — the locked-draw Brief is purely informational (status + next-free-draw timestamp). No navigation target made sense for the FYI; drop it after Brief.showBanner renders. The NVM dismiss + dedicated `<time>` slot carry all the affordance the user needs.

(2) Dynamic aria-selected on the SPREAD combobox — previously the SAO option was hardcoded `aria-selected="true"`. When active_draw is non-SAO (e.g. Celtic Cross), server-rendered state was internally inconsistent: hidden value = waite-smith, aria-selected = SAO. JS init's force-sync (which reads aria-selected to override autofill on hidden) then overwrote the correct hidden value w. SAO — corrupting the picker's state silently. Made aria-selected + `.sea-select-current` visible label both branch on `default_spread`.

(3) Defensive cross.data-spread sync on init — after the autofill force-sync settles `hidden.value` from the aria-selected source-of-truth, mirror it onto `.my-sea-cross[data-spread]` + re-run syncLabels. Idempotent when server-rendered state is internally consistent; corrective when a prior page state (Firefox bfcache restoring a Celtic-Cross DOM, mid-draw session restored) left a stale `data-spread` that SCSS-hides the wrong subset of cells. User-reported 2026-05-20: after locking a Celtic Cross + DEL + reload, all 6 slots remained visible on the picker w. SAO labels — exactly the symptom of cross.data-spread="waite-smith" surviving an otherwise-fresh server render.

Tests: 116 gameboard ITs + 5 iter-4b FTs green. The dynamic aria-selected behavior is implicitly covered by the existing default-spread IT (no regression on the SAO=true baseline); the bf-cache scenario is hard to express as a deterministic FT/IT — the defensive sync is a safety net, not a behavioral spec.

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 00:27:50 -04:00
Disco DeDisco
c1a8133345 My Sea iter 4b polish: Brief banner uses standard portaled .note-banner (Gaussian glass atop h2); next-free-draw datetime in dedicated <time> slot (not "Invalid Date"); DEL guard reuses shared #id_guard_portal from base.html — TDD
UX refactor on top of iter 4b (b76d3c5) per user direction:

(1) Brief banner — replaced custom `.my-sea-brief` markup + SCSS w. a call to `Brief.showBanner` from note.js. Now matches the my-notes / my-sign default-deck-warning Briefs exactly: standard `.note-banner` portaled atop the h2 w. Gaussian-glass backdrop-filter blur. Tagged `.my-sea-locked-banner` for FT disambiguation only — no visual override.

(2) Brief timestamp — fix for "Invalid Date" rendering in note.js's `<time class="note-banner__timestamp">` slot. Previously passed `created_at: ''` to `Brief.showBanner` → `new Date('')` returns Invalid Date → `toLocaleDateString` renders "Invalid Date". Now passes the next-free-draw ISO timestamp as `created_at` (server emits via `|date:'c'`). After Brief.showBanner returns, the `_showFreeDrawLockedBrief` JS overwrites the rendered text w. the more detailed `D, M j @ g:i A` format ("Wed, May 20 @ 11:57 PM") — leaves the ISO `datetime=` attribute intact for accessibility. The `line_text` no longer carries the timestamp inline (it's redundant w. the dedicated slot).

(3) DEL guard portal — replaced custom `#id_my_sea_del_portal` fullscreen modal + `.my-sea-del-portal` SCSS w. a call to `window.showGuard` from base.html, targeting the shared `#id_guard_portal`. Same Gaussian-glass tooltip the room gear-menu DEL flow uses: no backdrop, positioned above the anchor button, standard `.btn-confirm OK` + `.btn-cancel NVM` pair. Bundled a non-breaking `options.yesLabel` extension to `show()` in base.html for future destructive flows that need a custom YES label (defaults to 'OK', resets on dismiss/confirm) — my-sea doesn't use it per user direction (the `.btn-confirm` class implies "OK"; destructive intent belongs on the trigger button, which is `.btn-danger DEL`).

Tests: 30 iter-4b ITs (model + lock + delete + saved-draw view branches) + 5 iter-4b FTs all green; IT/FT assertions updated to target the shared portal markup (`#id_guard_portal.active`, `.guard-yes`, `.guard-no`, `.note-banner.my-sea-locked-banner`).

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 00:12:52 -04:00
Disco DeDisco
b76d3c5dff My Sea iter 4b: MySeaDraw persistence + LOCK HAND POST + DEL guard + Brief banner; rewrite obsolete spread-switch FT; fix bud-panel CI race on gatekeeper FT — Sprint 5 iter 4b of My Sea roadmap — TDD
Iter 4b lands server persistence of the iter-4a client-side hand. New MySeaDraw model (FK user, spread, hand JSONField in draw order, sig snapshot, created_at) w. 1/24h quota window; new endpoints /gameboard/my-sea/lock (POST, 409 on quota-active, 400 on partial hand) + /gameboard/my-sea/delete (POST, idempotent). LOCK HAND now collects the in-progress hand from DOM, POSTs, and on success un-hides a Brief banner inline (no page reload — preserves iter-4a FT picker refs). DEL post-LOCK opens #id_my_sea_del_portal w. uniform 'Are you sure?' copy; CONFIRM POSTs delete + reloads to landing. Brief banner carries the next-free-draw timestamp + a NVM dismiss. Saved-draw render bypasses the sign-gate via _resolve_sig (sig snapshot on the draw is used even if user.significator was cleared later) + bypasses the landing phase (the saved hand IS what the user came to see). Per-position slot rendering extracted to _my_sea_slot.html. DRY follow-up: card_dict() extracted to apps.epic.utils — gameroom sea_deck + my-sea _my_sea_deck_data now share one source of truth (prevents drift like the iter-4a-follow-up Major Arcana fix from recurring).

Pipeline #316 fixes bundled: (a) functional_tests.test_game_my_sea.MySeaCardDrawTest.test_switching_spread_resets_in_progress_hand was obsoleted by the iter-4a follow-up's spread-lock-after-first-draw — the test premise (mid-draw spread switching resets hand) no longer matches behavior (switching is blocked outright). Rewrote as test_first_draw_locks_spread_combobox, which pins .sea-select--locked after first draw + verifies DEL releases it. (b) functional_tests.test_game_room_gatekeeper.GatekeeperTest.test_second_gamer_drops_token_into_open_slot failed in CI on ElementNotInteractableException when clicking #id_bud_panel .btn.btn-confirm — the bud panel's scaleX(0)→scaleX(1) 0.2s CSS transition wasn't settled by click-time, so Selenium read scroll-into-view against a near-zero-width target. Added a wait_for on getBoundingClientRect().width > 100 so the click waits for the animation to finish. Local passes consistently; CI was 1+ frame slower than the implicit 'find element' wait.

Tests: 1085 IT/UT green in 55s; 35 my_sea FTs green in 5m; new ITs in MySeaDrawModelTest (8), MySeaLockHandViewTest (7), MySeaDeleteDrawViewTest (5), MySeaViewWithSavedDrawTest (9); new FTs in MySeaLockHandTest (5).

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 23:54:32 -04:00
Disco DeDisco
31ed2bda0e Billnote palette swatch: DRY w. .swatch via palette-swatch-bg mixin; fixes --quaUser→--quiUser drift
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed
Note swatches (right-side thumbnail + modal preview tiles) were inlining their own linear-gradient that ended on --quaUser instead of --quiUser, so the 4th band rendered the wrong palette colour (e.g., Maryland's red showed up on the Baltimorean Note where the My Palettes applet's matching tile shows none). Extracted the canonical gradient into a @mixin palette-swatch-bg in _palette-picker.scss and @include'd it from .swatch, .note-item__palette + .note-swatch-body so all three share one source of truth.

Code architected by Disco DeDisco <discodedisco@outlook.com>

Git commit message Co-Authored-By:

Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 22:09:01 -04:00
Disco DeDisco
b6e93b9d64 My Sea iter-4a follow-up batch: modal port + draw polish + label positioning + Major Arcana fix — TDD
User-driven bug-squash + UX-polish cycle on top of iter 4a (ca2a62f). All 14 fixes ship behind the same iter-4a banner since they close the substage's UX gaps without expanding scope to iter 4b's persistence layer.

SeaDeal modal port — extracted apps/gameboard/_partials/_sea_stage.html shared by gameroom + my-sea; aliased .my-sea-picker w. id=id_sea_overlay so SeaDeal.init() finds it; FLIP click → SeaDeal.openStage delegation instead of bare _fillSlot. Fixes the user-reported 'thumbnail disappears' bug — slot was landing at opacity 0 (.--filled w.o .--visible) because SeaDeal's _hideStage (which adds --visible on modal dismiss) was never running. 3 new FTs cover the modal flow.

Spread lock + DEL reshuffle — _lockSpread/_unlockSpread toggle .sea-select--locked class on the combobox; first deposit locks, _resetHand unlocks. _reshuffleDeck Fisher-Yates over combined piles + re-rolls 25% reversal axis on DEL so successive DELs don't re-deal the same hand. Verified Claudezilla: 3 DEL cycles produced distinct lay cards (150 → 114 → 155).

Cover/cross empty slots — subtle dotted outline (transparent bg + 0.25 alpha border) w. --duoUser mask reveal on hover/touch. Per the user spec; rule lives in _card-deck.scss (shared between gameroom + my-sea). Plus matching label-opacity (0.25 idle → 0.6 hover) via CSS :hover ancestor propagation.

DOS spec — Solution moved from cover → crown per user correction. DRAW_ORDER ['loom', 'cross', 'crown']; POSITION_LABELS {loom: Desire, cross: Obstacle, crown: Solution}; SCSS hide list flipped from [leave, crown, lay] → [leave, cover, lay]; FT/IT assertions updated.

SAO → DOS soft-reload bug — Firefox autofill on hidden input restored the previous-session DOS value, tripping combobox.js's change-event guard. Fix: autocomplete=off + force-sync hidden.value from server-rendered aria-selected option in init. Captured as feedback_firefox_autofill_hidden_inputs (generalizable trap).

.sea-pos-label outside .sea-card-slot — moved label to be a sibling of the slot in the cell, so SeaDeal innerHTML clobber on draw doesn't erase it. Per-position absolute positioning touching slot borders: crown/cover above (translate -50%, 0.1rem, scaleY 1.2); lay/cross below (translate -50%, -0.1rem, scaleY 1.2); leave left, CCW (writing-mode vertical-rl + rotate 180deg + scaleX 1.2); loom right, CW (writing-mode vertical-rl + scaleX 1.2). scaleX for rotated labels (not scaleY) — perpendicular to text-flow is the visible-width direction after rotation. .my-sea-cross gap bumped to 1.75rem for label clearance.

Escape Velocity label swaps — POSITION_LABELS for escape-velocity: {crown: Crown, leave: Lay, cover: Cover, cross: Cross, loom: Loom, lay: Leave}. Replaces the Waite-Smith Behind/Beneath/Before per user spec.

SPREAD dropdown portal — .my-sea-form-col .sea-form-main { overflow: visible } + .sea-select-list { z-index: 1000 } so the dropdown extends past the form-main scroll area + sits above the picker stacking ints. Gameroom .sea-form-main still scrolls (only my-sea opts out).

Major Arcana polarity-split rendering — added 9 missing _card_dict keys to my-sea's _my_sea_deck_data to match gameroom epic.views.sea_deck's contract: levity_emanation, gravity_emanation, levity_reversal, gravity_reversal, italic_word, keywords_upright, keywords_reversed, energies, operations. Without these StageCard.populateCard falls through to plain name_title for trumps 19-21 + cards 48-49. Iter 4b cleanup candidate: extract apps.epic.utils.card_dict() to DRY the now-identical helpers.

Tests deferred — user explicitly belayed FT runs during the bug-fix substage. Iter 4b will re-establish a green sweep before its commit lands.

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 22:02:27 -04:00
Disco DeDisco
ca2a62fd84 My Sea client-side card draw + DEL + LOCK HAND visual lock — Sprint 5 iter 4a of My Sea roadmap — TDD
Two-step deposit flow lifted from gameroom sea.js's `_fillSlot`: click a polarity stack → FLIP btn appears on the active stack → click FLIP → top card pops from that polarity's pile and deposits into `DRAW_ORDER[currentSpread][_filled]`. Per-spread hand-size completion (3 for any three-card spread, 6 for Celtic Cross variants) flips LOCK HAND from disabled to enabled. DEL fully resets — every filled slot reverts to `.sea-card-slot--empty` w. its `.sea-pos-label` re-rendered, piles re-clone from the immutable server payload, LOCK HAND re-disables. Switching spreads mid-draw triggers the same reset (position-subset + draw-order both change). LOCK HAND click visually locks the picker (`.my-sea-picker--locked` + `.btn-disabled` on stacks/DEL/itself) — server persistence defers to iter 4b.

**Card source**: new `_my_sea_deck_data(user)` helper in `apps/gameboard/views.py` mirrors the gameroom `epic.views.sea_deck` JSON contract — same `_card_dict` shape (id/name/arcana/suit/number/corner_rank/suit_icon/name_group/name_title/qualifiers/reversed). Differences from the room version per spec lock:

- No `room` context; excludes only the **current user's significator** (no other seated gamers).
- Backup-deck fallthrough: `user.equipped_deck or DeckVariant.filter(slug='earthman').first()` — mirrors `personal_sig_cards`, keeps the no-deck-equipped path working.
- Reversal probability hardcoded at 0.25 per the iter 3 spec lock. (Future per-user config will share a helper w. the gameroom's `stack_reversal_probability`.)

Deck data flows in via `{{ sea_deck_data|json_script:"id_my_sea_deck" }}` — Django's built-in script-tag JSON embedder. JS reads `id_my_sea_deck`'s textContent on init + maintains `_levityPile` / `_gravityPile` working copies that shift one card per deposit. DEL re-clones from the immutable initial payload rather than re-fetching (server is stateless wrt this client-side dealing — same shuffle survives DEL).

`.my-sea-picker--locked` SCSS: `.sea-deck-stack.btn-disabled` gets `pointer-events: none; opacity: 0.5` per [[feedback_btn_disabled_pointer_events]] convention. Hand state freezes; only iter 4b's LOCK HAND POST can mutate the persisted state from there.

**FTs** (9 in new `MySeaCardDrawTest`, using a `_draw_one(picker, polarity)` helper that clicks the stack + waits for the FLIP btn to surface + clicks FLIP):
- deck JSON embedded w. two polarity halves, disjoint card ids;
- user significator excluded from both halves;
- first LEVITY draw lands in SAO's first slot (`.sea-pos-lay`) w. `.sea-card-slot--filled.sea-card-slot--levity` + corner_rank inside;
- second draw (GRAVITY) lands in SAO's second slot (`.sea-pos-cover`) w. polarity reflected;
- 3 draws complete the SAO hand → LOCK HAND `disabled` attribute drops;
- DEL resets every filled slot, LOCK HAND re-disables;
- LOCK HAND click adds `.my-sea-picker--locked` + `.btn-disabled` on the stacks;
- switching to MBS mid-draw wipes the in-progress hand.

**ITs** (6 in new `MySeaDeckDataViewTest`):
- context `sea_deck_data` has `levity` + `gravity` keys, both lists;
- user significator absent from both halves;
- halves are disjoint sets of card ids;
- card dicts carry `id` / `corner_rank` / `suit_icon` / `reversed` (bool); shape matches gameroom contract;
- template embeds via `<script id="id_my_sea_deck" type="application/json">`;
- no-equipped-deck users get the Earthman backup pile (not empty).

Tests: 41/41 FT green across test_bill_my_sign + test_game_my_sea; 1055/1055 IT/UT green in 53s.

**Deferred to iter 4b** (server persistence):
- `MySeaDraw` model (FK to user, spread name, JSON field for hand layout, created_at);
- LOCK HAND POST endpoint → commits the hand to the DB;
- 1/24h FREE DRAW quota check + the eventual FREE DRAW → DRAW SEA btn-label swap;
- Sig stage card full populate (name/qualifier/keywords/FYI/SPIN/FLIP) — currently corner rank + suit icon only.

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 20:02:20 -04:00
Disco DeDisco
f154d660bd My Sea per-spread positions + draw-order JS config + position labels — Sprint 5 iter 3 follow-up — TDD
User-locked spec 2026-05-19: each three-card spread uses a DIFFERENT 3-position subset of the 6 surrounding positions, in its own draw order. Replaces the iter-3 binary `data-spread-shape="three-card|six-card"` model w. per-spread `data-spread="<value>"`. Closes iter 3 cleanly + scaffolds the draw-order data iter 4 will consume.

Position subsets (per spread):
  PPF → leave (1) · cover (2) · loom  (3)
  SAO → lay   (1) · cover (2) · crown (3)
  MBS → crown (1) · lay   (2) · loom  (3)
  DOS → loom  (1) · cross (2) · cover (3)
  Waite-Smith     → all 6 surrounding (cover · cross · crown · lay · loom · leave)
  Escape Velocity → all 6 surrounding (cover · cross · lay · leave · crown · loom)

All 6 cells continue to render in DOM unconditionally — `.my-sea-cross[data-spread="<value>"]` SCSS rules hide inactive positions per spread via `display: none`. Cover/cross live nested inside `.sea-pos-core` so their absolute-overlay positioning rules from `_card-deck.scss:1310-1331` carry over for free.

**Position labels** (re-appropriated `.sea-stack-name` typography per user) — `.sea-pos-label` inside each empty `.sea-card-slot--empty` carries the per-spread caption. Server-renders SAO's labels by default (lay=Situation, cover=Action, crown=Outcome); JS swaps labels via `POSITION_LABELS[spread]` lookup on combobox change. Inactive-for-spread positions render their span w. empty `textContent` so JS only has to set text, never toggle visibility. Celtic Cross variants share the gameroom's existing position vocabulary (Crown/Beneath/Cover/Cross/Before/Behind).

**DRAW_ORDER JS const** baked into the inline picker IIFE — array of position names per spread, ready for iter 4's deck-click-deposit logic to consume. Exposed via `window._mySeaDrawOrder` so iter-4 click handlers can `window._mySeaDrawOrder[currentSpread][nextSlotIdx]` to resolve the target position. No click handlers wired yet — iter 4 territory.

**Selenium trap caught**: the combobox click-twice-on-the-toggle bug — re-clicking the combobox while `aria-expanded='true'` closes the dropdown (combobox.js's toggle behavior). Test 3's spread-cycling iterates through 6 spreads, each needs the dropdown OPEN before clicking a new option; added a `_pick(value)` helper that checks `aria-expanded` first.

Files:
- `templates/apps/gameboard/my_sea.html` — `.my-sea-cross[data-spread]` w. server-rendered default; each empty slot wraps a `<span class="sea-pos-label" data-position="<name>">` (SAO labels seeded inline, others empty initially); inline IIFE adds `DRAW_ORDER` + `POSITION_LABELS` consts + `syncLabels()` that swaps captions on `change`.
- `static_src/scss/_gameboard.scss` — drops the `data-spread-shape="three-card"|"six-card"` rules; adds 4 per-spread visibility rules (PPF/SAO/MBS/DOS). Celtic Cross variants inherit the gameroom's full 3×3 grid w. no overrides. `.sea-pos-label` style mirrors `.sea-stack-name` from _card-deck.scss line 1557 (small-uppercase-letter-spaced-scaleY) sans the polarity color — these aren't deck identifiers, just spread-position captions.
- `apps/gameboard/tests/integrated/test_views.py` — IT `test_cross_carries_initial_three_card_spread_shape` renamed + retargeted to `data-spread="situation-action-outcome"`; new IT `test_template_renders_sao_position_labels_on_default` pins the seeded SAO labels + empty spans for inactive positions.
- `functional_tests/test_game_my_sea.py` — iter-2's `test_picker_hides_six_card_only_positions_by_default` renamed to `test_picker_renders_sao_default_position_subset` w. SAO-specific visibility expectations (lay/cover/crown visible; leave/loom/cross hidden). iter-3's `test_picking_celtic_cross_reveals_six_card_positions` rewritten + expanded to `test_picking_spread_swaps_data_spread_and_position_visibility` — cycles through all 6 spreads, asserts `data-spread` attribute + per-position `is_displayed()` for each. New `test_per_spread_position_labels_render_and_update` cycles through 5 spreads (SAO default + 4 switches) asserting captions match the spec.

Tests: 33/33 FT green across test_bill_my_sign + test_game_my_sea; 1049/1049 IT/UT green in 52s.

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 19:38:53 -04:00
Disco DeDisco
fd5db951a7 My Sea form col + SPREAD dropdown w. 3-card/6-card section dividers — Sprint 5 iter 3 of My Sea roadmap — TDD
Picker phase form col: SPREAD combobox w. 6 spread options under 2 horizontal section dividers ("3-card spreads" / "6-card spreads"), reversal-% caption, GRAVITY + LEVITY deck swatches, LOCK HAND + DEL btns. Default = Situation, Action, Outcome (a 3-card spread). Selecting a 6-card spread (Celtic Cross Waite-Smith or Escape Velocity) swaps `.my-sea-cross[data-spread-shape]` from `three-card` to `six-card` — revealing the crown / lay / cross cells that the default 3-card variants hide.

Naming correction (user-locked): the spread itself is a "three-card spread" not a "three-card cross" — "cross" stays scoped to the Celtic Cross variants (6-card spreads). CSS class `.my-sea-cross` carries grid-container semantics regardless of which spread shape is active; the spread-vs-cross distinction lives at the spread-name layer only.

- **View** (gameboard/views.py): `my_sea` adds `default_spread = "situation-action-outcome"` + `reversals_pct = 25` context keys.
- **Template** (my_sea.html): renders all 7 cross cells (crown/leave/core+cover+cross/loom/lay) unconditionally + adds `data-spread-shape="three-card"` to `.my-sea-cross`. Form col DRY-reuses gameroom `_sea_overlay.html`'s `.sea-form-col` shape — `.sea-form-main` w. `.sea-field` (SPREAD label + reversal hint + custom combobox) + `.sea-stacks` (GRAVITY + LEVITY swatches) + `.sea-form-actions` (LOCK HAND + DEL). 6 options + 2 dividers in the combobox `<ul>`; dividers are `role="presentation"` so `combobox.js` skips them naturally. Inline IIFE listens for the hidden `<input id="id_sea_spread">`'s `change` event + sets `.my-sea-cross`'s `data-spread-shape` based on whether the value is in `['waite-smith', 'escape-velocity']`. No new combobox.js wiring — the existing module's `change`-bubbling contract feeds straight in.
- **SCSS** (_gameboard.scss):
  - `.my-sea-cross[data-spread-shape="three-card"]` — single-row `"leave core loom"` grid + `display: none` on crown/lay/cross.
  - `.my-sea-cross[data-spread-shape="six-card"]` — inherits the gameroom `.sea-cross`'s 3×3 grid + reveals all cells.
  - `.sea-select-divider` — section header style mirrors `.kit-bag-label`'s small-uppercase-underlined-letter-spaced --quaUser/0.75 treatment but HORIZONTAL (kit-bag uses `writing-mode: vertical-rl`; dropdown menus are flat). `pointer-events: none` belt-and-braces against accidental click/hover.
  - `.my-sea-form-col` — width-constrains the form col so the picker's cross + form sit side-by-side.

**Iter-2 contract updated** (cells in DOM, hidden via CSS for 3-card default):

- FT `test_picker_does_not_render_forsaken_positions` → renamed to `test_picker_hides_six_card_only_positions_by_default` — asserts the 3 cells are in the DOM but `is_displayed() == False` so iter-3's spread switch can reveal them via CSS without re-rendering.
- IT `test_picker_does_not_render_forsaken_positions` → renamed to `test_picker_renders_six_card_only_positions_for_spread_switch` — assertContains the classes (server now renders them unconditionally).

**Tests**:

- 4 FTs in new `MySeaSpreadFormTest`: combobox renders 6 options + 2 dividers w. correct labels, default is Situation/Action/Outcome (hidden input value + visible current-label span + cross's data-spread-shape), picking Celtic Cross flips data-spread-shape to six-card + reveals crown/lay/cross, form col carries DECKS swatches + LOCK HAND + DEL + reversal-% caption. Combobox `<li>` options are inside `aria-expanded='false'` listbox → use `get_attribute("textContent")` not `.text` (which returns "" for Selenium-hidden elements).
- 7 ITs in new `MySeaSpreadFormTemplateTest`: default_spread + reversals_pct context keys, all 6 options + both labels render, 2 dividers render w. expected text, default option carries aria-selected="true", cross's initial data-spread-shape="three-card", form col DECKS + buttons + reversal hint render.

Tests: 32/32 FT green across test_bill_my_sign + test_game_my_sea; 1048/1048 IT/UT green in 52s.

Card-draw mechanics (clicking a deck swatch deposits a card into the next empty slot; LOCK HAND commits the draw) defer to iter 4 — this iter ships the spread-selection + layout-shape switch UI; the buttons are stubs (LOCK HAND starts disabled, DEL is a placeholder).

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 17:23:25 -04:00
Disco DeDisco
f5fc1e15f8 My Sea picker phase: three-card cross (sig + cover/leave/loom) — Sprint 5 iter 2 of My Sea roadmap — TDD
After the FREE DRAW click on iter 1's landing swaps `data-phase` to `picker`, the picker now renders a stripped Celtic Cross: user's saved significator pinned in `.sea-pos-core`, three drawn-card drop zones around it — cover (overlaid on sig), leave (left of core), loom (right of core). Crown / lay / cross from the gameroom's 6-position spread are deliberately forsaken (user-locked spec).

DRY w. the gameroom sea-overlay: reuses `.sea-cards-col` + `.sea-cross` + `.sea-crucifix-cell` + `.sea-pos-*` + `.sea-card-slot--empty` + `.sea-sig-card` classes & their _card-deck.scss styling (1181-1331). Only divergence from the room: a `.my-sea-cross` modifier in `_gameboard.scss` overrides `grid-template-areas` from the room's `". crown . / leave core loom / . lay ."` 3×3 to a single-row `"leave core loom"` — drops the crown + lay rows since those positions are forsaken. Cover stays nested inside `.sea-pos-core` so the absolute-overlay rules from _card-deck.scss line 1310-1331 carry over for free.

Picker bg = `rgba(var(--duoUser), 1)` on `.my-sea-page[data-phase="picker"]` — parallels `.my-sign-page[data-phase="picker"]` from _card-deck.scss line 704, so the landing→picker swap reads as a continuous surface (hex face → felt) like on /billboard/my-sign/.

The sig card renders w. `data-card-id="{{ significator.id }}"` + `.fan-corner-rank` + `.fa-solid {suit-icon}` (mirrors the gameroom's `.sea-sig-card` minimal markup at `_sea_overlay.html` line 33-39). Full card-face / FYI / SPIN wiring deferred — iter 3 lands the form col + interactive draw flow.

View context: `my_sea` now passes `significator` (FK pass-through) + `significator_reversed` so the template can render the corner rank + suit icon at render time without re-fetching.

- 3 FTs in new `MySeaPickerPhaseTest`: sig card w. `data-card-id` matching `user.significator.id` in `.sea-pos-core`; cover/leave/loom empty drop zones render; crown/lay/cross absent. Shared `_enter_picker_phase()` helper polls for `data-phase='picker'` after the ~800ms seat-1C animation delay.
- 4 ITs in new `MySeaPickerPhaseTemplateTest`: server-render contract for sig in core + cover/leave/loom classes + forsaken-positions-absent + picker entirely absent when user has no sig (4b gate precedence).

Tests: 28/28 FT green across test_bill_my_sign + test_game_my_sea (~219s); 1041/1041 IT/UT green (53s).

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 16:06:14 -04:00
Disco DeDisco
285597b467 My Sea FREE DRAW + seat-1C seated transition — Sprint 5 iter 1 follow-up — TDD
Iter-1 follow-up after the user re-spec'd the FREE DRAW click behavior:

- **Btn label** DRAW SEA → FREE DRAW (the 1/24h free-quota draw). Element ID `id_draw_sea_btn` retained — describes intent, not label, so a future sprint can conditionally swap the label back to DRAW SEA once the daily free has been used (at which point the btn calls the room gatekeeper partial for token-deposit per [[project-my-sea-roadmap]] Sprint 6).
- **Chair seat markup** — each `.table-seat` now renders w. `.fa-chair` + `.seat-position-label` (1C-6C) + `.position-status-icon.fa-solid.fa-ban` mirroring the room's hex grammar from `_table_positions.html`. **`.seat-position-label`** (not `.seat-role-label`) because my-sea is the solo flow — no roles — and the existing room class carries role-grammar semantics that don't apply here. New class gets its own grid placement in `_gameboard.scss` (col 2 / row 1 default, col 1 for left-side seats 3/4/5 per the room's flip rule).
- **FREE DRAW click flow** — (1) seat 1C immediately gains `.seated` class & its `.fa-ban` icon swaps to `.fa-circle-check`; (2) after 800ms (so the user sees the seat animation against `_room.scss`'s 0.6s `color`/`filter` transition on `.fa-chair`), `data-phase` swaps to `picker` & landing hides. Seat 1C-only because my-sea is single-user-per-page until friend-invite lands — the user always occupies the lowest-numeral seat.
- **`.table-seat.seated` SCSS** in `_gameboard.scss` — `--terUser` chair color + `drop-shadow(--ninUser)` glow. Mirrors `_room.scss:626` `.table-seat.active .fa-chair` styling but uses a stable `.seated` class (semantically distinct: `.active` = current turn in a multi-user room, `.seated` = draw-locked occupant in the solo flow). Status icon green via the existing `_room.scss:616` `.position-status-icon.fa-circle-check` rule — no new color rule needed.
- **FT/IT updates** — renamed `test_landing_renders_hex_with_draw_sea_btn` → `…_free_draw_btn` w. "FREE DRAW" label assertions; T2 extended to assert `.position-status-icon.fa-ban` on each seat at render time; T3 (formerly `…_transitions_to_picker_phase`) rewritten as `test_free_draw_click_seats_user_in_1C_then_swaps_phase` to pin the full click contract: 1C goes `.seated` + `.fa-circle-check`, seats 2-6 unchanged, picker phase swap after the delay. New IT `test_landing_renders_position_status_ban_icon_on_each_seat` asserts initial ban + `.seat-position-label` counts.

**Substring trap caught** (worth a sticky note): bare class-name substrings (`fa-ban`, `position-status-icon`) appear ALSO in the inline JS handler's `classList.remove(…)` / `querySelector(…)` arg strings → `html.count("fa-ban") = 7`, not 6. Tightened IT assertions to match the full class attribute (`class="position-status-icon fa-solid fa-ban"`) — never count bare class names in `assertContains` / `html.count` when the same class is JS-manipulated client-side.

Tests: 25/25 FT green across test_bill_my_sign + test_game_my_sea (165s); 1037/1037 IT/UT green (49s).

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 15:48:07 -04:00
Disco DeDisco
de48ae226d My Sea DRAW SEA landing — Sprint 5 iter 1 of My Sea roadmap — TDD
DRAW SEA landing UX on /gameboard/my-sea/ for users past the [[sprint-my-sea-sign-gate-may19]] gate. DRY table hex (reused from the room shell + my-sign Sprint 4a iter 3) w. 6 chair seats labeled 1C-6C (placeholder for friend-invite per the My Sea roadmap "Six chairs retained even in solo" anchor) + central DRAW SEA `.btn-primary` mirroring SCAN SIGN on /billboard/my-sign/. Click swaps `.my-sea-page[data-phase]` from `landing` to `picker`; the picker UX itself (three-card cross w. cover/leave/loom + form col / spread dropdown / decks / LOCK HAND / DEL) lands in iters 2 + 3.

The 'C' suffix on the chair labels = "Chair" (user-locked); no role semantics (this is a solo draw, not a 6-player role assignment). `.table-seat` CSS class + `data-slot` attribute preserved so the room's existing `[data-slot="N"]` positioning rules (`_room.scss` L583-588) carry over for free — no SCSS fork; just a new `.seat-label` span inside each seat.

The 'Default deck warning' Brief banner from /billboard/my-sign/ fires verbatim when `user.equipped_deck` is None (the user is headed for a draw against the Earthman [Shabby Cardstock] backup unless they equip one first). Tagged `.my-sea-intro-banner` so FTs disambiguate from other Briefs. Same FYI (→ /gameboard/) / NVM (dismiss) action grammar.

Bundled: BACK→NVM label swap (user-edited mid-sprint) in the existing sign-gate. CSS class `.my-sea-sign-gate__back` retained — the swap was label-only — so existing FTs targeting the class still pass; docstrings + comments updated for accuracy.

Files:
- `apps/gameboard/views.py` — `my_sea` view adds 2 context keys: `no_equipped_deck` (bool) + `show_backup_intro_banner` (= user_has_sig AND no_equipped_deck). The sig-gate path still wins precedence.
- `templates/apps/gameboard/my_sea.html` — `.my-sea-page[data-phase="landing"]`; new `.my-sea-landing` block w. room-shell hex + `#id_draw_sea_btn` + 6 `.table-seat[data-slot="N"]` w. `<span class="seat-label">NC</span>`; new `.my-sea-picker` placeholder (`display:none` til DRAW SEA click); inline `<script>` for the click→data-phase swap + scaleTable re-fire on next tick (mirrors my-sign's iter-3 RAF dispatch); copies the my-sign Brief banner script block verbatim w. `.my-sea-intro-banner` post-render tag.
- `static_src/scss/_gameboard.scss` — new `.my-sea-page` + `.my-sea-landing` + `.my-sea-picker` rule blocks mirroring `.my-sign-page` / `.my-sign-landing` from `_card-deck.scss`. `.seat-label` styled in `--terUser` to match the chair iconography.
- `apps/gameboard/tests/integrated/test_views.py` — `+7 ITs` in new `MySeaDrawSeaLandingViewTest` pinning the context keys + presence/absence of `#id_draw_sea_btn` + 6 `data-slot=N` w. `NC` labels + Sprint 4b gate's precedence over the new landing.
- `functional_tests/test_game_my_sea.py` — `+5 FTs` in new `MySeaDrawSeaLandingTest` for the visible UX (hex + DRAW SEA, 6 seats labeled 1C-6C, click→picker phase swap, Brief banner on no-deck, no banner when deck equipped). Uses new `_assign_sig` helper from [[sprint-sig-page-helper-may19c]] for the user-w-sig precondition.

Tests: 25/25 FTs green across test_bill_my_sign + test_game_my_sea in 219s; 1036/1036 IT/UT green in 50s (+7 from baseline).

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 15:15:37 -04:00
Disco DeDisco
4d1c74a2af FT helper: sig_page.py — _seed_earthman_sig_pile + _assign_sig — Sprint 4c of My Sea roadmap — TDD
Pure test-infra refactor. Sprint 5+ (sea-select / DRAW SEA / latest-draw rendering) all need a "user has sig" precondition without walking the picker — extracting it once now prevents Sprint 5's setUps from copy-pasting the inline assignment dance again.

Two helpers in new `src/functional_tests/sig_page.py` (mirrors the `room_page.py` / `post_page.py` / `my_posts_page.py` convention — underscored to signal "test infrastructure, not API surface", public within `functional_tests/`):

- `_seed_earthman_sig_pile()` — re-seeds Earthman DeckVariant + the 16 MIDDLE court cards (Maid/Jack/Queen/King × BRANDS/CROWNS/BLADES/GRAILS) that `personal_sig_cards(user)` returns. Hoisted verbatim from the duplicate definitions in `test_bill_my_sign.py` + `test_game_my_sea.py` introduced in [[sprint_serialized_rollback_ft_fix_may19]]. Major 0/1 are deliberately NOT seeded — `_filter_major_unlocks` in `personal_sig_cards()` strips them for users w.o the matching Note unlocks, which is the default state in solo FTs.
- `_assign_sig(user, card=None, reversed_flag=False)` — sets `user.significator` + `significator_reversed` directly, bypassing the picker UI. Returns the assigned card so downstream assertions can use it. `card=None` defaults to `personal_sig_cards(user)[0]` (the same target the picker happy-path FT uses).

Call sites updated:
- `test_bill_my_sign.py` — drops the local `_seed_earthman_sig_pile` definition (32 lines); imports from `sig_page`. `MySignClearTest.setUp` now uses `_assign_sig(self.gamer)` instead of the 4-line manual sig-assignment block.
- `test_game_my_sea.py` — drops the local `_seed_earthman_sig_pile` definition (22 lines); imports from `sig_page`. `MySeaSignGateTest`'s two "user w. sig" tests (#4 + #6) swap their 2-line `self.gamer.significator = ... ; .save(...)` blocks for `_assign_sig(self.gamer, self.target_card)`.

Diff stat: +69 lines (new helper module), -59 lines (duplicate code removed). Net +10 LOC but the duplication trap is closed — single source of truth for sig-state FT setup.

20/20 FT green across both files in 174s post-refactor.

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 14:22:49 -04:00
Disco DeDisco
c8a603484e My Sign DEL btn: clear-sign affordance on SCAN SIGN landing — Sprint 4b-adjacent of My Sea roadmap — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
Pre-spec'd in [[sprint-my-sea-sign-gate-may19]] as the unblocker for tomorrow's visual verification of 4b's no-sig branch — admin user (@disco) had a saved sig from Sprint 4a testing & there was no in-UI affordance to undo it short of DB surgery. Lands ahead of the deferred 4b visual verify so dev users can toggle between sig/no-sig states on Claudezilla.

- Endpoint: `path("my-sign/clear", views.clear_sign, name="clear_sign")` — POST sets `User.significator = None` + `significator_reversed = False`, redirects to picker; GET is a no-mutation redirect to picker (mirrors save_sign's GET handling). `login_required(login_url="/")`. No trailing slash per [[feedback_url_convention_actions_no_trailing_slash]] (action endpoint, not page).
- Template (my_sign.html): `<form id="id_clear_sign_form" class="my-sign-clear-form">` w. `<button id="id_clear_sign_btn" class="btn btn-danger">DEL</button>`, rendered ONLY when `current_significator` is set; sits inside `.my-sign-landing` as a sibling of `.room-shell` so it's bound to the landing-phase UI alone (picker phase already has its own NVM unlock affordance on focused thumbnails).
- SCSS: anchored bottom-right of `.my-sign-landing` via `position: absolute; bottom: .75rem; right: 1rem` — `.my-sign-landing` gains `position: relative` to scope the absolute. `.btn-danger` carries the destructive treatment; "DEL" mirrors post.html gear menu's DEL convention from [[sprint-post-polish-may13]].
- 3 FTs in new `MySignClearTest` class — covers: btn renders on landing when sig saved (T1, asserts text "DEL" + `.btn-danger` class); btn absent when no sig (T2); click POSTs, reloads, & wipes `User.significator` + `significator_reversed` in DB (T3).
- 6 ITs in new `ClearSignViewTest` + `MySignClearAffordanceTemplateTest` — covers: login_required gate, POST wipes both fields w. redirect-back, GET redirects w.o mutation, POST-w/o-existing-sig is idempotent no-op, template renders btn only when sig set, template's form action targets `clear_sign` reverse.
- 1029 IT/UT green in 47s (+6 from baseline); 20/20 FT green across test_bill_my_sign + test_game_my_sea in 165s.

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 14:13:20 -04:00
Disco DeDisco
a636e940b7 fix CI FT regression: My Sign + My Sea setUpClass ContentType collision — pipeline #313
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
Sprint 4a-cont (400762c) + 4b (cd0add1) introduced `serialized_rollback = True` on `MySignPickerTest`, `MySignBackupDeckTest`, and `MySeaSignGateTest` to keep migration-seeded `DeckVariant` + `TarotCard` rows alive across TransactionTestCase flushes. Locally each file ran clean, but in the full pipeline #313 `test-FTs-non-room` stage all three classes errored in setUpClass:

  django.db.utils.IntegrityError: UNIQUE constraint failed:
  django_content_type.app_label, django_content_type.model

Mechanism: every prior `TransactionTestCase`-derived class in the bucket flushed without `inhibit_post_migrate`, so Django's post-migrate signal recreated `django_content_type` rows. When these three classes hit `_fixture_setup` & tried to deserialize the saved DB snapshot, the inserts collided on `(app_label, model)`. No serialized rollback ⇒ no collision; mixing in the same DB run is the trap.

Fix: drop `serialized_rollback = True` from all 3 classes & inline-reseed via `get_or_create` per the canonical pattern already used by [test_admin_tarot.py](src/functional_tests/test_admin_tarot.py) & [room_page.py](src/functional_tests/room_page.py) (see [[feedback_transactiontestcase_flush]]) — new `_seed_earthman_sig_pile()` module-level helper in each file restores `DeckVariant(slug='earthman')` + the 16 MIDDLE court cards (Maid/Jack/Queen/King × BRANDS/CROWNS/BLADES/GRAILS) that `personal_sig_cards(user)` returns.

Adjacent bug uncovered once the snapshot reload was gone: [test_game_my_sea.py](src/functional_tests/test_game_my_sea.py) `_seed_gameboard_applets` had been seeding `my-palette` w. `context='gameboard'` — but `my-palette` is not a gameboard applet (it's a dashboard slug; the real palette applet is `palette` per migration 0003). The applets template iterates every applet & includes `apps/<context>/_partials/_applet-<slug>.html`, so seeding the bogus row made /gameboard/ try to load a partial that doesn't exist → TemplateDoesNotExist 500 → `#id_applet_my_sea` never rendered. `serialized_rollback = True` had been masking this because the snapshot restored the migration-correct applet rows (which never had my-palette as a gameboard entry to begin with). Swapped `my-palette` for the real `new-game` gameboard applet & corrected grid-cols/rows on `game-kit` to match the 0003 seed (4×3, not 4×6).

Tests: full sweep across all three classes runs green locally (17/17 in 142s).

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 11:05:39 -04:00
Disco DeDisco
76e1bfc9ad My Sea applet: split sign-gate vs empty-state ITs after Sprint 4b layered the gate ahead of the empty placeholder
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed
- test_my_sea_applet_renders_empty_state_for_new_user was written before
  Sprint 4b (cd0add1) added the applet-side sign-gate; a fresh user now
  hits the gate branch in _applet-my-sea.html, never reaching .my-sea-empty.
- Renamed to test_my_sea_applet_renders_sign_gate_for_user_without_sig
  + added test_my_sea_applet_renders_empty_state_for_user_with_sig_no_draws
  which sets user.significator (via personal_sig_cards) before re-fetching
  /gameboard/. Both branches now pinned.

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 01:57:41 -04:00
Disco DeDisco
cd0add1e3c My Sea sign-gate — Sprint 4b of My Sea roadmap — TDD
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed
/gameboard/my-sea/ standalone page + /gameboard/ My Sea applet gated behind User.significator. When no sig is saved, render a Look!-formatted Brief-style line — "Look!—pick your sign before drawing the Sea." — w. BACK (.btn-cancel → /gameboard/) + FYI (.btn-info → /billboard/my-sign/) action buttons in `--terUser` ink ; gate is inline content (not portaled like .note-banner) — it IS the page content until a sig is picked, not a transient nudge ; applet partial mirrors the gate via `{% if not request.user.significator_id %}` w. a `.my-sea-sign-gate--applet` denser variant (just FYI, no BACK — user's already on the gameboard); .my-sea-sign-gate__line shrinks from 1.1rem → 0.85rem + padding from 1.5rem → 0.5rem ; my_sea view passes `user_has_sig = request.user.significator_id is not None` so the standalone template branches at server side (avoids a request.user template-context-processor dependency in the standalone page) ; .woodpecker/main.yaml routes test_game_my_sea.py to the test-FTs-non-room stage by default (FT doesn't yet touch the table hex — Sprint 5+ will bring the hex into my_sea via the same DRY .room-shell stack as my_sign, at which point the file gets moved to test-FTs-room) ; TDD trail — 6 FTs in test_game_my_sea.py covering standalone gate copy + FYI/BACK href targets (T1-T3), with-sig skips gate + renders draw shell (T4), applet mirrors gate w. FYI (T5), applet w. sig falls back to .my-sea-empty (T6); all written red against the un-implemented gate before view/template/SCSS landed ; KNOWN: visual verification on existing admin user (@disco) blocked by lack of a clear-sign affordance (he has a sig saved from Sprint 4a testing); adjacent feature spec'd in [[sprint_my_sea_sign_gate_may19]] memory — likely lands as a CLEAR btn in the picker's saved-sig stage state, deferred to next session

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 01:38:55 -04:00
Disco DeDisco
5b06d902a8 My Sign picker iteration 3 — SCAN SIGN landing hex + OK/NVM thumbnail two-step + duoUser picker bg — Sprint 4a-cont — TDD
Two-phase picker. Landing phase renders the DRY 1-chair table hex w. a central SCAN SIGN .btn-primary; clicking it swaps the page to picker phase (hex hides, sig-card grid + always-present stage frame + SAVE SIGN visible). Stage frame previews the saved sig on landing if User.significator is set ; sig-card selection lifts the room's two-step OK/NVM-on-thumbnail pattern via `.sig-card-actions` w. `.sig-ok-btn`/`.sig-nvm-btn`: click thumb → `.sig-focused` (CSS reveals OK badge, stage previews card, no lock); click OK → `.sig-reserved--own` (CSS swaps OK→NVM badge, `.sig-stage--frozen` reveals stat block + FLIP, SAVE SIGN enables); click NVM → unlock + clear focus + disable SAVE SIGN ; SAVE SIGN form pinned `position:absolute; bottom:0.75rem; right:1rem` to .my-sign-stage so it stops shifting across the stage row when the stat block reveals on lock (was getting shoved left as a flex item alongside the stat-block reveal) ; .my-sign-page mirrors .room-page's `flex:1; min-height:0; display:flex; flex-direction:column` so the DRY hex container chain propagates real height down into #id_game_table for room.js's scaleTable() to compute against (was reading 0 + leaving the hex unscaled at 200×231 in a 360×320 scene) ; stage min-height gated to picker phase (`.my-sign-page[data-phase="picker"] .my-sign-stage`) — landing-phase stage is natural-sized so the hex centers in the bigger available area instead of being bottom-anchored by a 376px stage reservation ; picker-phase bg uses `rgba(var(--duoUser), 1)` so the transition from "hex face" → "card pile on felt" reads as a continuous surface rather than a context swap ; room sig-select media queries re-scoped to `.sig-overlay .sig-deck-grid` so they don't bleed into my-sign — my-sign gets its own breakpoint cascade: 6×3rem (portrait) → 9×3rem (≥900px landscape) → 18×3rem (≥1600px) → 18×5rem (≥2200px); thresholds bumped from sig-select's 1400/1800px so 18×col + sidebar/footer margins clear the viewport at fluid-rem ceiling (rem=22 → 18×3rem=1188px + 220px margins=1408, safe with 1600px floor) ; default `repeat(6, 1fr)` collapsed to 0-width when paired w. `align-self:center` (no parent width for `fr` to resolve against, hence the dotted-line miniscule cards in portrait); fixed `repeat(6, 3rem)` at portrait default fixes it ; SCAN SIGN font-size 0.75rem (vs .btn-primary's default 0.875rem) so the 2-line "SCAN/SIGN" label fits inside the 4rem circle without crowding the border — treated as a smaller variant via `#id_scan_sign_btn` rule scoped under .my-sign-landing ; room.js's scaleTable() runs on DOMContentLoaded before flex layout flushes (#id_game_table.clientWidth/Height read 0 at that moment) — added `requestAnimationFrame → dispatchEvent('resize')` tick at the end of the inline IIFE so scaleTable re-fires once layout settles ; tests — 6 FTs in test_bill_my_sign.py rewritten for the new flow: test_landing_renders_dry_hex_with_scan_sign_button pins the 1-chair hex + central SCAN SIGN + hidden picker grid; test_scan_sign_click_transitions_to_picker_phase pins the phase swap (hex hides, grid shows); test_click_thumbnail_shows_OK_btn_without_locking pins step 1 (focus + OK appears, no lock yet); test_OK_click_locks_thumbnail_and_enables_save_sign pins step 2 (lock + NVM appears + SAVE SIGN enables + persists to /billboard/ applet); test_NVM_click_deselects_and_disables_save_sign pins NVM unlock cycle; test_landing_previews_saved_sig_on_stage pins the on-load saved-sig preview behavior — all green visually verified across portrait + landscape

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 01:11:41 -04:00
Disco DeDisco
ab5b4c95dd My Sign picker: hover-preview, click-lock, NVM-unlock + polarity SCSS port + bigger stage — Sprint 4a-cont iteration 2
User-driven polish on iteration 1: separate hover-preview from click-lock semantics (room sig-select pattern), add NVM to unlock, port the room's `.sig-overlay[data-polarity]` polarity-themed CSS to also target `.my-sign-page[data-polarity]`, bump the stage card width so it occupies a bigger slice of the viewport ; **state machine** (inline JS in my_sign.html): three discrete states — (a) idle: stage frame visible but empty (stage card hidden via display:none, stat block hidden via `.sig-stage--frozen` absence, FLIP btn hidden via CSS); (b) hover: hovered .sig-card populates the stage card (preview); mouseleave clears it; mouseover-mouseout sequence guards against transient gaps when moving between adjacent thumbnails (relatedTarget closest('.sig-card') check); (c) locked: click on any grid card freezes the stage — populates content, adds `.sig-stage--frozen` to .sig-stage (which surfaces .sig-stat-block + .my-sign-flip-btn via CSS), enables SAVE SIGN, reveals NVM. Subsequent hovers ignored while locked. NVM click reverts to idle (clears content, hides stat-block + FLIP, disables SAVE, hides NVM) ; **new template** elements: NVM `<button id="id_nvm_sign_btn" class="btn btn-cancel">` next to SAVE SIGN in the form, hidden by default (style="display:none") + revealed on lock. Stage card re-acquires `style="display:none"` (hidden on load, JS-shown on hover/lock). `sig-stage--frozen` class no longer initial — JS-added on click ; **polarity SCSS port** (_card-deck.scss L820-905): extended `.sig-overlay[data-polarity="levity"]` + `[data-polarity="gravity"]` selector lists to include `.my-sign-page[data-polarity="levity"]` + `[gravity"]`. Rules inside (e.g. `.sig-card { background: rgba(--secUser) }`, `.sig-stage-card .fan-card-name { color: --quiUser }`, stat-face-label colour flips, text-shadow polarity variants) automatically apply on the my-sign page since `data-polarity` lives on the page wrapper (descendants .sig-card + .sig-stage-card both inherit). Moved `data-polarity` from `.my-sign-stage` to `.my-sign-page` in the template + JS so descendant scoping works (was a stage-scoped attr in iteration 1, which couldn't reach the sibling .sig-deck-grid) ; **bigger stage** (_card-deck.scss): `.my-sign-page { --sig-card-w: clamp(140px, 36vw, 220px); }` — scales w. viewport, 140px floor for portrait, 220px ceiling for landscape, ~36vw in between. Stage card + stat block both width-driven by this var so they scale together. The clamp() ceiling matches the room sig-select's typical sized card on a mid-laptop ; **FLIP btn visibility** (_card-deck.scss): `.my-sign-flip-btn { display: none }` at rest; `.my-sign-stage.sig-stage--frozen .my-sign-flip-btn { display: inline-flex }` on lock. The btn's position (absolute, bottom-left of card) was already added in iteration 1 ; **on-load lock-restore**: if `User.significator` is set, the picker auto-locks that card via `_lock(savedCardEl)` so the user sees their persisted choice in the locked-state UI (stat block + FLIP visible) instead of an idle empty frame. Polarity initial value (data-polarity on .my-sign-page) reflects `current_significator_reversed` — False=gravity (default), True=levity ; **regression**: 7 FTs in test_bill_my_sign green in 57s. Visual verify deferred to user — picker should now show: idle empty stage + grid below; hover thumbnail → stage card preview; click → preview persists + stat block + FLIP appear; NVM → back to idle; FLIP click → horizontal-perspective Y-axis rotation w. polarity content swap mid-animation. Polarity-themed colour styles (levity inverted palette / gravity stark contrast / per-polarity text-shadows) now apply on my-sign matching the room sig-select look

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 00:15:13 -04:00
Disco DeDisco
559bdc2de7 My Sign picker: stage visible on load, FYI panel, SPIN/FLIP split w. perspective-flip animation — Sprint 4a-cont
User-driven polish on the Sprint 4a picker so it's usable parity-w-room-sig-select (per the Schizo-screenshot reference). My initial pass collapsed SPIN + FLIP into one button — user clarified the correct architecture: **SPIN** stays in the `.sig-stat-block` (room pattern, btn-reverse, toggles orientation 180° + reveals reversal_qualifier), while **FLIP** lives at the bottom-left of the stage card as a `.btn-reveal` (game-kit fan carousel pattern, toggles polarity gravity↔levity w. a horizontal-perspective Y-axis rotation animation). Gravity is the default upright polarity per user — significator_reversed=False → gravity, True → levity ; **template changes** (my_sign.html): (a) `.sig-stage-card` no longer carries inline `display:none` — stage frame visible on page load, before any card click; (b) `.sig-stage` carries `.sig-stage--frozen` modifier from the start so the stat-block shows alongside the stage card (room CSS gates `.sig-stat-block { display: block }` behind this class); (c) stat-block btn relabeled "FLIP" → "SPIN" + restored to btn-reverse / orientation-toggle semantics; (d) new `<button class="btn btn-reveal my-sign-flip-btn">FLIP</button>` outside the stat-block at .sig-stage scope, positioned absolute via new SCSS (bottom-left of stage card, mirroring game_kit.html's #id_fan_flip placement); (e) FYI btn + `_sig_fyi_panel.html` partial included alongside SPIN in stat-block — pinned w. id_my_sign_fyi_panel; (f) all 18 card data-* attrs filled (data-levity-qualifier / data-gravity-qualifier / data-levity-emanation / data-gravity-emanation / data-levity-reversal / data-gravity-reversal / data-energies / data-operations / data-italic-word / data-correspondence) so StageCard.populateCard has everything it needs to render qualifiers + reversal-face text per polarity; (g) data-polarity on .my-sign-stage drives populator polarity arg + (future) polarity-themed styling, initialised from `current_significator_reversed` (False=gravity, True=levity) ; **JS changes** (inline script in my_sign.html, includes apps/epic/stage-card.js): (a) on card click → StageCard.fromDataset → populateCard(stageCard, card, _polarity()) + populateKeywords on stat-block + buildInfoData/renderFyi on FYI panel + sig-focused class on grid cell; (b) FYI btn click toggles `.fyi-open` on stat-block (room pattern — CSS reveals the .sig-info panel + PRV/NXT); (c) PRV/NXT cycle thru _fyiData; (d) SPIN click toggles `.stage-card--reversed` + `.is-reversed` on stat-block (orientation, preview-only — not persisted); (e) FLIP click runs `_flipPolarityAnimated()` — 500ms Y-axis rotateY(90deg) midpoint animation lifted from game-kit.js's `_flipActive`, swaps polarity at offset 0.5 so the new face shows through the 2nd half-rotation, preserves SPIN orientation by including ' rotate(180deg)' in both keyframes when stage-card--reversed is on, in-flight `dataset.flipping` flag prevents re-triggering mid-animation; (f) on-load: if user has a saved sig (`.my-sign-page[data-current-card-id]`), find that grid card + auto-select it so stage shows the persisted choice ; **SCSS** (_card-deck.scss): new `.my-sign-flip-btn` rule positioning the btn absolute z-index:25 bottom:0.4rem left:calc(1.5rem + 0.4rem) — accounts for .sig-stage's padding-left:1.5rem so the btn lands at the visual bottom-left of the stage card; .btn-reveal styling (magenta/cyan) inherited from existing _button-pad.scss; no animation SCSS (the 500ms rotateY is in JS via element.animate()) ; **deferred**: `.sig-overlay[data-polarity="levity"]` / `[data-polarity="gravity"]` themed color overrides at _card-deck.scss:805-885 are scoped to `.sig-overlay` and won't apply to `.my-sign-stage[data-polarity]` until those selectors are extended (or duplicated under a .my-sign-stage sibling). User flagged the visual delta but the picker is functionally complete w.o the polarity-themed colors — followup sub-sprint ; **regression**: 7 FTs in test_bill_my_sign green in 57s; no IT/UT changes needed (only template + SCSS). User-pre-staged rootvars.scss tweak picked up

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 23:58:39 -04:00
Disco DeDisco
39767c72c2 fix CARTE multi-seat Role-Select bug on navigate-away + back; My Sign applet rename
**CARTE bug** (user-reported on iPhone): a CARTE gamer who contributed their deck to multiple gate slots could fill ≥1 role for ≥1 seat, navigate away (BYE → dashboard, CONT GAME → return, etc.), come back to the room — and the JS guard on .card-stack would wrongly fire "Equip card deck before Role select" + block further role picks, even though the deck was demonstrably in play on existing seats. Symmetric for the "stay in room during Role Select" variant the user thought we'd squashed before (the prior fix was 759ce8d for the multi-slot SELECT path, but the room VIEW context never got the same treatment) ; **root cause**: `select_role()` at epic/views.py:619-621 clears `user.equipped_deck` after the first role pick ("deck committed to room"). The room view's role-select context at epic/views.py:286 then passes `equipped_deck_id = user.equipped_deck_id` to the template — which is now None — and the template renders `data-equipped-deck=""` → JS guard at role-select.js:165 sees the empty string and fires the "no deck" warning. The deck IS in play; the context just isn't recognizing seat-level deck assignment as a deck source ; **fix** (epic/views.py:286ish): when `user.equipped_deck_id` is None, fall back to the deck_variant of any of the user's seats in this room (order_by slot_number for determinism). The guard now sees a non-empty id and the fan opens. Storage-side unchanged — seat.deck_variant remains the canonical "this deck is in play on this seat" signal, and the user's deck-third contribution per role (PC=levity brands+crowns / NC=levity trumps / SC=levity grails+blades / AC=gravity grails+blades / EC=gravity trumps / BC=gravity brands+crowns) flows from existing `select_role` logic that inherits deck_variant from the first seat ; **TDD trail** — 2 new ITs in `SelectRoleMultiSeatTest` (apps.epic.tests.integrated.test_views): T1 pins the context (`response.context["equipped_deck_id"]` equals the existing seat's deck_variant_id after `user.equipped_deck` clears); T2 pins the template (rendered `data-equipped-deck="<id>"` not `""`). Initial reds — `None != 2` + `data-equipped-deck=""` substring assertion. Fix lands both green ; **bundled: My Sign applet rename** — user clarified naming convention 2026-05-18: **applets** use the "My X" prefix (My Sign, My Sea, My Posts), **standalone pages** use the "Game/Dash/Bill X" prefix (Game Sign page, Game Sea page, Game Kit page). Sprint 4a's initial migration set the applet name to "Game Sign" — corrected after the user saw the gear-menu toggle list reading the wrong word. Applet template header link "Game Sign" → "My Sign" (user-edited); migration 0010 added to update the Applet row's `name` in already-migrated DBs (dev + staging); applets/0009 frontmatter + defaults updated to "My Sign" in case of a fresh migrate-from-zero; test seed helpers in billboard test_views.py + functional_tests/test_bill_my_sign.py updated to "My Sign". Slug stays `my-sign` (URL + selectors stable) ; **bundled: rootvars.scss** — user-modified mid-session (pre-staged) ; 1022 IT/UT green in 46s — no regressions; 4 ITs in SelectRoleMultiSeatTest green (2 pre-existing CARTE multi-seat ITs + 2 new return-trip context ITs)

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 23:18:32 -04:00
Disco DeDisco
66b2947e8c My Sign: Brief banner + Earthman [Shabby Cardstock] backup deck when no equipped — TDD
User-reported gap on /billboard/my-sign/ — admin user's only deck was in-use as `TableSeat.deck_variant` in another room (Wonderbeard) → `equipped_deck` cleared → previous my-sign template showed "Equip a card deck first…" w. no actionable next step. User scoped fix: don't force equip, just nudge via a Brief banner "Look!—no deck is equipped. Navigate to the Game Kit to equip one (FYI) or (NVM) proceed with the Earthman [Shabby Cardstock] deck.", title "Default deck warning". NVM dismisses + picker proceeds against an Earthman card pile labeled in-copy as the temporary backup; FYI links to /gameboard/ (Game Kit equip). User-modified line text in template: "Paperboard" → "Cardstock" mid-session ; **helper fallback** (epic/models.py): `personal_sig_cards(user)` now falls back to `DeckVariant.objects.filter(slug='earthman').first()` when `user.equipped_deck` is None — same 16-or-18 card pile, just sourced from the canonical Earthman deck rather than the empty FK. No new DeckVariant row needed; "Shabby Cardstock" is purely UX framing (cards are the same TarotCard records the room sig-select uses). Preserves the existing helper signature so no callers had to change ; **view + template** (billboard/views.py + my_sign.html): view passes `no_equipped_deck` + `show_backup_intro_banner` flags. Template removes the old `{% if not equipped_deck %}` forced-equip branch — picker now renders unconditionally w. cards from the backup helper when no deck is equipped. Brief banner fires via `Brief.showBanner({...})` on DOMContentLoaded when `show_backup_intro_banner` is true — gets h2-overlay positioning + NVM behavior + portal styling for free (per [[sprint-baltimorean-note-unlock-may18]] portrait h2 measurement in note.js's `_alignToH2`). Added `<script src="note.js">` to my_sign.html since the page didn't load it before. Post-render JS tags the Brief w. a `.my-sign-intro-banner` class so FTs (and any future my-sign-specific styling) can distinguish this nudge from other Briefs on the page ; **TDD trail** — 4 new FTs in `MySignBackupDeckTest` (test_bill_my_sign.py): T1 banner renders w. "Default deck warning" title + "no deck is equipped" + "Shabby Cardstock" copy + both action btns visible; T2 picker still populates 16 cards from backup; T3 NVM click removes the banner from the DOM; T4 FYI href ends w. /gameboard/. Initial reds (`NoSuchElementException` on all 4) confirmed before implementation. Plus 1 new IT in `PersonalSigCardsTest` pinning the helper fallback (16 cards w. all `c.deck_variant.slug == "earthman"`) ; pre-existing change picked up: `static_src/scss/rootvars.scss` (user-modified mid-session) ; 1020 IT/UT green; 7 FTs green (3 picker happy-path + 4 backup deck) in 56s. Sprint 4a-follow complete — primary deferral from Sprint 4a (deck-source fallback UX) now landed. Unblocks Sprint 4b (My Sea gating w. --terUser link to /billboard/my-sign/ when no sig set)

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 22:49:49 -04:00
Disco DeDisco
400762c0e5 Game Sign picker @ /billboard/my-sign/ + billboard applet — Sprint 4a of My Sea roadmap — TDD
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>
2026-05-18 22:23:24 -04:00
Disco DeDisco
5e71b1d5da Baltimorean: post-attribution titles now read "Baltimorean" not "Ard!" — TDD
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>
2026-05-18 21:42:58 -04:00
Disco DeDisco
bf44628536 My Sea applet shell — Sprint 3 of the My Sea roadmap
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>
2026-05-18 19:45:57 -04:00
Disco DeDisco
df9cf1eee8 CI: route trinket FTs to test-FTs-room stage alongside game_room_*
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
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>
2026-05-18 19:41:37 -04:00
Disco DeDisco
5e5bc5a6af COIN: unequip on deposit (parity w. CARTE) ; fix FT false-positive masking the bug
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>
2026-05-18 19:35:08 -04:00
Disco DeDisco
d2491c5e1b COIN: Carte treatment in Game Kit applet — data-current-room-name + deposited-state btn-disabled — TDD ; PASS skeleton FT
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>
2026-05-18 19:25:46 -04:00
Disco DeDisco
79706e817a iOS focus-zoom prevention — input font-size floor @ 16px ; JS fallback strengthened
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
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>
2026-05-18 18:32:45 -04:00
Disco DeDisco
8066ac289f iOS viewport zoom-reset on form-field exit — global IIFE in base.html
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>
2026-05-18 18:22:08 -04:00
Disco DeDisco
7165974905 table hex layout: fill aperture + enlarge hex from 160×185 → 200×231 ; chair clearance preserved
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>
2026-05-18 18:19:11 -04:00
Disco DeDisco
fbe6c12ded fix CAST SKY click opening tray instead of Sky Select — TDD
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>
2026-05-18 17:21:32 -04:00
Disco DeDisco
1ccb045889 Baltimorean "Ard!" → "Baltimorean" rename across inline surfaces (banner / scroll line / my-notes Title row); navbar DON greeting keeps "Ayo, Ard!" as the sole Ard! flair — and a companion PronounsAppletFlowTest sync-point fix for the 200/Brief response path introduced by the Baltimorean unlock loop in 435a192 ; FT fix (functional_tests/test_game_kit.py) — PronounsAppletFlowTest.test_pronoun_flip_propagates_to_billscroll_and_most_recent was written pre-Baltimorean when set_pronouns always returned 204 → reload; the Baltimorean loop split the response into 200-w-brief (first-bawlmorese, no reload — Brief banner would be lost) vs 204-reload (other pronouns), so the test's step-4 wait_for(.gk-pronoun-card.active[data-pronoun='bawlmorese']) hung indefinitely on the Brief banner because the active class only updates after the reload that no longer happens; sync point swapped to wait_for(.note-banner) — the Brief banner's appearance is the natural post-commit signal that the server saved the pronoun (after which step-5/6 navigate to billboard + scroll to verify "yos" prose) ; rename core (drama/models.py) — Note.grant_if_new else branch's attr_combo inline attribution now uses note.display_name instead of note.display_title, so the scroll line reads "Look!—new Note unlocked. Baltimorean recognizes @disco the Baltimorean." instead of "…the Ard!." (for stargazer/schizo/nomad display_name == display_title so the line is unchanged; only baltimorean's two values diverge — display_title="Ard!" for navbar flair, display_name="Baltimorean" for everywhere else); Brief.title field also swapped from display_title to display_name so the banner title slot reads "Baltimorean" not "Ard!" — admin-grant branch (_ADMIN_NOTE_SLUGS: super-schizo, super-nomad) still uses display_title for the "honorary title of {X}" phrase because that branch DOES want the don-able title there ("Schizoid Man" / "Stranger") ; my-notes card "Title:" row rename — added card_title key to _NOTE_DISPLAY["baltimorean"] ({"greeting": "Ayo,", "title": "Ard!", "card_title": "Baltimorean"}) + new Note.card_title property that falls through _NOTE_DISPLAY[slug]["card_title"] then defaults to display_title; billboard/views.py _my_notes_context swaps "recognition_title": n.display_titlen.card_title so the my-notes Baltimorean card shows "Title: Baltimorean" instead of "Title: Ard!" — super-schizo's card still reads "Title: Schizoid Man" because card_title falls back to display_title for slugs w.o an override ; ONLY Ard! surface remaining: navbar DON greeting via User.active_title.display_title (lyric/models.py:158) — display_title itself was deliberately untouched so the navbar still flips Welcome, EarthmanAyo, Ard! after DON, which is the entire point of the Baltimorean flair ; existing DB rows w. stored "the Ard!" Line.text + Brief.title="Ard!" from prior dev-DB grants will NOT update — those columns are persisted at grant_if_new time, not computed live; user has accepted dev-DB wipe-and-re-acquire as the path forward, so no data migration ; tests — drama/tests/unit/test_models.py +2 UTs (test_baltimorean_card_title_is_baltimorean pins the override, test_stargazer_card_title_falls_back_to_display_title pins the fallback for non-overridden slugs); drama/tests/integrated/test_note_brief.py test_first_grant_creates_post_line_and_brief updated assertion brief.title == note.display_name (was display_title) to reflect the new contract; dashboard/tests/integrated/test_views.py test_brief_payload_carries_baltimorean_title expects "Baltimorean" not "Ard!"; functional_tests/test_bill_baltimorean.py T1 banner title check swapped "Ard!" → "Baltimorean" + module docstring line 10 updated — T4 (navbar DON greeting flip to "Ayo, Ard!") preserved verbatim, the one surface where Ard! still lives ; full FT suite for baltimorean 6/6 green in 53s; drama + dashboard + billboard ITs/UTs 290 green in 16.5s — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
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-18 10:54:30 -04:00
Disco DeDisco
435a192349 Baltimorean Note unlock loop — full UX from bawlmorese pronoun pick → Brief banner → DON → palette modal → dashboard swatch ; rootvars.scss adds the Baltimorean (Blt) hue family (red 200,16,46 / yellow 255,212,0 / white 255,255,255 / black 0,0,0 / purple 26,25,95 / orange 221,73,38 — Maryland-flag-derived plus a --sixBlt: 162,170,173 neutral) + two .palette-baltimore / .palette-maryland palette classes wiring those hues into the standard --priUser--decUser slots; companion section-header rename "/* X Palette */" → "/* X Hues */" across rootvars to disambiguate raw hue families (Precious Metal / Cosmic Metal / Chroma / Earthman / Technoman / Inferno) from actual palette classes — section-comment-only, no rule-level change ; baltimorean entry added in 3 registries that drive the loop: _NOTE_DISPLAY (drama/models.py) — {"greeting": "Ayo,", "title": "Ard!"} so DON flips navbar Welcome, EarthmanAyo, Ard!; _NOTE_TITLES (dashboard/views.py, user-pre-staged) — drives the "recognized via Baltimorean" copy on dashboard palette swatches; _NOTE_META (billboard/views.py) — Baltimorean title + the literal description "Aaron earned an iron urn." + palette_options [palette-baltimore, palette-maryland] feeding the my-notes swatch modal ; set_pronouns view rewired (dashboard/views.py) — first-time pronouns = bawlmorese selection calls Note.grant_if_new(user, "baltimorean") + returns {"brief": brief.to_banner_dict()} JSON @ 200; idempotent on repeat (the grant_if_new returns brief=None on second call so the 204 path resumes naturally); non-bawlmorese choices stay on the original 204 contract ; client wiring: game-kit.js pronouns commit() handles the 200 JSON path — resp.json().then(data => Brief.showBanner(data.brief)) instead of reload (reload would lose the just-fired banner); 204 still reloads to update active pronoun card; game_kit.html pulls in apps/dashboard/note.js so Brief is in scope on the Game Kit page (it wasn't before) ; Brief banner placement fix — note.js showBanner() now measures the .row .col-lg-6 h2 at render-time + sets inline top so the banner portals SQUARELY OVER the page h2 letter-spread wordmark instead of parking at the SCSS-default top: 0.5rem (which had it lurking above the wordmark area on every page); portrait-only (gated if window.innerWidth > window.innerHeight return) — landscape h2 lives in a writing-mode: vertical-rl fixed sidebar column + would need a full banner reorientation (writing-mode + flex-direction restyle of banner contents) to "overlay" sensibly, deferred to a follow-up sprint ; tests: drama/tests/unit/test_models.py (new file) — 5 UTs for _NOTE_DISPLAY[baltimorean] greeting/title/name + stargazer smoke tests; dashboard/tests/integrated/test_views.py — SetPronounsBawlmoreseUnlockTest (9 ITs covering first-bawlmorese-returns-200-w-brief / Note granted / title Ard! / square_url to /billboard/my-notes/ / idempotent on repeat / non-bawlmorese unaffected / bawlmorese-after-other still grants); existing SetPronounsViewTest.test_post_each_valid_choice docstring updated to flag the bawlmorese 200 branch ; functional_tests/test_bill_baltimorean.py (new file) — 6 FTs walking the full UX: T1 Game-Kit pronouns click → Brief banner w. Ard! title + Look! prose + ?-square + FYI nav; T2 idempotent repeat-click (no re-fire); T3 my-notes Baltimorean item carries the Aaron quote verbatim; T4 DON flips navbar greeting Welcome, EarthmanAyo, Ard!; T5 palette modal offers Baltimore + Maryland swatches (and not Bardo/Sheol); T6 Baltimore swatch click previews → OK commits → dashboard Palette applet shows the swatch unlocked w. data-description carrying Baltimorean + non-empty data-unlocked-date + Note.palette = palette-baltimore in DB — all 6 green in 51s; full IT/UT sweep 997 → green in 45s — TDD
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 02:17:07 -04:00
Disco DeDisco
bc77296dd4 +52 IT/UT to close IT/UT-only coverage gaps (93% → 96%) — full suite 983 tests in 47s ; UTs in epic/tests/unit/test_models.py — TarotCardEmanationForTest (4) covers emanation_for(polarity) w. levity/gravity overrides + fallback to name_title for cards w.o a polarity split (cards 48-49 are the only polarity-split cards in the deck so this method is sparsely exercised by ITs); TarotCardReversalForTest (4) covers reversal_for(polarity) w. polarity-split + reversal_qualifier fallback + further fallthrough to emanation_for; TarotCardNameSplitTest (4) covers name_group/name_title colon-split parsing (prefix-w-colon / suffix / no-colon edge); TarotCardCautionsJsonTest (2) covers the cautions_json JSON serialiser ; UTs in epic/tests/unit/test_utils.py — PlanetHouseFallbackTest +1 happy-path test (degree=15 lands in house 1 w. sequential cusps) for the normal cusp-match branch alongside the existing pathological fallback test; TopCapacitorsTest (6) covers all top_capacitors() branches — empty dict / None / all-zero counts (the L56 max(counts.values()) <= 0 fallback that was uncovered) / single-winner / tie-clockwise-order / enriched dict {"count":N} input shape ; ITs in epic/tests/integrated/test_models.py — TarotDeckDrawTest extended w. 5 tests for remaining_count (happy + no-deck-variant fallback to 0) + draw() happy-path (returns n tuples of (TarotCard, bool) / appends to drawn_card_ids / never repeats cards across consecutive draws); existing ValueError + shuffle tests preserved ; ITs in epic/tests/integrated/test_views.py — SigEventRetractionTest (4 tests) covers the three data["retracted"] = True paths that the FT test_game_room_select_sig.py walks transitively but no IT pins directly: sig_unready retracts prior SIG_READY (L937), sig_ready retracts prior SIG_UNREADY (L907), sig_reserve action=release while ready retracts prior SIG_READY + records fresh SIG_UNREADY (L823); SigReserveInvalidCardIdTest (1) covers TarotCard.DoesNotExist → 400 (L840-841) ; SigSelectGravityContextTest (3) covers the user_polarity = 'gravity' branch (L322) + the gravity_sig_cards lookup (L357) — all existing SIG_SELECT context tests use the founder-as-PC-levity setup so these branches sat uncovered; logs in as gamers[5] (BC role) + asserts user_polarity + sig_cards match gravity_sig_cards() output ; SeaDeckViewTest (7) mirrors the test_game_room_select_sea.py FT but isolates the JSON contract — covers 403 when unseated, empty halves when seat has no deck_variant (L1255-1256 early-out), two-halves shape, ~even split, card_dict keys (id/name/arcana/corner_rank/suit_icon/name_group/name_title/reversed/qualifiers), reversed field is bool, claimed-significator exclusion via room.table_seats.exclude(significator__isnull=True) ; ITs in dashboard/tests/integrated/test_views.py — ProfileViewTest +2 (reserved-handle "adman" rejection — L116-117: username stays unchanged + redirect to /); KitBagViewTest (3) covers the kit_bag view's panel render w. TITHE-sort branch (L169-175) + login guard ; ITs in dashboard/tests/integrated/test_sky_views.py — SkyViewTest +2 (saved birth datetime renders in user's sky_birth_tz via astimezone L300-306 — 16:00 UTC → 12:00 EDT; invalid-tz string triggers ZoneInfoNotFoundError → swallowed pass → UTC fallback at 16:00) ; ITs in gameboard/tests/integrated/test_views.py — EquipTrinketViewTest +2 (POST equips trinket + returns 204 — L83-85; non-owner POST returns 404 via get_object_or_404); UnequipTrinketViewTest +2 (POST clears matching equipped_trinket — L107-110; POST of non-matching token is a 204 no-op, the implicit else branch) ; .coveragerc omit gains */reset_staging_db.py per user — mgmt cmd was the only 0%-stmt module that wasn't exercised by tests at all + we agreed it's deliberately untested staging-side code ; palette-monochrome-dark rebalance in rootvars.scss — --quiUser/--sixUser/--sepUser remapped to (secAg / quaAg / priPt) instead of (quaAg / terAg / secAg), shifting the secondary/subtle/deep-subtle anchors up the silver gradient so the palette reads more cleanly under the new sig-stage card colours from 3242873 ; uncovered remnants from earlier analysis intentionally left in place — consumers.py at 68% (channels-tag tests excluded; would need --tag=channels run), Carte Blanche slot navigation + sky_dice + tarot_deck preview view paths (the "bigger investments" tier from session triage; FT-covered + the IT setup is heavier than the immediate value), defensive except fallbacks that need contrived inputs to fire, and a handful of __str__s/pass branches not worth a test apiece — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 01:07:13 -04:00
Disco DeDisco
3242873625 btn-primary label renames + stage-card polarity color refinements — two interleaved threads from one session, committing together since both touch sig + sea stage cards ; LABEL RENAMES: PICK SIGS → SCAN SIGS (room.html #id_pick_sigs_btn), PICK SKY → CAST SKY (room.html #id_pick_sky_btn × 2), PICK SEA → DRAW SEA (room.html #id_pick_sea_btn), TAKE SIG → SAVE SIG (sig-select.js _takeSigBtn.textContent × 2 callsites + section comment) — Element IDs (id_pick_sky_btn etc.), URL names (epic:pick_sigs, epic:pick_sky), and Python state enums (TableStatus.PICK_SKY, PICK_SEA, SIG_SELECT) intentionally retained as stable identifiers; the renamed text is purely the .btn-primary user-facing label ; FT + IT mentions of the old labels swept in test_game_room_select_{sig,sky,sea,role}.py, test_billboard.py, setup_sea_session.py mgmt cmd, apps/epic/{views,utils,models,tasks,tests/integrated/test_views}.py, SigSelectSpec.js, sky_overlay/sea_overlay/dashboard/sky.html, _card-deck.scss, _sky.scss — all docstring/comment references updated for cascade-grep cleanliness ; STAGE-CARD COLOR + CLASS REFINEMENTS (earlier in session): sig-stage card text colour split per polarity — gravity gets --terUser on .fan-card-name + .fan-card-reversal-{name,qualifier} + .sig-qualifier-{above,below}, levity gets --quiUser on the same five slots; all selectors prefixed w. .sig-stage-card to match the 0,4,0 specificity of the default .sig-stage .sig-stage-card .fan-card-face .sig-qualifier-* rule (without the prefix the polarity overrides lose the cascade — .sig-qualifier-below was visibly stuck on the default --quiUser) ; .stat-face-label gets polarity-inverse colours — gravity stat-block bg is --secUser (opposite of card's --priUser) so the label takes --quiUser to stay legible; levity is the symmetric flip (label = --terUser on --priUser stat-block bg) ; levity card title/qualifier drop-shadow swapped from rgba(0,0,0,…) → rgba(255,255,255,…) — dark drop reads as harsh smudge against the inverted-frame levity --secUser bg; applied to both sig-overlay[data-polarity="levity"] stage card AND sea-stage--levity via $_sea-title-shadow-levity (former shared $_sea-title-shadow split into per-polarity {levity,gravity} variants) ; reversal-face class/content alignment so each .fan-card-reversal-* class always carries its semantic content — DOM order per arcana type controls visual layout after the 180° SPIN (DOM-second appears visually on top): Major → title in .fan-card-reversal-name @ DOM-second (visually top after spin), qualifier in .fan-card-reversal-qualifier @ DOM-first; Non-major → title in .fan-card-reversal-name @ DOM-first (visually bottom after spin), qualifier in .fan-card-reversal-qualifier @ DOM-second (preserves the original "qualifier word reads first after spin" layout for Middle/Minor arcana — e.g. "Relieving / Eight of Crowns" not "Eight of Crowns / Relieving") ; _tarot_fan.html renders per-arcana DOM order directly (Django template branches handle both layouts); sig + sea overlays render a fixed two-<p> skeleton (one DOM order) so stage-card.js's populator dynamically rewrites the two <p>s' className per arcana — Major/override branch flips DOM-second to .fan-card-reversal-name + content, DOM-first to .fan-card-reversal-qualifier; non-major branch keeps DOM-first as .fan-card-reversal-name + title, DOM-second as .fan-card-reversal-qualifier + reversalQualifier-or-polarity-fallback ; SigSelectSpec.js + SeaDealSpec.js fixtures + Major reversed-face assertion updated for the new semantic — TDD
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 00:25:10 -04:00
Disco DeDisco
ace8612099 tray apparatus scales w. fluid rem; sig-select 9×2 middling breakpoint — $handle-exposed was 48px fixed while #id_tray_btn is 3rem, so on big-rem viewports (clamp(14px, 2.4vmin, 22px) → up to 22px on tall screens, btn=66) the btn's flex parent (#id_tray_handle) shrank the btn from 66×66 → 48×66 via default flex-shrink:1 in portrait (elongated tall ellipse), and in landscape the btn overflowed the 48px-tall handle vertically (extending 9px past viewport top in closed state); fix: $handle-exposed: 3rem matches the btn so it fills the exposed area at every rem; $handle-rect-h: 4.5rem (was 72px) gives the visible rail thickness a touch of breathing room around the btn at every scale; landscape rules in the same partial that hard-coded 48px / 72px (#id_tray_handle { height: 48px }, #id_tray_grip { bottom: calc(48px/2 - 0.125rem); width: 72px }) now reference the variables so they track in sync — tray.js _computeBounds() swapped from _btn.offsetWidth/Height_handle.offsetWidth/Height for the same reason: even with the SCSS fix, measuring the btn would re-introduce the offset when btn and handle drift (which they shouldn't now, but the handle is the layout-defining element so measure it directly); id_kit_btn added as fallback for id_gear_btn (which no longer renders on the room page) so the open-state landscape wrap height anchors to the bottom-right kit btn instead of the full viewport — id_tray_handle cached on the module via _handle ref alongside _btn and cleared in reset() ; sig-select grid jumped straight from 6 cols (narrow landscape) → 18 cols × 3rem at min-width: 900px, but 18×3rem + 7rem modal margins needs ~1376px to clear at rem=22 so the cards spilled off the sides on common 1280-wide laptops + the previous-era 9×2 middling layout had simply been dropped; new cascade in _card-deck.scss mirrors the comment's documented intent: 6 cols default landscape (row layout, stage beside grid) → 9 cols × 3rem at min-width: 900px (column layout, stage above grid) → 18 cols × 3rem at min-width: 1400px → 18 × 5rem at min-width: 1800px (unchanged) — verified in Claudezilla across iphone-14 portrait (rem=14, btn=42 square, handle right edge at viewport right), 816×826 portrait near-landscape (rem=19.6, btn=58.75 square no longer elongated), 1149×751 landscape mid (rem=18, btn=54 square at viewport top, 9×2 grid), 1789×1111 desktop XL (rem=22, btn=66 square at viewport top, 18×1 grid)
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 23:21:02 -04:00
Disco DeDisco
f9cd08a510 CI: FT stages run in parallel, --parallel dropped intra-stage — bud-btn click sites bypass scroll-into-view ; .woodpecker/main.yaml restructures the FT split — test-FTs-non-room + test-FTs-room now both depends_on: test-two-browser-FTs (instead of room serially depending on non-room) so they fan out + run concurrently, each w. its own DATABASE_URL: sqlite:////tmp/test_db_{non_room,room}.sqlite3 outside the shared workspace mount so the two stages can't see each other's SQLite file + the #296 EOFError-on-half-created-test-db blocker is gone; --parallel flag dropped from both manage.py test invocations because the empirical wall-clock from #302-304 (151 tests in ~42 min, 83 tests in ~16 min — ~16-17s/test avg either way) shows ~1-1.5x speedup at best — Firefox spawn cost (~3-5s cold-start × 38 tests/worker) + RAM pressure (4 headless Firefoxes ≈ 1.5-2GB working set on a quadcore DO droplet) + SQLite file-lock contention eat most of the gain the cores would otherwise give, while the contention amplifies every transient-DOM flake we've spent the last 2 days chasing (login-race in #300/303 → 054b0aa + ad0041d, gecko-perms in #302/303 → ad0041d, ElementNotInteractable in #304 → this commit, Jasmine-timeout in #303ad0041d); stage-level parallelism gets back the wall-clock reduction (~16min savings vs serial stages) w.o. amplifying within-stage contention — net wall clock should be ~60-65min for both FT stages running concurrently vs ~58min today, but flake exposure drops dramatically; downstream screendumps + build-and-push already list both FT steps in their depends_on so they naturally gate on the slower of the two ; test_core_bud_btn.py wraps 4 unwrapped find_element(...).click() sites in wait_for(execute_script("arguments[0].click()", btn)) — the _open_panel_and_invite helper feeding 6 GatekeeperBudBtnAsyncInviteTest tests + the standalone GatekeeperBudBtnDuplicateInviteErrorTest test_duplicate_invite_shows_error_brief_and_fyi_flashes_slot click + both .note-banner--duplicate .note-banner__fyi clicks (one on the share-flow at line 433, one on the gatekeeper-invite-flow at line 622) — pipeline #304 errored on the OK button click w. ElementNotInteractableException: Element <button id="id_bud_ok"> could not be scrolled into view because the post-send_keys autocomplete dropdown on _bud_invite_panel.html briefly overlapped the OK button under CI contention so Firefox refused the scroll-into-view; same pattern + same fix shape as confirm_guard in base.py — execute_script bypasses Selenium's scroll-into-view gate entirely + wait_for absorbs any leftover transient state via WebDriverException retry ; commit only ships the test-side fix + CI restructure; if the CI changes work as intended (green pipeline #305) the docker-rebuild option for python-tdd-ci:latest still stands as the next ~5min/pipeline win, separately filed in project_ci_remove_pip_install_deferred.md — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 14:12:38 -04:00
Disco DeDisco
ad0041db74 FT flake mitigations triggered by pipelines #302/#303 — three independent fixes consolidated in one commit; test_admin_tarot._login_to_admin now waits on "Site administration" body substring before returning, same shape as 054b0aa's test_admin.py fix — the helper used to click submit + return immediately, letting the three TarotAdminTest tests race their subsequent browser.get(/admin/epic/tarotcard/) against the in-flight POST → 302 → admin home navigation so on a slow CI runner the new GET cancelled the unfinished POST, the session cookie was never set, the browser landed back on /admin/login/?next=…, and the downstream assertion saw the login-page body ('Earthman Deck' not found in 'Django administration\nEmail:\nPassword:' in #303, plus a NoSuchElementException for "The Schiz" on test_admin_earthman_card_detail's link-text click) — wait_for w. assertIn retries til the post-login page actually renders so the rest of the helper's callers start from a settled state ; test_jasmine swaps wait_for(check_results)wait_for_slow(check_results, timeout=60) — the spec suite has grown well past the 10s MAX_WAIT the default wait_for decorator affords + under CI contention (parallel Selenium workers competing for CPU on the same droplet) Jasmine's .jasmine-overall-result was still reporting "Running..." at 10s when the assertion fired w. (no detail) failure list (#303); check_results body is unchanged — "Running..." doesn't match the 0 failures regex so it falls into the failure branch + raises AssertionError, which wait_for_slow naturally retries til the result settles or 60s elapses ; base._make_browser wraps webdriver.Firefox(options=options) in try/except WebDriverException w. one sleep+retry when "geckodriver" appears in the error message — covers the spawn race under --parallel where multiple workers hit the same binary mid-permission-set + one of them gets 'geckodriver' executable may have wrong permissions (1 test in #302 + 1 test in #303, different test classes each time, confirming infra not test-logic); narrow filter on "geckodriver" so a genuine install fault still fails fast — both attempts would surface the same error in <1s ; deferred option filed to memory (project_ci_remove_pip_install_deferred.md) — dropping the pip install -r requirements.dev.txt line from each FT step would save ~5 min/pipeline (CI image drifted from requirements.dev.txt since a21e6aa so the install actually downloads 30+ packages every step instead of the intended "already satisfied" no-op verify) but loses the dep-drift safety net; declined for now, revisit when wall-clock pain > safety value — TDD
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 12:53:49 -04:00
Disco DeDisco
db10f345e4 display_name → at_handle for every user-rendering point around the recent-activity surfaces: scroll.html actor <strong>, Most Recent Scroll applet actor <strong>, My Games row body actor prefix, My Scrolls row body actor prefix, My Buds page bud-name span, navbar identity, _bud_panel.html data-sharer-name (consumed by the dynamic post-line-author append on share success) — at_handle was always the right filter for these slots: it produces @<username> when the user has set one, falling back to the truncated email (which already carries an @) so we don't double-prefix; the _my_buds_applet_item.html row was already on at_handle from the 3-col sprint, so this commit just brings the rest of the surfaces in line; _navbar.html swap also drops the literal @ that prefixed {{ user|display_name }} — that literal predated at_handle + worked for users w. usernames (gave @disco) but produced @<email>@<domain> for users w. no username yet; navbar wait_to_be_logged_in(email) FT helper keeps working since the email still appears as a substring whether rendered as @disco@test.io (old, no username) or disco@test.io (new); _bud_add_panel.html's client-side _appendBudEntry JS gains an inline at_handle mirror — display.indexOf('@') >= 0 ? display : '@' + display — since the server's add_bud response packs username or email under the username key (semantic mismatch w. the key name but stable) so the JS has to detect the email case itself; test_bill_my_buds.py two .bud-name text assertions ("alice""@alice") updated for the new prefix; 931 ITs + targeted FT regression on test_bill_my_buds + test_core_navbar + test_core_login green
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed
2026-05-13 01:09:43 -04:00
Disco DeDisco
f7fa250804 .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
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 00:43:03 -04:00
Disco DeDisco
e2040fda8f 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
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 00:27:39 -04:00