Mechanical rename: 5 files (sky-wheel.js, _sky.scss, _sky_overlay.html, SkyWheelSpec.js x2), 24 in-place edits across templates/views/urls/SCSS/JS/tests/CLAUDE.md. URL names epic:natus_save → epic:sky_save (epic namespaced, no clash w. dashboard:sky_save), JS module NatusWheel → SkyWheel, DOM ids id_natus_* → id_sky_*, BEM classes natus-* → sky-*, dashboard sky_natus_data/sky_natus_preview collapsed to sky_data/sky_preview_data. No DB migration needed (User.sky_chart_data + GameEvent.SKY_SAVED already used sky-prefix). 778 ITs + Jasmine green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9.1 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 /dashboard/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 → sky → 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.
JSONField .exclude(data__key=value) on SQLite
.exclude(data__retracted=True) on a row whose data has no retracted key resolves to WHERE NOT (NULL = TRUE) → NULL → SQL filters that row out. The exclude becomes "exclude rows where the key is True OR missing" instead of "exclude rows where the key is True". PostgreSQL evaluates this correctly, so the bug only manifests in local dev / SQLite ITs. If you mean exclude only when the key exists and equals X, do the predicate in Python after fetching a buffered queryset (see _billboard_context for the pattern). The same trap applies to .filter(data__key=value) — you'll silently miss rows where the key is missing.
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).