diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ed3775..8472d17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ The application version is maintained in `package.json` and displayed in the adm No changes recorded yet. +## 1.36.0 - 2026-10-11 + +### Changed + +- Series: "Who scores series points" is now an explicit choice, **Every driver (open series)** (default) or **Club members only**, instead of an easy-to-miss "leave the club empty" rule. The club field shows only for members-only. Series saved with a club keep scoring members only. The README describes both options. + ## 1.35.1 - 2026-10-10 ### Fixed diff --git a/README.md b/README.md index 89f8753..73b45ee 100644 --- a/README.md +++ b/README.md @@ -360,7 +360,9 @@ Menu **Series / cup** groups several races into a championship: - **Rounds:** any number. Tick existing races, or create them from a list of dates. One race per date and class is created with the chosen race format; then add drivers and generate heats in Race Setup as usual. - **Points table:** the ÖRC table (100, 90, 82, 75, 69 ... then -4 per place, min 10), 100-80-70 ..., F1, or your own list with a step and minimum after it. - **Counted results:** best N (e.g. 9 of 12), or all run rounds except the X worst. A cancelled round then lowers the count by itself. Absence counts as 0 and can be dropped. -- **Club:** only drivers from that club (taken from the driver register) score. Drivers from other clubs race and appear in the day result, and the club's drivers are re-ranked without them. Leave the club empty to let everyone score. +- **Who scores series points** is a choice per series: + - **Every driver (open series)**, the default. Drivers from any club race and score. + - **Club members only.** Pick the club; the club comes from each driver in the driver register. Drivers from other clubs still race and appear in the day result, but get no series points, and members are re-ranked without them (place 1 among members gets the winner's points). - **Day result:** each round is ranked by its final standings (A before B), then drivers without a final in qualifying order, or by qualifying alone if no finals ran. A driver with no registered lap gets 0. - **Ties:** most wins among counted results, then 2nd places and so on, then the latest common round. - **Standings:** shown per class with dropped results in brackets. They can be printed or exported as PDF; wide tables switch to landscape. diff --git a/README.sv.md b/README.sv.md index 8ac66d0..701e36a 100644 --- a/README.sv.md +++ b/README.sv.md @@ -349,7 +349,9 @@ Menyn **Serier / cup** samlar flera race i en serie: - **Deltävlingar:** valfritt antal. Bocka i befintliga race eller skapa dem från en datumlista. Ett race per datum och klass skapas med valt raceformat; lägg sedan till förare och generera heat i Race Setup som vanligt. - **Poängtabell:** ÖRC (100, 90, 82, 75, 69 ... sedan -4 per plats, lägst 10), 100-80-70 ..., F1, eller en egen lista med minskning och lägsta värde efter den. - **Räknade resultat:** bästa N (t.ex. 9 av 12), eller alla körda utom X sämsta. Då minskar antalet automatiskt om en deltävling ställs in. Frånvaro räknas som 0 och kan strykas. -- **Klubb:** bara förare från klubben (enligt förarregistret) får seriepoäng. Förare från andra klubbar kör och syns i dagens resultat, och klubbens förare rangordnas om utan dem. Tom klubb betyder att alla får poäng. +- **Vem får seriepoäng** väljs per serie: + - **Alla förare (öppen serie)**, standard. Förare från alla klubbar kör och får poäng. + - **Endast klubbmedlemmar.** Välj klubb; klubben hämtas från varje förare i förarregistret. Förare från andra klubbar kör och syns i dagens resultat men får inga seriepoäng, och medlemmarna rangordnas om utan dem (bästa medlem får vinnarpoängen). - **Dagens resultat:** varje deltävling rankas efter slutresultatet (A före B), sedan förare utan final i kvalordning, eller enbart efter kvalet om inga finaler körts. Den som saknar registrerat varv får 0. - **Lika poäng:** flest segrar bland räknade resultat, sedan andraplatser osv., sedan senaste gemensamma deltävling. - **Ställning:** visas per klass med strukna resultat inom parentes. Den kan skrivas ut eller exporteras som PDF; breda tabeller blir liggande A4. diff --git a/ROADMAP.md b/ROADMAP.md index 354befd..64b5fbb 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -805,6 +805,15 @@ Verified 2026-10-10: - the PDF payload has the same row. - 65 browser scenarios pass. +### Follow-up: open series or members only (2026-10-11) + +The user pointed out that drivers from other clubs must be able to score in a normal series. The ÖRC example is the case that needs the members-only rule, and the README didn't make the choice clear. + +- [x] `series.scoring` is `all` (default for new series) or `club`. A series saved with a club before the field existed normalizes to `club`, so its behaviour doesn't change. The club field is shown only for `club`. The print/PDF subtitle says "members of X only". +- [x] README (sv/en) describes both options. + +Verified: 262 unit tests pass (open series with a club filled in, members-only, defaults, legacy). The browser test sets members-only and checks that the guest is excluded, then switches to every driver and checks that the guest scores and wins round 1 and that the club field hides, then switches back. 68 browser scenarios pass. + Not done yet: the rule that a disqualification from a whole round gives a 0 that may not be dropped (ÖRC draft p. 6). Also no public web page for the standings, only print/PDF. ## Training stats: practice by default, race laps separate diff --git a/package-lock.json b/package-lock.json index a0d52c4..26db4c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rc-racecore", - "version": "1.35.1", + "version": "1.36.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rc-racecore", - "version": "1.35.1", + "version": "1.36.0", "hasInstallScript": true, "dependencies": { "better-sqlite3": "^12.11.1", diff --git a/package.json b/package.json index a9d20f6..893cb53 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-racecore", - "version": "1.35.1", + "version": "1.36.0", "private": true, "description": "RC RaceCore live timing and race control with AMMC ingest and SQLite persistence", "engines": { diff --git a/src/app.js b/src/app.js index bdf8384..6928320 100644 --- a/src/app.js +++ b/src/app.js @@ -542,7 +542,7 @@ const seriesCell = (table, row, event) => { const text = result.absent ? "-" : String(result.points); return result.counted ? text : `(${text})`; }; -const seriesSubtitle = (series) => [series.club, t("series.count_rule", { rule: series.countMode === "drop" ? t("series.count_drop_value", { count: series.countValue }) : t("series.count_best_value", { count: series.countValue }) })].filter(Boolean).join(" • "); +const seriesSubtitle = (series) => [series.scoring === "club" && series.club ? t("series.subtitle_members", { club: series.club }) : "", t("series.count_rule", { rule: series.countMode === "drop" ? t("series.count_drop_value", { count: series.countValue }) : t("series.count_best_value", { count: series.countValue }) })].filter(Boolean).join(" • "); const printSeries = (series, tables) => openPrintWindow(series.name, ` @@ -710,9 +710,13 @@ const TRANSLATIONS = { "series.settings": "Inställningar", "series.name": "Namn", "series.name_placeholder": "t.ex. Winter Championship 2026/2027", - "series.club": "Klubb (bara dess förare får poäng)", - "series.club_all": "tomt = alla får poäng", - "series.club_hint": "Förare från andra klubbar kör och syns i dagens resultat men får inga seriepoäng; klubbens förare rangordnas om utan dem. Klubben hämtas från förarregistret.", + "series.scoring": "Vem får seriepoäng", + "series.scoring_all": "Alla förare (öppen serie)", + "series.scoring_club": "Endast klubbmedlemmar", + "series.club": "Klubb", + "series.club_placeholder": "t.ex. Östersunds RC", + "series.club_hint": "Förare från andra klubbar kör och syns i dagens resultat men får inga seriepoäng; medlemmarna rangordnas om utan dem. Klubben hämtas från förarregistret.", + "series.subtitle_members": "endast medlemmar i {club}", "series.classes": "Klasser (en tabell per klass)", "series.no_classes": "Skapa klasser först under Klasser.", "series.points_preset": "Poängtabell", @@ -2432,9 +2436,13 @@ const TRANSLATIONS = { "series.settings": "Settings", "series.name": "Name", "series.name_placeholder": "e.g. Winter Championship 2026/2027", - "series.club": "Club (only its drivers score)", - "series.club_all": "empty = everyone scores", - "series.club_hint": "Drivers from other clubs race and appear in the day result but get no series points; the club's drivers are re-ranked without them. The club comes from the driver register.", + "series.scoring": "Who scores series points", + "series.scoring_all": "Every driver (open series)", + "series.scoring_club": "Club members only", + "series.club": "Club", + "series.club_placeholder": "e.g. Östersunds RC", + "series.club_hint": "Drivers from other clubs race and appear in the day result but get no series points; members are re-ranked without them. The club comes from the driver register.", + "series.subtitle_members": "members of {club} only", "series.classes": "Classes (one table per class)", "series.no_classes": "Create classes first under Classes.", "series.points_preset": "Points table", diff --git a/src/series_logic.js b/src/series_logic.js index 89d3300..53bf4d5 100644 --- a/src/series_logic.js +++ b/src/series_logic.js @@ -4,8 +4,8 @@ // // Rules supported (see ROADMAP "Series / cup"): // - points per place from a table, then a fixed step down to a minimum for later places; -// - only drivers from the series' club score (others still race and appear in the day -// result); club drivers are re-ranked without them; +// - who scores: every driver (open series), or only members of the series' club - others +// still race and appear in the day result, and members are re-ranked without them; // - a driver with no registered lap in the event gets 0 points and no place; // - count the best N results, or all completed events minus X (so a cancelled event // reduces the count automatically); @@ -37,6 +37,9 @@ export function normalizeSeries(series = {}, { uid } = {}) { id: String(series.id || (uid ? uid("series") : "")), name: String(series.name || "").trim(), club: String(series.club || "").trim(), + // "all" = every driver scores; "club" = only drivers whose club matches. Series saved before + // this choice existed keep their behaviour: a club filled in meant members only. + scoring: series.scoring === "club" || (series.scoring === undefined && String(series.club || "").trim()) ? "club" : "all", classIds: Array.isArray(series.classIds) ? [...new Set(series.classIds.map(String).filter(Boolean))] : [], eventIds: Array.isArray(series.eventIds) ? [...new Set(series.eventIds.map(String).filter(Boolean))] : [], pointsTable: table.length ? table : preset.table.slice(), @@ -67,7 +70,7 @@ export function scoreSeriesEvent(series, dayResult, { getDriverClub = () => "" } let place = 0; (dayResult || []).forEach((row) => { if (!row?.driverId || scored.has(row.driverId)) return; - if (series.club && !sameClub(getDriverClub(row.driverId), series.club)) return; + if (series.scoring === "club" && series.club && !sameClub(getDriverClub(row.driverId), series.club)) return; if (!row.hasLap) { scored.set(row.driverId, { place: null, points: 0, driverName: row.driverName }); return; diff --git a/src/series_view.js b/src/series_view.js index 269b92a..5e29905 100644 --- a/src/series_view.js +++ b/src/series_view.js @@ -16,7 +16,11 @@ function renderSeriesForm(series, { t, escapeHtml, state }) { return `
-