Skip to content

Development Guide

Welcome to the Trusty Track development guide! This document provides instructions on how to set up your environment, run the application locally, and execute tests.

πŸ—οΈ Project Overview

Trusty Track is a race management application built with:

  • Backend: Python (FastAPI, Strawberry GraphQL, SQLAlchemy, SQLite, Alembic)
  • Frontend: TypeScript (React 19, Vite, urql + graphcache). Plain CSS with custom properties β€” no component or utility framework.

It is designed to run as a single process on a machine at the venue, often a Raspberry Pi, serving one operator and a few read-only display screens over the local network.

πŸ“‹ Prerequisites

Ensure you have the following installed:

  • Python 3.10+ β€” 3.10 is the floor (Debian's system interpreter on a Pi); 3.12 is what the installers and release builds pin. CI tests both.
  • Node.js 22+ & npm β€” 22 is the floor; .nvmrc holds the version CI, the Docker image and the Pi installer all use.
  • uv β€” the Python toolchain used throughout.
  • Git

πŸ”™ Backend Development

The backend lives in backend/, but the Python project is rooted at the repo root β€” pyproject.toml, the virtualenv, and every uv run command are run from there, not from inside backend/.

1. Setup

uv sync

2. Running the Server

Start the development server with live reload:

uv run uvicorn backend.api.main:app --reload --port 8005

The API will be available at http://localhost:8005. GraphQL is at /graphql; interactive REST docs are at /docs.

3. Running Tests

uv run pytest
# One file, verbose
uv run pytest backend/tests/test_scoring_scope.py -v
# One test
uv run pytest backend/tests/test_scoring_scope.py -k leaderboard

The suite runs one worker per core, which takes it from about a minute and a half to under half a minute. Turn that off when you are debugging a failure β€” output arrives in order again, and pdb works:

uv run pytest -n0 backend/tests/test_scoring_scope.py

4. Linting and Types

Both gate CI over the whole tree, so run them before pushing:

uv run ruff check .
uv run ruff format --check .
uv run mypy backend

mypy is configured with an exemption list in pyproject.toml rather than a list of modules to check β€” a new module is covered from the day it is written, and the list can only shrink. It is down to one module, backend.api.schema; everything else, including all of backend/domain/ (held to disallow_untyped_defs), is checked.

5. Database

The application uses SQLite. The database file trusty-track.db is created in the data directory (~/.trustytrack by default, override with TRUSTYTRACK_DATA_DIR) on first run.

Migrations

Schema changes are managed with Alembic. init_db() runs alembic upgrade head on startup, so a running app always has an up-to-date schema. If migrations fail, startup fails β€” a half-migrated database that appears to start normally is worse than a clear error.

Run Alembic through ./scripts/migrate.sh, not uv run alembic directly. The bare CLI opens whatever TRUSTYTRACK_DATA_DIR resolves to, which is ~/.trustytrack for anyone who has not set it β€” your own real install, if this machine has ever run the app, complete with its database and every uploaded photograph. scripts/migrate.sh runs Alembic against a disposable scratch directory instead, migrated to head first so --autogenerate diffs against a realistic (if empty) schema.

After changing anything in models.py, generate a migration:

./scripts/migrate.sh revision --autogenerate -m "describe the change"

Review the generated file before committing β€” autogenerate is a good first draft, not a finished migration. Then apply it and verify there is no remaining drift:

./scripts/migrate.sh upgrade head
./scripts/migrate.sh check

alembic check compares the models against the target database, so it reports Target database is not up to date if you have not upgraded first.

backend/tests/test_migrations.py::test_migrations_reproduce_the_models runs that same check in CI, so a model change without a matching migration fails the build.

Other useful commands:

./scripts/migrate.sh current
./scripts/migrate.sh history
./scripts/migrate.sh downgrade -1

If you run uv run alembic by hand anyway, a backstop in backend/migrations/env.py refuses to touch the default ~/.trustytrack once it already holds a configured race, unless TRUSTYTRACK_DATA_DIR is set explicitly or TRUSTYTRACK_ALLOW_UNSAFE_MIGRATION=1 is β€” it never applies to the app's own startup migration, which keeps running against the real data directory regardless. See CLAUDE.md's "Database migrations" section for the reasoning.

Databases created before Alembic was adopted are detected on startup (application tables present, no alembic_version table), stamped at 0001_baseline, and then upgraded forward. Migration 0002 checks whether groups.debug_mode already exists before adding it, because the old hand-rolled ALTER TABLE may or may not have succeeded on any given install.

🎨 Frontend Development

The frontend is located in the frontend/ directory.

1. Setup

cd frontend
npm install

2. Running the UI

Start the Vite development server:

npm run dev

The application will be accessible at http://localhost:5173.

3. Running Tests

Run the frontend unit tests using vitest:

npm test -- --run

4. Linting and Types

npm run lint
npx tsc --noEmit

5. End-to-End Tests

A real backend and a real browser, on their own ports and their own database:

cd frontend && npm run test:e2e

The first run needs Playwright's browser:

cd frontend && npx playwright install chromium

These are deliberately few and broad β€” they exist to catch the failures the unit suites cannot, where the served page, the GraphQL round trip and the normalized cache have to work together. Anything worth asserting in detail belongs in a component test.

6. Generated GraphQL Types

frontend/schema.graphql and frontend/src/gql/ are generated from the live Strawberry schema β€” never edit them by hand. After any backend schema change:

npm run codegen

CI fails if the committed output is stale (npm run codegen:check), so a backend schema change cannot silently drift from the frontend's types.

πŸš€ Running the Full Stack

To install and run the application as a single process:

# Install everything (prerequisites: Python 3.10+, Node.js 22+)
./scripts/install.sh

# Start the server (serves both backend and frontend on port 8005)
./scripts/serve.sh

You can start both the backend and frontend simultaneously with live reload using:

./scripts/run_dev.sh

Option 3: Manual Start (Development)

To run the full application manually, you need two terminal windows:

Terminal 1 (Backend):

uv run uvicorn backend.api.main:app --reload --port 8005

Terminal 2 (Frontend):

cd frontend && npm run dev

The Vite dev server proxies /api/* to the backend with the /api prefix stripped, so an endpoint that only exists at /api/… works in production and 404s in development. Register both forms, as /graphql and the printables barcode do.

βœ… Pre-commit Hooks

pre-commit runs Ruff, pytest, ESLint, Vitest and a frontend build on every commit:

uv run pre-commit install

πŸ“š Building the Docs and the Website

The user guide is MkDocs Material, and mkdocs build --strict gates CI β€” a broken image path or a link to a missing page fails the build:

uv run --group docs mkdocs serve
uv run --group docs mkdocs build --strict

The guides are half of https://trusty-track.com/. The other half is the landing page in www/ β€” one HTML file and one stylesheet, no framework and no build step of its own. scripts/build_site.sh puts them together the way Cloudflare Pages does, with the landing page at the root and the documentation under /docs/:

./scripts/build_site.sh
python3 -m http.server -d dist 8080

Serve the result rather than opening dist/index.html from the filesystem β€” every link on the landing page is root-relative, so file:// resolves them against the disk root and none of them work.

The landing page links into the guides about fifteen times, and shows the logo and four screenshots straight out of docs/assets/. mkdocs --strict does not look at www/, so backend/tests/test_landing_page_links.py is what fails when a renamed page leaves the front door on a 404. Deployment is written up in deploy/cloudflare/README.md.

πŸ› Troubleshooting

Issue Solution
Port 8005/5173 in use Stop other running processes or allow the tools to pick the next available port (check console output).
CORS Errors The backend is configured to allow * origins in development. Ensure you are accessing the frontend via localhost matching the CORS config.
Database Locks SQLite can occasionally lock if a process crashes. Restart the backend server.
Missing Dependencies Re-run uv sync (backend) or npm install (frontend).
alembic hits your real database Use ./scripts/migrate.sh instead of uv run alembic … β€” it points Alembic at a scratch directory automatically. If you use the bare CLI anyway, it refuses on its own once your default database holds a configured race (#689); set TRUSTYTRACK_DATA_DIR or TRUSTYTRACK_ALLOW_UNSAFE_MIGRATION=1 if you mean it.

πŸ“Έ Regenerating Documentation Screenshots

The screenshots in docs/assets/screenshots/ are generated by Playwright specs in frontend/e2e/docs/, each walking the real UI against a real backend:

Spec Covers
screenshot-first-run.spec.ts The setup wizard and an empty Home page
race-day.spec.ts Race setup and race day
screenshot-observation.spec.ts The audience displays
screenshot-race-stats.spec.ts The Stats page
screenshot-awards.spec.ts Awards and the ceremony
screenshot-balanced.spec.ts Balanced racing
screenshot-bulk-upload.spec.ts Bulk photo upload
screenshot-elimination.spec.ts Ladderless elimination
screenshot-free-race.spec.ts Free race setup and results
screenshot-printables.spec.ts Print sheets and the check-in scanner
screenshot-settings.spec.ts System Settings and the activity log
screenshot-slowest-race.spec.ts The slowest race bracket
screenshot-timers.spec.ts Timer settings and the timer check page

If a screen you changed appears in one of these, re-run it and commit the new PNGs β€” a stale screenshot is a documentation bug that no build catches.

Prerequisites

Playwright's Chromium browser must be installed:

cd frontend
npx playwright install chromium

Running

cd frontend && npx playwright test --config playwright.screenshots.config.ts

The specs run in parallel against one backend β€” as many at a time as your machine has cores to spare β€” and the whole set takes about twenty seconds. One of them runs on its own first, covering the things that belong to the whole install rather than to a race: the setup wizard, which only exists before the install is configured; the empty Home page; and the operator PIN, which while it is set would stop every other spec's mutations.

Or one spec at a time:

cd frontend && npx playwright test --config=playwright.screenshots.config.ts e2e/docs/screenshot-printables.spec.ts

That first spec still runs, because the one you asked for needs a configured install to photograph. It adds a few seconds.

This will: 1. Start a fresh backend with an isolated, empty database, on a port and a data directory derived from this checkout (see frontend/e2e/environment.ts), so two worktrees can run this at once 2. Start a fresh frontend dev server pointing at that backend 3. Walk through the UI (create a race, add dens, import test data, etc.) and capture screenshots 4. Write the updated PNGs to docs/assets/screenshots/

Every run is clean β€” the test data directory is wiped before the backend starts, so screenshots always reflect a fresh install.

πŸ“ Directory Structure

  • backend/api/ β€” FastAPI app, Strawberry GraphQL schema, per-operation loaders.
  • backend/db/ β€” SQLAlchemy models, CRUD helpers, Alembic-backed init.
  • backend/domain/ β€” pure rules: scheduling, scoring, advancement, lanes, printables. No SQLAlchemy, no Strawberry, and typed strictly.
  • backend/services/ β€” scoring and stats wired to the database, timer devices.
  • backend/migrations/ β€” Alembic environment and versions.
  • frontend/src/features/<area>/ β€” one slice per area, each with its own pages/, components/, and graphql/queries.ts.
  • docs/ β€” the MkDocs site, served at /docs/ on trusty-track.com.
  • www/ β€” the landing page at the root of trusty-track.com.
  • deploy/ β€” how the site and the old documentation address are published.
  • scripts/ β€” install, serve, dev, schema-export and site-build scripts.

πŸ“ Keeping the Docs Current

Docs are part of the change, not a follow-up. When you land something:

If you changed… Update
A screen the guides describe The relevant docs/*.md, and re-run its screenshot spec
The GraphQL schema or a REST endpoint docs/design.md
Anything an agent needs to know CLAUDE.md, or the file it indexes under .claude/rules/

mkdocs build --strict catches broken links and missing images. It cannot catch prose that is merely wrong, which is the failure that actually happens β€” so check the page you invalidated.