Commit Graph

853 Commits

Author SHA1 Message Date
Disco DeDisco
20ae79e5bd migration collapse: 72 → 32 — fresh initials per app + the seed/data chain preserved verbatim
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
- schema history flattened into regenerated 0001/0002_initial per app
  (dashboard: model-less since the billboard move → no migrations at
  all; tooltips untouched); 26 data migrations survive w. their
  ORIGINAL content, only dependencies rewired to the collapsed chain
- mixers split: epic/0007_finalize_earthman_deck + lyric/0014 keep only
  their RunPython halves (renames/AddFields live in the initials now;
  lyric file renamed 0014_seed_taxman); epic/0004_seed_earthman_deck
  create() kwargs mapped to the post-0007 field names (reversal→
  reversal_qualifier, mechanisms→energies, articulations→operations) —
  the fresh initial starts at the FINAL schema, seed DATA unchanged
- dropped pure backfills (no-ops on a fresh DB): billboard title/author
  + admin_solicited, lyric token in_use_since, drama
  grant_super_notes_to_existing_superusers — NB recreated superusers
  need super-schizo/super-nomad re-granted by hand
- verified: fresh migrate end-to-end; deck/astro/applet/shop/system-
  user end-state spot-checked (incl. 0007's U+2011 trump-8 hyphen +
  pip MINOR split); makemigrations --check clean; 1812 apps tests +
  25 channels + seed-map FTs + Jasmine all green
