Imported from cfbender/the-gathering (
AGENTS.md). Install upstream withnpx skills add cfbender/the-gathering. Copyright stays with the author.
AGENTS.md
Project Structure
The Gathering is a self-hosted Commander (Magic: The Gathering) game tracker: a Rust JSON API and realtime server plus a Vite/React single-page app, shipped as one container or as a tarball for the Proxmox LXC installer.
rust/— the backend (Cargo workspace; seerust/README.md):crates/the-gathering(axum + sqlx server: API, webcam table channels over Socket.IO (socketioxide on the server,socket.io-clientin the frontend), Discord bot, background jobs) andcrates/sfu(the webcam table's str0m WebRTC SFU). Shared Magic code (Scryfall, deck-list sources, name normalization, commander rules) comes from thelotusgit dependency; report lotus gaps instead of forking it here.rust/migrations/*.sql— the schema's only source, one file per migration (<14-digit UTC version>_<name>.sql). The versions continue the numbering existing databases record inschema_migrations.rust/schema.sql— the generated dump of those migrations, committed for review;rust/.sqlx— committed query metadata for offline builds.rust/crates/the-gathering/src/web/— router (mod.rs), auth guards, session/CSRF, the SPA shell, and the/apihandlers inweb/api/.rust/crates/the-gathering/tests/integration/— HTTP and domain tests, compiled as one test binary (main.rslists the modules; add a new file there);support/is the harness.tests/fixturesholds recorded payloads.
assets/react/— the React app. Product code is organized undersrc/features/(games, decks, imports, and admin); thin TanStack Router adapters live insrc/routes/, androuteTree.gen.tsis generated. Shared presentation and UI primitives remain insrc/components/. Tailwind 4 + daisyUI themes live insrc/app.css.priv/static/— static files the server serves; the frontend build writespriv/static/assets/react.- Card-recognition models (dataset, training, evaluation, ONNX bundle export and publishing) live in the separate Oracle repository, which ManaVault shares; like ManaVault, this app downloads bundles Oracle publishes as
scanner-bundle-*GitHub releases of this repository intoDATA_DIR/cardid(card_id/update.rs) and serves them from there. Dockerfile,docker-entrypoint.sh,docker-compose.yml— production container build and startup flow;deploy/proxmox/— the LXC installer/updater and its test;.github/workflows/release.yml— release tarballs (tags,nightlyfrom main,previewfrom other branches).mise.toml— pinned toolchain (Rust, SQLite, Node, aube) and tasks..agents/setupand.agents/resume— orb bootstrap scripts;.amp/services.yaml— the review portal service.
Common Commands
Run commands through mise to use the pinned toolchain:
mise install
mise run setup # dependencies, server build, dev database with demo data
mise run dev # Rust server on $PORT (default 4000) + Vite dev server on 5173
mise run rust:check # schema dump check, cargo fmt --check, clippy -D warnings, cargo test (in rust/)
mise run precommit # rust:check + aube run precommit
mise run rust:new-migration add_thing # creates rust/migrations/<UTC timestamp>_add_thing.sql
mise run rust:sqlx-prepare # rebuild rust/schema.sql and rust/.sqlx after a schema or query change
Run cargo directly in rust/ with mise exec -- cargo .... While developing Rust code, check queries against the schema directly: mise run rust:schema, then in rust/ export SQLX_OFFLINE=false DATABASE_URL="sqlite://$PWD/target/schema.db?mode=ro".
The server binary also has maintenance commands: the-gathering migrate, seed (dev demo data), create-admin USERNAME, bootstrap-admin, catalog-sync, and catalog-backfill. THE_GATHERING_ENV selects dev, test, or prod defaults (prod when unset).
JavaScript tooling goes through aube (the package manager) and Vite Plus (vp):
mise exec -- aube install --frozen-lockfile
mise exec -- aube run build # production bundle into priv/static/assets/react
mise exec -- aube exec vp check # fmt + lint + typecheck
mise exec -- aube exec vp test run
Use mise exec -- aube instead of invoking aube or npm directly. Fresh orbs do not expose aube on PATH. Note that vp check is a Vite Plus built-in, so call it via aube exec vp check rather than the npm script.
In development the Vite dev server (port 5173, or VITE_PORT) is the browser entry point; it proxies everything except its own assets to the backend. In an orb, .amp/services.yaml already runs this stack (mise run dev) as the the-gathering-review service, so check amp orb service status the-gathering-review (or ss -ltnp) before starting another server, and reuse the existing one.
Adding a migration
mise run rust:new-migration <snake_case_name>and write the SQL. Tables that must be rebuilt (SQLite cannot alter most constraints) start withPRAGMA foreign_keys = OFF;; the migrator then runs that file statement by statement instead of in one transaction.- Data changes that need code rather than SQL go in
data_stepinrust/crates/the-gathering/src/db/migrate.rs, keyed by the migration's version. mise run rust:sqlx-prepareto regeneraterust/schema.sqlandrust/.sqlx, and commit all three. The server applies pending migrations at boot (and withthe-gathering migrate).
Never edit or renumber a migration that has shipped; existing databases have recorded its version.
Production/container commands are documented in README.md.
Development Notes
- The backend is API-only: render UI in React and serve data from
/apihandlers. The server renders only the SPA shell.
JSON API conventions
- Handlers live in
rust/crates/the-gathering/src/web/api/and are routed inweb/mod.rs, grouped by the guards they need (require_authenticated_user,require_admin,require_sudo_mode) applied withroute_layer. Admin-sensitive changes use bothrequire_adminandrequire_sudo_mode. - Handlers return
Result<_, ApiError>(src/error.rs) instead of building error responses by hand. Validation failures are aValidationError(src/validation.rs, built withValidator) and render as{"errors": {"field": ["message"]}}in check order, with list rows as{"seats": [{}, {"field": ["message"]}]}; other errors as{"errors": {"detail": "..."}}. - Successful responses wrap the payload in
{"data": ...}(single object or list) viaweb::api::data. - Use plural resource paths and standard REST actions (
GET /api/games,POST /api/games,GET /api/games/:id,PATCH,DELETE). Paginate lists withpage/per_pagequery params when they can grow unbounded. - Handlers take typed inputs through
web::extract::{JsonBody, QueryParams, PathParam}, whose rejections are JSONApiErrors (400, or 413/415). Request bodies are flat objects (no{"user": {...}}wrapper). Partial updates usepatch::Patch<T>fields with#[serde(default)]so a missing key,null, and a value differ; missing required fields are validation errors (422), wrong JSON types are 400. - Every
/apirequest that changes state needs the CSRF token; the frontendapi()helper inassets/react/src/lib/api.tssends it and rejects withApiError(carryingerrors) on non-2xx responses. Use it for all requests. - Server state in React goes through TanStack Query (
useQuery/useMutation,QueryClientProviderinmain.tsx; thequeryClientis also in router context). Key queries by resource, for example["games", id]. - Authenticated handlers read the user from the
CurrentUserrequest extension set by the auth layer. - Use the shared
reqwestclients for HTTP requests (Scryfall, Discord, deck-list sites); lotus'sDecklistClientenforces the SSRF allowlist for deck links. - Follow existing module and React component patterns. Keep changes small and focused.
- Frontend styling uses Tailwind utilities and daisyUI component classes; theme tokens are defined in
assets/react/src/app.css. Usecn()fromsrc/lib/cn.tsto merge classes. Shared primitives (Button, Card, Dialog, DropdownMenu, Popover, Select, Tabs, Switch, ToggleGroup, ported from ManaVault on Radix) live insrc/components/ui/; page scaffolding (PageHeader,PageSection,EmptyPanel) is insrc/components/app-shell.tsx. The "liquid glass" look is keyed onhtml[data-theme-style="glass"](default; users can pick Classic in Settings), so glass rules inapp.cssmust stay scoped to that attribute. Color palettes (Claret default, plus Nord, Catppuccin, Tokyo Night, Gruvbox, Everforest, Kanagawa, Night Owl, Dracula, Rosé Pine, Solarized, Monochrome) are a third orthogonal axis:data-paletteon<html>, with per-mode token overrides insrc/palettes.css. Palette and surface style are saved per user (users.palette/users.theme_style,PATCH /api/session/appearance), and the server's SPA shell (rust/crates/the-gathering/src/web/shell.rs) renders them onto<html>for the first paint.localStorage(the-gathering:palette,the-gathering:theme-style) is only the signed-out fallback, and light/dark stays per device. Palette ids live inPALETTES(src/lib/theme.tsx),PALETTES(rust/crates/the-gathering/src/accounts/user.rs), andpalettes.css, so keep all three in sync. Keep palette roles consistent: secondary is the green, accent the gold used for winners. - Rust backend: keep the lotus conventions (no
unsafe; nounwrap/expect/panic!/indexing/asoutside tests; literal regexes viacrate::regex::compile;sqlx::query!macros checked against the schema;crate::db::beginfor write transactions). JSON shapes, status codes, and error messages are what the frontend depends on; change them together with the frontend and add tests alongside behavior. - The browser session is a typed
SessionDatain an encrypted cookie (web/session.rs); add fields there rather than new cookies. Timestamps areUtcDateTimeorIsoDate(db/time.rs) in rows, records, and JSON, neverString; decode columns withAS "col: UtcDateTime". - The webcam table's realtime connection is Socket.IO (
web/channels/, socketioxide;socket.io-clientviafeatures/webcam-table/table-channel.ts). Table events that expect a reply are acknowledged with{"ok": response}or{"error": reason}; change server and client together, with tests on both sides (tests/integration/webcam_*,table-channel.test.ts). - Deck chooser scripts are sandboxed Lua 5.4 (mlua, vendored):
games/deck_script.rsruns them andgames/chooser_context.rsbuilds thedeckandctxtables.docs/deck-chooser-scripts.mdis the field reference: the script editor renders its tables (so the Dockerfile copies it into the frontend build), andtests/integration/deck_chooser_scripts.rsfails when a field is missing from it or a shipped example infeatures/decks/deck-scripts/errors, so add new fields tochooser_context.rsand the guide together. - Comments that name an earlier release or the Elixir server explain an on-disk format this server still reads (session cookies and sealed credentials in
legacy.rs, stored import identities inimports/etf.rs, saved table sessions,schema_migrationsnumbering). Write other comments about the behavior itself, not about how earlier code did it. - Run the narrowest relevant tests before reporting completion, and
mise run precommitwhen a change is complete. - Each Rust integration test gets its own temporary SQLite database (
tests/integration/support), so tests run in parallel in one process. Process-wide state (the tracing subscriber) is shared: capture logs withsupport::capture_logs(), which is per thread. - Compile times: dev builds are incremental and link with mold when installed (
rust/scripts/link-gcc;.agents/setupinstalls it).mise run rust:sweepdrops stale incremental caches;mise run rust:benchmeasures the dev loop (numbers inrust/notes/compile-times.md). - CI (
.github/workflows/quality.yml) runs theprecommitsteps as parallel jobs: the Rust job (schema dump check, online query check,rust:check,deploy/proxmox/test.sh) and the frontend job (vp check,vp test run, build). Keep the workflow in sync with theprecommittask. - For UI changes, verify the rendered result through the review portal and leave the service running.
- Update documentation when project structure, setup, or runtime behavior changes.
Git Commit Policy
- Every Git commit must use a Conventional Commits message.
- Commit as the current thread's user using their configured Git identity.
- Never add
Co-authored-bytrailers or credit Amp, an AI agent, or another co-author. - Never push unless the user asks. When they do, verify the commit has no co-authorship trailers, then push the current branch and confirm it matches its upstream.
<CRITICAL_INSTRUCTION>
Backlog.md Workflow
This project uses Backlog.md for task and project management.
Use Backlog only for sizeable implementation work that is worth documenting because it benefits from durable planning, decisions, progress tracking, or handoff notes. Do not run backlog instructions overview or any other Backlog command automatically at the start of a request. Skip Backlog for questions, explanations, operational actions, commits and pushes, quick fixes, and small mechanical, configuration, or documentation changes.
When work genuinely warrants Backlog, run mise exec -- backlog instructions overview, search for an existing task first, and then read only the relevant task instructions. The Backlog CLI is managed by mise and may not be directly available on PATH, especially during first-time orb setup.
Before task lifecycle actions, read the matching detailed guide:
mise exec -- backlog instructions task-creationbefore creating or splitting tasksmise exec -- backlog instructions task-executionbefore planning, changing status or assignee, adding a plan or implementation notes, or implementing task workmise exec -- backlog instructions task-finalizationbefore checking acceptance criteria, writing final summaries, or moving tasks to terminal statuses
Use mise exec -- backlog <command> --help before running unfamiliar commands. Help shows options, fields, and examples.
Do not edit Backlog task, draft, document, decision, or milestone markdown files directly. Use mise exec -- backlog so metadata, relationships, and history stay consistent.
</CRITICAL_INSTRUCTION>
