From 24cf7d9522d6820a14a99f819bcf5e0b03b15735 Mon Sep 17 00:00:00 2001 From: larssand Date: Sat, 14 Mar 2026 12:25:11 +0100 Subject: [PATCH] =?UTF-8?q?Open=20Practice=20=C3=A4r=20nu=20inlagt.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.js | 72 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 56 insertions(+), 16 deletions(-) diff --git a/src/app.js b/src/app.js index a58f76b..23dad75 100644 --- a/src/app.js +++ b/src/app.js @@ -11,7 +11,7 @@ const NAV_ITEMS = [ { id: "guide", titleKey: "nav.guide", subtitleKey: "nav.guide_sub" }, ]; -const SESSION_TYPES = ["free_practice", "practice", "qualification", "heat", "final"]; +const SESSION_TYPES = ["open_practice", "free_practice", "practice", "qualification", "heat", "final"]; const STORAGE_KEY = "rc_timing_control_v1"; const DEFAULT_LANGUAGE = "sv"; @@ -64,6 +64,7 @@ const TRANSLATIONS = { "dashboard.connect_decoder": "Anslut Decoder", "dashboard.recent_sessions": "Senaste sessioner", "dashboard.free_practice": "Fri träning", + "dashboard.open_practice": "Open Practice", "session.none_yet": "Inga sessioner ännu.", "classes.create": "Skapa klass", "classes.placeholder": "Klassnamn (t.ex. 2WD Buggy)", @@ -168,6 +169,7 @@ const TRANSLATIONS = { "events.export_heat_sheet": "Exportera heatsheet", "events.pdf_heat_sheet": "PDF heatsheet", "events.free_practice_note": "Free Practice visar löpande varvtider och används inte för seedning.", + "events.open_practice_note": "Open Practice visar alla inkommande transpondrar löpande. Om ingen förare matchar visas bara transpondern.", "events.generate_qualifying": "Skapa kval från practice", "events.clear_generated_qualifying": "Rensa genererade kval", "events.generate_finals": "Skapa finaler från kval", @@ -373,6 +375,8 @@ const TRANSLATIONS = { "status.leader": "LEDARE", "status.seeded": "SEED", "status.free_practice": "FREE", + "status.open_practice": "OPEN", + "session.open_practice": "open practice", "session.free_practice": "fri träning", "session.practice": "träning", "session.qualification": "kval", @@ -421,6 +425,10 @@ const TRANSLATIONS = { "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.", "guide.free_practice_3": "Leaderboarden visar varv, senaste varv, bästa varv, gap till framförvarande och eget delta mot föregående varv.", + "guide.open_practice_title": "Open Practice", + "guide.open_practice_1": "Använd Open Practice när du vill att systemet bara ska lista alla transpondrar som kommer in.", + "guide.open_practice_2": "Om transpondern inte matchar en registrerad förare visas transpondernumret som namn.", + "guide.open_practice_3": "Open Practice påverkar inte seedning, kval eller finaler.", "overlay.title": "Overlay", "overlay.subtitle": "Extern leaderboard-skärm", "overlay.no_active": "Ingen aktiv session vald.", @@ -511,6 +519,7 @@ const TRANSLATIONS = { "dashboard.connect_decoder": "Connect Decoder", "dashboard.recent_sessions": "Recent Sessions", "dashboard.free_practice": "Free Practice", + "dashboard.open_practice": "Open Practice", "session.none_yet": "No sessions yet.", "classes.create": "Create Class", "classes.placeholder": "Class name (e.g. 2WD Buggy)", @@ -615,6 +624,7 @@ const TRANSLATIONS = { "events.export_heat_sheet": "Export heat sheet", "events.pdf_heat_sheet": "PDF heat sheet", "events.free_practice_note": "Free Practice shows rolling lap times and is not used for seeding.", + "events.open_practice_note": "Open Practice shows all incoming transponders live. If no driver matches, only the transponder is shown.", "events.generate_qualifying": "Generate qualifying from practice", "events.clear_generated_qualifying": "Clear generated qualifying", "events.generate_finals": "Generate finals from qualifying", @@ -820,6 +830,8 @@ const TRANSLATIONS = { "status.leader": "LEADER", "status.seeded": "SEED", "status.free_practice": "FREE", + "status.open_practice": "OPEN", + "session.open_practice": "open practice", "session.free_practice": "free practice", "session.practice": "practice", "session.qualification": "qualification", @@ -868,6 +880,10 @@ const TRANSLATIONS = { "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.", "guide.free_practice_3": "The leaderboard shows laps, last lap, best lap, gap to the car ahead and your own delta versus the previous lap.", + "guide.open_practice_title": "Open Practice", + "guide.open_practice_1": "Use Open Practice when you want the system to simply list every transponder that comes in.", + "guide.open_practice_2": "If the transponder does not match a registered driver, the transponder number is shown as the name.", + "guide.open_practice_3": "Open Practice does not affect seeding, qualifying or finals.", "overlay.title": "Overlay", "overlay.subtitle": "External leaderboard screen", "overlay.no_active": "No active session selected.", @@ -2307,6 +2323,7 @@ function renderEventManager(eventId) {

${t("events.seed_best_laps_hint")}

${t("events.free_practice_note")}

+

${t("events.open_practice_note")}

${renderTable( @@ -3112,7 +3129,13 @@ function renderTiming() { }

${t("timing.remaining")}: ${formatCountdown(sessionTiming?.remainingMs ?? 0)}

${t("timing.total_passings")}: ${result.passings.length}

- ${active.type === "free_practice" ? `

${t("events.free_practice_note")}

` : ""}` + ${ + active.type === "free_practice" + ? `

${t("events.free_practice_note")}

` + : active.type === "open_practice" + ? `

${t("events.open_practice_note")}

` + : "" + }` : `

${t("timing.no_active")}

` } ${showFinishedBanner ? `

${t("timing.race_finished")}

` : ""} @@ -3359,6 +3382,17 @@ function renderGuide() {
+
+

${t("guide.open_practice_title")}

+
+
    +
  • ${t("guide.open_practice_1")}
  • +
  • ${t("guide.open_practice_2")}
  • +
  • ${t("guide.open_practice_3")}
  • +
+
+
+

${t("guide.host_title")}

@@ -4283,7 +4317,10 @@ function parseRtcTime(value) { } function resolveCompetitor(session, transponder) { - const isFreePractice = String(session?.type || "").toLowerCase() === "free_practice"; + const sessionType = String(session?.type || "").toLowerCase(); + const isOpenPractice = sessionType === "open_practice"; + const isFreePractice = sessionType === "free_practice"; + const isOpenMonitoringSession = isOpenPractice || isFreePractice; if (session.mode === "track") { const matchingAssignments = (session.assignments || []).filter((a) => { const car = state.cars.find((c) => c.id === a.carId); @@ -4324,7 +4361,7 @@ function resolveCompetitor(session, transponder) { const driver = state.drivers.find((d) => d.transponder === transponder); if (driver) { - if (!isFreePractice && Array.isArray(session.driverIds) && session.driverIds.length && !session.driverIds.includes(driver.id)) { + if (!isOpenMonitoringSession && Array.isArray(session.driverIds) && session.driverIds.length && !session.driverIds.includes(driver.id)) { return { key: `ignore_${driver.id}`, ignore: true, @@ -4342,7 +4379,7 @@ function resolveCompetitor(session, transponder) { return { key: `driver_tp_${transponder}`, driverId: null, - driverName: isFreePractice ? `TP ${transponder}` : t("common.unknown_driver"), + driverName: isOpenPractice ? transponder : isFreePractice ? `TP ${transponder}` : t("common.unknown_driver"), carId: null, carName: t("common.unknown_car"), }; @@ -4355,6 +4392,8 @@ function buildLeaderboard(session) { const useTargetTieBreak = session.status === "finished"; const useSeedRanking = ["practice", "qualification"].includes(sessionType) && Number(session.seedBestLapCount || 0) > 0; const isFreePractice = sessionType === "free_practice"; + const isOpenPractice = sessionType === "open_practice"; + const isRollingPractice = isFreePractice || isOpenPractice; const rows = Object.values(result.competitors).map((row) => { const totalElapsedMs = getCompetitorElapsedMs(session, row); const distanceToTargetMs = Math.abs(targetMs - totalElapsedMs); @@ -4371,13 +4410,13 @@ function buildLeaderboard(session) { previousLapMs, lapDeltaMs, comparisonMs: - isFreePractice + isRollingPractice ? row.bestLapMs || row.lastLapMs || Number.MAX_SAFE_INTEGER : useSeedRanking && seedMetric ? seedMetric.totalMs : totalElapsedMs, resultDisplay: - isFreePractice + isRollingPractice ? formatLap(row.bestLapMs || row.lastLapMs) : useSeedRanking && seedMetric ? `${seedMetric.lapCount}/${formatRaceClock(seedMetric.totalMs)}` @@ -4386,7 +4425,7 @@ function buildLeaderboard(session) { }); rows.sort((a, b) => { - if (isFreePractice) { + if (isRollingPractice) { if (a.comparisonMs !== b.comparisonMs) { return a.comparisonMs - b.comparisonMs; } @@ -4427,12 +4466,12 @@ function buildLeaderboard(session) { } return { ...row, - gap: formatLeaderboardGap(row, leader, { useSeedRanking, useTargetTieBreak, isFreePractice }), - leaderGap: formatLeaderboardGap(row, leader, { useSeedRanking, useTargetTieBreak, isFreePractice }), + gap: formatLeaderboardGap(row, leader, { useSeedRanking, useTargetTieBreak, isFreePractice: isRollingPractice }), + leaderGap: formatLeaderboardGap(row, leader, { useSeedRanking, useTargetTieBreak, isFreePractice: isRollingPractice }), gapAhead: formatLeaderboardGap(row, rows[index - 1], { useSeedRanking, useTargetTieBreak, - isFreePractice, + isFreePractice: isRollingPractice, selfLabel: t("status.leader"), }), lapDelta: formatLapDelta(row.lapDeltaMs), @@ -4788,11 +4827,12 @@ function renderRaceStandingsTable(rows, emptyLabel) { function getSessionSortWeight(session) { const order = { - free_practice: 0, - practice: 1, - qualification: 2, - heat: 3, - final: 4, + open_practice: 0, + free_practice: 1, + practice: 2, + qualification: 3, + heat: 4, + final: 5, }; return order[String(session?.type || "").toLowerCase()] || 99; }