- REQUIRES a DB reset wherever an old django_migrations table exists:
  staging postgres gets dropped + remigrated (user-sanctioned); local
  sqlite already recreated

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 13:58:10 -04:00
Disco DeDisco
65dd18cdb7 infra: nginx configs mirror the live certbot-managed versions (cicd-playbook now idempotent); drop infra/** from deploy-staging's path filter
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
- the repo's pre-certbot HTTP-only gitea/woodpecker confs CLOBBERED the
  live SSL config on every cicd-playbook run, and the certbot task's
  creates: guard meant TLS never came back — verified by live diff
  2026-06-10. Both confs now carry the certbot 443 blocks + the live 1G
  client_max_body_size, so the play converges instead of clobbering
- deploy-staging listed infra/** in when.path while its dependency
  build-and-push did not → an infra-only push (1536fea) pruned
  build-and-push but selected deploy-staging → "depends on unknown
  step" config error (pipeline #385). deploy-staging's paths are now a
  subset of its dependency chain's; ansible changes are applied by
  hand, not CI, and a deploy without a fresh image is a no-op re-pull

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 13:20:20 -04:00
Disco DeDisco
1536feaf21 infra: weekly docker + gitea-archive prune cron on the CI droplet
- 2026-06-10 incident: 48G disk hit 100% — pipelines died w. ENOSPC in
  the UT/IT dependency install + the Woodpecker repo UI 500'd (SQLite
  couldn't write; the repos LIST still rendered, pure read). Culprits:
  ~15G stale CI images/build cache + 19G of gitea repo-archive cache
  (crawlers on the public instance generate zip/tar snapshots faster
  than gitea's @midnight archive_cleanup reclaims them)
- cicd/docker-prune.sh → /etc/cron.weekly/docker-prune (installed live
  via ssh too — running the full playbook clobbers the droplet's nginx
  config, known template bug): image/builder/container prune keeping
  the last 168h + a repo-archive cache wipe
- gitea container restarted to unwedge the post-receive queues the
  disk-full era jammed (pushes landed in the bare repo but the UI +
  Woodpecker webhook never heard about them)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 13:12:42 -04:00
Disco DeDisco
8130122b1b clock place URL carries the acting ?seat — fixes the silent 403 when a multi-seat owner drives another circle's turn — TDD
- the felt POSTed to the bare clock/place URL, so _acting_seat fell
  back to the CANONICAL seat: the prompt rendered (the page GET is
  ?seat-aware) but the tap 403'd silently — the sky/sea felts' d09dca5
  seat-threading lesson, replayed on the seed felt
- data-clock-place-url now bakes in ?seat={{ current_slot }} (mirror of
  _sky_overlay's seat-carried data-URLs)
- ITs: multi-seat owner places via ?seat=5 after the bare URL 403s;
  overlay markup carries clock/place?seat=
- _tap_sign FT helper selects the wedge INSIDE execute_script — a
  Python-side handle goes stale when a drawRim repaint rebuilds the svg
  between find and dispatch

[[project-voronoi-spec]]

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 12:57:13 -04:00
Disco DeDisco
32db203543 Set the Game Clock — ephemeris narrowing (hard CSP): placements shrink the REAL date window; narrowed range prints below the prompt; unreachable signs dim + reject — TDD
- PySwiss gains its FIRST reverse lookup: GET /api/windows/?placements=
  Uranus:Aquarius,…&next=Saturn — sign_windows (per-planet stride scan +
  bisection edge refine to the hour) folds thru narrowed_windows
  (slowest planet first, each scan restricted to the prior intersection
  so the fast bodies only ever scan slivers) over the game window
  (settings GAME_WINDOW_START/END, default 1781-03-13 — Uranus's
  discovery — → 2100-12-31, the snapshot span); present_signs reports
  the next planet's reachable signs. Self-validating UTs (every window
  forward-checked at midpoint + edges) + 8 API ITs
- epic clock_windows endpoint (lazy, table_sky-shaped — room views stay
  HTTP-free) proxies the lookup, cached per room+placements (six felts
  polling one ritual state = ONE upstream call; failures cached 60s);
  fails OPEN {available:false} when PySwiss is unreachable
- place_clock_planet enforces the HARD constraint: a sign outside the
  narrowed windows' reach → 409 sign_unreachable; fail-open w.o PySwiss
  (the ritual never bricks on microservice downtime); PlaceClockPlanet
  ITs sever PySwiss in setUp so the turn walk stays deterministic
  against a live local service
- felt: #id_clock_windows readout below the prompt for ALL viewers —
  "1995-04-01 → 1998-04-17 · 2 windows" — fetched at parse + after own
  placement + on every clock_placement broadcast; drawRim opts gain
  allowedSigns → unreachable wedges .nw-sign--blocked (dimmed, inert,
  no handlers); SkyWheelSpec R10/R11
- SeedMapClockNarrowingTest FT stubs PySwiss in-process (real proxy,
  real gating): readout renders, blocked Aries won't place, allowed
  Pisces lands Saturn, readout re-narrows

[[project-voronoi-spec]]

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 12:30:16 -04:00
Disco DeDisco
b2ddd98956 Set the Game Clock — increment 2: placements broadcast LIVE over the room WS + the turn hands off 5→1 (Saturn next) — TDD
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed
- place_clock_planet now fans _notify_clock_placement out to the room
  group on every landed placement: full placements (open SEED MAP felts
  repaint — ONE shared map per room, updating asynchronously) + the
  next turn (next_planet, next_slot); nothing broadcast on rejections
- RoomConsumer clock_placement pass-through; CLOCK_SLOT_BY_PLANET
  inverse map
- seed overlay: data-clock-slot embeds the viewer's position circle;
  the room:clock_placement window listener adopts the placements,
  repaints, and when next_slot is THIS circle gains the placement
  affordance live (_ensurePrompt + _placeable) — no reload between turns
- turn progression 6→1 was already general server-side (increment 1's
  _clock_placeable_for); now pinned by ITs (circle 5 blocked before
  Uranus / Saturn after / full roster walk Uranus→Saturn→Jupiter→Mars→
  Sun→Moon + post-ritual 403) + the circle-5 reload-path FT
- new SeedMapClockBroadcastTest (channels, two browsers): circle 6
  places Uranus → circle 5's open felt live-gains the glyph + the
  "Place Saturn" prompt → Saturn flows back; _seed_clock_room /
  _tap_sign FT helpers shared across clock classes

[[project-voronoi-spec]] [[feedback-channels-broadcast-must-originate-in-daphne]]

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 11:58:54 -04:00
Disco DeDisco
080d44e10c seed-map rim placement rides the pointer pair, not click — fixes Uranus never placing on touch screens — TDD
- iOS withholds a tap's synthesized click whenever a document-level
  mousemove/mouseover handler (the room page runs several tooltip
  machines) mutates the DOM — the wedge lit its :hover feedback but
  _onPickSign never fired on mobile staging
- drawRim placement mode now binds pointerdown→pointerup w. a 10px
  slop radius (drag/scroll intent fires pointercancel or drifts past
  it); click stays only as the no-PointerEvent fallback — no double-
  pick on desktop
- SkyWheelSpec R7 retold as the pure pointer tap (no click), + R7b
  compat-click no-double-pick + R7c drag-cancel; R9 tap-ified
- seed map FT dispatches the pointer pair on the wedge

[[project-voronoi-spec]]

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 11:39:24 -04:00
Disco DeDisco
14afb108c0 Set the Game Clock — increment 1: the position-circle-6 gamer places Uranus in a sign on the shared game wheel — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
The SEED MAP rim's planets are no longer auto-computed — they are PLACED by the
gamers in turn (circle 6→1, Uranus→Saturn→Jupiter→Mars→Sun→Moon; Mercury/Venus
Sun-derived; Neptune/Pluto excluded). This is the first turn: the wheel starts
SIGNS-ONLY (planets eliminated) and the gamer at POSITION CIRCLE 6 places Uranus
by clicking a sign wedge. Roadmap step 21. See project_voronoi_spec.

KEYED ON THE POSITION CIRCLE (slot_number), NOT the role — select_role assigns
roles freely, so position 6 can hold any role; in a generic room BC merely
defaults there. Tests INVERT the slot→role defaults (position 6 = PC, position 1
= BC) so a role-keyed bug would fail them.

- Room.clock_placements JSONField (migration 0020): {planet: sign}, the ritual
  state. The 9ed8771 auto-sky infra (table_sky / sky_chart / convened_at) stays
  DORMANT — future home of the resolved sky; the rim no longer reads it.
- epic.place_clock_planet POST {planet, sign}: the acting seat must be the circle
  whose turn it is (its circle's planet, every earlier planet placed, this one
  not yet) + a real zodiac sign → persists. _clock_placeable_for shared by the
  endpoint + the seed-felt ctx so gate & affordance can't drift. 403 wrong
  circle/turn/seat, 400 bad sign.
- SkyWheel.drawRim(svg, data, opts): opts.placeable turns the sign wedges into
  placement targets (.nw-sign--placeable + click → opts.onPickSign(sign)); still
  singleton-pure (no module writes — Jasmine R9).
- _seed_map_overlay.html: rim renders the placements (sign-midpoint glyph);
  empty → signs-only; the gamer whose turn it is gets the #id_clock_prompt + the
  clickable wedges → POST → adopt the server's placements + repaint (reload-safe).
- _sky.scss: .nw-sign--placeable re-enables pointer-events on the click-through
  rim + a hover brighten; the .clock-prompt label.
- ctx: clock_placements_json + clock_placeable in _role_select_context (drops the
  now-unused room_sky_json). Repointed the 9ed8771 rim FT/ITs (auto-sky →
  placements, empty = signs-only).
- Coverage: Jasmine drawRim R7–R9 (placement clickable / inert without opts /
  singleton-safe); epic PlaceClockPlanetTest (position-keyed, role-inverted) +
  repointed rim ITs; FT position-6 places Uranus → glyph + persist. 1021
  epic+gameboard ITs green; live-verified in Firefox.

DEFERRED: turn progression 5→1 + WS live-broadcast (increment 2); the CSP
ephemeris narrowing + resolving placements → a datetime (later).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 01:06:14 -04:00
Disco DeDisco
9ed877168e SEED MAP shared wheel rim: the table's OWN sky (planets-only, canonical signs) rings the tessellation — one frame for all six gamers — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
The Voronoi felt gains a stripped sky-wheel rim drawn from the ROOM's own sky
(Room.sky_chart) — identical for every gamer, updating toward the shared map —
with the tessellation sized into the wheel's freed hub. Roadmap step 21, Step
2's coordinate frame.

- Room.convened_at + Room.sky_chart (migration 0019); pick_roles stamps
  convened_at at gate-close (stamp only — no HTTP on the transition)
- epic.table_sky: lazy planets-only chart via PySwiss at the null location
  (geocentric longitudes need only the convened TIME; houses/ASC/MC need a
  birth LOCATION a virtual table lacks → omitted), cached on Room.sky_chart;
  legacy rooms key off created_at; seated-gamer gated, 502 on PySwiss down
- SkyWheel.drawRim(svg, data): pure static renderer — canonical asc=0 frame,
  signs ring + planet glyphs only, NO element ring / centre disc / houses /
  axes / aspects / tooltips; never writes the interactive wheel's singleton
  state; returns {size, cx, cy, r, hubR} so the felt sizes the map into the hub
- _seed_map_overlay.html: rim draws on open; map svg shrinks to 2×hubR +
  .voronoi-map--rimmed clip; lazy table-sky fetch on open; preload-then-repaint
  so a cold-cache open doesn't strand the zodiac glyphs; ResizeObserver on the
  col (not the self-sized map svg)
- _sky.scss: stacked centred svgs in .seed-map-col; .seed-wheel pointer-events
  none; circle clip on the rimmed map
- room_sky_json ctx in _role_select_context; rootvars: --sixUser/--octUser
  nudged within the Trs ramp (parallel palette tune)
- drawRim Jasmine suite (R1–R6: signs+planets, strip, hub geometry, static
  placement, singleton untouched, signs-only fallback) in both spec copies;
  epic TableSkyViewTest + convened_at stamp + seed-overlay rim ITs; FT rim
  assertions (12 signs, 3 planets, no stripped/located layers, hub sizing)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 00:33:23 -04:00
Disco DeDisco
cde556b178 SEED MAP felt: 2D d3-delaunay Voronoi/Delaunay dual graph (roadmap step 21, Step 1) — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
New inline --duoUser felt (sibling of CAST SKY / DRAW SEA) that opens on
#id_seed_map_btn once the 6-card sea hand completes (hand_complete); paints a
Voronoi cell layer (territory) + a Delaunay edge layer (adjacency) from
PLACEHOLDER seeds. Card-driven seeding (the 6 Celtic-Cross cards) is Step 2.

- voronoi-map.js: window.SeedMap.draw/drawPlaceholder/clear over the bundled
  d3.min.js (d3-delaunay ships in the v7.9.0 UMD bundle — no new dep); a
  ResizeObserver re-tessellates to fill the felt on resize; data-seed reads d3's
  ring.index (survives skipped/degenerate cells in Step 2)
- _seed_map_overlay.html felt + room.html include + has-seed-stage (gated on hand_complete)
- three-way felt close (T3): openSeed closes sky+sea; openSky/openSea reciprocally close seed
- .room-menu-seed gear NVM pane + room-views.js seed-open branch
- _sky.scss felt block (T1: no aperture-fill light; T2: chained selector; fills the pane) + _room.scss aperture pin
- VoronoiMapSpec Jasmine + game_room_seed_map FT + 5 felt ITs; CarteTray NVM count 2->3

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 21:02:21 -04:00
Disco DeDisco
02c4307a95 sky-wheel aspect micro-tooltip: give it its OWN portal id so it stops battling the wallet/kit over #id_mini_tooltip_portal — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
The Aspected/Unaspected micro-tooltip (the DON|DOFF state read-out, mirroring the
game-kit Equipped/Unequipped chip) showed on the standalone sky.html but NOT on
the home-page SkyDrive applet or the gameroom Sky Select felt. Root cause: the
sky-wheel grabbed #id_mini_tooltip_portal — the SAME element the wallet + game-kit
token tooltips use (wallet.js / wallet-shop.js / gameboard.js). On the home page,
where the My Wallet AND SkyDrive applets coexist, the wallet left an inline
display:none on the shared element that the sky-wheel's class-based `.active`
{display:block} could never override (inline > stylesheet). sky.html only worked
because it has no wallet. Live trace: the element even carried the wallet's
"In-Use: <room>" text while the sky tried to show "Unaspected".

Fix — separate elements, no shared state:
- sky-wheel.js reads its own #id_aspect_mini_portal (not #id_mini_tooltip_portal).
- _gameboard.scss: the italic/right-aligned aspect-portal rule + `.active`
  {display:block} rescoped to #id_aspect_mini_portal. The wallet/kit keep
  #id_mini_tooltip_portal (_wallet-tokens.scss) untouched.
- home.html now ships BOTH portals (wallet #id_mini_tooltip_portal +
  #id_aspect_mini_portal); sky.html renames its portal; room.html adds
  #id_aspect_mini_portal in the SKY_SELECT block (covers initial CAST SKY + the
  saved-wheel revisit — same #id_sky_tooltip block).

Verified live (Claudezilla): home-page applet + sky.html now show the aspect
micro-tooltip (display:block, "Unaspected" ⇄ "Aspected" on DON|DOFF) with the
wallet element untouched. TDD: SkyWheelSpec +1 — a planet activation touches only
#id_aspect_mini_portal, leaving a sibling #id_mini_tooltip_portal (w. its inline
display:none + content) untouched. 506+1 Jasmine specs green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 02:00:40 -04:00
Disco DeDisco
011e4b2d5a sea affinity prose reversals + reelhouse sea/sky reel-up swipe machine + SCROLL gear OK close — TDD
Three threads:

1. Reversal-aware SEA_DRAWN prose. The affinity clause now reads "the {orientation}
   of the {card}" — "emanation" upright, "reversal" when the drawn card is reversed
   — and a reversed card's abbrev gains a ℞ (U+211E, the sky wheel's retrograde
   glyph) <sub> subscript inside the existing no-wrap span, e.g. "(Q ♥℞)".
   _sea_affinity_for now carries the per-card `reversed` flag (it was in the hand
   entry but discarded) → event.data → to_prose via d.get("reversed", False).
   _card_abbrev gains reversed_flag=False so the shared SIG_READY caller is
   untouched (significators are picked, never drawn reversed). TDD: drama ITs +2
   (reversal orientation + ℞; upright emanation w.o ℞), epic IT +1 (reversed flows
   end-to-end), existing SEA_DRAWN prose tests updated to "emanation of the".

2. Reelhouse sea/sky reel-up swipe machine. The burger Sea/Sky reopen btns are
   .active from anywhere in the reelhouse, but opening a felt while parked on the
   reelhouse pane pinned the aperture there (html.*-open freezes the scroll) with
   the felt stranded off-screen in the hex pane (scroll-locked; NVM was the only
   escape). New RoomViews.scrollToHex(cb): from the reelhouse it reels the aperture
   UP to the ROOM hex, then fires the open cb on settle (the banner h2 reel follows
   via the existing IO); already at the hex → synchronous (in-hex CAST SKY / DRAW
   SEA unchanged). afterDescent (scrollend + 700ms ceiling) hoisted out of the Text
   sub-btn machine + shared. Both sub-btn handlers (burger-btn.js sky, _sea_overlay
   sea) route through it. Pinned in Jasmine (RoomViewsSpec +2) per the headless-
   drops-delayed-scroll trap — the delayed-vs-synchronous DECISION, not real scroll.

3. SCROLL gear OK now closes the menu. room-scroll.js's filter-submit handler
   referenced an UNDECLARED `roomMenu` → ReferenceError thrown AFTER applyFilter
   ran, so the filter applied but the menu never closed. Look it up via
   getElementById (mirrors the ATLAS OK / applets close). FT +1.

506 Jasmine specs + 8 room-scroll FTs + 61 drama + 649 epic/billboard ITs green.

[[project-sea-select-scroll-provenance]] [[feedback-headless-delayed-scroll-dropped]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 01:27:03 -04:00
Disco DeDisco
80391b37c2 scroll: tag the disembody + relinquish un-do events as Fable, not Frame — TDD
SIG_UNREADY ("disembodies POSS Significator") + SEA_RELINQUISHED ("relinquishes
POSS affinity with the X") are the character un-doing its Significator / Sea —
the un-do counterparts of the fable SIG_READY / SEA_DRAWN. They were stranded in
FABLE_VERBS' blind spot, so the scroll tagged them data-base="frame" with a bare
@handle instead of "@handle's character", and the Fable filter never caught them
(a disembody log showed under Frame, split from its embody pair).

Add both to FABLE_VERBS so they render data-base="fable" + the "@handle's
character" stub and ride the Fable filter with their pair. The billboard recent-
room EMBED still drops SIG_UNREADY noise (a verb-based exclude, untouched); the
redact-pair machinery is verb-keyed too, so is_fable has no functional effect
beyond display/filter.

TDD: drama ITs +2 — sig-unready + sea-relinquished render fable + character
stub; is_fable test moves both verbs to the fable group (only table/gate events
— deposits, chair assignment — stay frame). 59 drama + 241 billboard + 407 epic
ITs green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 23:54:51 -04:00
Disco DeDisco
04ab673cea scroll filter: Redact is a per-base MODIFIER, not a standalone tag — unchecking Frame/Fable hides that tag's struck rows too — TDD
The SCROLL (+ Billscroll) Frame/Fable/Redact filter treated redact as a 3rd
mutually-exclusive label, so a struck row showed whenever Redact was checked
regardless of its underlying tag. Per user-spec, Redact now layers on the base:
a struck row shows only when BOTH its base (Frame/Fable) AND Redact are checked.
So Fable+Redact (Frame off) hides every Frame log incl. redacted Carte Blanche
lines; Frame+Redact (Fable off) hides @disco's-character Fable logs even though
Redact is on.

- _scroll.html: each row now carries data-base (frame|fable, ALWAYS — the
  underlying tag) alongside data-label (collapses to redact when struck).
- room-scroll.js + billboard scroll.html applyFilter (mirrored): visible =
  base-checked AND (not struck OR redact-checked).

TDD: drama ITs +1 — data-base asserted on fable/frame/struck-fable/struck-frame
renders (struck keeps its base). Room scroll FT +2 — fable+redact/frame-off
hides all frame incl. struck; frame+redact/fable-off hides the struck fable
despite Redact on. Existing redact-hides-struck + billboard gear-flow FTs still
green (their struck rows are fable-based + never toggle Fable). 57 drama ITs + 7
room-scroll FTs + 3 billboard gear FTs green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 23:47:34 -04:00
Disco DeDisco
7ca9d4d7d9 sky/sea select NVM: carry the acting ?seat so a CARTE gamer returns to his OWN pos-circle, not pos 1 — TDD
The gear's CAST SKY + DRAW SEA NVM panes linked to a bare epic:room with no ?seat, so on cancel a CARTE multi-seat gamer fell back to owned[0] (pos 1): revisiting Sky/Sea Select at pos 2 + clicking NVM landed him on pos 1's view (e.g. pos 1's SEED MAP) instead of pos 2's own DRAW SEA. Append ?seat={{ current_slot }} to both felt NVMs (guarded on current_slot) — the same fix shape room_gate already applies to the CONT GAME btn + gate NVM (table_url). Single-seat gamers get their sole slot; spectators (current_slot None) fall back to the bare URL.

TDD: CarteTrayFollowsSelectedSeatTest +2 — test_sky_sea_nvm_carry_acting_seat (?seat=4 → both panes' NVM carry seat 4) + test_sky_sea_nvm_default_targets_lowest_owned (no ?seat → seat 1 = lowest owned). 17 ITs green.

[[project-sig-select-seat-switch-open-problems]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 23:33:02 -04:00
Disco DeDisco
a2d28e556e game room voice: regression-guard the recede toggle — 6 depositors (on) collapsing back to a sole depositor flips voice off (stateless gate, no hysteresis) — TDD
RoomVoiceActivationTest gains test_voice_toggles_off_when_depositors_recede_to_one: asserts active at high-water (6 distinct cost-current depositors), then collapses all filled slots to one gamer + asserts off — proving the >1 gate has no memory of the prior count (recomputed each render). Pairs with the existing flips-on test (1->2) for the full up/down round-trip. 14 ITs green.

[[project-my-sea-invite-voice-blueprint]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 23:26:46 -04:00
Disco DeDisco
97d5522807 role select scroll log: less robotic phrasing + role code — 'assumes Nth Chair, where SUBJ will start the game as the Role [XC]' — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
Was 'assumes Nth Chair; SUBJ will start the game as the Role.' Now joins with ', where' and appends the role code in a no-wrap bracket (the Player [PC], the Builder [BC]…), matching the SkyDrive/Sea abbrev treatment. Chair stays capitalised (user-confirmed). 56 drama ITs green; the billscroll FT's 'assumes 1st Chair' assertion still holds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 23:19:27 -04:00
Disco DeDisco
b7f943cd38 palette: brighten --secGn to 0,200,100 (re-spread the green ramp); position-status .fa-ban back to --priRd, .fa-circle-check on the new --secGn
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 23:08:07 -04:00
Disco DeDisco
c683f02676 atlas: tab the merged post-line username off its text (min-width 4rem, like the POST view) so it reads distinctly from a provenance log's running prose
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 23:02:35 -04:00
Disco DeDisco
031658b80b scroll: keep the fable "'s character" stub BOLD (still --secUser, not --quaUser) — only the username pops
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 23:00:00 -04:00
Disco DeDisco
cadfc5e864 scroll provenance: SkyDrive prose + element abbrevs; Fable tag + character-actor stub; --quaUser usernames; no-wrap abbrevs — TDD
A batch of character-creation provenance polish (user-spec 2026-06-09):

- SKY_SAVED prose: 'beholds the skyscape of POSS birth, which yields OBJ a unique X capacity' -> 'fires up the SkyDrive to observe that POSS cosmic origins yield OBJ a unique X [Xc] capacity' — each capacitor now carries its short code (Ossum [Om], etc.) from a new epic.utils.CAPACITOR_ABBREV (mirrors ELEMENT_INFO.abbr in sky-wheel.js). SEA_DRAWN 'Sea of cards' -> 'Sea of Cards'; the AC clause 'sinister' -> 'offhand connections'.

- Fable tag: a new GameEvent.is_fable (SIG_READY / SKY_SAVED / SEA_DRAWN) makes the shared _scroll.html render data-label='fable' (not 'frame'; struck still wins -> 'redact') + prefix '@handle's character' — a stub for the character's name once set. Both scroll gear filters (room + billscroll) gain a Fable checkbox so fable rows stay filterable.

- Usernames: the scroll @handle <strong> is now bold --quaUser (breaks up the --secUser monotony, mirroring .atlas-row-who); the fable "'s character" stub sits OUTSIDE the strong so it stays --secUser. Mirrored into the ATLAS via .atlas-row-body strong (provenance rows embed the whole SCROLL body).

- No-wrap: card parentheticals (Q [icon]) + element brackets [Om] ride white-space:nowrap spans so an abbreviation never splits across a line wrap. Shared _card_abbrev helper for SIG_READY + SEA_DRAWN.

TDD: drama prose tests reworked (SkyDrive + bracket codes, no-wrap spans, Sea of Cards, offhand); is_fable + scroll-render ITs (fable->fable label + character stub; non-fable->frame; struck fable->redact). 70 drama + 918 epic+billboard ITs green. Existing billscroll/room-scroll filter FTs unaffected (their frame events are ROLE_SELECTED/deposits, their redact are struck SIG_READY).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 22:54:38 -04:00
Disco DeDisco
a6db8c628f sea affinity prose: reinsert the (rank icon) parenthetical + pluralize the NC verb (they/yo leave, he/she/it leaves) — TDD
Two user follow-ups to the personalized affinity prose: (1) the corner-rank + suit-icon abbrev rides the card name again (e.g. 'the Queen of Pentacles (Q <i class=fa-crown></i>)'), mirroring SIG_READY — majors render just the numeral, e.g. '(I)'. (2) The NC clause conjugates the subject verb: they + yo are treated as PLURAL -> 'they leave' / 'yo leave behind'; the singular he/she/it keep the 3rd-person 'leaves'.

TDD: per-role-clauses test updated to 'they leave'; new yo-pluralizes + he-keeps-leaves cases; a with-abbrev PC case asserting the full rank+icon parenthetical. 51 drama+epic ITs green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 22:23:19 -04:00
Disco DeDisco
b0d153ebc1 sea affinity scroll log: personalized per-Role prose with pronouns (draws POSS Sea of cards, where the CARD ...) — TDD
Replaces the generic 'finding affinity with the X in the Crown' SEA_DRAWN prose with a role-specific clause per user-spec 2026-06-09 — the @handle is template-prepended, pronouns (subj/poss) resolve via the actor:

  PC: draws POSS Sea of cards, where the CARD crowns all POSS loftiest illusions. NC: ...the CARD traces all the narratives SUBJ leaves behind. EC: ...always looms before POSS calling. SC: ...covers all POSS righteous conduct. AC: ...always crosses POSS sinister connections. BC: ...lays all POSS foundational work.

to_prose branches on data['role']; 'The ' still stripped so a qualifier butts the proper name; an unknown role falls back to a generic 'marks POSS affinity' clause. The stored position_label/corner_rank/suit_icon are now unused by the prose but left in the event data (harmless). TDD: drama prose tests reworked (per-role clauses + bawlmorese pronoun substitution) + the epic affinity-prose IT updated to the PC crown clause. 49 drama+epic ITs green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 22:08:31 -04:00
Disco DeDisco
144ec78b1f two-browser sig FTs: read --priYl off :root live instead of a hardcoded rgb(255,207,52) — palette-tuning-resilient (build 377 fix)
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
Both SigSelectChannelsTest cursor/reservation FTs hardcoded the OLD --priYl = rgb(255,207,52); the palette tuning moved --priYl to 255,227,82, so the rendered NC role colour no longer matched and both failed in the test-two-browser-FTs stage (build 377). Now each reads --priYl off document.documentElement at run time and compares whitespace-stripped (so 255,227,82 matches rgb(255,227,82) and the rgb()/rgba() box-shadow forms alike) — future palette tweaks to --priYl won't re-break them. Docstrings de-hardcoded too. Couldn't run the channels/two-browser stage locally (needs Redis + dual Firefox); verified --priYl is a :root base var + py_compile clean; the assertion now tracks whatever --priYl renders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 20:56:45 -04:00
Disco DeDisco
35d05a6490 drama: migration for the GameEvent.verb choices (SEA_DRAWN + SEA_RELINQUISHED) — unblocks the CI makemigrations --check
The Sea Select Scroll-log sprint (d28046f) added two verbs to GameEvent.VERB_CHOICES but not the AlterField migration Django generates for a choices change, so the pipeline's makemigrations --check failed early. 0005_alter_gameevent_verb regenerates it; applies clean. No data change (choices is validation-only).

- bundled (parallel work): rootvars.scss ongoing palette tuning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 20:34:27 -04:00
Disco DeDisco
4aee5016c1 dubbodeck per-card FLIP back: the sig stage FLIPs each card to its OWN deck's back, not the seat's — finishes the cross-deck story — TDD
Pairs with the dubbodeck assembly: a mixed pile can now hold cards from up to three decks per polarity, so the single server-rendered back (the viewer's seat deck) was wrong for any cross-deck card. Now per-card: each .sig-card thumbnail carries data-back-image-url (its OWN deck back, only for non-polarized image decks; empty otherwise); stage-card.js fromDataset reads back_image_url + _setImageMode repoints the stage .sig-stage-card-back-img at the FOCUSED card's back and shows/hides the FLIP affordance per card (a backless card in the pile hides FLIP + drops any flipped state).

- _sig_select_overlay.html: the stage back-img + FLIP now render on sig_pile_has_backs (ANY pile card is a non-polarized image deck) instead of the viewer's-seat-deck gate, so an RWS grails/blades card in a PC-earthman viewer's pile can FLIP. Initial src dropped (JS sets it on first focus). epic/views.py computes the flag where sig_cards is set; _court_cards/_major_cards gained select_related(deck_variant) to keep the per-card deck access N+1-free.

TDD: 3 ITs in SigSelectUnifiedStageTest (non-polarized image deck -> per-card /static back url + sig_pile_has_backs + stage back-img/FLIP rendered; polarized deck -> empty back + no FLIP element; text-only deck -> empty back). 562 epic view+model ITs green. The JS repoint/hide is the stage-Jasmine debt (deferred) — manually verified.

- bundled (parallel work): rootvars.scss ongoing palette tuning.

[[project-deck-segment-model]] [[project-image-based-deck-face-rendering]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 20:33:19 -04:00
Disco DeDisco
034639d335 game room voice: gate on >1 cost-current depositor over the 7d initial period (not grace), not the phase — solo/CARTE-all-6 stays off — TDD
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed
Refines the room voice activation per user-spec: voice mirrors my_sea's window but over the seat's 7d INITIAL cost period (GateSlot.cost_current — within [filled_at, filled_at+7d), NOT the renewal grace). voice_active now needs the gate CLOSED (table_status set = ROLE_SELECT onset) + the viewer seated + MORE THAN ONE distinct gamer holding a FILLED, cost-current seat. A sole depositor — including a CARTE owner occupying all 6 seats (one gamer) — keeps voice OFF (no one to talk to); a 2nd qualifying gamer flips it on; it toggles back off once the cost period lapses into grace. Replaces the earlier phase-set gate (ROLE/SIG/SKY only) — voice now spans the whole 7d window incl. IN_GAME (the phase ceiling at SEA_SELECT made the 7d expiry unreachable).

TDD: RoomVoiceActivationTest reworked (13 ITs) — active w. 6 depositors across ROLE/SIG/SKY + persists in IN_GAME; inactive for a single depositor (CARTE-all-6), flips on once a 2nd gamer qualifies, inactive when filled 8d ago (grace), inactive before the gate closes, inactive for a non-seated viewer; room-id / muted-at passthrough + active-btn markup unchanged. 736 epic+drama ITs green.

[[project-my-sea-invite-voice-blueprint]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:53:59 -04:00
Disco DeDisco
b4ffab186e dubbodeck: assemble each sig pile per-segment from the contributing seat's own deck (cross-deck), not one shared deck — TDD
Each polarity's 18-card sig pile now assembles from THREE seats by segment, each FROM THAT SEAT'S OWN deck: CROWNS/BRANDS courts (8) from the cb role's deck, GRAILS/BLADES courts (8) from the gb role's, majors 0/1 (2) from the tr role's — levity cb/gb/tr = PC/SC/NC, gravity = BC/AC/EC. So an RWS King of Grails (SC seat) can sit beside a Minchiate Queen of Wands (PC seat) in one levity pile, each carrying its own deck_variant -> per-card face/back art, NO schema change (cards already carry deck_variant).

- models.py: new _POLARITY_SEGMENT_ROLES + _seat_deck_for_role + _court_cards + _major_cards + _polarity_sig_cards. A missing seat/deck falls back to _room_deck_variant, so a single-deck (or CARTE-solo) room assembles the IDENTICAL 18-card pile it always did (16 courts + 2 majors). sig_deck_cards is now the UNION of both polarity piles (note-unfiltered) -> select_sig's pick validation (views.py:1664) accepts a card from EITHER deck/polarity with no view change. Dropped the now-dead _sig_unique_cards; _sig_unique_cards_for_deck stays for personal_sig_cards (my_sign, single equipped deck).

- TDD: DubbodeckAssemblyTest (4 ITs) — levity grails/blades come from the SC seat's distinct deck while crowns/brands stay earthman; the gravity pile is unaffected by a levity-seat deck; the validation set spans both decks; CARTE-solo one-deck feeds BOTH polarity piles sharing pks (no cross-polarity dedup). Existing SigDeckCompositionTest (36/16/16/4) + SigCardHelperTest (single-deck counts, note unlocks, share-pks, empty fallback) green — single-deck behavior preserved. 736 epic+drama ITs green.

- bundled (parallel work): rootvars.scss ongoing palette tuning.

[[project-deck-segment-model]] [[project-image-based-deck-face-rendering]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:53:45 -04:00
Disco DeDisco
6f1729010f billscroll FT: backdate the 'recent' event 3h into the clock-time bucket — fixes test_recent_event_shows_time_format after the relative_ts <60min change
The shared relative_ts gained a <60min -> 'N min' bucket (a02f347), so a just-now auto_now_add event renders 'N min', not the g:i a clock time BillscrollEntryLayoutTest.test_recent_event_shows_time_format asserts -> red locally + in CI from that commit on. Backdate the 'recent' event 3h (still recent vs. the >1yr old event, but >60min) so it renders clock time, mirroring the existing old-event backdate. Data-only change; the layout/columns test is age-agnostic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:53:27 -04:00
Disco DeDisco
2d4a2c5b5c post view: bottom-anchor the post-line thread (flex column + justify-content: flex-end) so short threads sit above the composer instead of the header
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:39:11 -04:00
Disco DeDisco
203596ee29 post view: restore the post-line grid layout (author | text | time, bordered + rounded) minus the --priUser fill & box-shadow — outlined rows, not filled pills
Follow-up to the reelhouse recolor: the POST thread keeps its earlier per-line grid (author / text / time columns, 0.1rem --secUser border at the .form-control radius, margin/padding) but drops the background-color + box-shadow per user-spec, so the lines read as outlined rows on the plain wash. The filled-pill chrome remains salvaged in YARN.

- bundled (parallel work): rootvars.scss ongoing palette tuning (--terPer + neighbouring slots).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:38:35 -04:00
Disco DeDisco
d28046f3da sea select scroll log: publish a Role<->Celtic-position affinity on the completing draw; redact + relinquish on DEL; re-publish on re-draw — TDD
The gameroom DRAW SEA phase now writes drama provenance, mirroring sig/sky. When a gamer's 6-card Celtic Cross COMPLETES, a SEA_DRAWN Scroll log publishes their affinity with the card sitting in their Role-correlated position.

- epic/views.py: ROLE_POSITION_MAP — the user's sixfold index (PC->crown, NC->leave, EC->loom, SC->cover, AC->cross, BC->lay; roles rotate each round, so a seat's CURRENT role drives it) + SEA_POSITION_LABELS (each spread's display label for a position KEY; Waite-Smith's Behind/Before/Beneath + Escape-Velocity's Leave/Loom/Lay both key to the same index). sea_save publishes SEA_DRAWN on the <6->6 completing transition only (a reload that re-POSTs the full hand can't double-publish); a re-draw first redacts the standing relinquishment, then publishes anew. sea_delete redacts the published affinity (the strikethrough) + records SEA_RELINQUISHED in its wake (the redact-pair). _sea_affinity_for mirrors SIG_READY's polarity-qualified name_title + corner abbrev; _redact_standing_sea_event tests 'not retracted' in PYTHON (the SQLite JSONField exclude-NULL trap).

- drama/models.py: SEA_DRAWN + SEA_RELINQUISHED verbs + to_prose ('draws {poss} Celtic Cross, finding affinity with the {card}{abbrev} in the {Position}.' / 'relinquishes {poss} affinity with the {card}.'); 'The ' stripped so a levity/gravity qualifier butts the proper name. The generic struck/retracted property renders the strikethrough + data-label=redact in _scroll.html unchanged.

TDD: 4 drama prose ITs (affinity statement, spread-label passthrough, relinquishment, struck-when-retracted) + 7 epic ITs (publish-on-complete, position=crown for PC, none-before-complete, no-double-publish-on-reload, DEL redacts+relinquishes, re-draw redacts-the-relinquishment+republishes, DEL-noop-when-nothing-published). 459 drama + epic-view ITs green.

- bundled (parallel work): rootvars.scss --sixUser/--sepUser/--octUser slot reassignments across the forest/khaki/blade palettes (tuning the new reelhouse h2 bgs) + a new --terMrb.

[[project-sea-select-scroll-provenance]] [[feedback-jsonfield-exclude-sqlite-null]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:35:45 -04:00
Disco DeDisco
039152a787 game room voice: light the burger voice btn for seated gamers across ROLE/SIG/SKY_SELECT; keep POST's composer inline (OK beside the input) — TDD
Voice (Phase C, room path) — the my_sea voice mesh now extends to the epic game room. room_view sets voice_active for a SEATED gamer (a TableSeat with their gamer) while table_status is in {ROLE_SELECT, SIG_SELECT, SKY_SELECT} — from ROLE_SELECT onset (all tokens committed, seats pre-created by pick_roles) continuously through SKY_SELECT, which hosts the in-page DRAW SEA felt; dark at IN_GAME. voice_room_id = the room UUID (the WebRTC mesh key), voice_muted_at = the persisted mute so an in-arc nav/refresh rejoins muted (mirrors my_sea). The burger fan's shared #id_voice_btn lights .active + carries data-room-id; room.html now includes voice-glow.js (the glow/pulse machine, coexisting w. the sea-btn glow handoff).

No consumer/JS change needed: RoomVoiceConsumer._can_join already gates an epic room on TableSeat membership, the ws/voice/<str:room_id> route serves both mysea-… + bare-UUID keys, and burger-btn.js/voice-mesh.js read data-room-id generically. TDD: RoomVoiceActivationTest (9 ITs — active across the arc, dark at IN_GAME, dark for a non-seated viewer, room-id = UUID, muted-at passthrough) + RoomVoiceConsumerEpicGateTest (2 channels ITs — seated gamer admitted + receives welcome; non-seated refused). 390 epic-view + 11 voice channels ITs green.

- _room.scss: POST composer kept as a flex row — the OK btn sits inline beside the 'Enter a post line' input (the felt/pill chrome stays salvaged in YARN, but the input<->OK row layout is retained; follow-up to 577ef30).

- bundled (parallel work): _room.scss reelhouse h2 font --priUser -> --secUser (SCROLL/POST/PULSE); rootvars.scss chroma-hue primaries brightened (yellow/lime/cyan/indigo/violet/fuschia/magenta).

[[project-my-sea-invite-voice-blueprint]] [[project-character-creation-spec]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:08:38 -04:00
Disco DeDisco
577ef30f5c room reelhouse: recolor per-view h2s; salvage POST's pill styling into YARN; revert POST to plain wash; colour-code ATLAS row accents; drop letter-spacing on the Sea of Cards italic 'of'
- _room.scss: SCROLL h2 --priUser/--sixUser, POST h2 --priUser/--sepUser, PULSE h2 --priUser/--octUser. New .room-view--yarn block = verbatim salvage of POST's --duoUser green-felt + input-pill styling (.post-* renamed .yarn-*, #id_post_table -> #id_yarn_table), ready for when YARN's backing model + markup land. POST reverts to the plain dark %applet-box wash — only its h2 override + a bare-<ul> reset on #id_post_table (no felt/pills/bullets) remain. ATLAS per-source row accents colour-coded to echo the source h2 bgs: [data-source=provenance] -> --sixUser, [data-source=post] -> --sepUser (YARN/PULSE don't feed it yet).

- _gameboard.scss: .my-sea-title-of gets letter-spacing: normal, dropping the h2's enforced 0.2em tracking so the short italic 'of' doesn't read gappy.

- bundled (parallel work): rootvars.scss chroma-hue primaries brightened (yellow/lime/cyan/indigo/violet/fuschia/magenta pri+ter ramps).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 18:55:34 -04:00
Disco DeDisco
a02f3473d5 tooltips: tense-aware expiry (expires/expired) + a <60min 'N min' bucket in the shared relative_ts — TDD
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed
The token + position-circle expiry tooltips hardcoded 'expires' and never
flipped to 'expired' once the time passed — a seat whose `cost_current_until`
(the 7d cost clock) lapsed still read 'expires 11:30 p.m.' (staging 2026-06-08).

- New `expiry_phrase(dt)` filter (lyric_extras): 'expires <when>' for a FUTURE
  datetime, 'expired <when>' for a PAST one — the verb carries the tense so the
  underlying `relative_ts` stays direction-agnostic. Wired into
  `Token.tooltip_expiry` + the position-circle `data-tt-expiry` (position-tooltip.js
  copies it verbatim, so no JS change).
- `relative_ts` gains a <60min → 'N min' bucket (buckets: 60min / 24h / 7d /
  12mo / >12mo). Per user-spec it stays SHARED, so scroll.html's provenance feed
  (+ post.html / my-games row-ts) now reads 'N min' for very recent events too.

TDD: relative_ts <60min past+future + the 1h boundary; expiry_phrase
none/future/past/wraps-relative_ts; billboard post-line test updated (3h→clock-
time, + new just-posted→'N min'). 727 lyric+billboard+gameboard ITs green.

Bundled (parallel work): rootvars.scss chroma-hue primaries brightened
(--priRd/Or/Gn/Tk/Bl/Id + --terGn).

[[project-position-circle-tooltips]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 18:20:09 -04:00
Disco DeDisco
a0499723d3 sig select: redirect a seatless multi-seat (CARTE) owner to ?seat=<owned[0]> so tray + overlay + reserve align — TDD
A CARTE owner (all 6 seats, both polarities) entering SIG_SELECT with no ?seat
saw the sig overlay + reserve URL locked to the canonical PC seat (one polarity)
while the TRAY followed `current_slot` = owned[0] (the lowest owned slot, often
the OTHER polarity). A sig reserved from that view filed against the WRONG seat
— the seat the owner thought he covered stayed empty — so that polarity never
reached 3-ready, its 12s countdown ran to 0 but the server-side `_fire` bailed at
`len(ready) < 3` and never advanced; the other polarity proceeded. Switching
pos-circles via GATE VIEW (sets ?seat) re-aligned every surface and unstuck it.

A 3-agent trace confirmed the mechanism + corrected my first guess: this is NOT a
WS problem (the cursor group only drives the cosmetic flashing numeral; the
SIG→SKY advance is a threading.Timer broadcasting to the room_<id> group every
socket joins). The stall is the misfiled reservation → 3-ready COMPLETENESS
failure, rooted in two seat resolvers disagreeing when seatless: the overlay /
sig_confirm use `_canonical_user_seat` (PC-first) while the tray / reserve use
`_viewer_current_slot` owned[0].

Fix: `room_view` redirects a seatless multi-seat owner (gate_slots.filter(gamer)
.count() > 1) in SIG_SELECT to ?seat=<current_slot>, so EVERY surface (tray,
overlay, reserve URL, WS cursor group) resolves to one seat via the already-correct
?seat path — the same realignment a GATE-VIEW switch does. SIG_SELECT-only
(SKY_SELECT already keys off selected_seat); single-seat gamers / non-owners / anon
fail the guard. Unaffected: the multi-gamer sig FTs (one seat each) + the WS-direct
CarteCursorGroupTest.

TDD: 6 ITs in CarteTrayFollowsSelectedSeatTest (redirect / ?seat-present no-redirect
/ overlay+tray agree post-redirect / single-seat / non-owner / SKY_SELECT
no-redirect); the red `'PC' != 'BC'` was the divergence itself. 381 epic-view ITs
green.

[[project-sig-select-seat-switch-open-problems]] [[feedback-ws-cursor-group-must-match-acting-seat]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 18:05:03 -04:00
Disco DeDisco
dcfa54f522 game kit: free a deposited trinket 7d after it goes in-use; retire COIN's room cooldown — TDD
A deposited trinket binds `Token.current_room` (the 'In-Use: <room>' Game Kit
label), set on deposit + cleared only on MANUAL return. Once a seat's token cost
lapsed (7d → GATE VIEW returns to prompt re-deposit) nothing freed the binding —
`cost_current` is a render-time prop, so no 7d mutation — leaving e.g. a CARTE
used a week prior stuck 'In-Use' (staging, 2026-06-08).

Fix — a uniform, type-agnostic in-use clock:
- New `Token.in_use_since`, stamped when `current_room` is set (`debit_token`
  COIN, `drop_token` CARTE). `release_lapsed_trinkets(tokens)` frees
  `current_room` + `in_use_since` + `slots_claimed` once held >= the room's
  `renewal_period` (7d). The SEAT is untouched — renewal grace (to 2xspan) +
  auto-BYE stay `_expire_lapsed_seats`'s job (a separate, later threshold).
- PASS/BAND never bind (reusable keys) -> no-op; COIN + CARTE covered by the one
  rule. Fires on the gameboard / `_game_kit_context` render (the gamer sees it
  freed immediately) + the cron backstop (`expire_lapsed_room_seats`).
- Migration 0018 backfills `in_use_since` from the earliest backing-slot
  `filled_at` so existing staging bindings release on accurate timing (old free
  on next sweep, recent keep grace) instead of every legacy NULL releasing early.

Retired COIN's bespoke ROOM cooldown: `debit_token` no longer sets
`next_ready_at` + `return_token` no longer clears it. `next_ready_at` is now
My-Sea-exclusive (the 24h DRAW cooldown, no room context) — the two clocks no
longer share a meaning + can't clash (user-spec 2026-06-08).

TDD: ReleaseLapsedTrinketsTest (6) + cron release + 2 gameboard-immediacy tests +
updated return/tooltip/bind assertions; 1124 lyric+epic+gameboard ITs green.

[[feedback-equip-slot-gates-trinket-use]] [[feedback-my-sea-cooldown-design]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 16:35:35 -04:00
Disco DeDisco
d50645b216 sea deck stack: rename the stale .sea-stack-ok class to .sea-stack-flip (the btn renders FLIP now) — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
Pure rename, no behavior change — the deck-stack reveal btn kept the name `.sea-stack-ok` from when it said OK; it has rendered FLIP for a while. Renamed across all 8 references: _card-deck.scss (reveal + pointer-events rules) + a _gameboard.scss comment; the 3 templates that emit or query it (_sea_overlay.html, _my_sea_deck_stack.html, my_sea.html inline JS); and the 3 tests that select it (test_game_room_select_sea FT, test_game_my_sea FT, test_sea_visit IT rendered-HTML asserts). Verified 47 green across all three surfaces (visit IT + gameroom PickSeaDeal stack FT + my_sea CardDraw FT).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 13:04:51 -04:00
Disco DeDisco
c9fc5a2fd4 sea-select deal FT: scroll the revealed FLIP btn into view before the is_displayed assert — TDD
test_clicking_stack_shows_ok_btn is the only PickSeaDealTest that pairs _choose_spread w. an is_displayed() assert (the six-draws test checks the class instead). Once a spread is OK'd the felt scroll-snaps onto two pages + the deck stacks sit on the page the post-OK rAF scroll-to-cross targets — but headless Firefox DROPS that delayed scroll, leaving the clicked stack's revealed FLIP btn off the visible page → is_displayed False. scrollIntoView the btn first so the assert reflects the `--active` reveal, not the scroll position. Surfaced in CI build 374's channels stage (a flake that 373 survived on retry; my bd9155c preview edits only shifted the timing).

[[feedback-headless-delayed-scroll-dropped]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 13:00:51 -04:00
Disco DeDisco
1a83c5f01c sea stage: gate the FLIP-back 0.3 polarity tint to the cloned dubbodeck (Sea Select); my_sea/visit monodeck backs render un-tinted
- the `.sea-stage--gravity/levity .sea-stage-card.is-flipped-to-back::after` 0.3 fill differentiates the two halves of a monodeck CLONED into a two-toned dubbodeck — only meaningful in Sea Select. my_sea / my_sea_visit draw a single monodeck that is never cloned, so the tint was just noise on their FLIP'd card-back
- the shared `_sea_stage.html` now takes a `sea_stage_dubbo` flag; only `_sea_overlay.html` (the gameroom Sea Select felt) passes it → `.sea-stage--dubbo` on #id_sea_stage. The two tint rules gate on `.sea-stage--dubbo.sea-stage--gravity/levity`; non-dubbo stages fall through to the empty, fill-less base `::after` (no visible overlay)

[[project-deck-segment-model]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 11:29:04 -04:00
Disco DeDisco
bd9155c13b my sea preview: namespace cells .sea-prev-pos-* to de-collide from the live cross; harden spread dropdown + CI retry — TDD
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed
- rename the spread-preview's `.sea-pos-*` cells to `.sea-prev-pos-*` (template + aliased Celtic-cross geometry under `.sea-cross--preview` in _gameboard.scss + the SeaDealSpec fixture) so a bare `.sea-pos-*` matches ONLY the live `.my-sea-cross`; fixes test_picker_renders_sao_default_position_subset (was 2≠1 — the preview duplicated every cross cell)
- spread dropdown: cap `.sea-select-list` w. max-height + overflow-y:auto, & JS-click the option in the `_pick` FT helper, so the last option (escape-velocity) can't land below the un-scrollable picker-modal fold in landscape (ElementNotInteractable, seen in CI build 373)
- _retry_failed.sh: anchor the FAIL/ERROR label sed to the FIRST paren group so parameterized subTest failures retry the real method label, not a bogus `position='…'` module (ModuleNotFoundError)

[[project-my-sea-roadmap]] [[feedback-collectstatic-before-ft]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 11:19:34 -04:00
Disco DeDisco
a0ded7f09b Sea Select FTs: drop the removed-LOCK-HAND tests + reveal the cross before stack interaction — TDD
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed
The 06-07 modal→felt scroll-snap refactor dropped LOCK HAND (→ AUTO DRAW +
auto-completion on the 6th draw) and moved the deck stacks into the cross-col
(`.sea-page--room:not(.sea-spread-chosen) .sea-cross-col{display:none}`), but the
channels `PickSeaDealTest` was never updated → 3 reds in the two-browser/channels
CI stage:
• test_lock_hand_btn_present_and_disabled + test_lock_hand_enables_after_six_draws
  — looked up the removed `id_sea_lock_hand` (NoSuchElement).
• test_clicking_stack_shows_ok_btn — the stack OK is hidden until a spread is OK'd.

Fix: deleted the disabled-LOCK-HAND test; rewrote the six-draws test to assert the
new synchronous completion (the deck-stack FLIP btns gain `.btn-disabled` the
instant the 6th card lands, before the 3s felt cascade — SEED MAP is IT-covered);
added a `_choose_spread()` helper (clicks `id_sea_confirm_spread`) so the
stack-interaction tests run against the revealed cross page. Pre-existing 06-07
staleness — NOT tonight's glow/swap/applet commits (verified: `_load_sea_overlay`
opens the sea felt with no sky felt open, so the new swap guard is a no-op).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 02:00:47 -04:00
Disco DeDisco
564100cadb Applets: retitle My Sky → SkyDrive + My Sea → Sea of Cards w. planetary recolor; match the gear-menu entries — TDD
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed
Applet titles + gear-menu toggle labels (Applet.name, via data migration 0014)
both retitled — a deliberate departure from the "My X" applet convention for
these two surfaces. Slugs (my-sky / my-sea) stay put.

Recolor (applet shells only — the standalone pages are tomorrow's todo):
• SkyDrive (uranium ramp): --sixU light-green title font, --priU deep-green
  shell bg; hover keeps the palette --ninUser highlight, glow takes the --sixU
  tint. Reverses to --priU font / --sixU bg on *-light palettes.
• Sea of Cards (neptunium ramp): --sixNp light-teal font, --priNp deep-teal shell;
  the conjunction "of" is a lowercase-italic span against the h2 uppercase
  transform. Reverses to --priNp / --sixNp on *-light palettes.

Mechanism: the shared applet title-link rule now reads --applet-title-fg (with a
--terUser fallback so every other applet is untouched); each applet sets that +
--applet-shell-bg, and a body[class*="-light"] override swaps the pair. No
specificity war.

Tests: data migration 0014 (reversible); FT/IT applet seeds + the My Sky heading
assertion updated to the new names; 531 dashboard/gameboard/applets ITs green.

[[feedback-applet-vs-page-naming-convention]] [[feedback-scss-id-context-specificity-trap]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 01:29:29 -04:00
Disco DeDisco
945d110171 Sea Select: keep the sky btn lit on a revisited felt (+ clean felt-swap); slow the glow-handoff ease-out — TDD
Symmetry (user-spec): Sky Select leaves the burger sea btn lit while the sky
felt is up; Sea Select now mirrors it — openSea disables ONLY id_text_btn, no
longer id_sky_btn, so a revisited Sea Select keeps the completed sky one click
away. An adversarial pass flagged that the two felts are equal-z (z-index:5)
siblings and neither open path drops the other's open-class, so leaving the sky
btn lit and clicking it from inside the sea felt would DOUBLE-OPEN (sea paints
over sky → confusing no-op) — the same latent stack already reachable in the
sky->sea direction on a both-complete reload. Fixed in BOTH directions: openSea
now closes the sky felt first + openSky closes the sea felt first (each exposes
its close on window: closeSeaFelt / closeSkyFelt), so clicking the other phase's
lit btn performs a clean SWAP. The text-btn disable/restore chain stays correct
across the swap (closeSky restores text, openSea re-disables + recaches it).

Glow: the glow-handoff pulse ease-OUT now runs ~2.8s (was ~1.4s) — moved the
cycle to 3.2s with the bright peak at 12.5%, keeping the ease-IN swell at ~0.4s
(user asked for a longer linger).

[[feedback-felt-aperture-fill-covers-felt]] [[feedback-inline-partial-script-defer-for-later-partial]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 00:57:24 -04:00
Disco DeDisco
82f4af9bcc Sea Select glow: defer sea-btn reopen + glow handoff past parse time; pulse the glow-handoff — TDD
room.html includes _sea_overlay.html (~L77) BEFORE _burger.html (~L167, which
holds #id_burger_btn + #id_sea_btn), so the overlay's two inline scripts
captured those btns at parse time → null → both bindings silently no-op'd:
the sea btn (active post-completion) did nothing on click + the burger stayed
stuck --priId because its glow-handoff transfer listener never bound. Defer both
the sea-btn REOPEN binding (_bindSeaReopen) & the burger→sea_btn→.sea-select
GLOW chain (_bindGlowHandoff) to DOMContentLoaded so the burger fan exists first.

Also make the glow-handoff halo PULSE (quick ease-in swell, slow ease-out decay
via per-segment timing fns + a lopsided 22/78 keystop split) instead of a flat
glow — the burger, then the sea btn after the handoff click, keep cueing
"click me to reopen your sea".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 00:39:38 -04:00
Disco DeDisco
0a6bfcf6cc Sea Select options: fixed-width chunks + 2-line spread names (add Rider-) — TDD
- The option chunks' width tracked the select label length (jumped between the
  two spreads). Fix the `.sea-form-col` to 19rem (a touch wider) so all three
  chunks share a standard width.
- Split the spread label onto two lines around the comma + add the "Rider-"
  prefix now there's room: "Celtic Cross," / "Rider-Waite-Smith" and "Celtic
  Cross," / "Escape Velocity". combobox.js now writes the current label via
  `innerHTML` (not textContent) so the `<br>` survives a selection — plain-text
  options (sky / my_sea) are unaffected (their innerHTML == textContent).
- ITs updated for the new gameroom labels (my_sea's single-line names untouched).
  953 epic+gameboard ITs + Jasmine green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 00:25:11 -04:00
Disco DeDisco
2bf439eab5 Sea Select options: disabled-btn contrast + AUTO DRAW scrolls back to the cross
- A disabled OK/DEL × inside a --priUser option chunk blended into it (the global
  `.btn-disabled` bg is also --priUser → no visible circle). Drop the disabled
  btns in `.sea-options-col` to the felt --duoUser so they read as a distinct
  disabled circle, like the deck-stack FLIP ×.
- AUTO DRAW now eases the felt back UP to the cross even when the user already
  OK'd the spread + scrolled DOWN to the options page — so he watches the cards
  land one-by-one. `_chooseSpread(slideIn)`: the OK reveal pins to the options
  (slide-in from above); AUTO DRAW (already chosen) skips the pin + just eases up
  to the cross. `_scrollToCross` now eases from the current scroll position.
- 12 PickSeaUnifiedFeltTest render ITs green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 00:14:25 -04:00
Disco DeDisco
1fe257a7a9 Sea Select options: OK beside the select + --priUser chunk rects — TDD
Restyle the spread-options page (post the scroll-snap refactor):
- OK `.btn-confirm` moves UP beside the `.sea-select` combobox (a new
  `.sea-select-row`), off the AUTO DRAW / DEL action row.
- OK gains `.btn-disabled` + × the moment the first card is drawn — inverse to
  DEL (which loses them then), simultaneous with the combobox locking. So
  `_chooseSpread` (OK) no longer locks; the lock + both btn states flip together
  at the first draw via `_setHasDrawn` + `_lockSpread`. Server-renders OK
  disabled/× when `saved_by_position`.
- The three chunks (spread/select/OK, the mini preview, AUTO DRAW/DEL) each get
  the same --priUser rounded rectangle as the GAME POST lines / composer
  (`_base.scss` `.form-control`): --priUser fill + half-alpha --secUser border +
  rounded + padding. The `.sea-form-col`/`-main` go transparent flex columns so
  the felt shows between the chunks.
- IT: OK enabled / DEL disabled when fresh; flips once a card is drawn.
  953 epic+gameboard ITs green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 00:02:14 -04:00
Disco DeDisco
edc9a49f06 Sea Select: refactor to scroll-snap options→cross (mirror Sky Select), drop the modal — TDD
The Gaussian spread modal couldn't hang off the burger #id_sea_btn anymore (that
button now also opens the felt). Mirror Sky Select's form→wheel scroll-snap
instead: the felt starts with the spread OPTIONS on the --duoUser felt; clicking
OK confirms the spread → the options shunt DOWN and the spread CROSS takes page 1
(scroll down to find the options again). No modal, no corner NVM.

- `_sea_overlay.html` restructured into `.sea-options-col` (the .sea-select
  combobox + mini preview + OK .btn-confirm + AUTO DRAW + DEL — NO deck stacks)
  and `.sea-cross-col` (the real .my-sea-cross + the Gravity/Levity deck stacks +
  the portaled stage). `#id_sea_overlay` is a `display:contents` passthrough so
  the two cols are the scroll-snap sections.
- OK (`#id_sea_confirm_spread`) → `_chooseSpread()`: adds `sea-spread-chosen` to
  the felt → SCSS engages `scroll-snap-type:y mandatory`, the cross-col gets
  `order:-1` (page 1), options shunt to page 2; locks the combobox; eases the
  scroller to the cross. AUTO DRAW also confirms first. A reload of an in-progress
  sea renders `sea-spread-chosen` (cross revealed) server-side.
- SCSS (`_sky.scss`): the sea felt is now a column scroller; `.sea-cross-col`
  `display:none` pre-confirm; the `sea-spread-chosen` scroll-snap block mirrors
  `body.sky-saved`. The options `.sea-form-col` goes transparent/content-sized
  (blends onto the felt, not the modal's --priUser card).
- Sea sub-btn: no longer activated by openSea; it's the POST-COMPLETION reopen
  affordance (cascade activates it + `sea_btn_active = hand_complete` ctx flag),
  an active click → `window.openSeaFelt()` (review the saved spread), like the
  sky btn. Removed the sea_btn open-modal IIFE + the corner NVM.
- IT: options-on-felt (combobox + OK + AUTO DRAW + DEL + preview) w. NO modal /
  NVM. 952 epic+gameboard ITs + Jasmine + PickSeaAsyncTransitionTest(3) green.

my_sea.html keeps its modal (untouched) — the gameroom intentionally diverges.

[[project-character-creation-spec]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 23:53:43 -04:00
Disco DeDisco
cf84fdc992 Sea Select: post-completion cascade — felt eases out → DRAW SEA → SEED MAP — TDD
Mirror CAST SKY's post-save cascade for the sea phase. When the 6-card spread
completes (live FLIP of the 6th card / AUTO DRAW finishing): linger ~3s on the
felt → the felt eases OUT (`.sea-page--cascade-out`, revealing the table-hex) →
DRAW SEA gives way to SEED MAP + the sea glow fires on the burger (handoff →
sea_btn) → +3s → SEED MAP eases IN. Same shape as CAST SKY → sky-btn glow →
DRAW SEA.

- `_room_hex_center.html`: SEED MAP joins the hex-phase-stack; DRAW SEA goes
  --out once `hand_complete`, SEED MAP --out until then (a reload of a complete
  sea lands on SEED MAP server-side = the cascade's end-state). SEED MAP → the
  Voronoi map (roadmap step 21) is a stub — it only needs to APPEAR here
- `_sea_overlay.html`: `_setComplete(on, live)` runs `_startSeaCascade()` on the
  LIVE completion (FLIP / AUTO DRAW pass `live=true`; init does not, so a reload
  doesn't re-animate). The completion-glow IIFE no longer self-starts on the
  data-state transition — the cascade adds `glow-handoff` to the burger; the IIFE
  keeps only the burger → sea_btn → .sea-select handoff
- `.sea-page--cascade-out` SCSS (mirrors `.sky-page--cascade-out`)
- ITs: SEED MAP --out pre-completion (DRAW SEA in); SEED MAP in + DRAW SEA --out
  when hand_complete. 952 epic+gameboard ITs + PickSeaAsyncTransitionTest(3) green

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 23:03:17 -04:00