sky_save now re-fetches from PySwiss server-side on save so stored chart_data always carries enriched element format (contributors/stellia/ parades). New sky/data endpoint serves fresh PySwiss data to the My Sky applet on load, replacing the stale inline json_script approach. natus-wheel.js: sign ring slices (data-sign-name) and house ring slices (data-house) now have click handlers with _activateSign/_activateHouse; em-dash fallback added for classic elements with empty contributor lists. Action URLs sky/preview, sky/save, sky/data lose trailing slashes. Jasmine: T12 sign tooltip, T13 house tooltip, T14 enriched element contributor display (symbols, Stellium/Parade formations, em-dash fallback). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
8.4 KiB
EarthmanRPG — Project Context
Originally built following Harry Percival's Test-Driven Development with Python (3rd ed., complete through ch. 25). Now an ongoing game app — EarthmanRPG — extended well beyond the book.
Browser Integration
Claudezilla is installed — a Firefox extension + native host for browser automation.
See .claude/skills/claudezilla-browser/SKILL.md for tool list, startup protocol, and setup reference.
STARTUP RULE: Call mcp__claudezilla__firefox_diagnose at the start of every conversation before any browser tool. If tools aren't listed in a session, open a new Claude Code conversation (MCP servers load at startup only).
Stack
- Python 3.13 / Django 6.0 / Django Channels (ASGI via Daphne/uvicorn)
- Celery + Redis (async email, channel layer)
- django-compressor + SCSS (
src/static_src/scss/core.scss) - Selenium (functional tests) + Django test framework (integration/unit tests)
- Stripe (payment, sandbox only so far)
- Hosting: DigitalOcean staging (
staging.earthmanrpg.me) | CI: Gitea + Woodpecker
Project Layout
The app pairs follow a tripartite structure:
- 1st-person (personal UX):
lyric(backend — auth, user, tokens) ·dashboard(frontend — notes, applets, wallet UI) - 3rd-person (game table UX):
epic(backend — rooms, gates, role select, game logic) ·gameboard(frontend — room listing, gameboard UI) - 2nd-person (inter-player events):
drama(backend — activity streams, provenance) ·billboard(frontend — provenance feed, embeddable in dashboard/gameboard)
src/
apps/
lyric/ # auth (magic-link email), user model, token economy
dashboard/ # Notes (formerly Lists), applets, wallet UI [1st-person frontend]
epic/ # rooms, gates, role select, game logic [3rd-person backend]
gameboard/ # room listing, gameboard UI [3rd-person frontend]
drama/ # activity streams, provenance system [2nd-person backend]
billboard/ # provenance feed, embeds in dashboard/gameboard [2nd-person frontend]
api/ # REST API
applets/ # Applet model + context helpers
core/ # settings, urls, asgi, runner
static_src/ # SCSS source
templates/
functional_tests/
Template directory convention
Templates live under templates/apps/<frontend-app>/, not under the backend app that owns the view logic. Specifically:
lyric/views →templates/apps/dashboard/epic/views →templates/apps/gameboard/drama/views →templates/apps/billboard/
Backend apps (lyric, epic, drama) have no templates/ subdirectory.
Dev Commands
# Dev server (ASGI — required for WebSockets; no npm/webpack build step)
cd src
uvicorn core.asgi:application --host 0.0.0.0 --port 8000 --reload --app-dir src
# Integration + unit tests (exclude channels)
python src/manage.py test src/apps --exclude-tag=channels
# Functional tests
python src/manage.py test src/functional_tests
See .claude/skills/TDD/SKILL.md for the full TDD cycle, test file conventions, base classes, and per-layer run commands. See .claude/skills/dev-server/SKILL.md for server startup options.
Multi-user manual testing — setup_sig_session
Creates (or reuses) a room at table_status=SIG_SELECT with all 6 slots filled. Prints one pre-auth URL per gamer.
python src/manage.py setup_sig_session
python src/manage.py setup_sig_session --base-url http://localhost:8000
python src/manage.py setup_sig_session --room <uuid>
Fixed gamers: founder@test.io (discoman), amigo@test.io, bud@test.io, pal@test.io, dude@test.io, bro@test.io — all superusers with Earthman deck. URLs use /lyric/dev-login/<session_key>/ pre-auth pattern.
CI/CD + Hosting
- Git remote:
git@gitea:discoman/python-tdd.git(port 222, key~/.ssh/gitea_keys/id_ed25519_python-tdd) - Gitea:
https://gitea.earthmanrpg.me| Woodpecker CI:https://ci.earthmanrpg.me - Push to
maintriggers Woodpecker → deploys to staging (staging.earthmanrpg.me) - Prod deploy:
git tag v1.0.0 && git push --tags→ triggersdeploy-prodstep (tag-based gate) - Two CI pipelines run in parallel:
.woodpecker/main.yaml(main app) +.woodpecker/pyswiss.yaml(PySwiss at charts.earthmanrpg.me) - Multi-browser FTs tagged
@tag("two-browser")run in a dedicated CI stage (test-two-browser-FTs) alongside--tag=channels;test-FTsstage is parallel-only - Hosting: DigitalOcean — main app on staging droplet; PySwiss on separate droplet (167.172.154.66)
- Email: Mailgun (
adman@howdy.earthmanrpg.me) | DNS: NameCheap
UI / Layout Conventions
Sidebar layout ($sidebar-w: 4rem)
Navbar is a fixed left sidebar; footer is a fixed right sidebar. Both are 4rem wide. Main container uses margin-left: $sidebar-w; margin-right: $sidebar-w. Landscape layout resets min-width to 0 on .gameboard-page and #id_dash_content (override of the @media (min-width: 738px) block that sets min-width: 666px).
Applet headings + page titles
- Section headings: plain
<h2>— browser default + body color inherited; no extra SCSS needed - Clickable headings:
<h2><a href="...">Text</a></h2>— globalbody arule supplies gold + hover glow - Page titles:
<span>Dash</span>suffixpattern (Dashwallet, Dashnote, Dashnotes)
Position vs Seat terminology
Circles around the table hex are positions (gate slot order, 1–6). After role assignment they become seats (PC→NC→EC→SC→AC→BC). CSS carries both: .table-seat.table-position. SLOT_ROLE_LABELS = {1:"PC", 2:"NC", 3:"EC", 4:"SC", 5:"AC", 6:"BC"} in epic/views.py.
Game Architecture
Token priority chain
select_token(user) in apps.epic.models: PASS → COIN → FREE → TITHE → None. debit_token handles each type's consumption rules (Coin cooldown, Free/Tithe expiry).
Two-step gate token flow
Drop → RESERVED → confirm/reject. _gate_context() builds slot state; _expire_reserved_slots() clears stale reservations after 60s. Views: confirm_token, reject_token (renamed return_token).
Room URL routing
epic:room view at /gameboard/room/<uuid>/. gatekeeper redirects there when table_status is set. Error redirects in select_role/select_sig use epic:room if table_status is set, else epic:gatekeeper.
SCSS Import Order
core.scss: rootvars → applets → base → button-pad → dashboard → gameboard → palette-picker → room → card-deck → natus → tray → billboard → tooltips → game-kit → wallet-tokens
Critical Gotchas
Tooltip portal pattern
mask-image on grid containers clips position: absolute tooltips. Use #id_tooltip_portal (position: fixed; z-index: 9999) at page root. See gameboard.js + wallet.js.
Applet menus + container-type
container-type: inline-size creates a containing block for all positioned descendants — applet menus must live OUTSIDE #id_applets_container to be viewport-fixed.
ABU session auth
create_pre_authenticated_session must set HASH_SESSION_KEY = user.get_session_auth_hash() and hardcode BACKEND_SESSION_KEY to the passwordless backend string.
Magic login email mock paths
- View tests:
apps.lyric.views.send_login_email_task.delay - Task unit tests:
apps.lyric.tasks.requests.post - FTs: mock both with
side_effect=send_login_email_task
game-kit.js selection persistence
window._kitTokenId must NOT be cleared on kit-bag close — users close the dialog before clicking the rails button. Selection persists until page navigation. No clearSelection() in game-kit.js.
Billboard timezone cookie
document.cookie = 'user_tz=' + Intl.DateTimeFormat().resolvedOptions().timeZone — no encodeURIComponent. Slashes in TZ names (America/New_York) are cookie-safe; encoding breaks the ZoneInfo lookup in TimezoneMiddleware.
CSS :has() for child-dependent styling
Use .parent:has(.child-class) to style a parent based on its contents without template changes. Example: .gate-slot:has(.drop-token-btn) makes CARTE OK-button circles match .reserved circles.
Plausible FT noise
Plausible analytics script in base.html fires a beacon during Selenium tests → harmless console error. Fix: {% if not debug %} guard around the script tag.
See .claude/skills/TDD/SKILL.md for test-specific gotchas (TransactionTestCase flush, static files in tests, Selenium text-transform, multi-browser CI, msgpack integer keys).