Commit Graph
Select branches
Hide Pull Requests
main
pre-drf
Select branches
Hide Pull Requests
main
pre-drf
-
82813e9fc1
A.5 my_sea.html central sig card image-rendering + SCSS lift-out fix — TDD. Sprint A.5 of [[project-image-based-deck-face-rendering]]: second visible surface after my_sign A.3. When the user's equipped deck is image-equipped (Minchiate today), the central significator card in the Celtic-Cross-style spread (
.sig-stage-card.sea-sig-cardinside .sea-pos-core) renders the transparent-PNG <img> w. contour-following arcana-color drop-shadow stroke + tray-card silhouette black shadow — same visual identity as my_sign's saved-sig stage card so the user's "this is my sig" anchor reads the same across both surfaces. Server-side template branch onsignificator.deck_variant.has_card_images: image branch renders<img class="sig-stage-card-img" src="{{ significator.image_url }}">+ adds.sig-stage-card--imagemarker class +data-arcana-key="{{ arcana }}"for the stroke-color selector; text branch keeps the existing corner-rank + suit-icon render unchanged (Earthman, RWS). No JS needed — central sig is statically rendered (vs my_sign's stage card which is JS-populated from the picker grid). Critical SCSS lift-out: the A.3.sig-stage-card--imagerule lived nested inside.sig-stage .sig-stage-card, scoped to my_sign.html's stage container only. my_sea's central sig isn't inside.sig-stage(lives in .sea-pos-core), so the rule wasn't applying — image rendered at native pixel dimensions (~620×1024 PNG) instead of being constrained to the card container, showing only a top-left portion (user bug-report 2026-05-25 PM: "It doesn't scale the img down for the sig — just a portion of the full img"). Fix: moved the entire.sig-stage-card.sig-stage-card--image { ... }block OUT of the.sig-stagenest into top-level scope so it applies to ANY.sig-stage-cardcarrying the--imageclass regardless of parent (my_sign's.sig-stage, my_sea's.sea-pos-core, future room.html's table center, future deck-bag UI). Same lift-out also expands thedisplay: nonelist to include.fan-corner-rank+> i.fa-solid— these elements appear in my_sea's text-mode central sig and need hiding when image-mode kicks in (my_sign's text mode uses the wrapped.fan-card-corner+.fan-card-faceclasses which were already covered). 2 new ITs inMySeaPickerPhaseTemplateTest: image-equipped Minchiate sig renders.sig-stage-card--imageclass + <img> w. correct v2-convention src; non-image Earthman keeps.fan-corner-ranktext + lacks --image class. Earthman Minchiate test fixture needs the super-nomad + super-schizo Note unlocks (granted manually viaNote.grant_if_newsince the post_save signal only fires on initial user creation, and we promote-to-superuser AFTER create) to let Il Matto (MAJOR 0) through_filter_major_unlocks. Tests: 2 new green; 1300/1300 IT+UT total green (70s; +2 from 750fef8's 1298). Visual verify pending: refresh /gameboard/my-sea/ w. Minchiate equipped + Il Matto as sig → central sig card should now scale the back image to fit the card container instead of showing a top-left crop. Sea Stage modal + drawn-card slot rendering (the bigger A.5 scope) still pending — they go through stage-card.js + the my-sea draw fetch endpoint, which need data-attr + JSON-payload extensions in a follow-up commit
Disco DeDisco
2026-05-25 01:20:07 -04:00 -
750fef890e
A.4 cont.: card-deck icon placeholder mode (no-deck-equipped) styled like empty dice slot — TDD. User polish 2026-05-25 PM after browser-verifying the kit-bag dialog: when no deck is equipped (kit-bag-placeholder branch fires) the new card-stack icon should not animate + should render w. the same dimmed
rgba(--quaUser, 0.x)palette as the existing fa-dice empty slot next to it, not the bright --terUser stroke / --priUser fill of an equipped-deck icon. Two SCSS changes: (1) Removed.deck-stack-icon:hover+.deck-stack-icon:activefrom the splay-trigger selector list — only wrapper-based selectors (.token.deck-variant, .kit-bag-deck) fire the fan-out now. Placeholder icons land inside .kit-bag-placeholder (or game_kit applet's .kit-item empty-state) which aren't in the trigger list, so they stay static no matter where the user hovers. (2) New.kit-bag-placeholder .deck-stack-icon+.kit-item .deck-stack-icondescendant-selector rule: drops color (= stroke via currentColor) torgba(--quaUser, 0.3)matching the existing .kit-bag-placeholder color (_game-kit.scss:143); drops .deck-stack-icon__card fill torgba(--quaUser, 0.15)(lower than the stroke alpha — user-dialed 2026-05-25 PM for a subtler look, the cards read as faintly-present "absence" rather than bright-but-grey). 1 new IT inKitBagViewTest(clears equipped_deck → asserts .kit-bag-deck absent, .kit-bag-placeholder present + carries svg.deck-stack-icon + lacks fa-id-badge). Tests: 1 new green; 1298/1298 IT+UT total green (71s; +1 from d26c45b's 1297). Visual verify pending: refresh /gameboard/ + clear equipped deck → kit-bag dialog Deck slot should show a dimmed static card-stack icon matching the dice slot's washed-out look
Disco DeDisco
2026-05-25 01:07:24 -04:00 -
d26c45bf77
A.4 cont.: deck back-image renders inside card-stack icon + kit-bag dialog Deck section adopts the icon + size+pattern polish — TDD. Three follow-up improvements after user browser-verified A.4's first cut: (1) image-equipped decks (Minchiate today, future Earthman) now render the deck's actual <deck-slug>-back.png as the card-stack icon's visible faces instead of the placeholder --priUser solid fill — feels like a real deck, not a generic stand-in. (2) The kit-bag dialog Deck section (
#id_kit_bag_dialog .kit-bag-deck) gets the same new card-stack icon (was still showing the old fa-regular fa-id-badge), with(×2)tooltip decoration on polarized decks for consistency w. the gameboard applet. (3) Visual polish: icon bumped 1.5× (1.5rem → 2.25rem width; 2.4rem → 3.6rem height, 5:8 aspect preserved); SVG <pattern> switched frompatternUnits=userSpaceOnUse(which painted the image at fixed user-space coordinates and let the rect slide out from under it on hover, reading as "low opacity" to the user) topatternUnits=objectBoundingBox + patternContentUnits=objectBoundingBox(transform-aware — image tracks the rect through rest-state offsets + hover fan-out). NewDeckVariant.back_image_urlproperty mirrors A.2'sTarotCard.image_urlpattern: returns full static-asset URL for<deck-slug>-back.pngwhen has_card_images=True, else empty string. Template partial_deck_stack_icon.htmlextended w. conditional<defs><pattern>block that renders only whendeck.has_card_imagesis true; each of the 3 card rects then carries an inlinestyle="fill: url(#deck-back-<short_key>)"overriding the SCSS defaultfill: rgba(--priUser, 1)(inline style beats CSS, the only way to opt out of the cascade default per-element). When no deck is passed (kit-bag placeholder branch) or deck has no images (Earthman + RWS), the partial falls through to the placeholder fill — single template handles both modes._kit_bag_panel.htmlDeck section: equipped-deck branch swaps<i class="fa-regular fa-id-badge">for{% include _deck_stack_icon.html with deck=equipped_deck %}+ adds(×2)span in --terUser forequipped_deck.is_polarized; placeholder branch swaps for the same include withoutdeck=so the partial's conditional falls through. SCSS reorg: lifted the.deck-stack-iconbase rules out of the#id_applet_game_kitnest (they were scoped to gameboard's Game Kit applet only) into top-level scope so the same SCSS applies in the kit-bag dialog context too. Hover/active/focus trigger selector list broadened to cover.deck-stack-iconitself +.token.deck-variantwrapper +.kit-bag-deckwrapper. 4 new ITs total: 2 inGameboardViewTest(image-equipped Minchiate's <pattern> defines + inline fill style on all 3 rects + asset URL ref; non-image Earthman has NEITHER pattern nor inline fill); 2 indashboard.KitBagViewTest(kit-bag Deck section renders svg.deck-stack-icon + lacks fa-id-badge; polarized equipped deck tooltip carries .tt-x2 — element-presence assertion since literal "×2" character had encoding issues in the dashboard test file vs the gameboard one, which is fine since the template-side rendering of the literal × is exercised by the parent template). Tests: 4 new green; 1297/1297 IT+UT total green (69s; +4 from A.4's 1293). Visual verify pending: refresh /gameboard/ → Minchiate icon should show 3 stacked Minchiate card-backs at 1.5× size, fan out on hover w. back image tracking; refresh kit-bag dialog → same icon visible in Deck section w. (×2) on Earthman tooltip
Disco DeDisco
2026-05-25 01:01:05 -04:00 -
b9bb73db69
A.4 card-deck stack icon + game_kit applet's Card Decks polarization (×2) tooltip decoration — TDD. Sprint A.4 of [[project-image-based-deck-face-rendering]] (folded down from the originally-standalone Sprint D per [[project-card-deck-icon]] 2026-05-25 PM scope-fold). Replaces the
<i class="fa-regular fa-id-badge">placeholder on.token.deck-variantin the gameboard's Game Kit applet w. a new inline SVG card-stack icon: 3 rect children (rx=2.5, 20×32 viewport units inside a 32×48 viewBox to land 5:8 tarot card aspect), stacked tightly at rest w. ±0.4px vertical micro-offsets (suggests stack depth without separating cards visually), whole stack rotated 5° clockwise via.deck-stack-icon__stackgroup transform. On:hover/:active/:focusof the parent.token.deck-variant, cards 2 + 3 fan out symmetrically — card 2 translates (-5px, -2px) + rotates -12°, card 3 translates (+5px, -2px) + rotates +12° — card 1 stays put on top. Fan-out CSS pseudo-classes match the existing JS-portal tooltip trigger so the splay animation + tooltip-appearance co-activate as user spec'd. Placeholder card-back design: solid--priUserfill +currentColorstroke (=--terUser); detailed Earthman planet-impact illustration deferred to a future art-asset commit (the SVG structure is ready to receive richer fills + pattern elements without re-jigging the stack/fan transforms). Drop-shadow for "lifted off the felt" depth cue:0.08rem 0.08rem 0.15rem rgba(0, 0, 0, 0.6)— softer than the my-sign-stage card's tray-card-style 1,1 black silhouette since the icon is small + always on a felt background. SVG itself usesoverflow: visibleso the fan-out exceeds the viewBox bounds;transform-box: fill-box+transform-origin: 50% 50%ensure rotation centers on each card's own geometric center (not the viewBox center). New_deck_stack_icon.htmlpartial intemplates/apps/gameboard/_partials/keeps the SVG markup DRY for the future room.html pile + deck-bag rollouts (per [[project-card-deck-icon]] "other surfaces deferred to later sprints"). New.tt-x2style in%tt-token-fieldsplaceholder mixin —--terUsercolor + font-weight 600 — appended inline in.tt-descriptionforis_polarized=Truedecks (Earthman today): "106-card Tarot deck (×2)" where the (×2) signals "double-polarized = 6 segments = fills 2× as many seats" per [[project-card-deck-icon]]'s decoration rule. Non-polarized decks (Tarot RWS, future Minchiate) render the description without the suffix. 3 new ITs inGameboardViewTest: SVG card-stack renders w. 3 rect children + fa-id-badge gone; polarized Earthman tooltip carries.tt-x2w. "×2" content; non-polarized RWS tooltip lacks.tt-x2. Out of scope this commit: the dedicated /game-kit/ page's.gk-deck-cardrectangles (different template —_game_kit_sections.html) keep their fa-id-badge for now; folding them into the new icon happens in a follow-up "Card Decks rectangle teardown" sprint per user spec ("by the time we finish A.8 the dynamically shaped rectangles around the deck <i> els and their names will be no more"). Tests: 3 new ITs green; 27/27 GameboardViewTest class green; 1293/1293 IT+UT total green (68s; +3 from A.3-polish's 1290). Visual verify pending: browser refresh expected to show the stacked-3-card icon w. 5° rest tilt, fan-out on hover, tooltip + (×2) decoration on Earthman
Disco DeDisco
2026-05-25 00:40:10 -04:00 -
436a710478
A.3-polish-2: thicker contour stroke (0.2rem each cardinal) + tray-card down-right black silhouette drop-shadow. User visual polish after browser-verifying A.3+A.3-polish: stroke thickness bumped from 1.5px → 0.2rem (~3.2px) per cardinal, giving ~6.4px combined apparent stroke (~2× prior); user-confirmed thickness is comfortable in palette-baltimore at the current sig-card-w. Bonus: appended the same silhouette black drop-shadow that
.tray-cell > imgcarries (_tray.scss:272,drop-shadow(1px 1px 2px rgba(0,0,0,1))) to the sig-stage-card image filter chain as a "lifted off the felt" depth cue — consistent w. the rest of the project's image-card treatment. Ordering matters in the filter chain: silhouette black comes AFTER the 4 stroke drop-shadows so it traces the STROKED contour, not just the original PNG alpha (otherwise the depth shadow would land underneath the orange-or-mustard stroke, partially occluded). 4-cardinal stroke is still adequate at 0.2rem; flagged in comment to bump to 8-direction (cardinals + diagonals) if we ever push past ~0.5rem since curved edges would otherwise show uneven thickness at gap-prone diagonals. Pure SCSS — no model/template/JS/test changes. Visual-only polish atop 50a12bc's A.3-polish
Disco DeDisco
2026-05-25 00:28:09 -04:00 -
50a12bccab
A.3-polish: cross-deck sig picker (MINOR + MIDDLE courts) + My Sea applet sig-decoupling — TDD. Two user-reported bugs caught during A.3 visual verify (2026-05-25 PM). Bug 1: my_sign picker shows only 2 cards (Major 0 + 1) for Minchiate-equipped users since
_sig_unique_cards_for_deckfilters byarcana=MIDDLEwhich Minchiate (and any non-Earthman tarot family) doesn't classify its courts as — Minchiate courts are MINOR per its standard structure. User spec confirmed: my_sign picker = courts + Major 0/1 for EVERY deck (NOT segment-limited, NOT arcana-classification-limited). Fix: broaden the filter toarcana__in=[MIDDLE, MINOR]so courts qualify regardless of how the deck classifies them. For Earthman, behavior unchanged (no MINOR 11-14 cards exist in seed — its courts are exclusively MIDDLE); for Minchiate + RWS, picker expands from 2 → 18 cards as designed. Two side-by-side suit queries (brands_crowns + blades_grails) collapse to a single 4-suit query since the union was already covering all 4 — that was historical artifact, not segment-limiting in effect. Bug 2: deleting the user's sig on /billboard/my-sign/ blanks the My Sea applet on /gameboard/ even though the saved MySeaDraw spread is still in the DB (visible on /billboard/my-sea/), reappearing only when any sig is re-selected. Root cause:_applet-my-sea.htmlgated the slot-render branch on{% if not request.user.significator_id %}first, treating no-sig as "no draws yet" regardless of actual draw state. But MySeaDraw rows carry their ownsignificator_idsnapshot at first-draw time (gameboard.models.MySeaDrawdoc lines 130-132) precisely so user-sig clearing doesn't invalidate saved draws — the template ignored that contract. Fix: invert the template branches — slot render now keys solely onmy_sea_slots; the sig-gate Brief banner only fires in the empty-state branch when ALSOnot request.user.significator_id(the "fresh user, no draws, no sig" case). MySeaDraw display now correctly decoupled from current sig state — sig deletion only matters for users who haven't drawn yet. Companion code:_sig_unique_cards_for_deckdocstring updated to articulate the cross-deck symmetry rule ("courts recognized by rank 11-14 regardless of arcana classification") + the spec-confirmed non-segment-limitation. 1 new regression IT inGameboardViewTest.test_my_sea_applet_renders_slots_even_when_user_significator_clearedlocks Bug 2's fix: creates a MySeaDraw row w. one filled slot, then sets User.significator=None, GETs /gameboard/, asserts the filled slot still renders + "No draws yet" empty state is absent. Tests: 1 new IT green; 810/810 epic+gameboard+billboard ITs green; 1290/1290 IT+UT total green (70s, +1 from A.3's 1289). No FT changes needed — Bug 1's fix changes the count of cards in the picker grid; existing FTs that count cards target Earthman where the count is unchanged. Visual verify still pending; user will confirm both fixes via Claudezilla browser session
Disco DeDisco
2026-05-25 00:16:55 -04:00 -
5e78e6b832
A.3 my_sign.html image-rendering — first visible surface — TDD. Sprint A.3 of [[project-image-based-deck-face-rendering]]. When the user's equipped deck has
has_card_images=True(Minchiate Fiorentine 1860-1890 today), the saved-sig stage card on /billboard/my-sign/ renders as an <img> over the irregular-shape transparent PNG with a contour-following arcana-colored stroke — not the text fan-card scaffold. First of 6 surfaces in the image-rendering rollout (my_sea + both billboard applets + room + game_kit follow in A.5+). NewTarotCard.image_urlproperty (consumes A.2's image_filename + DeckVariant.has_card_images + django.templatetags.static.static() to produce a full static-asset URL) — empty string when has_card_images=False so legacy text-only decks (Earthman, RWS) pass through transparently.my_sign.htmlpicker grid.sig-cardelements gaindata-image-url+data-arcana-keyattrs (the latter for stroke-color CSS selection); the.sig-stage-cardscaffold gains a hidden<img class="sig-stage-card-img">slot that JS swaps visible when image-mode is active.stage-card.jsextendsfromDatasetto read image_url + arcana_key; new_setImageMode(stageCard, card)toggles the.sig-stage-card--imagemarker class + setsdata-arcana-keyon the stage card + populates the img src/alt; called frompopulateCardso all existing sig-stage flows pick up image rendering automatically (text-mode decks still pass through since image_url is empty). SCSS: new.sig-stage-card.sig-stage-card--imagerule hides the.fan-card-corner+.fan-card-facetext scaffold, strips the rectangular border/padding, and applies a 4-cardinal-directionfilter: drop-shadow()stack to the<img>so the stroke FOLLOWS the alpha contour of the PNG instead of tracing a rectangular bounding box (per user spec 2026-05-25 PM clarification — early draft used a rectangular border which doesn't match the irregular-card aesthetic). Stroke color is driven by a CSS custom prop--img-stroke-colordefaulting torgba(var(--quiUser), 1)(cream — minor + middle arcana);[data-arcana-key="MAJOR"]override flips it torgba(var(--terUser), 1)(gold) per Q2 lock. mobile-safe — filter on raster images works cross-browser (the [[feedback-mobile-svg-glow]] dead-end was specifically SVG glow, not raster drop-shadows). New_seed_minchiate_image_fixtures()helper infunctional_tests/sig_page.pyre-seeds the minimal Minchiate fixture (DeckVariant + Il Matto + Papa Uno) needed for image FTs after TransactionTestCase's flush wipes migration data — mirrors the existing_seed_earthman_sig_pilepattern per [[feedback-transactiontestcase-flush]]. NewMySignImageRenderingTest.test_saved_sig_renders_as_img_for_image_deckFT seeds Minchiate + creates a superuser test gamer (superuser auto-gets super-nomad + super-schizo Notes via the User post_save signal, which_filter_major_unlocksthen lets through to expose Il Matto in the picker grid — otherwise Minchiate's sig pool is empty since it has no MIDDLE arcana cards), equips Minchiate, saves Il Matto as sig, visits /billboard/my-sign/, asserts the stage card displays + contains an <img> w. src ending in the v2-convention filenameminchiate-fiorentine-1860-1890-trumps-00-il-matto.png+ carries.sig-stage-card--imagemarker class. Out of scope for this commit (deferred to A.3 follow-up polish + A.5+): the full stat-block restructure (top-left rank+suit chip Q♥ inline w. EMANATION/REVERSAL header; title in arcana-color font; keyword reposition; FYI panel re-anchor — per the locked Q3 spec) — image card-face ships now w. the existing stat-block layout to land the visible-win first. Tests: 1 new FT green; 15/15 my_sign FT class green (no regression on the 14 existing tests); 1289/1289 IT+UT total green (68s, unchanged from A.2 since no new ITs in this commit — FT covers the wiring end-to-end). Sprint A backend foundation (A.0+A.1+A.2) + first visible surface (A.3) all landed; 5 surfaces remain (A.5-A.8 + A.4's card-deck icon)
Disco DeDisco
2026-05-25 00:04:18 -04:00 -
91df482dd8
A.2 TarotCard.image_filename + display_suit_name properties — TDD. Sprint A.2 of [[project-image-based-deck-face-rendering]]. Adds two per-card derived properties that consume the new
DeckVariant.familyfield (locked in A.0) to translate canonical-Earthman SUIT enum (BRANDS/CROWNS/GRAILS/BLADES) into family-authentic filename slugs + UI labels per [[reference-card-image-naming-convention]] v2.DeckVariantgains the family-mapping tables + methods (suit_slug/suit_display/trump_category);TarotCardconsumes them viaimage_filename+display_suit_name. Two mapping tables live on DeckVariant (single source of truth for per-family vocab):_SUIT_SLUG_BY_FAMILY(4 families × 4 suits = 16 entries: earthman is identity-mapped {BRANDS→brands, CROWNS→crowns, GRAILS→grails, BLADES→blades}; italian is {BRANDS→batons, CROWNS→coins, GRAILS→cups, BLADES→swords}; english is {BRANDS→wands, CROWNS→pentacles, GRAILS→cups, BLADES→swords}; playing is {BRANDS→clubs, CROWNS→diamonds, GRAILS→hearts, BLADES→spades}) and_TRUMP_CATEGORY_BY_FAMILY(earthman+italian use "trumps", english uses "majors" matching Modern Tarot's "Major Arcana", playing is None since 52-card decks have no trump category — jokers handled separately when a playing deck is seeded).DeckVariant.suit_slug(canonical)returns the filename slug;suit_display(canonical)returns capitalized UI label (via slug.capitalize());trump_categoryis a property since it takes no per-card argument.TarotCard.image_filenamebranches on arcana: MAJOR returns<deck-slug>-<trump-category>-<NN>-<card-slug>.png(NN = zero-padded number per v2 convention, e.g. 00 for Il Matto; card-slug carries the italian name like "il-gobbo" or english like "the-fool"); MINOR/MIDDLE returns<deck-slug>-<suit-slug>-<NN>[-<court>].pngwhere court suffix is "page"/"knight"/"queen"/"king" for ranks 11-14 (tarot family courts; playing-family's 3-court jack/queen/king deferred to playing-deck-seed sprint).display_suit_namereturns capitalized family-authentic suit name ("Batons" for italian BRANDS, "Pentacles" for english CROWNS) or empty string for major arcana (no suit). Both properties are pure-derived — no schema migration needed, no DB writes; the template (Sprint A.3+) decides whether to render <img src=image_filename> based ondeck.has_card_images. RWS deck's image_filename returns a path even though has_card_images=False (path is correct per convention; just no file exists at that path yet — once RWS images are sourced, flip the flag). 17 new ITs inCardImageFilenameA2Testcover: Minchiate trumps (Il Matto rank-00, Il Gobbo rank-11, Le Trombe rank-40, L'Acqua rank-21 w. apostrophe-restored slug); Minchiate minors (Ace of Batons pip-with-no-court-suffix, Ten of Coins, Page of Cups w. court suffix, King of Swords); RWS post-revocab (Ace of Cups uses english-family "cups" slug despite suit=GRAILS, The Fool uses "majors" category, King of Pentacles uses "pentacles" slug despite suit=CROWNS); Earthman identity-mapped (BRANDS→brands); display_suit_name across all 3 tarot families (italian BRANDS→"Batons", italian CROWNS→"Coins", english CROWNS→"Pentacles", earthman BRANDS→"Brands"); empty for majors. Tests: 17 new green; 1289/1289 IT+UT total green (63s; +17 from A.1's 1272). Out of scope: A.3 wires my_sign.html's first render branch (the visible-win first surface); A.4 builds card-deck icon + game_kit applet; A.5-A.8 DRY across my_sea + both billboard applets + room
Disco DeDisco
2026-05-24 23:37:16 -04:00 -
a4ac25605d
A.1 seed Minchiate Fiorentine 1860-1890 deck (97 cards) — TDD. Sprint A.1 follow-up to [[project-image-based-deck-face-rendering]]. Creates the actual Minchiate Fiorentine
DeckVariant(separate from the renamed-from-fiorentine-minchiate RWS Tarot now living attarot-rider-waite-smithper A.0). Slugminchiate-fiorentine-1860-1890matches the asset dir committed in0add163(98 PNGs atsrc/apps/epic/static/apps/epic/images/cards-faces/minchiate-fiorentine-1860-1890/); Sprint A.2'simage_filenameproperty will usedeck.slugto point at those images. Schema fields set:family='italian'(drives display + filename slug mapping per [[reference-card-image-naming-convention]] — BRANDS→batons, CROWNS→coins, GRAILS→cups, BLADES→swords),has_card_images=True(first deck w. images shipped),is_polarized=False(Earthman remains the only polarized deck),is_default=False(Earthman is default),card_count=97. 97 TarotCard rows seeded: 41 trumps (Il Matto at rank 0 per the unnumbered-Fool-gets-sortable-position convention from [[reference-card-image-naming-convention]]; then 40 numbered 1-40) + 56 minors (4 suits × 14 cards = pip 1-10 + page=11 + knight=12 + queen=13 + king=14 per the v2 convention's number-prefixed-courts decision). Trump names are Italian (Papa Uno / Papa Due / La Temperanza / La Forza / La Giustizia / La Ruota della Fortuna / Il Carro / Il Gobbo / L'Impiccato / La Morte / Il Diavolo / La Casa del Diavolo / La Speranza / La Prudenza / La Fede / La Carita / Il Fuoco / L'Acqua / La Terra / L'Aria + 12 zodiac signs + La Stella / La Luna / Il Sole / Il Mondo / Le Trombe). Card-suit canonical enum stays BRANDS/CROWNS/GRAILS/BLADES per A.0's lock; minor card NAMES use Italian-family display vocab ("Page of Batons" not "Page of Brands") since names are the user-facing label whereas suit is the structural identity. 16 trumps carry acorrespondencefield pointing to their RWS Tarot equivalent (Il Matto→The Fool, Il Carro→The Chariot, Il Gobbo→The Hermit, L'Impiccato→The Hanged Man, La Morte→Death, Il Diavolo→The Devil, La Casa del Diavolo→The Tower, La Temperanza→Temperance, La Forza→Strength, La Giustizia→Justice, La Ruota della Fortuna→Wheel of Fortune, La Stella→The Star, La Luna→The Moon, Il Sole→The Sun, Il Mondo→The World, Le Trombe→Judgement); the 25 Minchiate-only trumps (5 popes + 4 theological/cardinal virtues + 4 elements + 12 zodiac) have no RWS parallel → empty correspondence.keywords_upright/keywords_reversedintentionally left empty[]: those are interpretive content the user owns; admin form (Sprint B) will enrich via UI rather than have them committed as code in a migration. Five trumps in the v2 filename convention have elided-apostrophe slugs restored (l-impiccato, l-acqua, l-aria, l-ariete, l-acquario); DB slug field matches (no apostrophe, but with the leadingl-prefix). 17 new ITs inMinchiateFiorentine1860SeedTestcover the deck attributes (name + family + has_card_images + is_polarized + card_count) + total row count (97) + arcana breakdown (41 trumps + 56 minors + 0 middle) + specific cards (Il Matto at rank 0 + Il Gobbo at rank 11 w. correspondence "The Hermit" + Le Trombe at rank 40 + 5 popes are MAJOR ranks 1-5 + Page of Batons + King of Coins) + canonical-suit-only check (no WANDS/CUPS/SWORDS/PENTACLES in DB) + court rank range (11-14 per suit). Tests: 17 new green; 1272/1272 IT+UT total green (64s; +17 from A.0's 1255). Out of scope: A.2 adds theTarotCard.image_filename+display_suit_nameproperties consumingdeck.familyfor per-family translation; A.3 wires my_sign.html's first render branch
Disco DeDisco
2026-05-24 23:32:19 -04:00 -
f107522b20
A.0 image-rendering schema + RWS rename + canonical-Earthman suit collapse — TDD. Sprint A.0 of [[project-image-based-deck-face-rendering]]. Adds three
DeckVariantfields:has_card_images(BooleanField default=True — Earthman keeps False until its artwork ships, every new deck defaults True),family(CharField choices=[earthman, italian, english, playing] default=earthman — drives per-family display + filename slug mapping per [[reference-card-image-naming-convention]]),is_polarized(BooleanField default=False — Earthman is True today; Sprint A.4 game_kit applet will render "(×2)" in --terUser for polarized decks; Sprint C+B segment model uses it for segment-count logic).TarotCard.SUIT_CHOICEScollapses from 8 values to 4 canonical Earthman values (BRANDS / CROWNS / GRAILS / BLADES); WANDS / CUPS / SWORDS / PENTACLES dropped — they were duplicative at the structural level sincesig_deck_cards+levity/gravity_sig_cardsalready treated [WANDS, BRANDS, CROWNS] as one segment and [SWORDS, BLADES, CUPS, GRAILS] as another (so the project already *functionally* equated them; the lock just makes that explicit). Per-family display vocab (batonsfor Italian,wandsfor English,clubsfor Playing) lives in Sprint A.2'sdisplay_suit_nameproperty, not in the enum. Audit 2026-05-25 revealed the existingfiorentine-minchiateDeckVariant is actually 78-card RWS Tarot in disguise (22 majors numbered 0-21 w. RWS names: The Fool / The Magician / ... / The World; 56 minors in 4 suits × 14 cards) — NOT Minchiate (which has 40 trumps + 1 Il Matto + 56 minors = 97 cards). Migration 0012 renames the slug →tarot-rider-waite-smith, name → "Tarot (Rider-Waite-Smith)", sets family='english', has_card_images=False, is_polarized=False — and revocabs its 56 minor cards' suits in-place (WANDS→BRANDS, CUPS→GRAILS, SWORDS→BLADES, PENTACLES→CROWNS) so they match the new canonical enum. FKs (User.equipped_deck, User.unlocked_decks, TableSeat.deck_variant, etc.) survive untouched — slug-only changes don't break referential integrity. Earthman fields set explicitly in 0012 too (family=earthman, has_card_images=False, is_polarized=True). Companion code simplifications:sig_deck_cards+_sig_unique_cards_for_deckqueries shrink fromsuit__in=[3 values]and[4 values]to[2 values]each (one per segment);TarotCard.suit_iconmapping shrinks from 8 entries to 4;gameboard.views.tarot_fan._suit_ordershrinks from 8 keys to 4. Existing test files updated:test_game_room_tray.py(largest update —self.fiorentine→self.rws,id_kit_fiorentine_deck→id_kit_tarot_deck(template-id derives from deck.short_key = first slug segment), assertion "Fiorentine" → "Rider-Waite-Smith");test_game_room_deck_contrib.py(same pattern, smaller);lyric/test_models.py+gameboard/test_views.py(slug literal swaps only);epic/test_models.py_make_sig_cardtest fixtures: "WANDS"→"BRANDS", "CUPS"→"GRAILS". 14 new ITs inDeckSchemaA0Testcover the schema additions + migration outcomes (field existence + choice values + earthman has all three fields set correctly + RWS rename verified + RWS cards use canonical suits + dropped enum values absent from SUIT_CHOICES). Tests: 14 new green; 1255/1255 IT+UT total green (38s); no regressions. Out of scope: Sprint A.1 will seed the actual Minchiate Fiorentine 1860-1890 (97-card) DeckVariant + TarotCard rows w. family='italian', has_card_images=True; A.2 adds theimage_filename+display_suit_nameproperties that consume the newfamilyfield; A.3+ wires the render branches across 6 surfaces
Disco DeDisco
2026-05-24 23:25:26 -04:00 -
0add163f5b
feat: import Minchiate Fiorentine 1860-1890 deck assets (98 PNGs, alpha-channel transparent bg) + naming convention v2 + pngquant optimization tooling. Public-domain 1860-1890 lithograph scans sourced from Wikimedia (single download series + trump 11 Il Gobbo individually-sourced from same era series); user removed white backgrounds in Photoshop to leave irregular card-shape with transparent canvas. Filenames v2-conformant per [[reference-card-image-naming-convention]] (revised from v1 of 2026-05-24): deck slug carries
-1860-1890publication-year suffix so future Minchiate Fiorentine variants from different publishers/eras coexist cleanly; courts use rank-number-prefix (batons-11-pagenotbatons-page) for linear sort key; trumps carry both numeric rank AND italian-name suffix (trumps-01-papa-uno,trumps-11-il-gobbo) for forensic identification across variant decks the user plans to sell. Il Matto (unnumbered Fool in Minchiate tradition) assigned rank00to give it a sortable position. Five trump filenames had elided-apostrophe slugs restored from the download source (-lacqua→-l-acquaetc.). Card-back at<deck-slug>-back.pngsorts alphabetically before all suit categories — no separatecard-back/subdir needed. pngquant 2.17.0 installed at C:\Users\adamc\AppData\Local\Programs\pngquant\, added to user PATH (effective next session) for future deck imports; ran with--quality=65-85 --speed=1 --strip --skip-if-largerfor 57.6% size reduction (86.6 MB → 36.7 MB total, 935 KB → 383 KB avg). Second pass at--quality=40-65hit pngquant's floor (only 0.5% further reduction — re-quantizing an already-quantized image has little headroom). Il Gobbo from Wikimedia was a dimensional outlier (1426x2366 vs siblings ~620x1024) — resized via System.Drawing HighQualityBicubic to 620x1029 before optimization. Format32bppArgb alpha channel verified intact across samples after optimization pass. Visually validated by user: cards must fill entire screen before any pixelization visible. Sprint A precursor —DeckVariant.has_card_imagestoggle + image-rendering template branch per [[project-image-based-deck-face-rendering]] follows in subsequent commits, will consume these assets in 6 surfaces (my_sign, my_sea, both billboard applets, room, game_kit). Asset set also unblocks downstream Sprint C+B [[project-deck-segment-model]] (admin form will require image upload + enforce naming convention) and Sprint D [[project-card-deck-icon]] (uses-back.pngas the deck-stack icon's repeating card-face). Future: when Sprint B's admin form ships, wire pngquant into anoptimize_card_imagesmanagement command so admin uploads auto-optimize on save. Gitignore linesrc/apps/epic/static/apps/epic/images/cards-faces/minchiate-fiorentine/dropped — v1 staging dir deleted (was only ever the rename-staging set; superseded by v2-named optimized set). Total disk delta: +36.7 MB binary content. No code changes — pure asset + convention import
Disco DeDisco
2026-05-24 22:39:21 -04:00 -
8a56ebff2c
fix: swap UserAdmin's
unlocked_decks+budsM2M widgets fromSelectMultipletofilter_horizontal(dual-listbox). The defaultSelectMultiplewidget renders ALL DeckVariant/User rows in a single listbox with only the currently-selected ones blue-highlighted — visually indistinguishable from "all of these are unlocked" if the reader doesn't notice the highlight state. This cost a half-hour staging bug investigation: Fiorentine Minchiate appeared in the listbox for admindiscoand was read as "unlocked", but the Game Kit + Card Decks applets correctly rendered only Earthman because only Earthman was actually inunlocked_decks(Fiorentine was an *available option*, not a selected value).filter_horizontalsplits into "Available" (left) + "Chosen" (right) panes with explicit arrows between — selected vs available is unambiguous. Same trap applies tobuds(also a bare M2M per [[project-deck-contribution-spec]] adjacent note), so fixing both. No model/template/test changes — just the admin widget.UserAdminTestonly exercises the changelist (/admin/lyric/user/), not the change form, so no test impact
Disco DeDisco
2026-05-24 17:05:14 -04:00 -
92df686d80
fix: significator_reversed=polarity bug + Pattern B name-swap rendering + qualifier-aware applet faces + sticky PAID DRAW + cooldown anchor on User + stat-block polarity unification across Sig/Sea/Fan/applets
Disco DeDisco
2026-05-23 15:06:35 -04:00 -
53cd7afeb4
feat: My Sea applet dynamic population + lay/leave POSITION_LABELS swap fix + My Sign applet stat-block + Brief-fied sign-gate + --duoUser olive on all four personal-data surfaces. Six visual+structural items batched across the dashboard/billboard/gameboard.
Disco DeDisco
2026-05-22 15:19:34 -04:00 -
1452de1a76
feat: My Sign saved-sig state — --duoUser bg, centred card+stat-block, stage card auto-rotates for reversed sigs on landing. Three follow-up polish items atop the
f609313read-only-saved-sig batch.
Disco DeDisco
2026-05-22 13:21:55 -04:00 -
f6093136f1
fix: shop tooltip price flex-pinned right (cross-file
#id_tooltip_portal .tt-title { display: block }was clobbering the flex h4) + My Sign page collapses to read-only card+stat-block when sig is saved + My Sign applet card gets proper 5:8 shell + Game Kit row space-evenly. Five visual polish items batched.
Disco DeDisco
2026-05-22 12:42:03 -04:00 -
e90f10fe47
feat: shop tooltip price moves to the title row, right-aligned --priGn. The
<h4 class="tt-title">already hasdisplay: flex; justify-content: space-between; align-items: baseline; gap: 0.5rem(from_tooltips.scss:31-46's.ttblock, originally meant for the.token-countchip pattern in Tokens row), so wrapping the name + price as two sibling<span>s inside the h4 auto-spaces: name pinned left, price pinned right, on the same baseline..tt-pricejoins.tt-expiry(priRd) +.tt-date(priGn) in the shared%tt-token-fieldsplaceholder at_tooltips.scss:8-19— same shape (1rem) as both, --priGn coloring to mirror.tt-date's "in the green" semantics for the payment cue. Standalone<p class="tt-price">line below the description is dropped (price now lives in the title row). 1211 IT/UT still green; no test changes needed — existing FT assertion (assertIn("$1", tithe1_tt)) reads.ttinnerHTML which still contains the dollar string in either position
Disco DeDisco
2026-05-22 02:32:05 -04:00 -
25f55f728a
feat: wallet Shop polish — microtooltip extraction, Shop-first ordering, DRY tooltip styling, writs rebalance, "no expiry" on all items. Visual-pass tweaks landing atop the 5-chunk Shop rollout (commits
8e476f5→d28cf7b). **Microtooltip extraction**:.tt-microbutton-portal(Chunk 4's wrap-inside-.tt) replaced w. a sibling.tt-microdiv on each.shop-tile.wallet.js'sinitWalletTooltipsclones BOTH into separate portals on hover —.tt→#id_tooltip_portal(main card),.tt-micro→#id_mini_tooltip_portal(small italic pill at bottom-right of main, mirroring Game Kit's Equipped/Unequipped/In-Use mini portal). Hover persistence covers both portals + the source tile w. a 200ms grace timer cancelled by mouseenter on any of the 3 zones. Capped items (BAND-owned) render NO btn at all — just "Already owned" microtext (mirrors Game Kit's status-only "Equipped" pill rather than the disabled-× pattern that lived in Chunk 4). **Tooltip-pin on guard open**:WalletTooltips.pin()/.unpin()exposed on window;wallet-shop.js's BUY click callspin()beforeshowGuard()+ bothonConfirm/onDismisscallbacks callunpin()→ the item tooltip stays visible behind the guard's "Buy {name} for ${price}?" prompt instead of orphaning. **Shop-first applet ordering**: newApplet.display_orderfield (default 100, lower = earlier; PK tie-break preserves legacy insertion-order for the existing 3 applets); seed migration setswallet-shop.display_order=10so Shop renders atop Balances/Tokens/Payment.applet_context()updated to.order_by("display_order", "pk"). NewWalletAppletOrderTest(2 ITs) pins Shop-first DOM order + view-context list. **DRY tooltip styling**: shop tooltip now uses the same 4-slot.tt-title/.tt-description/.tt-shoptalk/.tt-expiryclasses as the Tokens row. NewShopItem.shoptalkfield for the italic flavor line (band-1 = "Unlimited free entry (BYOB)" split out of description; tithes blank). NewShopItem.tooltip_expiry()method returns "no expiry" — eternal-stock convention (all current items; seasonal listings could override later). **Writs rebalance**: locked 2026-05-22 — tithe-1 144→12 writs, tithe-5 750→60 writs. Description text updated in lockstep ("1 Tithe Token + 12 Writs" / "5 Tithe Tokens + 60 Writs"). **Badge tweak**: ×N badge shrunk 2rem → 1.5rem + nudged further off-tile (top: -0.7rem, right: -1rem) so most of the underlying icon stays visible. **SCSS**:.tt-microhidden in source DOM (portal-only);#id_mini_tooltip_portalmostly mirrors gameboard's mini at_gameboard.scss:140but allows BUY-btn label to wrap onto multiple lines (white-space: normalon.tt-buy-btn);.tt-already-ownedstyled w.--secUseritalic at 0.85rem to match Game Kit pills. **Migrations** — 5 new:lyric/0010_repricing_tithe_writs(writs + description),lyric/0011_shopitem_shoptalk(schema),lyric/0012_seed_shop_shoptalk(band split),applets/0012_applet_display_order(schema),applets/0013_wallet_shop_display_order(Shop atop). All idempotent. **TDD** — 5 new ITs acrosstest_shop_models.py(shoptalkdefault + per-item assertions,tooltip_expirymethod, updated tithe writs values,WalletAppletOrderTest), 1 new FT (test_shop_buy_guard_portal_pins_item_tooltip— programmatically dispatches mouseenter/mouseleave to exercise the pin/unpin race), 3 new Jasmine specs (T6 pin-on-click, T7 unpin-on-confirm, T8 unpin-on-dismiss). Existing FT band-owned assertion switched to.tt-micro(no.tt-buy-btnpresent), Jasmine T2 rewritten to assert no btn renders. **3 traps caught** mid-build: (a) multi-line{# #}comment leaked into DOM again (cf [[feedback-django-comments-single-line-only]]) — pinned the trap; (b)spyOn(window, 'fetch')Jasmine double-spy collision (cf trapped previously); (c) async pollution whereafterEachrestoreswindow.Stripe=undefinedbefore_doBuy's continuation hits it — fixed by per-test never-resolving fetch mock. 1211 IT/UT + 9 wallet FTs green; Jasmine SpecRunner verified visually (FT hangs Selenium-side on spec count). Pipeline will sweep all FTs
Disco DeDisco
2026-05-22 02:21:10 -04:00 -
d28cf7b538
chore: drop legacy
#id_tithe_token_shopblock from Balances applet — Chunk 5 (final) of [[project-wallet-shop-expansion]]. The inline1 Tithe Token +144 Writs $1.00/5 Tithe Tokens +750 Writs $4.00token-bundle HTML in_applet-wallet-balances.htmlwas display-only (no purchase wiring was ever attached) + has been fully superseded by the dedicated Shop applet shipped in Chunks 2-4. Per the locked decision in the scope doc, Balances is now read-only — writs + esteem totals only — and the Shop is the canonical purchase surface. **Removed**: 8 lines of<div id="id_tithe_token_shop">w. 2.token-bundlechildren. **Replaced with** a{% comment %}pointer noting the move so the next archeologist looking at the Balances HTML doesn't reinvent the wheel. **Dropped tests**:WalletViewTest.test_wallet_page_shows_tithe_token_shop+:test_tithe_token_shop_shows_bundleITs + the legacytest_user_can_purchase_tithe_token_bundleFT — all asserted the now-removed selector. Replaced w. a comment pointing to the 3 new shop FTs (test_shop_applet_renders_seeded_items_with_icons_and_badges,test_shop_buy_click_opens_guard_portal_with_purchase_prompt,test_shop_band_already_owned_shows_disabled_buy_btn) + the model + view ITs intest_shop_models.py+test_shop_views.py. 1206 IT/UT (was 1208 — 2 stale ITs gone) + 8 wallet FTs (was 9 — 1 stale FT gone) green
Disco DeDisco
2026-05-22 01:23:07 -04:00 -
81b3c112b4
feat: wallet Shop applet — tile grid + BUY-ITEM microbutton + Stripe.js wiring — Chunk 4 of [[project-wallet-shop-expansion]]. The shop applet (slug
wallet-shop, seeded in Chunk 2) now renders the catalog as a horizontal grid of.shop-tileicons:tithe-1($1, fa-piggy-bank),tithe-5($4, fa-piggy-bank w.×5badge),band-1($20, fa-ring). Each tile hosts a hover-portaled tooltip carrying name + description + price + a.tt-microbutton-portalw. a.btn-primaryBUY ITEM button — clicking opens#id_guard_portalw. "Buy {name} for ${price}?" prompt; confirming triggers Stripe.js confirmCardPayment then POSTs to /shop/confirm + reloads. Items where the user's owned-count has hitmax_owned(eg. BAND, owned=1, cap=1) render w..btn-disabled+ × glyph + "Already owned" microtooltip text — visible-but-unbuyable per the locked decision. View context —walletview +toggle_wallet_appletsview both passshop_items(decorated w. per-user.availablevia the new_shop_items_for(user)helper) +default_payment_method_id+stripe_publishable_key. SCSS —.wallet-shop(flex column wrapping.shop-gridflex row),.shop-tile(inline-flex tooltip target),.shop-badge(2rem circle, --quaUser glyph on --quiUser bg, top-right corner per spec),.tt-microbutton-portal(column-flex, BUY btn + 'Already owned' caption styling). JS inwallet-shop.jsexposes a singletonWalletShopmodule (matching the project'sBrief/SeaDeal/StageCardmodule pattern) w. a testedinitWalletShop()method — uses event delegation on the shop root (so portal-relocated buy btns still hit the handler) + a DOM-keyeddata-shop-wiredflag (not a module-level boolean) so per-test fixture rebuilds re-wire cleanly. Wired intowallet.htmlafterwallet.js. **TDD** — 5 Jasmine specs inWalletShopSpec.js: T1 click-on-enabled-BUY opens guard w. correct prompt; T2 click-on-disabled-BUY no-op; T3 onConfirm POSTsshop_item_slugto/shop/buy; T4 init idempotent (calling twice doesn't double-wire); T5 missing-root no-throw. **2 Jasmine traps caught**: (a)spyOn(window, 'fetch')collides if another spec already spied on fetch — switched to save+restore via per-test_origFetchcapture; (b) T3 async pollution — sync assertion passed,afterEachrestoredwindow.Stripe=undefined, then_doBuy's async continuation hitStripe(pubKey)and threw "Unhandled promise rejection". Fixed by T3-local fetch mock returning a never-resolving promise so the chain pauses at the first await. **3 new FTs** intest_dash_wallet.py: tiles + icons + ×5 badge + tooltip prose; BUY click opens guard portal + NVM dismisses; BAND-already-owned shows disabled BUY w. 'Already owned' microtext (reads viatextContentsince.ttisdisplay: none). FT trap caught:TransactionTestCasewipes both migration-seeded Applets + ShopItems → setUp must re-seed both manually (mirrorstest_shop_views.py's_seed_starting_itemspattern). 1208 IT/UT + 9 wallet FTs + 5 Jasmine specs green
Disco DeDisco
2026-05-22 01:15:05 -04:00 -
410664fb0f
feat: shop PaymentIntent flow —
shop_buy+shop_confirm+stripe_webhook— Chunk 3 of [[project-wallet-shop-expansion]]. Three-endpoint split per the locked Stripe design: webhook is authoritative for fulfillment (resilient to 3DS, browser closes, network drops); sync/shop/confirmis a best-effort UX speedup (fulfills immediately when Stripe.js confirms client-side, no waiting for webhook delivery); both callPurchase.fulfill()which is idempotent — whichever lands first wins, the other becomes a no-op via thestatus==SUCCEEDEDguard. **POST /dashboard/wallet/shop/buy** (form-encodedshop_item_slug): looks up active ShopItem (404 if missing/inactive); enforcesmax_ownedviais_available_for(user)(409 if cap hit, eg already-owned BAND); requires a saved PaymentMethod (402 otherwise — picks most-recent viaorder_by('-pk').first()per the open-Q note in the scope doc); creates Stripe PaymentIntent (amount=item.price_cents, currency=usd, customer=user.stripe_customer_id, payment_method=pm.stripe_pm_id, automatic_payment_methods={enabled, allow_redirects=never} for in-window 3DS); createsPurchasew. pi.id; backfills pi.metadata.purchase_id viaPaymentIntent.modifyso the webhook handler can resolve back to the row; returns{client_secret, purchase_id}JSON for Stripe.jsconfirmCardPayment. **POST /dashboard/wallet/shop/confirm** (form-encodedpurchase_id): retrieves PI from Stripe, ifstatus=='succeeded'callspurchase.fulfill(); returns{status}JSON. 404 if the purchase doesn't belong torequest.user. Idempotent — re-firing after fulfill is a safe no-op. **POST /stripe/webhook** (csrf_exempt, mounted at root/stripe/webhookso the URL stays stable across app-routing refactors w. Stripe's dashboard config): verifies signature viastripe.Webhook.construct_eventagainstSTRIPE_WEBHOOK_SECRETenv var (400 on mismatch — Stripe won't retry on 4xx, only 5xx); onpayment_intent.succeededlooks up Purchase bymetadata.purchase_idw. fall-back tostripe_payment_intent_id(both unique). Unknown event types are no-op 200 (Stripe sendscharge.dispute.createdetc. + would retry indefinitely on 5xx). NewSTRIPE_WEBHOOK_SECRET = os.environ.get(...)setting; user swaps it on staging+prod per the live-mode env-var-only decision. TDD — 17 ITs intest_shop_views.pyacross 3 classes:ShopBuyViewTest(7 cases — login required, success path creates PI + Purchase w. correct shape, PI.create called w. correct args, unknown slug 404, inactive item 404, max_owned 409, no PM 402);ShopConfirmViewTest(5 cases — login required, succeeded PI triggers fulfill, processing PI leaves PENDING, idempotent on already-SUCCEEDED, other user's purchase 404);StripeWebhookViewTest(5 cases — sig mismatch 400, succeeded event triggers fulfill, unknown event type 2xx no-op, duplicate delivery idempotent, unknown purchase_id 2xx no-op). All Stripe API calls mocked viamock.patch('apps.dashboard.views.stripe'). 1208 IT/UT green
Disco DeDisco
2026-05-22 00:42:09 -04:00 -
849ef3c310
feat: ShopItem + Purchase models + seed
tithe-1/tithe-5/band-1+ wallet-shop Applet — Chunk 2 of [[project-wallet-shop-expansion]].ShopItemis the admin-managed catalog:slug,name,description,icon(FA class),badge_text(eg "×5"),price_cents,granted_token_type(any Token type),granted_count,granted_writs(default 0),max_owned(nullable; BAND=1),display_order,active.is_available_for(user)enforcesmax_ownedby comparing user's owned-count of the granted token type.price_display()renders cents → "$1" / "$4.20" for tooltip prose.Purchaseis the per-tx audit trail:user+shop_item+stripe_payment_intent_id(unique) +status(PENDING/SUCCEEDED/FAILED/REFUNDED) +amount_centssnapshot +granted_writssnapshot +granted_token_idsJSONField (PKs of minted tokens) +created_at+succeeded_at.fulfill()is idempotent — short-circuits if status==SUCCEEDED + refuses non-PENDING rows so a webhook + sync/shop/confirmracing each other can't double-mint. Schema migrationlyric/0008_shopitem_purchaseautogenerated. Seed migrationlyric/0009_seed_shop_itemspopulates the 3 starting items per locked decisions: tithe-1 ($1 → 1 TITHE + 144 writs, no cap, order=10); tithe-5 ($4 → 5 TITHE + 750 writs, no cap, badge "×5", order=20); band-1 ($20 → 1 BAND + 0 writs, max_owned=1, order=30). Applet migrationapplets/0011_seed_wallet_shop_appletadds thewallet-shopApplet (context=wallet, 12 cols × 3 rows). Stub_applet-wallet-shop.htmllands w. just<section id="id_wallet_shop">+<h2>Shop</h2>—_applets.html's auto-include-by-slug pattern would 500 the wallet page on TemplateDoesNotExist otherwise (caught mid-Chunk-2 by the full app suite). Chunk 4 fills in the shop-tile grid + BUY-ITEM microtooltip + Stripe.js wiring. TDD — 22 ITs intest_shop_models.py:ShopItemModelTest(9 cases — minimal create, defaults for granted_writs / max_owned / active,is_available_forw/ + w/o max_owned cap, str repr),PurchaseModelTest(8 cases — minimal create, PI ID uniqueness constraint, fulfill mints tokens + grants writs + marks SUCCEEDED + records granted_token_ids + is idempotent on re-fire + creates N tokens for bundle),SeededShopCatalogTest(4 cases pin tithe-1 / tithe-5 / band-1 row shapes + display_order ascending),SeededWalletShopAppletTest(1 case pins Applet seeded). 1191 IT/UT green
Disco DeDisco
2026-05-22 00:30:59 -04:00 -
8e476f5658
feat: wallet Tokens applet shows CARTE + BAND + COIN + PASS independently — Chunk 1 of the Shop applet rollout per [[project-wallet-shop-expansion]]. Pre-Chunk-1 the
_applet-wallet-tokens.htmltemplate used a{% if pass_token %} ... {% elif band %} ... {% elif coin %}chain that suppressed 2-of-3 trinkets from the wallet whenever the user held a higher-priority one — bad UX since the equip slot is now the user's opt-in for trinket-as-token use per [[feedback-equip-slot-gates-trinket-use]], so ALL owned trinkets need visibility. Fix: dropped the elif chain → independent{% if %}blocks for PASS / BAND / COIN; added a new CARTE block w.fa-money-checkicon mirroring the Game Kit's render. View context (apps.dashboard.views.wallet+:toggle_wallet_applets) now passescarte = user.tokens.filter(token_type=Token.CARTE).first()alongside the existing pass/band/coin keys (nois_stafffilter — CARTE has no admin gate). TDD — newWalletTokensAppletAllTrinketsVisibleTest(9 ITs): 6 pin individual#id_<token>visibility for a staff user holding all 5 types, 2 pin view-context shape (carte+bandkeys), 1 pins CARTE-on-non-staff. New FTtest_wallet_tokens_applet_shows_all_owned_trinket_typesreads BAND/CARTE.ttinnerHTMLdirectly (no hover ceremony — already covered by the COIN/FREE hover paths intest_new_user_wallet_shows_starting_balances) to pin the new template blocks server-render full tooltip prose. **Trap caught mid-build**: initial multi-line{# ... #}Django comment leaked as plain text into the rendered DOM (Django's hash-comment is single-line only), pushing the COIN tile off-screen + breaking the existing hover FT. Switched to{% comment %}...{% endcomment %}. Captured in [[feedback-django-comments-single-line-only]] — symptom signature: previously-passing Selenium hover times out + screendump shows literal{# ...text near the broken element. 1169 IT/UT + 6 wallet FTs green
Disco DeDisco
2026-05-21 23:07:42 -04:00 -
eb8666ba40
fix: my-sea drawn cards no longer always render levity-coded — yesterday's
feedback_polarity_must_agree_across_surfacesfix (f59c1af) added.my-sea-page[data-polarity="..."]to the shared.sig-overlay, .my-sign-pagepolarity block at_card-deck.scss:919. Worked for the spread-center sig (.sea-sig-card) but silently bled into the drawn-card stage modal: the stage's element carries BOTH classes.sig-stage-card sea-stage-card(per_sea_stage.html:12), so the shared rule's.sig-stage-carddescendant selector matched. Specificity.my-sea-page[data-polarity="levity"] .sig-stage-card= 0,3,0 silently beat the card-specific.sea-stage--gravity .sea-stage-card= 0,2,0 (set by sea.js's_showStage(isLevity)at line 104-108) → every drawn card on my-sea rendered the user's-sig polarity instead of the deck-stack it was actually drawn from. Room.html Sea Select unaffected (no.my-sea-pageancestor on the stage there). User-reported 2026-05-21 — symptom: a gravity card opened in my-sea stage shows the light/cream levity styling even though the card came from the gravity deck. Fix: drop.my-sea-page[data-polarity]from the shared selector list at_card-deck.scss:917-919+:972-974; add a NEW dedicated rule at the end of the shared block scoped tightly to.sig-stage-card.sea-sig-card(0,4,0 specificity) — the central sig stays page-polarity-driven (yesterday'sMySeaPolarityMatchesMySignTeststill pins this) but every other.sig-stage-carddescendant (drawn-card stages, future spread elements) is free to follow its own polarity. Gravity is the default rendering for.sea-sig-cardper the base rule at:1379so only the levity override needs an explicit block. 6/6 existing polarity + picker ITs green; visual verify deferred to user. Trap captured: [[feedback-page-polarity-scope-trap]] — multi-class elements (.A.B) match both shared (.A) AND scoped (.B) selectors, so any new page wrapper added to a shared block needs an audit of every descendant selector in the block for nested polarity overlap
Disco DeDisco
2026-05-21 15:04:53 -04:00 -
ca169be0fb
fix: CI Postgres teardown —
RobustCompressorTestRunner.teardown_databasesnow force-closes lingering connections before Django's DROP. CI steptest-UTs-n-ITs(python manage.py test apps, full suite incl. channels-tagged tests) was failing post-test even when all 1165 tests passed —psycopg2.errors.ObjectInUse: database "test_python_tdd_test" is being accessed by other users / DETAIL: There is 1 other session using the database. Two-step leak: (1)core.settings.DATABASES['default']['conn_max_age']=600keeps Postgres connections alive in the per-thread pool for 10 min (prod-perf default); (2) Channels'database_sync_to_async(16 call sites acrossapps.epic.tests.integrated.test_consumers'sCursorMoveConsumerTest+SigHoverConsumerTest) runs in a process-wide asgiref threadpool — each worker thread accumulates its own DB connection that outlives the test + sits idle in the pool when teardown fires. Postgres refuses DROP while ANY session targets the row. Local dev unaffected:--exclude-tag=channelsskips the consumer tests + SQLite has no DROP step. **Fix** lives entirely incore/runner.py's teardown override —connections.close_all()covers the main thread's runner connection; iteratingold_config+ runningSELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = %s AND pid <> pg_backend_pid()against each test DB kicks any worker-thread session still pinning the row. Both safe on a green run (DB about to be dropped anyway) + scoped tovendor == "postgresql"so SQLite local-dev is a clean no-op. ProdCONN_MAX_AGE=600untouched — fix lives in the test runner, NOT in settings. 19/19 lyric UTs green via the new runner path (smoke verify the override is benign on SQLite); Postgres-side validated next CI run. Trap captured: [[feedback-test-teardown-conn-leak]] — symptom signatureRan NNNN tests / OK / Destroying.../ObjectInUse: ...belongs in CI-fail triage notes so future flakes get diagnosed in seconds instead of test-hunting
Disco DeDisco
2026-05-21 14:24:41 -04:00 -
8dd4347dbe
fix: gate token-picker now equip-gated —
User.equipped_trinketis the sole opt-in for trinket-as-token use at BOTH gatekeepers (/gameboard/room/<id>/gate/+/gameboard/my-sea/gate/). Old flat-priority chain (PASS→BAND→COIN→FREE→TITHE) silently consumed a DOFFed-but-owned COIN when the user clicked the rails —current_roomadvanced, no inventory decrement, wallet looked unchanged. User-reported 2026-05-21 as "free for all" admit when no trinket equipped. Root cause:select_token+_select_my_sea_tokenignoredequipped_trinket_identirely + just grabbed the highest-priority owned token regardless of equip state, making the equip slot a decorative no-op. **Fix**: both pickers now start fromuser.equipped_trinket_id; equipped PASS (staff)/BAND/COIN-with-no-current-room → return it; equipped CARTE → fall through (CARTE is opt-in via kit-bag click that setstoken_idPOST param routed throughdrop_token's explicit branch, NOTselect_token); my-sea additionally checks COIN cooldown (next_ready_at <= now); no equipped trinket OR equipped trinket invalid → FREE (FEFO) → TITHE → None. **Fresh-query defense**: pickers queryuser.tokens.filter(pk=user.equipped_trinket_id).first()instead of the cacheduser.equipped_trinketFK descriptor — descriptor goes stale across mid-request state changes + bites tests wheretokens.all().delete()triggers SET_NULL cascade but the Python object stays unrefreshed (SQLite reuses deleted PKs so a coincidentally-matching new token slips through). TDD — newSelectTokenEquipGatedTest(7 ITs) +SelectMySeaTokenEquipGatedTest(6 ITs) pin: skip-unequipped-COIN → FREE; skip-unequipped-BAND → TITHE; no equip + no consumables → None; CARTE equipped → falls through; equipped-COIN-in-use-elsewhere falls through; staff with unequipped PASS falls through; my-sea cooldown-COIN-equipped falls through. **Existing tests updated** (5 cases pinned the old flat-priority semantic + needed equipping explicit before assertion):SelectTokenTest.test_returns_pass_for_staff+test_returns_band_when_equipped+test_pass_wins_when_equipped_over_band+SelectMySeaTokenTest.test_pass_wins_priority_for_staff(now equip PASS first);ConfirmTokenPriorityViewTest.test_pass_not_consumed_and_coin_not_leased+TokenPriorityTest.test_staff_backstage_pass_bypasses_token_cost(FT) now DON the PASS before clicking rails.SelectMySeaTokenTest.setUpaddsrefresh_from_db()aftertokens.all().delete()so the cascade SET_NULL on equipped_trinket_id is reflected in the Python object. 1160 IT/UT + 5 TokenPriority FTs green. Trap captured: [[feedback-equip-slot-gates-trinket-use]]
Disco DeDisco
2026-05-21 13:56:59 -04:00 -
f59c1af89a
fix:
/gameboard/my-sea/sig polarity now matches/billboard/my-sign/— two-bug stack. **Bug 1 (primary):**my_sea.html:10had{% if significator_reversed %}gravity{% else %}levity{% endif %}— INVERTED frommy_sign.html:22's{% if current_significator_reversed %}levity{% else %}gravity{% endif %}+ its JS_polarity()(revInput.value === '1' ? 'levity' : 'gravity'). SameUser.significator_reversedvalue produced opposite polarity styling across the two surfaces → a levity sig picked on my-sign rendered gravity-styled on my-sea (--priUserbg +--secUsertext); a gravity sig rendered levity-styled. User-reported 2026-05-21. **Bug 2 (latent, masked by Bug 1):**.sea-sig-cardhardcoded.fan-corner-rank+itocolor: rgba(var(--secUser), …)— fine against gravity's--priUserbg, but against levity's--secUserbg (set by.sig-stage-cardin the polarity rule at_card-deck.scss:935-943) the rank + suit-icon collided w. the bg and disappeared. Bug 1 was hiding this: levity sigs were getting rendered gravity-styled (visible), so the invisibility only surfaced for gravity sigs (which got levity-styled). Fixing Bug 1 alone would've exposed Bug 2 for the previously-fine levity case → fix both in one shot. **Bug 2 fix:** switch.fan-corner-rank+itocolor: currentColorw. opacity preserved (0.85 / 0.75); add explicitcolor: rgba(var(--secUser), 1)on the default.sig-stage-card.sea-sig-cardrule so gravity inherits secUser; levity polarity rule already sets.sig-stage-card { color: rgba(var(--priUser), 1) }so it cascades down through currentColor. TDD — newMySeaPolarityMatchesMySignTest(2 ITs) pins both pages to the sameUser.significator_reversed → data-polaritymapping: unreversed → gravity on BOTH surfaces; reversed → levity on BOTH. 1147 IT/UT green. Visual verify deferred to user — the SCSS edge case wasn't reachable via Selenium (computed-style-on---secUser would require palette resolution at runtime)
Disco DeDisco
2026-05-21 12:40:08 -04:00 -
99ffdb3943
feat:
Token.BAND(Wristband) — non-admin variant of PASS, admin-awarded via Django admin to any user (NOT auto-granted on signal, NOis_staffcoupling, NO model-layer guard). Mirrors PASS at runtime — fills 1 gate slot, never consumed, stays equipped, nocurrent_roomtie, no expiry, no In-Use microtooltip — but separates the policy concerns so PASS stays a deliberate staff-only trinket while BAND becomes the regular-user version (promotional / play-reward / staging give-away). Tooltip prose: name "Wristband", desc "Admit All Entry" (shared w. PASS — phrasing reflects the never-depleted lifetime, not multi-slot semantics), shoptalk "Unlimited free entry (BYOB)", expiry "no expiry".fa-ringicon across all 4 surfaces (Game Kit applet#id_kit_wristbandbetween PASS + CARTE, gk-trinkets section, kit-bag dialog Trinket slot, wallet PASS→BAND→COIN elif chain). Priority chain — PASS → BAND → COIN → FREE → TITHE — wired identically into bothapps.epic.models.select_token(room gatekeeper) +apps.gameboard.models._select_my_sea_token(my-sea gatekeeper); BAND wins over consumables for any holder while PASS still wins for staff who happen to hold both.debit_token+debit_my_sea_tokentreat BAND same as PASS: slot marked FILLED w.debited_token_type=BAND, token row preserved,current_roomuntouched,equipped_trinketunchanged. View contexts (gameboard,toggle_game_applets,_game_kit_context,wallet,toggle_wallet_applets) pass abandkey — universal lookup, NOis_stafffilter. Migrationlyric/0007_alter_token_token_type— choices-only AlterField. TDD — 5 FTs intest_trinket_wristband.py(test_band_not_auto_equipped_after_award,test_band_tooltip_renders_full_prose,test_band_uses_fa_ring_icon,test_equipped_band_shows_equipped_mini_tooltip,test_equipped_band_shows_doff_active_don_disabled); 4 tooltip UTs (BandTokenTooltipTest); 5 model ITs (BandTokenAdminAwardTest— no-auto-grant for non-staff + staff, admin-can-award to either branch, not-auto-equipped); 2 priority-chain ITs (test_returns_band_when_held_and_no_pass,test_pass_still_wins_over_band_for_staff); 1 debit IT (test_debit_band_does_not_consume_or_unequip). 1145 IT/UT + 5 FT green. A boost-pass / promo-band w. richer semantics (multi-slot admit, time-window, etc.) lands as YET-ANOTHER token_type later — keep BAND the minimal "PASS minus admin gate" trinket so the policy axis stays clean. Captured in [[sprint-band-trinket-may21]] alongside the standing auto-commit rule [[feedback-auto-commit-after-build]]
Disco DeDisco
2026-05-21 12:33:09 -04:00 -
0f60c73f3b
fix:
Token.PASSis now model-enforced as staff-only —Token.clean/saveraise ValidationError when a non-staff user is the FK target. Staging bug 2026-05-21 — admin awarded a PASS to a non-admin via Django admin; row was created + showed in the user's wallet, but every game-side surface (gameboard, game-kit, gate-padselect_token,_select_my_sea_token) had always filtered PASS behindis_staff, so the token was unequippable + unusable. Fiveis_staff-gated PASS surfaces made PASS a deliberate staff-only trinket; the wallet was the lone outlier surfacing it. Bundled: wallet view (+ HTMX toggle partial) now gatespass_tokenbehindis_staffmirroring the gameboard pattern — defense-in-depth in case any future bypass writes a stray row. TDD — new ITs:PassTokenStaffOnlyGuardTest(model raises for non-staff, accepts for staff, leaves other token types unaffected);WalletPassTokenVisibilityTest(3 cases pin wallet + HTMX gating);TokenAdminFormTest.test_pass_token_for_non_staff_user_is_invalid+test_pass_token_for_staff_user_is_valid. Adjusted 2 existing tests that incidentally exercised the now-blocked pattern (test_paid_draw_with_pass_does_not_consume,test_pass_token_is_not_consumed— both flipis_staff = Trueinline beforeToken.objects.create); dropped PASS fromtest_other_token_types_do_not_require_expires_at's loop (covered by the new dedicated tests). 1133 IT/UT green. A non-admin "boost-pass" variant lands as a distincttoken_typelater, NEVER by relaxing the staff gate — captured in [[feedback-pass-token-staff-only]]
Disco DeDisco
2026-05-21 00:35:55 -04:00 -
97a6da28a5
fix: manual my-sea draws persist on refresh + reloaded slots stay clickable — root cause was SeaDeal stamping
slot.dataset.posKeyw. selector form (".sea-pos-cover") while my-sea's inline_collectHandFromDom+ template's_my_sea_slot.htmluse raw names ("cover"). Key mismatch silently dropped manual draws from the lock POST → server rejected empty hand → no row → refresh showed empty state. AUTO DRAW worked only because it assembled fullHand w. raw posNames directly, bypassing the broken collector. TDD — 2 new FTs pin the contract: - test_manual_draw_persists_on_refresh - test_reloaded_slot_can_reopen_stage_modal_on_click
Disco DeDisco
2026-05-20 15:08:49 -04:00 -
bb44aa326a
fix: AUTO-DRAWn my-sea cards are now clickable to re-open the stage modal —
SeaDeal.register(card, posSelector, isLevity)public method populates_seaHand+ delegates to SeaDeal's internal_fillSlotso the overlay click handler can resolve_seaHand[pos]for auto-drawn slots (previously short-circuited → silent no-op). AUTO DRAW in my_sea.html now calls register instead of the inline_fillSlotshim — also fixes adataset.posKeyinconsistency (inline stored raw "cover", SeaDeal stores ".sea-pos-cover"; click handler reads SeaDeal's form). User-reported 2026-05-21. TDD — new FTtest_auto_drawn_slots_can_reopen_stage_modal_on_clickpins the contract
Disco DeDisco
2026-05-20 14:53:05 -04:00 -
31cb8dfc1d
CI: route test_game_my_sea*.py to test-FTs-room stage — 49 my-sea FTs DRY-reuse the room-shell hex + sea-cross picker (same Selenium surface as test_game_room_* + test_trinket_*), so they belong w. the heavy room flows instead of bloating test-FTs-non-room. Filename-regex partition stays clean (13 room + 24 non-room = 37 total, no overlap)
Disco DeDisco
2026-05-20 13:20:23 -04:00 -
899e626265
CI:
_retry_failed.shwraps both FT steps — single-flake retries cost ~22s instead of a full 35-min step re-run. Parses Django'sFAIL:/ERROR: test_method (full.dotted.path)lines from stdout, re-runs only those labels (deduped + sorted). Green first runs skip the retry; first-run crashes w. no parseable labels propagate the original exit code without masking infra problems
Disco DeDisco
2026-05-20 13:14:26 -04:00 -
f348a19312
my-sea portrait SPREAD dropdown opens UP, not down —
top: 100%was extending the list below the form col, which on portrait sits flush at the bottom of the visible aperture w. navbar/footer pinned beneath it (options unreachable).bottom: 100%(+ margin flipped to bottom) grows the list into the abundant green aperture above. Chained&.sea-form-colper [[feedback-scss-import-order-specificity]] to beat card-deck's later-loaded base
Disco DeDisco
2026-05-20 12:04:51 -04:00 -
bc4565f161
my-sea portrait form-col grid fix: chain
.sea-form-col.my-sea-form-colso the 2-class selector beats_card-deck.scss's base.sea-form-col { display: flex }regardless of source order (card-deck loads AFTER gameboard in core.scss, so the prior 1-class selector lost to source order and the grid never took effect)
Disco DeDisco
2026-05-20 12:02:07 -04:00 -
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: contentson.sea-form-mainflattens the intermediate wrapper so its children participate directly in the grid
Disco DeDisco
2026-05-20 11:58:41 -04:00 -
191dad5365
my-sea hex-btn state-machine FT pin: extend
test_landing_renders_hex_with_free_draw_btnto 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
Disco DeDisco
2026-05-20 11:56:09 -04:00 -
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)
Disco DeDisco
2026-05-20 11:52:17 -04:00 -
db443b7533
Revert universal
.btn-disabled→ × pseudo-element overlay (iter-4c); restore case-by-case×rendering convention. My Sea DEL btn now swapsDEL↔×in lockstep w. its.btn-disabledtoggle (matches game-kit tooltip + DON/DOFF pattern). User-spec 2026-05-20.
Disco DeDisco
2026-05-20 09:56:19 -04:00 -
4417b8c972
My Sea iter 6c: bud-btn invite stub +
#id_my_sea_menugear (NVM-only, %applet-menu-styled, on both /gameboard/my-sea/ and the gatekeeper) + PAID DRAW now deletes the row and redirects to?phase=pickerso the user drops straight into picking cards instead of looping back to GATE VIEW — Sprint 5 iter 6c of My Sea roadmap — TDD
Disco DeDisco
2026-05-20 09:47:47 -04:00 -
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
Disco DeDisco
2026-05-20 02:50:54 -04:00 -
d2c34d44d3
My Sea iter 6a follow-up: gatekeeper layout mirrors room exactly —
.gate-title-panelw. "@<handle>'s Sea" +.gate-top-roww..gate-main-panel(token slot) +.gate-roles-panel(PAID DRAW square), all on shared--priUserpanel chrome — TDD
Disco DeDisco
2026-05-20 02:31:51 -04:00 -
3fc5491372
My Sea iter 6a: gatekeeper page + INSERT/REFUND/PAID DRAW endpoints + MySeaDraw deposit fields +
_select_my_sea_token/debit_my_sea_tokenhelpers (CARTE blocked, COIN 24h cooldown not 7-day) + Sprint 6 FT skeleton — Sprint 5 iter 6a of My Sea roadmap — TDD
Disco DeDisco
2026-05-20 02:29:08 -04:00 -
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
Disco DeDisco
2026-05-20 01:34:03 -04:00 -
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
Disco DeDisco
2026-05-20 00:27:50 -04:00 -
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_portalfrom base.html — TDD
Disco DeDisco
2026-05-20 00:12:52 -04:00 -
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
Disco DeDisco
2026-05-19 23:54:00 -04:00 -
31ed2bda0e
Billnote palette swatch: DRY w. .swatch via palette-swatch-bg mixin; fixes --quaUser→--quiUser drift
Disco DeDisco
2026-05-19 22:09:01 -04:00 -
b6e93b9d64
My Sea iter-4a follow-up batch: modal port + draw polish + label positioning + Major Arcana fix — TDD
Disco DeDisco
2026-05-19 22:02:27 -04:00 -
ca2a62fd84
My Sea client-side card draw + DEL + LOCK HAND visual lock — Sprint 5 iter 4a of My Sea roadmap — TDD
Disco DeDisco
2026-05-19 20:02:20 -04:00 -
f154d660bd
My Sea per-spread positions + draw-order JS config + position labels — Sprint 5 iter 3 follow-up — TDD
Disco DeDisco
2026-05-19 19:38:53 -04:00 -
fd5db951a7
My Sea form col + SPREAD dropdown w. 3-card/6-card section dividers — Sprint 5 iter 3 of My Sea roadmap — TDD
Disco DeDisco
2026-05-19 17:23:25 -04:00 -
f5fc1e15f8
My Sea picker phase: three-card cross (sig + cover/leave/loom) — Sprint 5 iter 2 of My Sea roadmap — TDD
Disco DeDisco
2026-05-19 16:06:14 -04:00 -
285597b467
My Sea FREE DRAW + seat-1C seated transition — Sprint 5 iter 1 follow-up — TDD
Disco DeDisco
2026-05-19 15:48:07 -04:00 -
de48ae226d
My Sea DRAW SEA landing — Sprint 5 iter 1 of My Sea roadmap — TDD
Disco DeDisco
2026-05-19 15:15:37 -04:00 -
4d1c74a2af
FT helper: sig_page.py — _seed_earthman_sig_pile + _assign_sig — Sprint 4c of My Sea roadmap — TDD
Disco DeDisco
2026-05-19 14:22:49 -04:00 -
c8a603484e
My Sign DEL btn: clear-sign affordance on SCAN SIGN landing — Sprint 4b-adjacent of My Sea roadmap — TDD
Disco DeDisco
2026-05-19 14:13:20 -04:00 -
a636e940b7
fix CI FT regression: My Sign + My Sea setUpClass ContentType collision — pipeline #313
Disco DeDisco
2026-05-19 11:05:39 -04:00 -
76e1bfc9ad
My Sea applet: split sign-gate vs empty-state ITs after Sprint 4b layered the gate ahead of the empty placeholder
Disco DeDisco
2026-05-19 01:57:41 -04:00 -
cd0add1e3c
My Sea sign-gate — Sprint 4b of My Sea roadmap — TDD
Disco DeDisco
2026-05-19 01:38:55 -04:00 -
5b06d902a8
My Sign picker iteration 3 — SCAN SIGN landing hex + OK/NVM thumbnail two-step + duoUser picker bg — Sprint 4a-cont — TDD
Disco DeDisco
2026-05-19 01:11:41 -04:00 -
ab5b4c95dd
My Sign picker: hover-preview, click-lock, NVM-unlock + polarity SCSS port + bigger stage — Sprint 4a-cont iteration 2
Disco DeDisco
2026-05-19 00:15:13 -04:00 -
559bdc2de7
My Sign picker: stage visible on load, FYI panel, SPIN/FLIP split w. perspective-flip animation — Sprint 4a-cont
Disco DeDisco
2026-05-18 23:58:39 -04:00 -
39767c72c2
fix CARTE multi-seat Role-Select bug on navigate-away + back; My Sign applet rename
Disco DeDisco
2026-05-18 23:18:32 -04:00 -
66b2947e8c
My Sign: Brief banner + Earthman [Shabby Cardstock] backup deck when no equipped — TDD
Disco DeDisco
2026-05-18 22:49:49 -04:00 -
400762c0e5
Game Sign picker @ /billboard/my-sign/ + billboard applet — Sprint 4a of My Sea roadmap — TDD
Disco DeDisco
2026-05-18 22:23:24 -04:00 -
5e71b1d5da
Baltimorean: post-attribution titles now read "Baltimorean" not "Ard!" — TDD
Disco DeDisco
2026-05-18 21:42:58 -04:00 -
bf44628536
My Sea applet shell — Sprint 3 of the My Sea roadmap
Disco DeDisco
2026-05-18 19:45:57 -04:00 -
df9cf1eee8
CI: route trinket FTs to test-FTs-room stage alongside game_room_*
Disco DeDisco
2026-05-18 19:41:37 -04:00 -
5e5bc5a6af
COIN: unequip on deposit (parity w. CARTE) ; fix FT false-positive masking the bug
Disco DeDisco
2026-05-18 19:35:08 -04:00 -
d2491c5e1b
COIN: Carte treatment in Game Kit applet — data-current-room-name + deposited-state btn-disabled — TDD ; PASS skeleton FT
Disco DeDisco
2026-05-18 19:25:46 -04:00 -
79706e817a
iOS focus-zoom prevention — input font-size floor @ 16px ; JS fallback strengthened
Disco DeDisco
2026-05-18 18:32:45 -04:00 -
8066ac289f
iOS viewport zoom-reset on form-field exit — global IIFE in base.html
Disco DeDisco
2026-05-18 18:22:08 -04:00 -
7165974905
table hex layout: fill aperture + enlarge hex from 160×185 → 200×231 ; chair clearance preserved
Disco DeDisco
2026-05-18 18:19:11 -04:00 -
fbe6c12ded
fix CAST SKY click opening tray instead of Sky Select — TDD
Disco DeDisco
2026-05-18 17:21:32 -04:00 -
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_recentwas written pre-Baltimorean whenset_pronounsalways 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-4wait_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 towait_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_newelse branch'sattr_comboinline attribution now usesnote.display_nameinstead ofnote.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.titlefield also swapped fromdisplay_titletodisplay_nameso the banner title slot reads "Baltimorean" not "Ard!" — admin-grant branch (_ADMIN_NOTE_SLUGS: super-schizo, super-nomad) still usesdisplay_titlefor 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 — addedcard_titlekey to_NOTE_DISPLAY["baltimorean"]({"greeting": "Ayo,", "title": "Ard!", "card_title": "Baltimorean"}) + newNote.card_titleproperty that falls through_NOTE_DISPLAY[slug]["card_title"]then defaults todisplay_title; billboard/views.py_my_notes_contextswaps"recognition_title": n.display_title→n.card_titleso 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 viaUser.active_title.display_title(lyric/models.py:158) —display_titleitself was deliberately untouched so the navbar still flipsWelcome, Earthman→Ayo, 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 atgrant_if_newtime, 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_baltimoreanpins the override,test_stargazer_card_title_falls_back_to_display_titlepins the fallback for non-overridden slugs); drama/tests/integrated/test_note_brief.pytest_first_grant_creates_post_line_and_briefupdated assertionbrief.title == note.display_name(wasdisplay_title) to reflect the new contract; dashboard/tests/integrated/test_views.pytest_brief_payload_carries_baltimorean_titleexpects "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
Disco DeDisco
2026-05-18 10:54:30 -04:00 -
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,173neutral) + two.palette-baltimore/.palette-marylandpalette classes wiring those hues into the standard--priUser…--decUserslots; 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 navbarWelcome, Earthman→Ayo, 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_pronounsview rewired (dashboard/views.py) — first-timepronouns = bawlmoreseselection callsNote.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 pronounscommit()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.htmlpulls inapps/dashboard/note.jssoBriefis 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 h2at render-time + sets inlinetopso the banner portals SQUARELY OVER the page h2 letter-spread wordmark instead of parking at the SCSS-defaulttop: 0.5rem(which had it lurking above the wordmark area on every page); portrait-only (gatedif window.innerWidth > window.innerHeight return) — landscape h2 lives in awriting-mode: vertical-rlfixed 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 / titleArd!/ square_url to /billboard/my-notes/ / idempotent on repeat / non-bawlmorese unaffected / bawlmorese-after-other still grants); existingSetPronounsViewTest.test_post_each_valid_choicedocstring 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 greetingWelcome, Earthman→Ayo, 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-descriptioncarryingBaltimorean+ non-emptydata-unlocked-date+ Note.palette = palette-baltimore in DB — all 6 green in 51s; full IT/UT sweep 997 → green in 45s — TDD
Disco DeDisco
2026-05-18 02:17:07 -04:00 -
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) coversemanation_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) coversreversal_for(polarity)w. polarity-split + reversal_qualifier fallback + further fallthrough to emanation_for;TarotCardNameSplitTest(4) coversname_group/name_titlecolon-split parsing (prefix-w-colon / suffix / no-colon edge);TarotCardCautionsJsonTest(2) covers thecautions_jsonJSON 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 alltop_capacitors()branches — empty dict / None / all-zero counts (the L56max(counts.values()) <= 0fallback that was uncovered) / single-winner / tie-clockwise-order / enriched dict {"count":N} input shape ; ITs in epic/tests/integrated/test_models.py —TarotDeckDrawTestextended w. 5 tests forremaining_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 threedata["retracted"] = Truepaths that the FTtest_game_room_select_sig.pywalks 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) coversTarotCard.DoesNotExist→ 400 (L840-841) ;SigSelectGravityContextTest(3) covers theuser_polarity = 'gravity'branch (L322) + thegravity_sig_cardslookup (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 matchgravity_sig_cards()output ;SeaDeckViewTest(7) mirrors thetest_game_room_select_sea.pyFT 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),reversedfield is bool, claimed-significator exclusion viaroom.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 thekit_bagview'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'ssky_birth_tzvia astimezone L300-306 — 16:00 UTC → 12:00 EDT; invalid-tz string triggersZoneInfoNotFoundError→ swallowedpass→ 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 viaget_object_or_404);UnequipTrinketViewTest+2 (POST clears matching equipped_trinket — L107-110; POST of non-matching token is a 204 no-op, the implicitelsebranch) ; .coveragerc omit gains*/reset_staging_db.pyper 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 from3242873; 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), defensiveexceptfallbacks that need contrived inputs to fire, and a handful of __str__s/passbranches not worth a test apiece — TDD
Disco DeDisco
2026-05-18 01:07:13 -04:00 -
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
Disco DeDisco
2026-05-18 00:25:10 -04:00 -
ace8612099
tray apparatus scales w. fluid rem; sig-select 9×2 middling breakpoint —
$handle-exposedwas48pxfixed while#id_tray_btnis3rem, 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 defaultflex-shrink:1in 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: 3remmatches the btn so it fills the exposed area at every rem;$handle-rect-h: 4.5rem(was72px) gives the visible rail thickness a touch of breathing room around the btn at every scale; landscape rules in the same partial that hard-coded48px/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/Heightfor 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_btnadded as fallback forid_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_handlecached on the module via_handleref alongside_btnand cleared inreset(); sig-select grid jumped straight from 6 cols (narrow landscape) → 18 cols × 3rem atmin-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.scssmirrors the comment's documented intent: 6 cols default landscape (row layout, stage beside grid) → 9 cols × 3rem atmin-width: 900px(column layout, stage above grid) → 18 cols × 3rem atmin-width: 1400px→ 18 × 5rem atmin-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)
Disco DeDisco
2026-05-17 23:21:02 -04:00 -
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 ownDATABASE_URL: sqlite:////tmp/test_db_{non_room,room}.sqlite3outside 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;--parallelflag dropped from bothmanage.py testinvocations 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 #303 →ad0041d); 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; downstreamscreendumps+build-and-pushalready list both FT steps in theirdepends_onso they naturally gate on the slower of the two ; test_core_bud_btn.py wraps 4 unwrappedfind_element(...).click()sites inwait_for(execute_script("arguments[0].click()", btn))— the_open_panel_and_invitehelper feeding 6 GatekeeperBudBtnAsyncInviteTest tests + the standalone GatekeeperBudBtnDuplicateInviteErrorTest test_duplicate_invite_shows_error_brief_and_fyi_flashes_slot click + both.note-banner--duplicate .note-banner__fyiclicks (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 viewbecause the post-send_keys autocomplete dropdown on_bud_invite_panel.htmlbriefly 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
Disco DeDisco
2026-05-13 14:12:38 -04:00 -
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 subsequentbrowser.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 swapswait_for(check_results)→wait_for_slow(check_results, timeout=60)— the spec suite has grown well past the 10s MAX_WAIT the defaultwait_fordecorator affords + under CI contention (parallel Selenium workers competing for CPU on the same droplet) Jasmine's.jasmine-overall-resultwas still reporting"Running..."at 10s when the assertion fired w.(no detail)failure list (#303); check_results body is unchanged —"Running..."doesn't match the0 failuresregex 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 wrapswebdriver.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 thepip install -r requirements.dev.txtline from each FT step would save ~5 min/pipeline (CI image drifted from requirements.dev.txt sincea21e6aaso 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
Disco DeDisco
2026-05-13 12:53:49 -04:00 -
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.htmldata-sharer-name(consumed by the dynamic post-line-author append on share success) —at_handlewas 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.htmlrow was already on at_handle from the 3-col sprint, so this commit just brings the rest of the surfaces in line;_navbar.htmlswap also drops the literal@that prefixed{{ user|display_name }}— that literal predatedat_handle+ worked for users w. usernames (gave@disco) but produced@<email>@<domain>for users w. no username yet; navbarwait_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) ordisco@test.io(new);_bud_add_panel.html's client-side_appendBudEntryJS gains an inline at_handle mirror —display.indexOf('@') >= 0 ? display : '@' + display— since the server's add_bud response packsusername or emailunder theusernamekey (semantic mismatch w. the key name but stable) so the JS has to detect the email case itself;test_bill_my_buds.pytwo.bud-nametext 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
Disco DeDisco
2026-05-13 01:09:43 -04:00 -
f7fa250804
.bud-duplicate-flash: auto-ease-out 3s after FYI + palette swap —
note.js'sBrief.showDuplicateBannerFYI handler nowsetTimeout(() => target.classList.remove('bud-duplicate-flash'), 3000)after the.add(); the existingtransition: color 600ms ease, text-shadow 600ms easerule 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-nameon /billboard/my-buds/,.post-recipienton post.html share-flow,.gate-slot.filledon the gatekeeper invite-flow) since they all flow through the same_bud.scss .bud-duplicate-flashselector + the sameBrief.showDuplicateBannerJS handler; new Jasmine spec D7b in NoteSpec.js usesjasmine.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
Disco DeDisco
2026-05-13 00:43:03 -04:00 -
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:hoverrule 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; newapps/applets/static/apps/applets/row-lock.jsIIFE module owns the touch-persistence state machine (single_lockedRowref,.row-lockedclass 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-pagenotes-lockedclick-lock state machine but lighter (no DON/DOFF, no greeting swap, no fetch), one document-level click listener bound once via_boundre-entry guard so beforeEach_init()calls in specs don't pile up handlers; loaded globally viabase.htmlnext toapplets.jssince 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 inRowLockSpec.jscover 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
Disco DeDisco
2026-05-13 00:27:39 -04:00 -
b2f1511c2d
my-scrolls / my-games applet rows: prepend actor
display_nameto the body cell — the latest event'sto_prosereturns the action alone ("deposits a Carte Blanche…") because scroll.html splits the row across<strong>{{ event.actor|display_name }}</strong>+ adjacent{{ to_prose|safe }}; the applet rows have a single middle column (<title> | <body> | <ts>) so they need both halves concatenated into.row-body; ROOM_CREATED welcome events (actor=None) keep rendering prose alone sinceto_prosealready reads "Welcome to <name>!" — the{% if item.latest_event.actor %}guard skips the prefix, mirroring the same actor-guarded<strong>we added to_partials/_scroll.html+_applet-most-recent-scroll.htmlonc03fb2bso welcome lines don't carry a bogus empty actor; 2 ITs added — BillboardViewTest.test_my_scrolls_applet_row_body_includes_actor_display_name + GameboardViewTest.test_my_games_row_body_includes_actor_display_name — scoped to<span class="row-body">...stuart...deposits...</span>(regex match on the .row-body cell content) so the assertion can't pass on actor renders outside the row (the Most Recent Scroll applet on /billboard/ renders the same actor too, separately — initial pass missed this andassertIn("acto", body)matched there instead, hiding the bug); BillboardViewTest also gains test_my_scrolls_applet_row_body_no_actor_prefix_for_welcome to lock in the no-empty-prefix contract for ROOM_CREATED welcome events; 931 ITs green; settings.local.json fix-up —Bash(git add *)(literal*would only match the exact string "git add *", notgit add -u) →Bash(git add:*)+ companion read-only git patterns (status / diff / log / show) so the in-session commit flow stops prompting — TDD
Disco DeDisco
2026-05-13 00:13:33 -04:00 -
054b0aa82b
test_admin FT: wait on post-login content, not on
<body>itself — pipeline #300 caught the flake (AssertionError: 'Site administration' not found in 'Django administration\nToggle theme...\nEmail:\nPassword:'); the pre-fixbody = self.wait_for(lambda: self.browser.find_element(By.TAG_NAME, "body"))resolved on the FIRST<body>Selenium found — which exists on the login page too — so on a slow CI runner the form submit hadn't navigated yet by the time wait_for completed, the assertion ran against the still-stale login-page body, and the test failed; locally the submit always completes inside the wait window (10s MAX_WAIT) so this never reproduces; fix: wait_for the"Site administration"substring directly viaassertIn(thewaitdecorator retries on AssertionError til MAX_WAIT, so the loop keeps polling the body's text content until the admin home page actually renders), THEN read body.text once + run the remaining Users / Tokens assertions inline — same shape as the working test_admin_tarot / test_admin_post_readonly login flows; 1 FT green locally w. no other changes — TDD
Disco DeDisco
2026-05-13 00:00:57 -04:00 -
5beb990623
brief banner: portal over the wordmark + h2 header area (small margin from viewport edges) w.
position: fixed; top: 0.5rem; left: 0.5rem; right: 0.5rem; z-index: 10000— mirrors#id_guard_portal's lift contract so the Brief escapes every game overlay's stacking context the same way the guard portal does; without this lift the Brief landed in document flow as nextSibling of#id_brief_banner_anchor(inside.container, which itself has no stacking context) — fine on pages w. no fixed overlays, but on room.html the gate / role-select / sig / sky backdrops (z-index 100-200,position: fixed; inset: 0,backdrop-filter: blur(...)) ate the Brief alive: the user saw the post-share / invite Brief outline blurred behind the Gaussian glass instead of in front of it; centring viamax-width: 960px+ auto inline margins matches.container's max-width so landscape viewports see the Brief centred over the page-content column rather than spanning the sidebar gutters; existingmargin-bottom: 0.75remdropped (vestigial — fixed positioning doesn't push siblings, and there's only one Brief at a time so the inline-stack case the margin was hedging on never happens); Django messages banner (the.note-banner.note-banner--messageshell rendered by{% if messages %}in base.html for magic-link confirmations / errors) inherits the same portal treatment since both kinds share the same base class — UX is now uniform regardless of which overlay the user is staring through when the message arrives; 42 Brief-touching FTs (test_core_bud_btn + test_bill_my_buds + test_bill_my_notes) green w. no positional assertion regressions — Selenium clicks.note-banner__nvm/.note-banner__fyi/ etc. the same w. position:fixed as it did inline
Disco DeDisco
2026-05-12 23:58:36 -04:00 -
c03fb2bab0
billscroll: first log entry on a fresh room is a system-authored
Welcome to <name>!greeting —epic.create_roomrecords aROOM_CREATEDGameEvent (actor=None) immediately afterRoom.objects.create(...)so every room's scroll opens w. the greeting before any user action;GameEvent.to_proseROOM_CREATED branch swapped from the unused"opens this room"legacy prose (verb was declared since the initial drama-app spike but no view recorded it — only test fixtures + AP federation tests touched it) →f"Welcome to {self.room.name}!", deliberately dropping the actor prefix the rest of the verbs lead w. since the welcome is the room's greeting, not a user action; scroll template (templates/core/_partials/_scroll.html+templates/apps/billboard/_partials/_applet-most-recent-scroll.html) gain anevent.actor-guarded<strong>so the welcome line renders w.o. a leading empty<strong></strong>whitespace gap, and the.drama-eventclass branches now readmine/theirs/system(the newsystemslot replaces the priorelse: theirsfallthrough when actor is None, opening room for system-line styling later w.o. mis-attributing the welcome to a phantom player); RoomCreationViewTest gains test_create_room_records_welcome_event_with_no_actor + test_create_room_welcome_event_renders_welcome_prose; the existing drama.tests.integrated.test_models tests (test_record_without_actor + test_events_ordered_by_timestamp) already exercise actor=None on ROOM_CREATED + the chronological-first position so the rendering contract holds; 928 ITs green — TDD
Disco DeDisco
2026-05-12 23:14:01 -04:00 -
c08dd145c3
applet rows: 3-col grid
<title> | <body> | <ts>mirroring post.html's.post-lineshape —_my_posts_applet_item / _my_buds_applet_item / _my_notes_item / _my_scrolls_item / _my_games_itemall gain a.applet-list-entry.row-3colw.<a class="row-title">(clickable, 35c/32+... server-side truncated via newlyric_extras.truncate_titlefilter) +<span class="row-body">(most-recent activity excerpt, dimmed 0.6 opacity, CSS-text-overflow: ellipsisclipped to whatever space remains — no server-side trunc here so the full line lives in the DOM for inspectors) +<time class="row-ts">(relative_tsformatted, sameminmax(3rem,auto)rightward column allocation post.html's.post-line-timeuses, font-size 0.75rem + opacity 0.5 + right-aligned + nowrap); SCSS gridminmax(4rem,auto) 1fr minmax(3rem,auto)lifted from.post-line's template so the timestamp column lines up across post.html / scroll.html / every applet list; per-applet data shapes —_recent_postsannotates each Post w.latest_line(Line FK ordered by -id, None for empty Note-unlock posts);_recent_budsselect_related('to_user__active_title')warms the bud's donned-Note FK in one query for the buds row body ("the {{ bud.active_title_display }}" + "since {{ bud.active_title.earned_at|relative_ts }}" — the "since " prefix is unique to this row since the ts is "when they donned it", not the row's own creation);_recent_notesattachesdescriptionfrom_NOTE_METAper slug;annotate_latest_event(rooms)helper added toapps.epic.utils(next torooms_for_user) — attachesroom.latest_eventper Room w. one.events.order_by('-timestamp').first()per item, used by_billboard_contextformy_rooms(My Scrolls applet) AND byapps.gameboard.views.gameboard+toggle_game_appletsformy_games(My Games applet), keeping the My Scrolls + My Games shapes symmetric;_billboard_context.my_rooms = annotate_latest_event(...)swapsrooms_for_user(...).order_by("-created_at")materialisation point — bud row's "no active title" branch silently drops body + ts cells so unrecognised buds still surface but don't fabricate a "since None" line; newtruncate_titlefilter is the existing_truncate_post_titleview helper hoisted into the template namespace (literal...past 35 chars, None-safe); 5 ITs in BillboardViewTest cover row content / row absence on missing activity / "since" prefix uniquely on the buds row + 1 in GameboardViewTest for My Games row event prose; deferred row-prose body content cap on<span class="row-body">purely to CSStext-overflow: ellipsisper user's "middle col should take up the remaining space" steer (initial pass also server-side trunc'd the body to 35c; removed) — TDD
Disco DeDisco
2026-05-12 23:06:55 -04:00 -
eccb84f92b
applet feed unification — My Buds + My Notes drop the [Feature forthcoming] / empty placeholders for live top-3 feeds, mirroring the long-standing My Posts pattern; all five in-grid list applets (My Posts / My Buds / My Notes / My Scrolls / My Games) now route their <ul> through a single shared partial
_applet-grid-list.html(newly extracted) so item rendering + empty-state row + scroll-buffer all live in one place —_applet-list-shell.html(the dedicated billbuds/billposts page shell) now internally includes the same grid-list partial for its inner <ul>, so the dedicated-page and in-grid lists share the same skeleton; new per-applet item partials_my_buds_applet_item.html(mirrors_my_buds_item.htmlw. data-bud-id + display_name),_my_notes_item.html(links to billboard:my_notes; uses display_name),_my_posts_applet_item.html(Post link + title),_my_scrolls_item.html(Room link to billboard:scroll),_my_games_item.html(Room link to epic:gatekeeper); view-side_billboard_contextgains_recent_buds(user)— sorts the User.buds auto-through table by-idso newest-added-first w.o. an explicit through model w. timestamps (manage[r.to_user for r in rows]) — +_recent_notes(user)(user.notes.order_by('-earned_at')[:limit]); same two helpers threaded intonew_post's GET-with-form-errors branch (line 270-274) so the rerender keeps the new applet content visible; 7 ITs added to BillboardViewTest covering recent_buds ordering / cap / empty + recent_notes ordering / cap / cross-user isolation / empty; SCSS —.applet-list / .applet-list-entry / .applet-list-bufferlifted from.applet-list-page .applet-scrollscope to top level so they apply in both surfaces; in-grid applets getdisplay: flex; flex-direction: column; .applet-list { flex: 1 }so the list scrolls within the applet box;#id_applet_my_gamesul-centring +.scroll-list+#id_applet_notes h2 { writing-mode: vertical-rl ... }overrides removed (centring was an empty-state-only behaviour, scroll-list + vertical-rl redundant w. the new shared rule + the %applet-box> h2rule); My Games items now left-aligned by default; empty-state row recovers the centred-italic-dim treatment via.applet-list-entry--empty { flex: 1; display: flex; align-items: center; justify-content: center; opacity: 0.6; font-style: italic }+.applet-list:has(> .applet-list-entry--empty) { display: flex; flex-direction: column }— so "No buds yet" / "No notes yet" / "No games yet" / "No scrolls yet" / "No posts yet" all centre in their applet aperture, reverting to the left-aligned stack the moment a real item lands; Most Recent Scroll's outer empty<p><small>No recent activity.</small></p>adopts the same.applet-list-entry .applet-list-entry--emptyclasses (section is already flex-column from existing rule) so it picks up the unified centred-italic-dim treatment
Disco DeDisco
2026-05-12 22:48:32 -04:00 -
6a7464ee4b
post.html: gear-btn + #id_post_menu (NVM / DEL / BYE) mirror room.html's #id_room_menu — all Posts get the gear w. NVM (→ billboard:my_posts); user-Posts (kind=USER_POST / SHARE_INVITE) additionally surface DEL for the author (POST → billboard:delete_post → hard-deletes the Post; cascades Lines via FK + clears shared_with M2M) and BYE for invitees (POST → billboard:abandon_post → removes request.user from post.shared_with; owner + other invitees keep the thread); admin-Posts (kind=NOTE_UNLOCK) intentionally render gear w. NVM only since the system thread isn't user-owned (defence-in-depth: both delete_post + abandon_post no-op on NOTE_UNLOCK so a forged POST can't bypass the menu's branch);
_post_gear.htmlpartial gates DEL/BYE onviewer_is_owner(set by view_post since the buds sprint) + post.kind, then includes the sharedapps/applets/_partials/_gear.htmlbtn; styling rides the existing applets.scss page-level pattern —.post-pagejoins.billboard-page / .room-page / .dashboard-page / .wallet-page / .gameboard-page / .billscroll-pagein the> .gear-btn { position: fixed; bottom: 4.2rem; right: 0.5rem }rule (and the landscape footer-sidebar centred variant),#id_post_menujoins the%applet-menuextension list + the page-level fixed-menu rule (bottom: 6.6rem; right: 1rem); 5 FTs in test_bill_post_gear.py (owner DEL flow, invitee BYE flow, 3 menu-shape assertions for owner/invitee/admin) + 11 ITs across DeletePostViewTest + AbandonPostViewTest (302 redirect target, side effect, GET-is-no-op, non-owner / non-invitee / NOTE_UNLOCK protection) — TDD
Disco DeDisco
2026-05-12 22:26:12 -04:00 -
c64d7b9534
my_posts: owner-posts pane title
@<handle>'s Posts→Posts by Me— symmetrises w. the right pane'sPosts by Others(both panes now read as point-of-view labels rather than one identity-stamped + one generic), and drops the@<handle>reveal from a screen the owner alone visits (handle is already in the navbar avatar tooltip);handle = owner.username or owner.emailderivation goes w. it since nothing else in the view referenced it
Disco DeDisco
2026-05-12 21:01:59 -04:00 -
4b47dabaf0
woodpecker main.yaml: serialise test-FTs-room behind test-FTs-non-room — both FT steps share the workspace AND fall back to SQLite (only test-UTs-n-ITs sets
DATABASE_URLto the postgres service), so running them concurrently collided onsrc/test_db.sqlite3: the second-to-start container hit a half-created DB and the runner'sType 'yes' to delete the existing test databaseprompt EOFed under non-interactive CI stdin (pipeline run #296); fix flipstest-FTs-room.depends_onfromtest-two-browser-FTs→test-FTs-non-roomso the heavy room cluster strictly follows the non-room bucket; section comment rewritten from "Parallel FT split" → "FT split (sequential for now)" w. the run #296 EOFError documented + two re-parallelisation paths spelled out for later (per-step distinct sqlite paths viaDATABASE_URL=sqlite:////tmp/test_db_<bucket>.sqlite3OR per-step distinct postgres DBs); the two stale "parallel" mentions (collectstatic note in test-two-browser-FTs + inline comment in test-FTs-room) also updated; screendumps + build-and-push depends_on unchanged — Woodpecker resolves the transitive ordering fine
Disco DeDisco
2026-05-12 20:44:04 -04:00 -
a21e6aa251
requirements: align dev w. prod — add celery, psycopg2-binary, redis to requirements.dev.txt (missing from the CI dev image's pre-installed set, which means the CI fallback
pip install -r requirements.dev.txtstep would not satisfy these prod runtime deps if the image lagged); bump requests 2.31.0 → 2.32.5 in requirements.txt to align w. the dev pin (eliminates the version-drift wedge where CI's dev-only install would silently downgrade-or-upgrade vs prod's pin)
Disco DeDisco
2026-05-12 20:10:22 -04:00 -
f9c05a3eba
functional_tests + CI: rename pass + structural consolidations + parallel test-FTs split — every FT file now starts with one of 6 prefixes (test_admin_* / test_bill_* / test_core_* / test_dash_* / test_game_room_* / test_trinket_*) plus the 4 page-roots test_billboard / test_dashboard / test_gameboard / test_jasmine, so the partition is unambiguous and stable for tooling (the previous mix of test_applet_*, test_room_*, test_component_*, ad-hoc names had no consistent grouping); session-side: merged test_gatekeeper_bud_btn.py into test_bud_btn.py (then user renamed to test_core_bud_btn.py) — both files drove the same #id_bud_btn UI in two contexts (post-share + gatekeeper invite) and shared the bud-btn.js skeleton, so consolidation was overdue; split test_component_cards_tarot.py into test_admin_tarot.py (just TarotAdminTest, sitting next to test_admin / test_admin_post_readonly) + 3 classes (TarotDeckTest / GameKitDeckSelectionTest / GameKitPageTest) appended to test_game_room_tray.py; updated stale
test_bud_btn.pyreferences in the test_core_bud_btn.py docstring + test_admin_post_readonly.py comment to point at the new filename; user-driven renames (22 files): test_applet_my_notes/posts → test_bill_my_*, test_applet_new_post[_line_validation] → test_bill_new_post[_line_validation], test_applet_my_sky → test_dash_my_sky, test_applet_palette → test_dash_palette, test_wallet → test_dash_wallet, test_login → test_core_login, test_navbar → test_core_navbar, test_sharing → test_core_sharing, test_layout_and_styling → test_core_styling, test_my_buds → test_bill_my_buds, test_bud_btn → test_core_bud_btn, test_deck_contribution → test_game_room_deck_contrib, test_game_invite → test_game_room_invite, test_room_gatekeeper → test_game_room_gatekeeper, test_room_role_select → test_game_room_select_role, test_room_sea_select → test_game_room_select_sea, test_room_sig_select → test_game_room_select_sig, test_room_sky_select → test_game_room_select_sky, test_room_tray → test_game_room_tray, test_component_tray_tooltip → test_game_room_tray_tooltip; the post_page.py / room_page.py helper modules from the May-12 sprint absorbed the cross-file FT imports that would otherwise have cascade-broken on these renames
Disco DeDisco
2026-05-12 20:06:25 -04:00 -
af1a90e76b
woodpecker main.yaml: A+B combined CI dependency speedup — test-UTs-n-ITs swaps image from
python:3.13-slimtogitea.earthmanrpg.me/discoman/python-tdd-ci:latest(same image as the two FT steps) so all 3 steps inherit therequirements.dev.txtdeps Dockerfile.ci already pre-installs; all 3 steps switchpip install -r requirements.txt→pip install -r requirements.dev.txtso the install collapses to ~5-10s of "already satisfied" verification per step (vs ~30-60s of unpinned PyPI resolver+download against requirements.txt); ~3-5 min saved per pipeline run; drift safety net preserved — pip still installs deltas if requirements.dev.txt is ahead of the image, so a stale image doesn't break CI, it just runs slower until the image is rebuilt + pushed
Disco DeDisco
2026-05-12 19:36:04 -04:00 -
8240de6b45
functional_tests/room_page.py: extract shared FT helpers into a dedicated module so renaming a test_X.py file doesn't cascade-break sibling imports —
_fill_room_via_orm(was in test_room_role_select, imported by test_room_tray + test_deck_contribution + test_game_invite + test_room_gatekeeper + test_room_sig_select),_assign_all_roles(was in test_room_sig_select, imported by test_room_tray + test_deck_contribution), and_equip_earthman_deck(duplicated verbatim in test_room_role_select + test_component_tray_tooltip — the test_component_tray_tooltip copy of_fill_room_via_ormwas also a near-duplicate, missing only the gamers-return both call sites discarded anyway);SIG_SEAT_ORDERconstant moves along since_assign_all_rolesdepends on it; mirrors the existing post_page.py pattern; underscored helper names kept so the "test infrastructure, not API" signal survives the relocation; dropped now-unused per-file imports (DeckVariant from test_room_role_select; Note/DeckVariant/TableSeat/TarotCard from test_room_sig_select; GateSlot from test_component_tray_tooltip); regression gate: GatekeeperTest (8 FTs) green via the new helper home; smoke-imports green across all 8 touched modules
Disco DeDisco
2026-05-12 19:23:08 -04:00 -
b97c4a0508
woodpecker main.yaml: workspace-shared pip cache via
PIP_CACHE_DIR: .pip-cacheon all 3 test steps (test-UTs-n-ITs / test-two-browser-FTs / test-FTs) — each step currently re-downloads the entire requirements.txt wheel set independently; Woodpecker mounts the workspace across steps in one run, so the wheels populated by step 1 are reused by steps 2-3, saving ~1-2 min of pip-install wall-clock per run (only download time — install still happens fresh per container); pyswiss.yaml untouched (separate pipeline, separate workspace, separate requirements); cache is per-run only — cross-run persistence would need a Woodpecker volume plugin
Disco DeDisco
2026-05-12 18:01:34 -04:00 -
3932b17256
aperture architecture: lift the page-locking foundation (html/body/.container overflow:hidden + flex-column + min-height:0; .row flex-shrink:0) from 5 per-page SCSS files into _base.scss — was opt-in per page via
body.page-billboard/page-dashboard/page-gameboard/page-sky/page-walletetc., with 5 near-identicalhtml:has(body.page-X) { overflow: hidden }+body.page-X { … }blocks duplicating the same rules; any page that forgot to setpage_classin its view context (e.g.epic.tarot_deck— never set) rendered without the aperture, letting applet borders + titles clip past the fixed navbar/footer sidebars at narrower viewports; foundation now universal, page-specific overrides stay scoped — gameboard keeps.container { overflow: clip }(Firefox seat-tooltip scroll-anchoring quirk) + billboard/dashboard/gameboard keep.row { margin-bottom: -1rem }(h2-row tightening); page_class context vars + body class hooks preserved (FTs at test_bud_btn.py:370 / :379 still assert on them); regression gate: 60 layout-sensitive FTs (billboard, my_buds, bud_btn, applet_my_posts, dashboard, wallet, gameboard, layout_and_styling, jasmine) + 43 room FTs (gatekeeper_bud_btn, room_gatekeeper, room_sky_select, sharing) all green
Disco DeDisco
2026-05-12 17:16:12 -04:00