Files
courtside/docs/API.md
T

95 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# HTTP and WebSocket endpoints
All pages are server-rendered HTML except where noted. `<slug>` is the URL-safe tournament id created from its name (e.g. `labor-day-2s`).
## Public
| Method | Path | Returns |
| --- | --- | --- |
| GET | `/` | List of tournaments. |
| GET | `/healthz` | `{ ok, tournaments, uptime }` JSON. Used by the Docker healthcheck. |
| GET | `/t/<slug>` | The QR landing page. Registration form while phase is `checkin`; otherwise the live board / results (client-rendered from state JSON). |
| POST | `/t/<slug>/register` | Form fields `name`, `captain`, `phone`, `players`, `avgAge` (optional, 5–99), `playerNames` (newline-separated). 303 to the team page on success, 400 with the form re-rendered on error. |
| GET | `/t/<slug>/teams/<id>` | The same personalized board addressed by public team id; this is what team-name links use. |
| GET | `/t/<slug>/team/<code>` | Personalized board for the team with that 4-character code. `?new=1` shows the post-registration confirmation instead. |
| GET | `/t/<slug>/display` | TV display mode of the board. |
| GET | `/t/<slug>/state.json` | The public state document (below). |
| WS | `/ws/<slug>` | Sends the public state document immediately on connect and again on every change. Clients send nothing. Server pings every 30 s. |
| GET | `/static/*` | CSS, JS, icons. |
### Public state document
```jsonc
{
"slug": "labor-day-2s", "name": "Labor Day 2s", "date": "2026-09-07", "notes": "", "phase": "live", // checkin | closed | live | final
"banner": null, "rules": { "teamSize": 2, "pointsTo": 21, "winBy": 2, "cap": 25, "bestOf": 1, "tiebreaks": ["headToHead","setRatio","pointRatio","pointDiff","seed"] },
"stages": ["pool","bracket"], "courtCount": 2,
"teams": [{ "id": "t1", "name": "Net Gains", "players": 2, "seed": 1, "status": "registered", "poolId": "A" }],
"withdrawn": [],
"pools": [{ "id": "A",
"standings": [{ "teamId": "t1", "name": "Net Gains", "w": 2, "l": 1, "setsW": 2, "setsL": 1, "pf": 59, "pa": 45, "pointDiff": 14, "decidedBy": "pointRatio", "status": "registered" }],
"matches": [ /* match objects */ ] }],
"bracket": [ /* match objects with stage bracket | losers | final */ ],
"courts": [{ "court": 1, "status": "open", "match": { /* match object */ } }, { "court": 2, "status": "paused", "match": null }],
"upNext": [{ /* match object */ , "court": 2, "etaMin": 12 }],
"avgMatchMin": 14, "champion": null,
"recentAlerts": [{ "kind": "on_deck", "text": "Net Gains vs Block Party: you're ON DECK for Court 2 (~12 min).", "at": 1757260000000, "teams": ["t1","t2"], "match": "m7" }],
"generatedAt": 1757260000000
}
```
A match object:
```jsonc
{ "id": "m7", "stage": "pool", "round": 2, "slot": null, "label": null,
"status": "live", // pending | on_deck | live | final | forfeit | void | bye
"court": 1,
"a": { "id": "t1", "name": "Net Gains" }, "b": { "id": "t2", "name": "Block Party" }, // null while TBD
"winner": null, "sets": [], "live": [14, 11], // running score while live, else null
"feeds": "m12", "feedsSide": "A", "conditional": false }
```
Phone numbers, captain names, and team codes never appear in the public document.
## Score keeper (Courtside Manager)
Per-court links issued from the desk. `k` is an HMAC of `<slug>:court<n>` under `SESSION_SECRET`; requests without a valid `k` get 403.
| Method | Path | Body (JSON) | Effect |
| --- | --- | --- | --- |
| GET | `/keeper/<slug>/<court>?k=…` | | The score keeper page for that court. |
| POST | `/keeper/<slug>/<court>/live` | `k`, `match`, `a`, `b`, `sets` (completed sets, `[[21,18]]`) | Update the running score of the live match on that court. Fails if `match` is not the one on the court. |
| POST | `/keeper/<slug>/<court>/final` | `k`, `match`, `sets` (all sets incl. the last) | Record the result, validated against the rules; the court engine then assigns the next match. Fails once the match is no longer live. |
## Organizer
All organizer routes require the session cookie set by `/admin/login` (or Cloudflare Access when `TRUST_CF_ACCESS=true`). Unauthenticated requests are redirected to the login page.
| Method | Path | Body (form or JSON) | Effect |
| --- | --- | --- | --- |
| GET | `/admin/login` | | Login form. |
| POST | `/admin/login` | `password`, `next` | Sets cookie, redirects to `next`. |
| GET | `/admin/logout` | | Clears cookie. |
| GET | `/admin` | | Desk home: tournament list and creation form. |
| POST | `/admin/new` | `name`, `date`, `courtCount`, `teamSize`, `pointsTo`, `winBy`, `cap` (0 = none), `bestOf`, `stages` (`pool,bracket` / `pool` / `bracket`), `timezone`, `notes` | Creates a tournament. |
| GET | `/admin/t/<slug>` | | Tournament desk. |
| GET | `/admin/t/<slug>/qr` | | Printable QR page. `qr.png` returns the PNG. |
| GET | `/admin/t/<slug>/keeper-qr` | | Printable page with one score keeper QR code per court. |
| POST | `/admin/t/<slug>/phase` | `phase` = `checkin` / `closed` / `live` / `final` | Change phase. `live` requires generated play and starts court assignment. |
| POST | `/admin/t/<slug>/settings` | `date`, `timezone` (IANA name, e.g. `America/Chicago`), `notes` | Edit tournament settings. All clock times on the board and desk are shown in this zone. |
| POST | `/admin/t/<slug>/banner` | `banner` | Set (or clear with empty) the persistent banner. |
| POST | `/admin/t/<slug>/broadcast` | `text` | One-time alert to all team pages. |
| POST | `/admin/t/<slug>/generate` | `what=pools`, `poolSize` — or `what=bracket`, `perPool`, `wildcards`, `type` (`single`/`double`), `thirdPlace`, `byAge` (bracket-only: age-matched round 1, random where no age) — or `what=manual`, `type`, `thirdPlace`, `a1`,`b1`,`a2`,`b2`,… (team ids per round-1 match; empty = bye) | Generate pools or bracket, automatically or from hand-picked pairs. |
| POST | `/admin/t/<slug>/regenerate` | | Clear all generated play (only before any score). |
| POST | `/admin/t/<slug>/score` | `match`, `sets` (`"21-18, 19-21, 15-9"`) | Record or correct a result. Validated against the rules. |
| POST | `/admin/t/<slug>/live` | `match`, `a`, `b` | Update the running score of a live match (JSON, used by the score pad). |
| POST | `/admin/t/<slug>/forfeit` | `match`, `team` | Record a forfeit by `team`. |
| POST | `/admin/t/<slug>/swap` | `match`, `court` | Move a live match to another open court. |
| POST | `/admin/t/<slug>/pushback` | `match` | Push a pending match two places back in the queue. |
| POST | `/admin/t/<slug>/court` | `court`, `op` = `pause`/`resume`, `reason` | Pause or resume a court. |
| POST | `/admin/t/<slug>/team` | `op` = `add` (`name`,`captain`,`players`) / `edit` (`id`,`name`,`captain`,`players`) / `seed` (`id`,`seed`) / `remove` (`id`) | Roster management. |
| POST | `/admin/t/<slug>/withdraw` | `id`, `mode` = `forfeit`/`void` | Withdraw a team. |
| POST | `/admin/t/<slug>/delete` | `confirm=yes` | Delete the tournament and its data. |
Form posts respond with a 303 back to the desk carrying `?msg=` or `?error=`. Requests with `Content-Type: application/json` get `{ ok: true, msg }` or `{ ok: false, error }` with a 400 on failure. Every action is recorded in the `events` table with the organizer identity as `actor`.