diff --git a/CHANGELOG.md b/CHANGELOG.md index 69fd63e..4fb1b39 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.37.1 - 2026-10-11 + +### Added + +- Guide: a new **Series / cup** tab with four sections: create a series, each race day, points and standings, and a worked example (a 12-round winter series with three classes, best 9, club members only). The guide overview also has a series card. + ## 1.37.0 - 2026-10-11 ### Added diff --git a/ROADMAP.md b/ROADMAP.md index 575cb92..c744f00 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -857,6 +857,11 @@ Verified 2026-10-11: - checks that the status shows and the format locks. - Screenshots of the table and the open driver list were checked. 70 browser scenarios pass. +### Follow-up: series in the in-app guide (2026-10-11) + +- [x] Guide tab `series` (between "Run a race" and "Training & results") with four panels (21 steps, sv/en) and an overview card. +- [x] Verified: `tests/browser_guide_tabs.js` includes the new tab in the tab-switch check, counts the 21 steps, checks that no untranslated key shows, and checks the overview card. 264 unit and 71 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 34df49b..5e54849 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rc-racecore", - "version": "1.37.0", + "version": "1.37.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rc-racecore", - "version": "1.37.0", + "version": "1.37.1", "hasInstallScript": true, "dependencies": { "better-sqlite3": "^12.11.1", diff --git a/package.json b/package.json index 57d686a..ee2d245 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-racecore", - "version": "1.37.0", + "version": "1.37.1", "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 53334af..0f127f3 100644 --- a/src/app.js +++ b/src/app.js @@ -1484,6 +1484,34 @@ const TRANSLATIONS = { "guide.tab_create": "Skapa ett race", "guide.tab_run": "Köra ett race", "guide.tab_training": "Träning & resultat", + "guide.tab_series": "Serie / cup", + "guide.series_title": "Serie / cup", + "guide.card_series_blurb": "Samla flera race i en serie med poäng per placering, strukna resultat och ställning per klass.", + "guide.series_create_title": "Skapa en serie", + "guide.series_create_1": "1. Lägg först in klasser och förare (med rätt klass). Ska bara klubbmedlemmar få poäng behöver förarens klubb vara ifylld under Förare.", + "guide.series_create_2": "2. Menyn Serier / cup -> Ny serie. Steg 1: namn och vilka klasser som ingår. Varje klass får en egen ställning, poängen blandas aldrig mellan klasser.", + "guide.series_create_3": "3. Steg 2: vem som får poäng (alla förare, eller endast klubbmedlemmar), poängtabell (t.ex. 100, 90, 82 ... eller egen lista) och hur många resultat som räknas (bästa N, eller alla utom X sämsta).", + "guide.series_create_4": "4. Steg 3: klistra in datumen (ett per rad), välj antal kvalomgångar, finaler och max bilar per heat, och tryck Skapa deltävlingar. Ett race skapas per datum och klass.", + "guide.series_create_5": "5. Allt sparas direkt när du ändrar något. En befintlig tävling kan också läggas till i serien från listan under tabellen.", + "guide.series_day_title": "Varje tävlingsdag", + "guide.series_day_1": "1. Allt görs i tabellen i steg 3, en rad per race (datum och klass).", + "guide.series_day_2": "2. Klicka på antalet förare och bocka ur dem som inte kör. Kopiera från förra tar samma startfält som förra deltävlingen i klassen; Ny förare lägger in en efteranmäld förare och bockar i hen.", + "guide.series_day_3": "3. Ändra vid behov antal kval, finaler och max bilar per heat för just den deltävlingen - det kan skilja mellan deltävlingar och klasser.", + "guide.series_day_4": "4. Skapa kval: antalet heat räknas ut efter hur många som är med och fördelas jämnt (9 förare, max 8 per heat -> 5 + 4). Formatet låses sedan.", + "guide.series_day_5": "5. Kör kvalen på Race Day. När kvalet är klart visas Skapa finaler. Status visar var deltävlingen är: ej startad, kval skapat, kval pågår, kval klart, finaler, klar.", + "guide.series_day_6": "6. Öppna tar dig till racet i Race Setup om du vill ändra något mer där. Ställningen i steg 4 uppdateras av sig själv.", + "guide.series_points_title": "Poäng och ställning", + "guide.series_points_1": "Dagens placering i en deltävling: slutresultatet (A-final före B-final), sedan förare utan final i kvalordning. Utan finaler används kvalordningen.", + "guide.series_points_2": "Endast klubbmedlemmar: förare från andra klubbar kör och syns i dagens resultat men får inga poäng, och medlemmarna rangordnas om utan dem.", + "guide.series_points_3": "Den som inte kört något registrerat varv får 0 poäng. Frånvaro räknas som 0 och kan strykas.", + "guide.series_points_4": "Bästa N räknas, eller alla körda utom X sämsta - då minskar antalet automatiskt om en deltävling ställs in. Strukna resultat visas inom parentes.", + "guide.series_points_5": "Lika poäng: flest segrar bland räknade resultat, sedan andraplatser osv., sedan senaste gemensamma deltävling. Annars delad placering.", + "guide.series_points_6": "Ställningen kan skrivas ut eller exporteras som PDF (breda tabeller blir liggande A4). Inga kontaktuppgifter visas.", + "guide.series_example_title": "Exempel: klubbens vinterserie", + "guide.series_example_1": "12 söndagar, klasserna 2WD, 4WD och Rookie, poäng 100, 90, 82, 75, 69, 64, 60, 56, 52, 48 och sedan -4 per plats ner till 10, bästa 9 av 12, endast klubbmedlemmar.", + "guide.series_example_2": "Steg 1: bocka i 2WD, 4WD och Rookie. Steg 2: Endast klubbmedlemmar + klubben, poängtabellen 100, 90, 82 ... (-4, lägst 10), Bästa N räknas = 9.", + "guide.series_example_3": "Steg 3: klistra in de 12 datumen, 3 kval, 3 finaler, 6 bilar per heat -> 36 race (12 datum × 3 klasser).", + "guide.series_example_4": "Varje söndag: välj förare per klass, Skapa kval, kör, Skapa finaler, kör. Ställningen per klass är klar direkt efter sista finalen.", "guide.tab_videos": "Videoguider", "warmup.title": "Transponderkontroll", "warmup.hint": "Under nedräkningen är banan öppen. Grön bock = transpondern har passerat linjen sedan nedräkningen började. Saknas någon kan du byta transponder direkt här, eller koppla en okänd transponder som just passerat.", @@ -3254,6 +3282,34 @@ const TRANSLATIONS = { "guide.tab_create": "Create a race", "guide.tab_run": "Run a race", "guide.tab_training": "Training & results", + "guide.tab_series": "Series / cup", + "guide.series_title": "Series / cup", + "guide.card_series_blurb": "Group several races into a series with points per place, dropped results and standings per class.", + "guide.series_create_title": "Create a series", + "guide.series_create_1": "1. First add classes and drivers (in the right class). For a members-only series, each driver's club must be filled in under Drivers.", + "guide.series_create_2": "2. Menu Series / cup -> New series. Step 1: name and which classes are included. Each class gets its own standings; points never mix between classes.", + "guide.series_create_3": "3. Step 2: who scores (every driver, or club members only), the points table (e.g. 100, 90, 82 ... or your own list) and how many results count (best N, or all but the X worst).", + "guide.series_create_4": "4. Step 3: paste the dates (one per line), pick qualifying rounds, finals and max cars per heat, and press Create rounds. One race is created per date and class.", + "guide.series_create_5": "5. Every change is saved straight away. An existing race can also be added from the list below the table.", + "guide.series_day_title": "Each race day", + "guide.series_day_1": "1. Everything is done in the table in step 3, one row per race (date and class).", + "guide.series_day_2": "2. Click the number of drivers and untick those not racing. Copy from previous takes the field of the previous round in the class; New driver registers a late entry and ticks them.", + "guide.series_day_3": "3. Change qualifying rounds, finals and max cars per heat for that round if needed - it can differ between rounds and classes.", + "guide.series_day_4": "4. Create qualifying: the number of heats follows how many take part, split evenly (9 drivers, max 8 per heat -> 5 + 4). The format then locks.", + "guide.series_day_5": "5. Run qualifying on Race Day. When it's done, Create finals appears. Status shows where the round is: not started, qualifying created, running, done, finals, done.", + "guide.series_day_6": "6. Open goes to the race in Race Setup if you want to change more there. The standings in step 4 update by themselves.", + "guide.series_points_title": "Points and standings", + "guide.series_points_1": "A round's placing: the final standings (A final before B final), then drivers without a final in qualifying order. Without finals, the qualifying order is used.", + "guide.series_points_2": "Members only: drivers from other clubs race and appear in the day result but get no points, and members are re-ranked without them.", + "guide.series_points_3": "A driver with no registered lap gets 0 points. Absence counts as 0 and can be dropped.", + "guide.series_points_4": "Best N count, or all run rounds except the X worst - then a cancelled round lowers the count by itself. Dropped results are shown in brackets.", + "guide.series_points_5": "Ties: most wins among counted results, then 2nd places etc., then the latest common round. Otherwise a shared position.", + "guide.series_points_6": "The standings can be printed or exported as PDF (wide tables switch to landscape A4). No contact details are shown.", + "guide.series_example_title": "Example: the club's winter series", + "guide.series_example_1": "12 Sundays, classes 2WD, 4WD and Rookie, points 100, 90, 82, 75, 69, 64, 60, 56, 52, 48 then -4 per place down to 10, best 9 of 12, club members only.", + "guide.series_example_2": "Step 1: tick 2WD, 4WD and Rookie. Step 2: Club members only + the club, points table 100, 90, 82 ... (-4, min 10), Best N count = 9.", + "guide.series_example_3": "Step 3: paste the 12 dates, 3 qualifying rounds, 3 finals, 6 cars per heat -> 36 races (12 dates × 3 classes).", + "guide.series_example_4": "Each Sunday: pick drivers per class, Create qualifying, run, Create finals, run. Each class's standings are ready right after the last final.", "guide.tab_videos": "Video guides", "warmup.title": "Transponder check", "warmup.hint": "The track is open during the count-down. Green tick = the transponder has crossed the line since the count-down began. If someone is missing, change their transponder right here, or link an unknown transponder that just crossed.", diff --git a/src/misc_views.js b/src/misc_views.js index bff50fc..c4627b6 100644 --- a/src/misc_views.js +++ b/src/misc_views.js @@ -1,5 +1,5 @@ let activeGuideTab = "start"; -const GUIDE_TAB_IDS = ["start", "create", "run", "training", "videos"]; +const GUIDE_TAB_IDS = ["start", "create", "run", "series", "training", "videos"]; function renderGuidePanel(titleKey, itemKeys, extras = "", { t }) { return ` @@ -35,6 +35,7 @@ export function renderGuideView({ dom, t }) { ${renderGuideOverviewCard("guide.race_title", "guide.card_race_blurb", { t })} ${renderGuideOverviewCard("guide.race_day_title", "guide.card_race_day_blurb", { t })} ${renderGuideOverviewCard("guide.team_title", "guide.card_team_blurb", { t })} + ${renderGuideOverviewCard("guide.series_title", "guide.card_series_blurb", { t })} ${renderGuideOverviewCard("guide.training_title", "guide.card_training_blurb", { t })} ${renderGuideOverviewCard("guide.archive_title", "guide.card_archive_blurb", { t })} ${renderGuideOverviewCard("guide.windows_title", "guide.card_decoder_blurb", { t })} @@ -48,6 +49,7 @@ export function renderGuideView({ dom, t }) { + @@ -121,6 +123,15 @@ export function renderGuideView({ dom, t }) { +
+
+ ${renderGuidePanel("guide.series_create_title", ["guide.series_create_1", "guide.series_create_2", "guide.series_create_3", "guide.series_create_4", "guide.series_create_5"], "", { t })} + ${renderGuidePanel("guide.series_day_title", ["guide.series_day_1", "guide.series_day_2", "guide.series_day_3", "guide.series_day_4", "guide.series_day_5", "guide.series_day_6"], "", { t })} + ${renderGuidePanel("guide.series_points_title", ["guide.series_points_1", "guide.series_points_2", "guide.series_points_3", "guide.series_points_4", "guide.series_points_5", "guide.series_points_6"], "", { t })} + ${renderGuidePanel("guide.series_example_title", ["guide.series_example_1", "guide.series_example_2", "guide.series_example_3", "guide.series_example_4"], "", { t })} +
+
+
${renderGuidePanel("guide.training_title", ["guide.training_1", "guide.training_2", "guide.training_3", "guide.training_4", "guide.training_5", "guide.training_6", "guide.training_7", "guide.training_8", "guide.training_9", "guide.training_10", "guide.training_11", "guide.training_12", "guide.training_13", "guide.training_14", "guide.training_15", "guide.training_16", "guide.training_17", "guide.training_18", "guide.training_19", "guide.training_20", "guide.training_21", "guide.training_22", "guide.training_23", "guide.training_24", "guide.training_25", "guide.training_26", "guide.training_27", "guide.training_28", "guide.training_29", "guide.training_30", "guide.training_31"], "", { t })} diff --git a/tests/browser_guide_tabs.js b/tests/browser_guide_tabs.js index 38dd1f6..b264c48 100644 --- a/tests/browser_guide_tabs.js +++ b/tests/browser_guide_tabs.js @@ -21,11 +21,12 @@ export async function runGuideTabsSmoke({ evaluate, navigate }) { assert.equal(visible.start, true, "start panel visible by default"); assert.equal(visible.create, false, "create panel hidden by default"); assert.equal(visible.run, false, "run panel hidden by default"); + assert.equal(visible.series, false, "series panel hidden by default"); assert.equal(visible.training, false, "training panel hidden by default"); assert.equal(visible.videos, false, "videos panel hidden by default"); console.log("PASS guide defaults to the Getting started tab with only that panel visible"); - for (const tab of ["create", "run", "training", "videos", "start"]) { + for (const tab of ["create", "run", "series", "training", "videos", "start"]) { await evaluate(`document.querySelector('[data-guide-tab="${tab}"]').click()`); assert.equal(await activeTab(), tab, `clicking the ${tab} tab activates it`); visible = await panelVisibility(); @@ -35,6 +36,14 @@ export async function runGuideTabsSmoke({ evaluate, navigate }) { } console.log("PASS clicking each guide tab shows only its own panel and hides the rest"); + // The series tab explains the whole series workflow, step by step. + await evaluate(`document.querySelector('[data-guide-tab="series"]').click()`); + const seriesText = await evaluate(`document.querySelector('[data-guide-panel="series"]').innerText`); + assert.equal(await evaluate(`document.querySelectorAll('[data-guide-panel="series"] .guide-list li').length`), 21, "four sections, 21 steps"); + assert.ok(!/guide\.series_/.test(seriesText), "every guide text is translated"); + assert.equal(await evaluate(`[...document.querySelectorAll("#guideOverview .guide-overview-card strong")].some((node) => /Serie|Series/.test(node.textContent))`), true, "the overview has a series card"); + console.log("PASS the guide has a Series / cup tab with the whole workflow"); + const videoLinkCount = await evaluate(`document.querySelectorAll('[data-guide-panel="videos"] a[href^="/docs/videos/"]').length`); assert.equal(videoLinkCount, 7, "all 7 video guide links live under the Video guides tab");