Centralized online results and event management for Jugger.
Or, what the fog site always wanted to be
See CONTRIBUTING for how to get involved.
- This README - architecture, conventions, end-to-end request trace.
app/README.md- what the backend factory does and how a request flows through the route -> service -> model -> db layers.app/models/README.md- the canonical domain shape (entity diagram, scope-column invariants, money / time conventions).docs/arctos-schedule-script.md- the ASS DSL.tests/README.mdplusTESTING.md- fixtures and how to write a test that pulls its weight.
After that, dip into the other per-directory READMEs as you encounter their code.
┌─────────────────┐ /_api/... ┌──────────────────┐ SQLAlchemy ┌──────────┐
│ Dioxus SPA │ ─────────────-> │ Flask backend │ ──────────────-> │ SQLite │
│ (Rust -> WASM) │ │ (gunicorn) │ │ (WAL) │
│ served by │ │ │ └──────────┘
│ nginx at / │ │ blueprints, │
└─────────────────┘ │ services, │
│ models │
└──────────────────┘
- Frontend. Single-page app written in Rust with Dioxus,
compiled to WebAssembly. nginx serves the SPA at
/. - Backend. Python 3.12 Flask app run under gunicorn. Serves only
JSON, exclusively under the
/_api/prefix./api/(no underscore) is reserved for a future public API and is not used. - Database. A single SQLite file (
tournament.db). WAL mode is enabled so the finalize-recording worker and HTTP handlers can share the file without blocking each other. Foreign keys are enforced viaPRAGMA foreign_keys = ONon every new connection - without that pragma SQLite ignoresFOREIGN KEYdeclarations entirely. - Schema migrations. Managed by Alembic. See
migrations/README.md.
| Path | What lives here |
|---|---|
app/ |
Flask backend: factory, blueprints, services, models, utils. |
app/models/ |
SQLAlchemy ORM models - the canonical domain shape. |
app/routes/ |
Flask blueprints. Every route lives under /_api/. |
app/services/ |
Application-workflow code that routes call into. |
app/utils/ |
Helpers: scheduling, the ASS Lisp DSL, datetime, video pipeline. |
app/domain/ |
Domain enums (MatchStatus, ScheduleType, ...). |
app/serializers/ |
DB -> JSON shape conversion. |
frontend/ |
Rust/Dioxus SPA. |
tests/ |
Pytest suite. See also TESTING.md. |
scripts/ |
Operational scripts (backups, data-quality checks). |
migrations/ |
Alembic migrations. |
setup/ |
Per-OS bootstrap (just setup shells out to these). |
build_system/ |
Dockerfile used to build the Sphinx user docs. |
docs/ |
End-user / Sphinx documentation (deploy runbook, ASS reference). |
static/ |
Static assets served by Flask. |
Top-level files:
run_app.py- WSGI entry point.gunicorn run_app:appis what production runs;python run_app.pyruns the dev server.models.py- re-exportsapp.models.*so bothfrom app.models import ...andfrom models import ...work.justfile- the canonical command surface. Runjust(orjust --list) to see every recipe.pyproject.toml- dependencies and tool config (ruff, mypy, pytest).alembic.ini- Alembic config; the env file lives inmigrations/env.py.init_db.py,reset_password.py- small CLI utilities (legacy; prefer the factory-based flow for new code).
Supported platforms: macOS on Apple Silicon and Ubuntu/Debian on x86_64. Windows isn't supported directly; use WSL.
- Install uv.
- Set up your SSL certs. If you're using nginx you can do this there and use certbot. If you're just testing, you can generate self-signed certs with:
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 365or
just certsThis writes cert.pem and key.pem to the repo root (valid for
365 days, CN=localhost). Override with
just cert_days=730 cert_subject=/CN=arctos.example.com certs,
and pass force=1 to overwrite existing certs.
- Create a
.envfile at the repo root with the variables you need:
ARCTOS_CORS_DEV=1
ARCTOS_API_BASE=http://127.0.0.1:8081
YOUTUBE_API_KEY=your_youtube_api_key
GOOGLE_CLIENT_SECRET=your_google_client_secret
GOOGLE_CLIENT_ID=your_google_client_id
SECRET_KEY=your_app_secret_keyIf you don't have some of these, you can leave them empty; they are
only needed for the sign in with google and youtube auto-seek
features. The SECRET_KEY variable must be a random value for
security reasons. You can get one by running
python -c "import os; print(os.urandom(12).hex())"Important
The ARCTOS_CORS_DEV and ARCTOS_API_BASE are only for dev
environments where you don't have a reverse proxy set up to direct
traffic and are thus hosting the frontend and backend on different
ports.
- Start the app:
just runThis loads .env, runs uv sync, and starts gunicorn. The defaults
match the example above (5 workers, binding 0.0.0.0:8081, using
cert.pem/key.pem). Override any of them on the command line, e.g.:
just workers=10 bind=0.0.0.0:9000 run
just certfile= keyfile= run # if you handle SSL elsewhere
just env_file=.env.prod run # use a different env fileTo store finalized match recordings in an s3 compatible bucket (I use
Backblaze B2) instead of local disk, set these environment variables
in your run script:
| Variable | Required | Description |
|---|---|---|
S3_VIDEO_BUCKET |
Yes | bucket name (create a private bucket in the B2 dashboard). |
S3_ENDPOINT_URL |
Yes (for B2) | B2 S3-compatible endpoint, e.g. https://s3.us-west-002.backblazeb2.com. Use the endpoint for the region where you created the bucket. |
AWS_REGION |
Yes (for B2) | Must match the endpoint region, e.g. us-west-002 or us-east-005. |
AWS_ACCESS_KEY_ID |
Yes | Application Key ID. Needs R/W access. |
AWS_SECRET_ACCESS_KEY |
Yes | corresponding secret key |
S3_PRESIGNED_EXPIRY_SECONDS |
No | Presigned URL lifetime in seconds (default 3600). |
Install the Dioxus CLI:
cargo install dioxus-clithen (for development) serve the app:
just frontend # equivalent to `cd frontend && dx serve`In production, you should run dx bundle --release and copy the
output files to somewhere that your reverse proxy can serve.
| Goal | Command |
|---|---|
| Run all tests | just test |
| Lint | just lint |
| Format | just format |
| Apply migrations | just db-migrate-safe |
| Generate a migration | just db-revision "snake_case_message" |
| Start dev backend | just dev (HTTP, :5006) or just run (TLS, :8081) |
| Start dev frontend | just frontend |
Coverage is configured under [tool.coverage.*] in pyproject.toml
with a soft fail_under = 30 floor; current actual is around 33% with
branch coverage on. See TESTING.md for the
HTML report path and how the threshold is meant to move.
- All API routes live under
/_api/. Tests and the frontend always hit/_api/.... - Routes return JSON, not redirects. Success is
200; validation failure is400; unauthenticated is401. Don't assert on redirect codes in tests. Result/Optionfor errors as values. Services returnResult[T, ArctosError]. The.Q()method (Rust's?) propagates errors when used inside@allow_Q-decorated functions. Seeapp/error_values.py.- Money is
Numeric(10, 2), neverfloat. Penny-exact reconciliation across many partial payments requires exact decimals. - Times are stored as naive UTC. The model layer converts client-supplied times to UTC and strips the tzinfo before persisting. The frontend handles timezone display.
- Join tables for multi-value data.
MatchReferee,MatchPlayer,HeadRefAllowList,CameraTimepointare accessed through the helpers inapp/services/dual_write.py, not by attribute on the parent model. - Tournaments belong to either a league or a
RegistrableConfig. This mutual exclusivity is enforced by a CHECK constraint. League events inherit the league's registration config; standalone events own theirs. Useapp.utils.helpers.get_registrable_config(tournament)to dereference correctly in either case.
Tracing a single request is the fastest way to learn the codebase.
Take POST /_api/<tournament_url>/register-team:
- Frontend (
frontend/src/) builds a form, POSTs viareqwestwith credentials. - nginx (in production) proxies anything starting with
/_api/to gunicorn; everything else is the SPA. In dev withARCTOS_CORS_DEV=1the browser hits Flask directly. - Flask routes the request to
app/routes/registration.py::register_team_for_tournament. - The route does a thin auth/shape check, then calls
RegistrationService.register_team(...)inapp/services/registration_service.py. - The service returns a
Result[T, ArctosError]. Routes pattern-match on the result and return JSON{success, ...}with HTTP 200/400. - The service mutates
TeamRegistrationrows via SQLAlchemy. ORM models live inapp/models/.db.session.commit()writes to SQLite.
Four layers: route -> service -> model -> db. Routes stay thin; business logic lives in services; persistence lives in models.
- I want to understand domain shapes ->
app/models/. - I want to add an endpoint ->
app/routes/(and probably a service inapp/services/). - I want to understand match scheduling ->
app/utils/scheduling.pyandapp/utils/MatchGraph.py. - I want to understand the skip-condition / ASS DSL ->
app/utils/parser.py,app/utils/grammar.lark, and the user-facing reference atdocs/arctos-schedule-script.md. - I want to deploy ->
docs/DEPLOY.md. - I want to add a database column ->
migrations/README.md. - I want to understand video upload / finalisation ->
app/utils/footage.py,app/utils/s3_video.py,app/utils/youtube_upload.py.
Bug reports and feature requests live on
GitHub. Branching, PR
process, and code-quality expectations live in
CONTRIBUTING.md. Test guidance lives in
TESTING.md.