diff --git a/README.md b/README.md
index 865a51d..14a5d4e 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,9 @@ RC timing app med sponsor-eventflöde (delade bilar/transpondrar mellan olika he
- 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
+ - seedmetoder för practice/kval: bästa N varv som summa, snitt eller konsekutiva varv
+ - valbar kval-poängtabell och tie-break-regler
+ - schemaavvikelse på `Översikt` mellan planerad och faktisk körtid
- 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
@@ -115,6 +118,37 @@ Praktiskt exempel:
- `Nollställ korrigering`
- korrigeringarna uppdaterar leaderboard och resultat direkt
+### Best laps / average / consecutive
+- Ställs per session med `Seedmetod` när `Seed bästa varv` är större än `0`
+- tillgängliga lägen:
+ - `Bästa N varv, summa`
+ - `Bästa N varv, snitt`
+ - `Bästa N konsekutiva varv`
+- leaderboard och ranking visar rätt format beroende på läge:
+ - summa: `3/00:48.321`
+ - snitt: `3 avg 16.107`
+ - konsekutiva: `3 con 00:49.005`
+
+### Poängtabeller och tie-break
+- Ställs i `Race Setup -> Hantera -> Raceformat`
+- poängtabeller för kval:
+ - `Placeringstal (1,2,3...)`
+ - `Fallande efter fältstorlek`
+ - `10-9-8-7-6-5-4-3-2-1`
+- tie-break för kvalranking:
+ - `Jämför räknade rundor`
+ - `Bästa enskilda varv`
+ - `Bästa runda / heatresultat`
+
+### Schemaavvikelse på Översikt
+- `Översikt` visar nu om dagen ligger före eller efter schema
+- planerad tid räknar:
+ - sessionens varaktighet
+ - plus follow-up tid
+- faktisk tid räknar:
+ - verklig tid från start till stopp
+ - eller pågående körtid om sessionen fortfarande kör
+
## Windows installation
Kör i PowerShell i projektmappen.
@@ -197,7 +231,7 @@ sudo ufw allow 8081/tcp
## Auto reload vid uppdatering
- Servern bevakar `index.html`, `src/app.js` och `src/styles.css`.
-- När du uppdaterar filer i `live_event` och sparar, laddar klienten om sidan automatiskt.
+- När du uppdaterar filer i `Live_RC` och sparar, laddar klienten om sidan automatiskt.
- Om backendkoden ändras, kör `npm restart`.
## Verifiera att SQLite sparar
diff --git a/src/app.js b/src/app.js
index bf623b7..77f14ae 100644
--- a/src/app.js
+++ b/src/app.js
@@ -69,6 +69,12 @@ const TRANSLATIONS = {
"dashboard.decoder_feed": "Decoder-feed",
"dashboard.backend_link": "Backend-länk",
"dashboard.audio_profile": "Ljudprofil",
+ "dashboard.schedule_drift": "Schemaavvikelse",
+ "dashboard.schedule_plan": "Planerad tid",
+ "dashboard.schedule_actual": "Faktisk tid",
+ "dashboard.on_time": "I fas",
+ "dashboard.ahead": "Före schema",
+ "dashboard.behind": "Efter schema",
"dashboard.live_note": "Snabb driftpanel för anslutning, overlay och ljud. Djupare konfig ligger kvar under Inställningar.",
"session.none_yet": "Inga sessioner ännu.",
"classes.create": "Skapa klass",
@@ -125,6 +131,11 @@ const TRANSLATIONS = {
"events.max_cars_placeholder": "Max bilar (valfritt)",
"events.start_mode": "Starttyp",
"events.seed_best_laps": "Seedning bästa varv",
+ "events.seed_method": "Seedmetod",
+ "events.seed_method_hint": "Hur bästa varv ska räknas när seedBestLapCount är större än 0.",
+ "events.seed_method_best_sum": "Bästa N varv, summa",
+ "events.seed_method_average": "Bästa N varv, snitt",
+ "events.seed_method_consecutive": "Bästa N konsekutiva varv",
"events.stagger_gap_sec": "Stagger-gap (sek)",
"events.session_settings": "Sessioninställningar",
"events.edit_session": "Inställningar",
@@ -146,8 +157,22 @@ const TRANSLATIONS = {
"events.qual_duration_hint": "Längd per kvalheat i minuter.",
"events.qual_start_mode": "Kval-start",
"events.qual_start_mode_hint": "Mass, position eller staggered för kvalomgångarna.",
+ "events.qual_seed_laps": "Kval bästa varv",
+ "events.qual_seed_laps_hint": "Antal varv som används för ranking i varje kvalheat när seedmetod är aktiv.",
+ "events.qual_seed_method": "Kval seedmetod",
+ "events.qual_seed_method_hint": "Hur kvalheat räknar varv när bästa-varvsläget används.",
"events.counted_qual_rounds": "Räknade kvalrundor",
"events.counted_qual_rounds_hint": "Hur många av kvalrundorna som räknas i slutrankingen.",
+ "events.qual_points_table": "Poängtabell",
+ "events.qual_points_table_hint": "Välj hur poäng per kvalomgång delas ut när Kval-scoring använder poäng.",
+ "events.qual_points_rank": "Placeringstal (1,2,3...)",
+ "events.qual_points_desc": "Fallande efter fältstorlek",
+ "events.qual_points_ifmar": "10-9-8-7-6-5-4-3-2-1",
+ "events.qual_tie_break": "Tie-break",
+ "events.qual_tie_break_hint": "Välj vilken regel som avgör lika resultat i kvalrankingen.",
+ "events.qual_tie_break_rounds": "Jämför räknade rundor",
+ "events.qual_tie_break_best_lap": "Bästa enskilda varv",
+ "events.qual_tie_break_best_round": "Bästa runda / heatresultat",
"events.cars_per_final": "Förare per final",
"events.cars_per_final_hint": "Max antal förare i varje A/B/C-final.",
"events.final_legs": "Final-heat per final",
@@ -510,6 +535,16 @@ const TRANSLATIONS = {
"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.",
+ "guide.qualifying_title": "Seedning, poängtabeller och tie-break",
+ "guide.qualifying_1": "Practice och kval kan nu använda tre seedmetoder: bästa N varv som summa, bästa N varv som snitt eller bästa N konsekutiva varv.",
+ "guide.qualifying_2": "Raceformat styr både Kval seedvarv och Kval seedmetod när nya kvalheat skapas från practice eller deltagarlistan.",
+ "guide.qualifying_3": "Kval-scoring kan kombinera poängläge med poängtabell: placeringstal, fallande efter fältstorlek eller IFMAR 10-9-8-7-6-5-4-3-2-1.",
+ "guide.qualifying_4": "Tie-break i kvalrankingen kan avgöras på räknade rundor, bästa enskilda varv eller bästa runda/heatresultat.",
+ "guide.qualifying_5": "Leaderboarden visar seedningsresultat i rätt format, till exempel 3/00:48.321, 3 avg 16.107 eller 3 con 00:49.005.",
+ "guide.dashboard_title": "Schemaavvikelse på Översikt",
+ "guide.dashboard_1": "Översikt visar nu skillnaden mellan planerad tid och faktisk körtid för alla startade sessioner.",
+ "guide.dashboard_2": "Planerad tid räknar sessionstid plus follow-up. Faktisk tid räknar verklig tid från start till stopp eller nuvarande tid om heatet fortfarande kör.",
+ "guide.dashboard_3": "Det gör det lättare att se om tävlingsdagen ligger före eller efter schema direkt från dashboarden.",
"overlay.title": "Overlay",
"overlay.subtitle": "Extern leaderboard-skärm",
"overlay.no_active": "Ingen aktiv session vald.",
@@ -615,6 +650,12 @@ const TRANSLATIONS = {
"dashboard.decoder_feed": "Decoder feed",
"dashboard.backend_link": "Backend link",
"dashboard.audio_profile": "Audio profile",
+ "dashboard.schedule_drift": "Schedule drift",
+ "dashboard.schedule_plan": "Planned time",
+ "dashboard.schedule_actual": "Actual time",
+ "dashboard.on_time": "On time",
+ "dashboard.ahead": "Ahead of schedule",
+ "dashboard.behind": "Behind schedule",
"dashboard.live_note": "Quick operations panel for connection, overlay and audio. Deeper configuration remains under Settings.",
"session.none_yet": "No sessions yet.",
"classes.create": "Create Class",
@@ -671,6 +712,11 @@ const TRANSLATIONS = {
"events.max_cars_placeholder": "Max cars (optional)",
"events.start_mode": "Start mode",
"events.seed_best_laps": "Best laps for seeding",
+ "events.seed_method": "Seed method",
+ "events.seed_method_hint": "How best laps should be evaluated when seedBestLapCount is greater than 0.",
+ "events.seed_method_best_sum": "Best N laps, total",
+ "events.seed_method_average": "Best N laps, average",
+ "events.seed_method_consecutive": "Best N consecutive laps",
"events.stagger_gap_sec": "Stagger gap (sec)",
"events.session_settings": "Session settings",
"events.edit_session": "Settings",
@@ -692,8 +738,22 @@ const TRANSLATIONS = {
"events.qual_duration_hint": "Length of each qualifying heat in minutes.",
"events.qual_start_mode": "Qualifying start",
"events.qual_start_mode_hint": "Mass, position or staggered for qualifying rounds.",
+ "events.qual_seed_laps": "Qualifying best laps",
+ "events.qual_seed_laps_hint": "Number of laps used for ranking in each qualifying heat when seed mode is active.",
+ "events.qual_seed_method": "Qualifying seed method",
+ "events.qual_seed_method_hint": "How qualifying heats evaluate laps when best-lap mode is used.",
"events.counted_qual_rounds": "Counted qualifying rounds",
"events.counted_qual_rounds_hint": "How many qualifying rounds count toward the final ranking.",
+ "events.qual_points_table": "Points table",
+ "events.qual_points_table_hint": "Choose how each qualifying round awards points when Qualifying scoring uses points.",
+ "events.qual_points_rank": "Placement values (1,2,3...)",
+ "events.qual_points_desc": "Descending by field size",
+ "events.qual_points_ifmar": "10-9-8-7-6-5-4-3-2-1",
+ "events.qual_tie_break": "Tie-break",
+ "events.qual_tie_break_hint": "Choose which rule resolves equal results in qualifying standings.",
+ "events.qual_tie_break_rounds": "Compare counted rounds",
+ "events.qual_tie_break_best_lap": "Best single lap",
+ "events.qual_tie_break_best_round": "Best round / heat result",
"events.cars_per_final": "Drivers per final",
"events.cars_per_final_hint": "Maximum number of drivers in each A/B/C final.",
"events.final_legs": "Final heats per main",
@@ -1056,6 +1116,16 @@ const TRANSLATIONS = {
"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.",
+ "guide.qualifying_title": "Seeding, points tables and tie-break",
+ "guide.qualifying_1": "Practice and qualifying can now use three seed methods: best N laps as total, best N laps as average or best N consecutive laps.",
+ "guide.qualifying_2": "Race format controls both Qualifying seed laps and Qualifying seed method when new qualifying heats are generated from practice or the participant list.",
+ "guide.qualifying_3": "Qualifying scoring can combine points mode with a points table: placement values, descending by field size or IFMAR 10-9-8-7-6-5-4-3-2-1.",
+ "guide.qualifying_4": "Qualifying tie-break can now be resolved by counted rounds, best single lap or best round / heat result.",
+ "guide.qualifying_5": "The leaderboard now shows seeded results in the correct format, for example 3/00:48.321, 3 avg 16.107 or 3 con 00:49.005.",
+ "guide.dashboard_title": "Schedule drift on overview",
+ "guide.dashboard_1": "Overview now shows the difference between planned time and actual elapsed time for all started sessions.",
+ "guide.dashboard_2": "Planned time includes session duration plus follow-up. Actual time uses real time from start to stop, or the current time if the heat is still running.",
+ "guide.dashboard_3": "That makes it easier to see whether the race day is running ahead or behind schedule directly from the dashboard.",
"overlay.title": "Overlay",
"overlay.subtitle": "External leaderboard screen",
"overlay.no_active": "No active session selected.",
@@ -1596,6 +1666,9 @@ function normalizeSession(session) {
startMode: session?.startMode || "mass",
staggerGapSec: Number(session?.staggerGapSec || 5) || 5,
seedBestLapCount: Math.max(0, Number(session?.seedBestLapCount || 0) || 0),
+ seedMethod: ["best_sum", "average", "consecutive"].includes(String(session?.seedMethod || "").toLowerCase())
+ ? String(session.seedMethod).toLowerCase()
+ : "best_sum",
followUpSec: Math.max(0, Number(session?.followUpSec || 0) || 0),
followUpStartedAt: Number(session?.followUpStartedAt || 0) || null,
driverIds: Array.isArray(session?.driverIds) ? session.driverIds : [],
@@ -1626,7 +1699,17 @@ function normalizeEvent(event) {
carsPerHeat: Math.max(2, Number(event?.raceConfig?.carsPerHeat || 8) || 8),
qualDurationMin: Math.max(1, Number(event?.raceConfig?.qualDurationMin || 5) || 5),
qualStartMode: normalizeStartMode(event?.raceConfig?.qualStartMode || "staggered"),
+ qualSeedLapCount: Math.max(0, Number(event?.raceConfig?.qualSeedLapCount || 2) || 2),
+ qualSeedMethod: ["best_sum", "average", "consecutive"].includes(String(event?.raceConfig?.qualSeedMethod || "").toLowerCase())
+ ? String(event.raceConfig.qualSeedMethod).toLowerCase()
+ : "best_sum",
countedQualRounds: Math.max(1, Number(event?.raceConfig?.countedQualRounds || 1) || 1),
+ qualifyingPointsTable: ["rank_low", "field_desc", "ifmar"].includes(String(event?.raceConfig?.qualifyingPointsTable || "").toLowerCase())
+ ? String(event.raceConfig.qualifyingPointsTable).toLowerCase()
+ : "rank_low",
+ qualifyingTieBreak: ["rounds", "best_lap", "best_round"].includes(String(event?.raceConfig?.qualifyingTieBreak || "").toLowerCase())
+ ? String(event.raceConfig.qualifyingTieBreak).toLowerCase()
+ : "rounds",
carsPerFinal: Math.max(2, Number(event?.raceConfig?.carsPerFinal || 8) || 8),
finalLegs: Math.max(1, Number(event?.raceConfig?.finalLegs || 1) || 1),
countedFinalLegs: Math.max(1, Number(event?.raceConfig?.countedFinalLegs || 1) || 1),
@@ -2134,6 +2217,7 @@ function handleSessionTimerTick() {
function renderDashboard() {
const active = getActiveSession();
+ const schedule = getScheduleDriftSummary();
const totalPassings = Object.values(state.resultsBySession).reduce(
(sum, x) => sum + (x.passings?.length || 0),
0
@@ -2194,6 +2278,19 @@ function renderDashboard() {
${state.settings.audioEnabled ? audioProfile : t("settings.passing_sound_off")}
${state.settings.finishVoiceEnabled ? t("settings.finish_voice") : "-"}
+
+ ${t("dashboard.schedule_drift")}
+ ${
+ schedule
+ ? `${schedule.driftMs === 0 ? t("dashboard.on_time") : schedule.driftMs < 0 ? t("dashboard.ahead") : t("dashboard.behind")} ${formatLap(Math.abs(schedule.driftMs))}`
+ : "-"
+ }
+ ${
+ schedule
+ ? `${t("dashboard.schedule_plan")}: ${formatLap(schedule.plannedMs)} • ${t("dashboard.schedule_actual")}: ${formatLap(schedule.actualMs)}`
+ : "-"
+ }
+