bkvargyasandClaude Fable 5.1 62efa4d127 Draw the bracket as a real tree with connectors; fit-to-screen and list views for phones
Matches are positioned per round with SVG connector lines between feeder and
next match, winners in bold, live matches tagged with their court, empty slots
labelled with what fills them (W R1.4, L R2.1), and the viewer's team highlighted
along its path. Double elimination renders winners, losers and finals trees.
Narrow screens default to a scaled fit-to-width view; a round-by-round list is
also available. Seed script gains --stage=bracket|double. Release 0.3.0.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MB7nCCAscYsb3zzkT6LHZi
2026-09-03 21:41:35 +00:00

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 for what's next and docs/KNOWN-GAPS.md for what to watch.

Quick start (local)

Requires Node 22.13 or newer (for node:sqlite).

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):

npm run demo
ADMIN_PASSWORD=letmein npm start
# open http://localhost:3000/t/demo-labor-day-2s

Deploy with Docker behind a Cloudflare Tunnel

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. 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 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

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. Endpoints: docs/API.md.

Releases

Versions and what changed in each: CHANGELOG.md. The running version is in the page footer and at GET /healthz.

License

MIT. See LICENSE.

S
Description
Self-hosted volleyball tournament desk: one QR code from check-in to live scores on two courts. Node + SQLite, Docker behind a Cloudflare Tunnel.
Readme MIT
132 KiB
2026-09-03 21:31:21 +00:00
Languages
JavaScript 86.9%
CSS 8.4%
Shell 4.1%
Dockerfile 0.6%