Files
python-tdd/CLAUDE.md
Disco DeDisco 599d40decd auth urls: mount apps.lyric.urls under /dashboard/ to mirror gameboard/epic & billboard/drama convention
- core/urls.py: replace `path('lyric/', …)` with second `path('dashboard/', include('apps.lyric.urls'))` alongside existing dashboard mount; no path-name collision (lyric paths: send_login_email, login, logout, dev-login/<key>/)
- IT test URL strings flipped /lyric/ → /dashboard/ (test_views.py)
- setup_sig_session + setup_sea_session pre-auth URL builders updated
- CLAUDE.md doc note updated
- Templates use unnamespaced `{% url 'logout' %}` / `{% url 'send_login_email' %}` so they auto-resolve; no template edits needed
- /admin/lyric/user/ admin URL untouched (driven by app_label, not URL conf)

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 00:18:36 -04:00

8.4 KiB
Raw Blame History

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 main triggers Woodpecker → deploys to staging (staging.earthmanrpg.me)
  • Prod deploy: git tag v1.0.0 && git push --tags → triggers deploy-prod step (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-FTs stage 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> — global body a rule supplies gold + hover glow
  • Page titles: <span>Dash</span>suffix pattern (Dashwallet, Dashnote, Dashnotes)

Position vs Seat terminology

Circles around the table hex are positions (gate slot order, 16). 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.

document.cookie = 'user_tz=' + Intl.DateTimeFormat().resolvedOptions().timeZoneno 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).