Courtside MVP: engine, web app, Docker + Cloudflare Tunnel deploy, docs
Tournament engine (pools, single/double elimination, standings with proper tiebreaks, withdrawals, two-court queue with ETAs and alerts), a single-process Node server with SQLite via node:sqlite and a WebSocket live board, organizer desk, QR landing page that follows the tournament phase, Dockerfile and compose with cloudflared, and documentation for deploying and running a day. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01MB7nCCAscYsb3zzkT6LHZi
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
# 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).
|
||||
|
||||
## License
|
||||
|
||||
MIT. See [LICENSE](LICENSE).
|
||||
Reference in New Issue
Block a user