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:
2026-09-03 20:31:46 +00:00
co-authored by Claude Fable 5.1
commit 5ade592384
39 changed files with 3103 additions and 0 deletions
+79
View File
@@ -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).