96 lines
5.4 KiB
Markdown
96 lines
5.4 KiB
Markdown
# Courtside
|
||
|
||
A self-hosted tournament desk for small volleyball tournaments (HOA days, club nights, beach 2s). One QR code carries a team from check-in, through registration, to live scores and "you're up on Court 2."
|
||
|
||
- **One QR code, four phases.** The code printed on the check-in table points at `/t/<slug>`. While registration is open it is the sign-up form. After the organizer closes registration it shows the roster, then the live board, then the final results. Nobody re-scans anything.
|
||
- **Two courts (or one, or four).** A court engine assigns the next match to whichever court frees up, keeps an "up next" queue with ETAs, and handles pauses, court swaps, and teams that leave mid-tournament.
|
||
- **Formats.** Round-robin pools, single elimination, double elimination (with bracket reset), pools-then-bracket, 2s/3s/4s/6s, rally scoring with configurable points, win-by, cap, and best-of.
|
||
- **Live everywhere.** Scores entered on the organizer's phone appear on every open page within a second over a WebSocket. Team pages get an on-screen alert (and a browser notification if allowed) when the team is on deck or up.
|
||
- **Nothing to install for players.** Plain web pages that work on any phone. Organizers get a mobile-friendly desk at `/admin`.
|
||
- **Small footprint.** One Node process, SQLite via Node's built-in `node:sqlite`, two npm dependencies (`ws`, `qrcode`), no build step. Runs comfortably on the smallest container you have.
|
||
|
||
Status: MVP. It has run a full simulated day and an HTTP smoke test but has not yet run a real tournament. See [docs/ROADMAP.md](docs/ROADMAP.md) for what's next and [docs/KNOWN-GAPS.md](docs/KNOWN-GAPS.md) for what to watch.
|
||
|
||
## Quick start (local)
|
||
|
||
Requires Node 22.13 or newer (for `node:sqlite`).
|
||
|
||
```bash
|
||
git clone https://gitea.cloudfreeiot.com/bkvargyas/courtside.git
|
||
cd courtside
|
||
npm ci
|
||
ADMIN_PASSWORD=letmein npm start
|
||
# open http://localhost:3000 — organizer desk at /admin
|
||
```
|
||
|
||
To see it with data, seed the demo tournament first (server stopped):
|
||
|
||
```bash
|
||
npm run demo
|
||
ADMIN_PASSWORD=letmein npm start
|
||
# open http://localhost:3000/t/demo-labor-day-2s
|
||
```
|
||
|
||
## Deploy with Docker behind a Cloudflare Tunnel
|
||
|
||
```bash
|
||
cp .env.example .env # set ADMIN_PASSWORD, SESSION_SECRET, CLOUDFLARE_TUNNEL_TOKEN
|
||
docker compose up -d --build
|
||
```
|
||
|
||
The app container publishes no host ports; the only way in is the tunnel, whose public hostname should route to `http://app:3000`. Full instructions, including the Cloudflare side, backups, and updating, are in [docs/DEPLOY.md](docs/DEPLOY.md). For a local run without Cloudflare use `docker compose -f docker-compose.local.yml up --build`.
|
||
|
||
## How a tournament day runs
|
||
|
||
1. **Create** the tournament in the desk: name, date, courts, team size, scoring rules, stages. Print the QR code from the desk.
|
||
2. **Check-in.** Teams scan and register themselves: team name, captain, mobile, player count. Each team gets a private link (`/t/<slug>/team/<code>`) that will show their schedule and alerts. The organizer can edit, add, remove, and reseed teams.
|
||
3. **Close registration**, generate pools (or a bracket), and **go live**. The engine puts the first matches on the courts and texts, well, shows, the next teams that they're on deck.
|
||
4. **Score** from the desk: a +/− pad per court shows the running score on the public board; "Mark final" records the result and the engine assigns the next match. Any score can be corrected later from the "All matches" list.
|
||
5. **Pool play ends**, generate the bracket from pool standings (top N per pool plus wildcards), keep scoring.
|
||
6. **Final.** The QR page becomes the results page and stays up.
|
||
|
||
The organizer guide in [docs/ORGANIZER-GUIDE.md](docs/ORGANIZER-GUIDE.md) covers withdrawals, pausing a court, moving a match, pushing a match back, broadcasts, and the TV display mode.
|
||
|
||
## Repository layout
|
||
|
||
```
|
||
engine/ Tournament logic with no I/O: formats, state, standings, court engine. Tested.
|
||
server/ HTTP + WebSocket server, SQLite store, page templates, static assets.
|
||
scripts/ seed-demo.js — create a demo tournament.
|
||
test/ node:test unit tests for the engine; smoke.sh end-to-end HTTP test.
|
||
docs/ Deploy guide, architecture, organizer guide, API, roadmap, known gaps.
|
||
simulate.js Plays out a full 12-team day on 2 courts and checks invariants.
|
||
Dockerfile, docker-compose.yml, docker-compose.local.yml, .env.example
|
||
```
|
||
|
||
## Development
|
||
|
||
```bash
|
||
npm test # engine unit tests
|
||
npm run sim # simulated day; try --seed=3 --bracket=double
|
||
npm run dev # server with auto-restart
|
||
ADMIN_PASSWORD=test PORT=3123 npm start & BASE=http://localhost:3123 ADMIN_PASSWORD=test npm run smoke
|
||
```
|
||
|
||
Architecture and the reasoning behind it: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). Endpoints: [docs/API.md](docs/API.md).
|
||
|
||
## Releases
|
||
|
||
Versions and what changed in each: [CHANGELOG.md](CHANGELOG.md). The running
|
||
version is in the page footer and at `GET /healthz`.
|
||
|
||
To cut a release: move the `[Unreleased]` notes in `CHANGELOG.md` under a new
|
||
`## [x.y.z] - date` heading, set the same version in `package.json`, commit, then tag
|
||
and publish the Gitea release with the changelog section as its notes:
|
||
|
||
```bash
|
||
git tag -a vx.y.z -m "Courtside x.y.z" && git push origin --tags
|
||
# Gitea → Releases → Draft a release on tag vx.y.z, or POST /api/v1/repos/bkvargyas/courtside/releases
|
||
```
|
||
|
||
A push to `main` deploys; the tag and release record which commit each version was.
|
||
|
||
## License
|
||
|
||
MIT. See [LICENSE](LICENSE).
|