From b9e8aa024b62a3605778b3acfbaed342f77c6f25 Mon Sep 17 00:00:00 2001 From: larssand Date: Sun, 15 Mar 2026 19:22:47 +0100 Subject: [PATCH] Add follow-up timing, invalid lap handling, corrections and docs --- README.md | 48 ++++++++++- src/app.js | 228 +++++++++++++++++++++++++++++++++++++++++++++---- src/styles.css | 6 ++ 3 files changed, 265 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 0fbb67d..865a51d 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,14 @@ RC timing app med sponsor-eventflöde (delade bilar/transpondrar mellan olika he - UI-separering: - `Event` = sponsor-event med delade bilar/transpondrar - `Race Setup` = riktiga race med personlig transponder per förare -- `Race Setup` innehåller nu även: + - `Race Setup` innehåller nu även: - välj exakt vilka förare som är med i racet - practice-ranking - kval-ranking med `poäng` eller `bästa resultat` - inbyggd guide för hur man skapar race steg för steg - beskrivningar direkt i alla fält under `Raceformat` + - `Follow-up tid` efter ordinarie racetid innan heatet stängs + - `Min varvtid` och `Max varvtid` för att filtrera shortcuts/felträffar och styra bättre statistik/stintlogik - sessionstyp `Free Practice` för löpande varvtider utan seedning - auto-generering av kvalheat från practice-ranking eller klasslista - reseeding av kommande kvalheat från aktuell ranking @@ -59,6 +61,8 @@ RC timing app med sponsor-eventflöde (delade bilar/transpondrar mellan olika he - Sessioninställningar för `Mass start`, `Position start`, `Staggered` - `Timing` visar grid/startordning för aktiv `Position start`-session - leaderboard visar både `gap till ledaren`, `gap till bilen framför` och `eget delta` mot förra varvet + - `Senaste passeringar` visar nu även ogiltiga varv med status `För kort varv` eller `Över maxvarv` + - manuella korrigeringar i `Tidtagning -> Detaljer`: `+1/-1 varv`, `+1/+5/-1 sek`, `Nollställ korrigering` - Practice/Kval kan seedas på bästa `2` eller `3` varv i sessionsinställningar - Persistens: - Frontend state i browser (`localStorage`) @@ -66,9 +70,51 @@ RC timing app med sponsor-eventflöde (delade bilar/transpondrar mellan olika he - Inbyggd `Guide`-meny i appen med steg-för-steg för: - Sponsor-event (10 personer / 4 bilar) - Vanligt race + - ogiltiga varv, follow-up och manuella korrigeringar - AMMC + npm setup på Windows och Linux - Språkval i UI: `SV` / `EN` +## Nya racefunktioner + +### Follow-up time +- Ställs i `Race Setup -> Hantera -> Raceformat` +- Kan också sättas per session när du skapar eller redigerar en session +- När ordinarie tid går ut går sessionen först in i `Follow-up aktiv` +- När follow-up-tiden är slut stängs sessionen automatiskt +- Genererade kval/finaler ärver follow-up från raceformatet + +### Min / Max varvtid +- Ställs i `Race Setup -> Hantera -> Raceformat` +- `Min varvtid`: + - varv snabbare än gränsen ignoreras som shortcut eller felträff +- `Max varvtid`: + - varv långsammare än gränsen räknas inte som giltigt varv + - används också för att bryta stintar och förbättra statistik + +Praktiskt exempel: +- bana runt `16 sek/varv` +- `Min varvtid = 11 sek` +- `Max varvtid = 60 sek` + +### Ogiltiga passeringar +- `Tidtagning -> Senaste passeringar` visar nu både giltiga och ogiltiga passeringar +- status som visas: + - `För kort varv` + - `Över maxvarv` +- ogiltiga passeringar markeras visuellt i listan + +### Manuella korrigeringar +- öppna `Tidtagning` +- klicka `Detaljer` på en förare / ett lag +- där finns: + - `+1 varv` + - `-1 varv` + - `+1 sek` + - `+5 sek` + - `-1 sek` + - `Nollställ korrigering` +- korrigeringarna uppdaterar leaderboard och resultat direkt + ## Windows installation Kör i PowerShell i projektmappen. diff --git a/src/app.js b/src/app.js index 3174f76..bf623b7 100644 --- a/src/app.js +++ b/src/app.js @@ -207,6 +207,8 @@ const TRANSLATIONS = { "events.finals_from_qualifying": "Kval-ranking", "events.finals_from_practice": "Practice-ranking", "events.finals_source_hint": "Välj om finalerna ska seedas från practice eller kval.", + "events.follow_up_sec": "Follow-up tid (sek)", + "events.follow_up_sec_hint": "Extra tid efter ordinarie racetid så sista bilarna kan avsluta innan sessionen stängs.", "events.min_lap_time": "Min varvtid (sek)", "events.min_lap_time_hint": "Varv snabbare än detta ignoreras som shortcut eller felträff.", "events.max_lap_time": "Max varvtid (sek)", @@ -273,6 +275,8 @@ const TRANSLATIONS = { "timing.remaining": "Nedräkning", "timing.elapsed": "Körtid", "timing.race_finished": "Race is finished", + "timing.follow_up": "Follow-up", + "timing.follow_up_active": "Follow-up aktiv", "timing.no_active": "Ingen aktiv session vald.", "timing.leaderboard": "Live leaderboard", "timing.recent_passings": "Senaste passeringar", @@ -295,6 +299,18 @@ const TRANSLATIONS = { "timing.detail_title": "Leaderboard-detaljer", "timing.lap_history": "Varvhistorik", "timing.no_lap_history": "Inga varv att visa.", + "timing.manual_corrections": "Manuella korrigeringar", + "timing.lap_adjustment": "Varvjustering", + "timing.time_penalty": "Tidspåslag", + "timing.penalty_add_lap": "+1 varv", + "timing.penalty_remove_lap": "-1 varv", + "timing.penalty_add_sec": "+1 sek", + "timing.penalty_add_5sec": "+5 sek", + "timing.penalty_remove_sec": "-1 sek", + "timing.penalty_reset": "Nollställ korrigering", + "timing.valid_passing": "Giltigt varv", + "timing.invalid_short": "För kort varv", + "timing.invalid_long": "Över maxvarv", "timing.total_time": "Total tid", "timing.clear_confirm": "Rensa all tiddata för denna session?", "timing.prompt_transponder": "Transponder", @@ -470,6 +486,9 @@ const TRANSLATIONS = { "guide.race_format_5": "Finaltid och Final-start styr varje finalleg, ofta med positionsstart.", "guide.race_format_6": "Bump-up per final och Reservera bump-platser används om förare ska kunna flyttas från lägre final till nästa main.", "guide.race_format_7": "Källa för finaler avgör om finalerna seedas från practice eller kvalrankingen.", + "guide.race_format_8": "Follow-up tid ger en extra uppsamlingsperiod efter ordinarie racetid innan heatet verkligen stängs.", + "guide.race_format_9": "Min varvtid filtrerar bort shortcuts och felträffar. Exempel: på en 16-sekundersbana kan du sätta 11 sekunder som min-gräns.", + "guide.race_format_10": "Max varvtid stoppar långa felvarv från att räknas och används också för att bryta stintar och förbättra statistik. Exempel: 60 sekunder.", "guide.free_practice_title": "Free Practice", "guide.free_practice_1": "Använd sessionstypen fri träning när du bara vill visa löpande varvtider.", "guide.free_practice_2": "Free Practice påverkar inte seedning till kval eller finaler.", @@ -485,6 +504,12 @@ const TRANSLATIONS = { "guide.team_4": "Efter att laget skapats kan du klicka Redigera lag för att ändra förare eller bilar.", "guide.team_5": "Skapa en session med typ Team Race och sätt tiden, t.ex. 240 minuter för 4 timmar.", "guide.team_6": "Starta sessionen i Tidtagning. Alla passeringar från lagets medlemmar summeras till lagets totalvarv.", + "guide.validation_title": "Ogiltiga varv, follow-up och manuella korrigeringar", + "guide.validation_1": "Senaste passeringar visar nu både giltiga och ogiltiga varv. För korta varv markeras som För kort varv och för långa som Över maxvarv.", + "guide.validation_2": "Ogiltiga kortvarv under min-gränsen räknas inte alls i leaderboard eller statistik.", + "guide.validation_3": "Ogiltiga långvarv över max-gränsen räknas inte som varv, men de kan bryta lap-basen så nästa giltiga varv börjar om korrekt.", + "guide.validation_4": "När ordinarie tid är slut kan sessionen gå in i Follow-up aktiv om du har satt Follow-up tid i raceformat eller sessionen.", + "guide.validation_5": "I Tidtagning -> Detaljer kan du ge +1/-1 varv och +1/+5/-1 sekunder som manuell korrigering. Det slår igenom direkt i leaderboarden.", "overlay.title": "Overlay", "overlay.subtitle": "Extern leaderboard-skärm", "overlay.no_active": "Ingen aktiv session vald.", @@ -728,6 +753,8 @@ const TRANSLATIONS = { "events.finals_from_qualifying": "Qualifying standings", "events.finals_from_practice": "Practice standings", "events.finals_source_hint": "Choose whether finals should be seeded from practice or qualifying.", + "events.follow_up_sec": "Follow-up time (sec)", + "events.follow_up_sec_hint": "Extra time after race duration so the last cars can finish before the session closes.", "events.min_lap_time": "Min lap time (sec)", "events.min_lap_time_hint": "Laps faster than this are ignored as shortcuts or false hits.", "events.max_lap_time": "Max lap time (sec)", @@ -794,6 +821,8 @@ const TRANSLATIONS = { "timing.remaining": "Countdown", "timing.elapsed": "Elapsed", "timing.race_finished": "Race is finished", + "timing.follow_up": "Follow-up", + "timing.follow_up_active": "Follow-up active", "timing.no_active": "No active session selected.", "timing.leaderboard": "Live Leaderboard", "timing.recent_passings": "Recent Passings", @@ -816,6 +845,18 @@ const TRANSLATIONS = { "timing.detail_title": "Leaderboard details", "timing.lap_history": "Lap history", "timing.no_lap_history": "No laps to show.", + "timing.manual_corrections": "Manual corrections", + "timing.lap_adjustment": "Lap adjustment", + "timing.time_penalty": "Time penalty", + "timing.penalty_add_lap": "+1 lap", + "timing.penalty_remove_lap": "-1 lap", + "timing.penalty_add_sec": "+1 sec", + "timing.penalty_add_5sec": "+5 sec", + "timing.penalty_remove_sec": "-1 sec", + "timing.penalty_reset": "Reset correction", + "timing.valid_passing": "Valid lap", + "timing.invalid_short": "Short lap", + "timing.invalid_long": "Over max lap", "timing.total_time": "Total time", "timing.clear_confirm": "Clear all timing data for this session?", "timing.prompt_transponder": "Transponder", @@ -991,6 +1032,9 @@ const TRANSLATIONS = { "guide.race_format_5": "Final duration and Final start control each final leg, often with position start.", "guide.race_format_6": "Bump-up per main and Reserve bump slots are used if drivers should move from a lower final into the next main.", "guide.race_format_7": "Source for finals decides whether finals are seeded from practice or qualifying standings.", + "guide.race_format_8": "Follow-up time adds an extra collection period after the scheduled race time before the heat is actually closed.", + "guide.race_format_9": "Min lap time filters out shortcuts and false hits. Example: on a 16-second track you can set 11 seconds as the minimum.", + "guide.race_format_10": "Max lap time stops long false laps from counting and is also used to split stints and improve driver statistics. Example: 60 seconds.", "guide.free_practice_title": "Free Practice", "guide.free_practice_1": "Use the free practice session type when you only want to show live lap times.", "guide.free_practice_2": "Free Practice does not affect seeding for qualifying or finals.", @@ -1006,6 +1050,12 @@ const TRANSLATIONS = { "guide.team_4": "After the team is created, click Edit team to change drivers or cars.", "guide.team_5": "Create a session with type Team Race and set the time, for example 240 minutes for 4 hours.", "guide.team_6": "Start the session in Timing. All passings from the team's members are added to the team's total laps.", + "guide.validation_title": "Invalid laps, follow-up and manual corrections", + "guide.validation_1": "Recent Passings now shows both valid and invalid laps. Short laps are marked as Short lap and long laps as Over max lap.", + "guide.validation_2": "Invalid short laps under the minimum threshold do not count in the leaderboard or statistics.", + "guide.validation_3": "Invalid long laps over the maximum threshold do not count as laps, but they can reset the lap base so the next valid lap starts correctly.", + "guide.validation_4": "When the scheduled time ends, the session can enter Follow-up active if Follow-up time has been configured in race format or on the session.", + "guide.validation_5": "In Timing -> Details you can apply +1/-1 lap and +1/+5/-1 seconds as manual corrections. The leaderboard updates immediately.", "overlay.title": "Overlay", "overlay.subtitle": "External leaderboard screen", "overlay.no_active": "No active session selected.", @@ -1546,6 +1596,8 @@ function normalizeSession(session) { startMode: session?.startMode || "mass", staggerGapSec: Number(session?.staggerGapSec || 5) || 5, seedBestLapCount: Math.max(0, Number(session?.seedBestLapCount || 0) || 0), + followUpSec: Math.max(0, Number(session?.followUpSec || 0) || 0), + followUpStartedAt: Number(session?.followUpStartedAt || 0) || null, driverIds: Array.isArray(session?.driverIds) ? session.driverIds : [], manualGridIds: Array.isArray(session?.manualGridIds) ? session.manualGridIds : [], gridCustomized: Boolean(session?.gridCustomized), @@ -1580,6 +1632,7 @@ function normalizeEvent(event) { countedFinalLegs: Math.max(1, Number(event?.raceConfig?.countedFinalLegs || 1) || 1), finalDurationMin: Math.max(1, Number(event?.raceConfig?.finalDurationMin || 5) || 5), finalStartMode: normalizeStartMode(event?.raceConfig?.finalStartMode || "position"), + followUpSec: Math.max(0, Number(event?.raceConfig?.followUpSec || 0) || 0), minLapMs: Math.max(0, Number(event?.raceConfig?.minLapMs || 0) || 0), maxLapMs: Math.max(0, Number(event?.raceConfig?.maxLapMs || 60000) || 60000), bumpCount: Math.max(0, Number(event?.raceConfig?.bumpCount || 0) || 0), @@ -2056,9 +2109,21 @@ function handleSessionTimerTick() { return { changed: false }; } + if (Number(active.followUpSec || 0) > 0) { + if (!active.followUpStartedAt) { + active.followUpStartedAt = Date.now(); + saveState(); + return { changed: true }; + } + if (timing.followUpRemainingMs > 0) { + return { changed: false }; + } + } + active.status = "finished"; active.endedAt = Date.now(); active.finishedByTimer = true; + active.followUpStartedAt = null; if (lastFinishAnnouncementSessionId !== active.id) { announceRaceFinished(); lastFinishAnnouncementSessionId = active.id; @@ -2833,6 +2898,7 @@ function renderEventManager(eventId) { ${SESSION_TYPES.map((s) => ``).join("")} + ` )} + ${renderRaceFormatField( + "events.follow_up_sec", + "events.follow_up_sec_hint", + `` + )} ${renderRaceFormatField( "events.min_lap_time", "events.min_lap_time_hint", @@ -3313,6 +3384,7 @@ function renderEventManager(eventId) { ).join("")} +