83 lines
5.8 KiB
Markdown
83 lines
5.8 KiB
Markdown
# 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`, `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.
|
|
|
|
## 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`), `notes` | Creates a tournament. |
|
|
| GET | `/admin/t/<slug>` | | Tournament desk. |
|
|
| GET | `/admin/t/<slug>/qr` | | Printable QR page. `qr.png` returns the PNG. |
|
|
| POST | `/admin/t/<slug>/phase` | `phase` = `checkin` / `closed` / `live` / `final` | Change phase. `live` requires generated play and starts court assignment. |
|
|
| 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` | Generate pools or bracket. |
|
|
| 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`.
|