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("")}
+