diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d1229f..8831e42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,13 @@ so you can always tell what a deployment is actually serving. ## [Unreleased] +## [0.6.2] - 2026-09-03 + +### Added +- Scoring rules (points to, win by, cap, best of) can be changed from the desk's Settings + area until the first score is entered, e.g. switching a set-up tournament to 15, win by 2, + cap 17. The tournament name is editable the same way. + ## [0.6.1] - 2026-09-03 ### Fixed diff --git a/docs/API.md b/docs/API.md index e734141..145c803 100644 --- a/docs/API.md +++ b/docs/API.md @@ -76,7 +76,7 @@ All organizer routes require the session cookie set by `/admin/login` (or Cloudf | GET | `/admin/t//qr` | | Printable QR page. `qr.png` returns the PNG. | | GET | `/admin/t//keeper-qr` | | Printable page with one score keeper QR code per court. | | POST | `/admin/t//phase` | `phase` = `checkin` / `closed` / `live` / `final` | Change phase. `live` requires generated play and starts court assignment. | -| POST | `/admin/t//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//settings` | `date`, `timezone` (IANA name, e.g. `America/Chicago`), `notes`, `name`; and until the first score: `pointsTo`, `winBy`, `cap` (0 = none), `bestOf`, `teamSize` | Edit tournament settings and scoring rules. All clock times are shown in the zone. | | POST | `/admin/t//banner` | `banner` | Set (or clear with empty) the persistent banner. | | POST | `/admin/t//broadcast` | `text` | One-time alert to all team pages. | | POST | `/admin/t//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. | diff --git a/package.json b/package.json index 2cbe4d2..689341a 100644 --- a/package.json +++ b/package.json @@ -1 +1 @@ -{"name":"courtside","type":"module","version":"0.6.1","scripts":{"test":"node --test test/*.test.js","sim":"node simulate.js","start":"node --disable-warning=ExperimentalWarning server/index.js","dev":"node --disable-warning=ExperimentalWarning --watch server/index.js","demo":"node --disable-warning=ExperimentalWarning scripts/seed-demo.js","smoke":"bash test/smoke.sh"},"description":"Self-hosted volleyball tournament desk: one QR code from check-in to live scores","main":"server/index.js","directories":{"test":"test"},"keywords":[],"author":"","license":"MIT","dependencies":{"qrcode":"^1.5.4","ws":"^8.21.3"},"engines":{"node":">=22.13"},"repository":{"type":"git","url":"https://gitea.cloudfreeiot.com/bkvargyas/courtside.git"}} \ No newline at end of file +{"name":"courtside","type":"module","version":"0.6.2","scripts":{"test":"node --test test/*.test.js","sim":"node simulate.js","start":"node --disable-warning=ExperimentalWarning server/index.js","dev":"node --disable-warning=ExperimentalWarning --watch server/index.js","demo":"node --disable-warning=ExperimentalWarning scripts/seed-demo.js","smoke":"bash test/smoke.sh"},"description":"Self-hosted volleyball tournament desk: one QR code from check-in to live scores","main":"server/index.js","directories":{"test":"test"},"keywords":[],"author":"","license":"MIT","dependencies":{"qrcode":"^1.5.4","ws":"^8.21.3"},"engines":{"node":">=22.13"},"repository":{"type":"git","url":"https://gitea.cloudfreeiot.com/bkvargyas/courtside.git"}} \ No newline at end of file diff --git a/server/routes.js b/server/routes.js index 5eca36c..459fe08 100644 --- a/server/routes.js +++ b/server/routes.js @@ -170,7 +170,21 @@ export function buildRouter({ registry, auth, store }) { if (body.timezone !== undefined) { if (body.timezone && !validTz(body.timezone)) throw new Error('Unknown time zone'); t.timezone = body.timezone || null; } if (body.date !== undefined) t.date = String(body.date).trim() || null; if (body.notes !== undefined) t.notes = String(body.notes).slice(0, 500); - t.emit({ type: 'settings', timezone: t.timezone, date: t.date }); + if (body.name !== undefined && String(body.name).trim()) t.name = String(body.name).trim().slice(0, 60); + // scoring rules can change until the first score is on the books + if (['pointsTo', 'winBy', 'cap', 'bestOf', 'teamSize'].some(k => body[k] !== undefined)) { + if (t.matches.some(m => ['final', 'forfeit'].includes(m.status))) throw new Error('Scores have been entered; rules are locked'); + const r = { ...t.rules }; + if (body.pointsTo !== undefined) r.pointsTo = +body.pointsTo; + if (body.winBy !== undefined) r.winBy = +body.winBy; + if (body.cap !== undefined) r.cap = +body.cap || 0; + if (body.bestOf !== undefined) r.bestOf = +body.bestOf; + if (body.teamSize !== undefined) r.teamSize = +body.teamSize; + if (!(r.pointsTo >= 5 && r.pointsTo <= 50) || !(r.winBy >= 1 && r.winBy <= 5) || ![1, 3, 5].includes(r.bestOf) || !(r.teamSize >= 1 && r.teamSize <= 6)) throw new Error('Rules out of range'); + if (r.cap && r.cap < r.pointsTo) throw new Error('Cap must be at least points-to (or 0 for no cap)'); + t.rules = r; + } + t.emit({ type: 'settings', timezone: t.timezone, date: t.date, rules: t.rules }); return 'Settings saved'; }); action('/banner', ({ t, body }) => { t.banner = String(body.banner ?? '').trim().slice(0, 200) || null; t.emit({ type: 'banner', banner: t.banner }); return t.banner ? 'Banner set' : 'Banner cleared'; }); diff --git a/server/views/admin.js b/server/views/admin.js index a94a7c5..a798c7d 100644 --- a/server/views/admin.js +++ b/server/views/admin.js @@ -81,6 +81,7 @@ export function adminTournament({ t, engine }, who, origin, msg, err, events, ke

The public QR page follows this. Going live starts assigning courts immediately.

+ ${!t.matches.some(m => ['final', 'forfeit'].includes(m.status)) ? `
ScoringLocked once the first score is entered.
` : ''}