Split- och depåstatistik är implementerad

This commit is contained in:
larssand
2026-08-25 20:00:55 +02:00
parent 8ea8b3a84e
commit abf887dccd
16 changed files with 564 additions and 46 deletions

View File

@@ -6,7 +6,7 @@ import { renderRaceFormatField, renderRaceFormatContextCard, getRaceSummaryItems
import { normalizeRaceTeam as normalizeRaceTeamLogic, normalizeStoredRacePreset as normalizeStoredRacePresetLogic, getRaceFormatPresets as getRaceFormatPresetsLogic, applyRaceFormatPreset as applyRaceFormatPresetLogic, buildRaceFormatConfigFromForm as buildRaceFormatConfigFromFormLogic, normalizeEvent as normalizeEventLogic, normalizeBrandingConfig as normalizeBrandingConfigLogic, resolveEventBranding as resolveEventBrandingLogic, getRaceManageStatuses as getRaceManageStatusesLogic, buildDefaultRaceWizardDraft as buildDefaultRaceWizardDraftLogic, getRaceWizardPreset as getRaceWizardPresetLogic, applyRaceWizardPresetDefaults as applyRaceWizardPresetDefaultsLogic, ensureRaceParticipantsConfigured as ensureRaceParticipantsConfiguredLogic, buildRaceSession as buildRaceSessionLogic, buildOpenPracticeBundle as buildOpenPracticeBundleLogic, buildTrackSession as buildTrackSessionLogic, createSponsorRounds as createSponsorRoundsLogic, buildRaceSessionsFromWizard as buildRaceSessionsFromWizardLogic, getRaceWizardSessionPlan as getRaceWizardSessionPlanLogic, getEventDrivers as getEventDriversLogic, getEventTeams as getEventTeamsLogic, getTeamDriverPool as getTeamDriverPoolLogic, findEventTeamForPassing as findEventTeamForPassingLogic, generateQualifyingForRace as generateQualifyingForRaceLogic, reseedUpcomingQualifying as reseedUpcomingQualifyingLogic, generateFinalsForRace as generateFinalsForRaceLogic, applyBumpsForRace as applyBumpsForRaceLogic } from "./event_race_logic.js";
import { getSessionTypeLabel as getSessionTypeLabelLogic, getStatusLabel as getStatusLabelLogic, isUntimedSession as isUntimedSessionLogic, getActiveSession as getActiveSessionLogic, getSessionTargetMs as getSessionTargetMsLogic, getSessionLapWindow as getSessionLapWindowLogic, isCountedPassing as isCountedPassingLogic, getVisiblePassings as getVisiblePassingsLogic, getPassingValidationLabel as getPassingValidationLabelLogic, getSessionTiming as getSessionTimingLogic, ensureSessionResult as ensureSessionResultLogic, buildLeaderboard as buildLeaderboardLogic, formatLapDelta as formatLapDeltaLogic, formatLeaderboardGap as formatLeaderboardGapLogic, getCompetitorElapsedMs as getCompetitorElapsedMsLogic, getCompetitorPassings as getCompetitorPassingsLogic, getCompetitorSeedMetric as getCompetitorSeedMetricLogic, getSessionEntrants as getSessionEntrantsLogic, buildPracticeStandings as buildPracticeStandingsLogic, getQualifyingPointsValue as getQualifyingPointsValueLogic, isHighPointsTable as isHighPointsTableLogic, compareNumberSet as compareNumberSetLogic, buildQualifyingTieBreakNote as buildQualifyingTieBreakNoteLogic, hasQualifyingPrimaryTie as hasQualifyingPrimaryTieLogic, buildQualifyingStandings as buildQualifyingStandingsLogic, formatTeamActiveMemberLabel as formatTeamActiveMemberLabelLogic, buildTeamRaceStandings as buildTeamRaceStandingsLogic, buildTeamStintLog as buildTeamStintLogLogic, getSessionGridEntries as getSessionGridEntriesLogic, getSessionGridOrder as getSessionGridOrderLogic, ensureSessionDriverOrder as ensureSessionDriverOrderLogic, buildFinalStandings as buildFinalStandingsLogic } from "./timing_logic.js";
import { getSessionTypeLabel as getSessionTypeLabelLogic, getStatusLabel as getStatusLabelLogic, isUntimedSession as isUntimedSessionLogic, getActiveSession as getActiveSessionLogic, getSessionTargetMs as getSessionTargetMsLogic, getSessionLapWindow as getSessionLapWindowLogic, isCountedPassing as isCountedPassingLogic, getVisiblePassings as getVisiblePassingsLogic, getPassingValidationLabel as getPassingValidationLabelLogic, getSessionTiming as getSessionTimingLogic, ensureSessionResult as ensureSessionResultLogic, buildLeaderboard as buildLeaderboardLogic, formatLapDelta as formatLapDeltaLogic, formatLeaderboardGap as formatLeaderboardGapLogic, getCompetitorElapsedMs as getCompetitorElapsedMsLogic, getCompetitorPassings as getCompetitorPassingsLogic, getCompetitorSeedMetric as getCompetitorSeedMetricLogic, getSessionEntrants as getSessionEntrantsLogic, buildPracticeStandings as buildPracticeStandingsLogic, getQualifyingPointsValue as getQualifyingPointsValueLogic, isHighPointsTable as isHighPointsTableLogic, compareNumberSet as compareNumberSetLogic, buildQualifyingTieBreakNote as buildQualifyingTieBreakNoteLogic, hasQualifyingPrimaryTie as hasQualifyingPrimaryTieLogic, buildQualifyingStandings as buildQualifyingStandingsLogic, formatTeamActiveMemberLabel as formatTeamActiveMemberLabelLogic, buildTeamRaceStandings as buildTeamRaceStandingsLogic, buildSessionChannelStats as buildSessionChannelStatsLogic, buildTeamStintLog as buildTeamStintLogLogic, getSessionGridEntries as getSessionGridEntriesLogic, getSessionGridOrder as getSessionGridOrderLogic, ensureSessionDriverOrder as ensureSessionDriverOrderLogic, buildFinalStandings as buildFinalStandingsLogic } from "./timing_logic.js";
import { renderDashboardView, renderClassesView, renderDriversView, renderCarsView } from "./core_views.js";
@@ -17,14 +17,14 @@ import { renderEventManagerMarkup } from "./event_manager_view.js";
import { renderEventManagerView } from "./event_manager_controller.js";
import { createDefaultAmmcConfigHelper, getManagedWsUrlHelper, loadAmmcConfigFromBackendHelper, saveAmmcConfigToBackendHelper, refreshAmmcStatusHelper, startManagedAmmcHelper, stopManagedAmmcHelper, applyPersistedStateHelper, hydrateFromBackendHelper, scheduleBackendSyncHelper, syncStateToBackendHelper, pingBackendHelper, checkAppVersionHelper, startAppVersionPollingHelper, startOverlaySyncHelper, startOverlayRotationHelper, startOverlayLiveRefreshHelper, ensureAudioContextHelper, playPassingBeepHelper, playFinishSirenHelper, playLeaderCueHelper, playStartCueHelper, playBestLapCueHelper, pushOverlayEventHelper, getSpeechVoicesHelper, speakTextHelper, announcePassingHelper, announceRaceStartedHelper, announceRaceFinishedHelper, handleSessionTimerTickHelper, tickClockHelper } from "./runtime_services.js";
import { connectDecoderHelper, disconnectDecoderHelper, processDecoderMessageHelper } from "./decoder_runtime.js";
import { normalizeTrainingState as normalizeTrainingStateLogic, syncTrainingLedger as syncTrainingLedgerLogic, buildDriverTrainingStats as buildDriverTrainingStatsLogic, buildDriverActivePracticeStats as buildDriverActivePracticeStatsLogic, buildTrainingRecommendation as buildTrainingRecommendationLogic, getTrainingGoalProgress as getTrainingGoalProgressLogic, getNextTrainingMilestone as getNextTrainingMilestoneLogic } from "./training_logic.js";
import { normalizeTrainingState as normalizeTrainingStateLogic, syncTrainingLedger as syncTrainingLedgerLogic, buildDriverTrainingStats as buildDriverTrainingStatsLogic, buildDriverTimingChannelStats as buildDriverTimingChannelStatsLogic, buildDriverActivePracticeStats as buildDriverActivePracticeStatsLogic, buildTrainingRecommendation as buildTrainingRecommendationLogic, getTrainingGoalProgress as getTrainingGoalProgressLogic, getNextTrainingMilestone as getNextTrainingMilestoneLogic } from "./training_logic.js";
import { renderTrainingView } from "./training_views.js";
import { renderTimingView, renderJudgingView } from "./timing_views.js";
import { renderTeamStintLog as renderTeamStintLogHelper, renderTeamRaceStandings as renderTeamRaceStandingsHelper, getSessionSortWeight as getSessionSortWeightHelper, getDriverDisplayById as getDriverDisplayByIdHelper, renderPositionGrid as renderPositionGridHelper, renderGridEditor as renderGridEditorHelper, getFinalMainLayouts as getFinalMainLayoutsHelper, renderFinalMatrix as renderFinalMatrixHelper, buildPrintBrandBlock as buildPrintBrandBlockHelper, buildRaceStartListsHtml as buildRaceStartListsHtmlHelper, buildRaceResultsHtml as buildRaceResultsHtmlHelper, buildTeamRaceResultsHtml as buildTeamRaceResultsHtmlHelper } from "./race_render_helpers.js";
import { renderTeamStintLog as renderTeamStintLogHelper, renderSessionChannelAnalysis as renderSessionChannelAnalysisHelper, renderTeamRaceStandings as renderTeamRaceStandingsHelper, getSessionSortWeight as getSessionSortWeightHelper, getDriverDisplayById as getDriverDisplayByIdHelper, renderPositionGrid as renderPositionGridHelper, renderGridEditor as renderGridEditorHelper, getFinalMainLayouts as getFinalMainLayoutsHelper, renderFinalMatrix as renderFinalMatrixHelper, buildPrintBrandBlock as buildPrintBrandBlockHelper, buildRaceStartListsHtml as buildRaceStartListsHtmlHelper, buildRaceResultsHtml as buildRaceResultsHtmlHelper, buildTeamRaceResultsHtml as buildTeamRaceResultsHtmlHelper } from "./race_render_helpers.js";
import { getManualCorrectionSummary as getManualCorrectionSummaryLogic, applyCompetitorCorrection as applyCompetitorCorrectionLogic, recalculateCompetitorFromPassings as recalculateCompetitorFromPassingsLogic, invalidateCompetitorLastLap as invalidateCompetitorLastLapLogic, restoreCompetitorLastInvalidLap as restoreCompetitorLastInvalidLapLogic, findPassingByUndoMarker as findPassingByUndoMarkerLogic, undoJudgingAdjustment as undoJudgingAdjustmentLogic, getJudgeFilteredRows as getJudgeFilteredRowsLogic, getJudgeFilteredLog as getJudgeFilteredLogLogic } from "./judging_logic.js";
@@ -76,6 +76,8 @@ const getVisiblePassings = (result) => getVisiblePassingsLogic(result, { isCount
const getPassingValidationLabel = (passing) => getPassingValidationLabelLogic(passing, { t });
const getSessionTiming = (session, nowTs = Date.now()) => getSessionTimingLogic(session, nowTs, { getSessionTargetMs });
const ensureSessionResult = (sessionId) => ensureSessionResultLogic(sessionId, { state });
const buildSessionChannelStats = (session) => buildSessionChannelStatsLogic(session, { ensureSessionResult });
const renderSessionChannelAnalysis = (session) => renderSessionChannelAnalysisHelper(session, { t, escapeHtml, renderTable, formatLap, formatRaceClock, buildSessionChannelStats });
const formatLapDelta = (deltaMs) => formatLapDeltaLogic(deltaMs, { formatLap });
const formatLeaderboardGap = (row, referenceRow, options = {}) => formatLeaderboardGapLogic(row, referenceRow, options, { t });
const getCompetitorElapsedMs = (session, row) => getCompetitorElapsedMsLogic(session, row);
@@ -100,7 +102,7 @@ const buildFinalStandings = (event) => buildFinalStandingsLogic(event, { getSess
const getManualCorrectionSummary = (row) => getManualCorrectionSummaryLogic(row, { formatLap });
const renderTeamStintLog = (session, rows) => renderTeamStintLogHelper(session, rows, { t, escapeHtml, buildTeamStintLog, formatTeamActiveMemberLabel, renderTable, formatRaceClock });
const renderTeamRaceStandings = (event) => renderTeamRaceStandingsHelper(event, { t, escapeHtml, buildTeamRaceStandings, getSessionTypeLabel, renderTable, formatTeamActiveMemberLabel, formatLap, renderTeamStintLog });
const renderTeamRaceStandings = (event) => renderTeamRaceStandingsHelper(event, { t, escapeHtml, buildTeamRaceStandings, getSessionTypeLabel, renderTable, formatTeamActiveMemberLabel, formatLap, formatRaceClock, renderTeamStintLog, buildSessionChannelStats });
const getSessionSortWeight = (session) => getSessionSortWeightHelper(session);
const getDriverDisplayById = (driverId) => getDriverDisplayByIdHelper(driverId, { state, t });
const renderPositionGrid = (session) => renderPositionGridHelper(session, { t, escapeHtml, getSessionGridEntries });
@@ -109,7 +111,7 @@ const getFinalMainLayouts = (event) => getFinalMainLayoutsHelper(event, { getSes
const renderFinalMatrix = (event) => renderFinalMatrixHelper(event, { t, escapeHtml, getFinalMainLayouts, getStatusLabel });
const buildPrintBrandBlock = (branding) => buildPrintBrandBlockHelper(branding, { escapeHtml });
const buildRaceStartListsHtml = (event) => buildRaceStartListsHtmlHelper(event, { t, state, escapeHtml, resolveEventBranding, getSessionsForEvent, getSessionSortWeight, getClassName, buildPrintBrandBlock, getSessionGridEntries, getSessionTypeLabel, getStartModeLabel, renderTable });
const buildRaceResultsHtml = (event) => buildRaceResultsHtmlHelper(event, { t, escapeHtml, resolveEventBranding, getClassName, buildPrintBrandBlock, renderRaceStandingsTableView, buildPracticeStandings, buildQualifyingStandings, buildFinalStandings, renderTeamRaceStandings });
const buildRaceResultsHtml = (event) => buildRaceResultsHtmlHelper(event, { t, escapeHtml, resolveEventBranding, getClassName, buildPrintBrandBlock, renderRaceStandingsTableView, buildPracticeStandings, buildQualifyingStandings, buildFinalStandings, renderTeamRaceStandings, getSessionsForEvent, renderSessionChannelAnalysis });
const getSessionsForEvent = (eventId) => getSessionsForEventHelper(state, eventId);
const getModeLabel = (mode) => getModeLabelHelper(mode, { t });
const normalizeStartMode = (mode) => normalizeStartModeHelper(mode);
@@ -149,7 +151,7 @@ const announceRaceStarted = (session) => announceRaceStartedHelper(session, { st
const announceRaceFinished = () => announceRaceFinishedHelper({ state, t, getActiveSession, pushOverlayEvent, playFinishSiren, speakText });
const handleSessionTimerTick = () => handleSessionTimerTickHelper({ getActiveSession, isUntimedSession, getSessionTiming, saveState, getLastFinishAnnouncementSessionId: () => lastFinishAnnouncementSessionId, setLastFinishAnnouncementSessionId: (value) => { lastFinishAnnouncementSessionId = value; }, announceRaceFinished });
const tickClock = () => tickClockHelper({ dom, currentLanguage, handleSessionTimerTick, getActiveSession, getCurrentView: () => currentView, getOverlayMode: () => overlayMode, renderView, updateHeaderState });
const buildTeamRaceResultsHtml = (event) => buildTeamRaceResultsHtmlHelper(event, { t, escapeHtml, resolveEventBranding, getClassName, buildPrintBrandBlock, buildTeamRaceStandings, renderTable, formatLap, renderTeamStintLog });
const buildTeamRaceResultsHtml = (event) => buildTeamRaceResultsHtmlHelper(event, { t, escapeHtml, resolveEventBranding, getClassName, buildPrintBrandBlock, buildTeamRaceStandings, renderTable, formatLap, renderTeamStintLog, renderSessionChannelAnalysis });
const connectDecoder = () => connectDecoderHelper({
state,
t,
@@ -198,7 +200,7 @@ const renderDrivers = () => renderDriversView({ state, dom, t, driverBrandFilter
const renderCars = () => renderCarsView({ state, dom, t, carBrandFilter: () => carBrandFilter, setCarBrandFilter: (value) => { carBrandFilter = value; }, selectedCarEditId: () => selectedCarEditId, setSelectedCarEditId: (value) => { selectedCarEditId = value; }, uid, saveState, renderView, renderTable, escapeHtml, setFormError, bindModalShell, normalizeCar });
const renderGuide = () => renderGuideView({ dom, t });
const renderOverlay = () => renderOverlayPageView({ state, dom, t, escapeHtml, formatLap, formatCountdown, formatElapsedClock, formatPredictedLapDelta, getActiveSession, buildLeaderboard, ensureSessionResult, getSessionTiming, getVisiblePassings, resolveEventBranding, buildPracticeStandings, buildQualifyingStandings, buildFinalStandings, getOverlayModeLabel, getStatusLabel, getObsOverlayConfig, buildOverlayPanels, normalizeStartMode, renderPositionGrid, getEventName, getSessionTypeLabel, getStartModeLabel, renderRaceStandingsTableView, renderTeamOverlay, renderObsOverlay, renderOverlayLeaderboard, renderOverlaySidePanel, formatTeamActiveMemberLabel, getSessionGridEntries, overlayViewMode, overlayMode, publicOverlayMode, overlayEvents, overlayRotationIndex, openOverlayWindow, buildOverlayUrl });
const renderTiming = () => renderTimingView({ state, dom, t, escapeHtml, formatLap, formatCountdown, formatElapsedClock, formatRaceClock, renderTable, uid, getActiveSession, ensureSessionResult, buildLeaderboard, getSessionTiming, getVisiblePassings, getEventName, getSessionTypeLabel, getStatusLabel, normalizeStartMode, getStartModeLabel, renderPositionGrid, connectDecoder, disconnectDecoder, applyCompetitorCorrection, invalidateCompetitorLastLap, restoreCompetitorLastInvalidLap, ensureAudioContext, validateTrackSessionForStart, announceRaceStarted, announceRaceFinished, saveState, updateHeaderState, renderView, normalizeDriver, normalizeCar, processDecoderMessage, getSelectedLeaderboardKey: () => selectedLeaderboardKey, setSelectedLeaderboardKey: (value) => { selectedLeaderboardKey = value; }, getQuickAddDraft: () => quickAddDraft, setQuickAddDraft: (value) => { quickAddDraft = value; }, getPassingValidationLabel, isCountedPassing, getCompetitorPassings, getManualCorrectionSummary, formatTeamActiveMemberLabel, lastFinishAnnouncementSessionId: () => lastFinishAnnouncementSessionId, setLastFinishAnnouncementSessionId: (value) => { lastFinishAnnouncementSessionId = value; }, lastOverlayLeaderKeyBySession, lastOverlayTop3BySession, overlayEvents: () => overlayEvents, setOverlayEvents: (value) => { overlayEvents = value; } });
const renderTiming = () => renderTimingView({ state, dom, t, escapeHtml, formatLap, formatCountdown, formatElapsedClock, formatRaceClock, renderTable, uid, getActiveSession, ensureSessionResult, buildLeaderboard, getSessionTiming, getVisiblePassings, getEventName, getSessionTypeLabel, getStatusLabel, normalizeStartMode, getStartModeLabel, renderPositionGrid, renderSessionChannelAnalysis, connectDecoder, disconnectDecoder, applyCompetitorCorrection, invalidateCompetitorLastLap, restoreCompetitorLastInvalidLap, ensureAudioContext, validateTrackSessionForStart, announceRaceStarted, announceRaceFinished, saveState, updateHeaderState, renderView, normalizeDriver, normalizeCar, processDecoderMessage, getSelectedLeaderboardKey: () => selectedLeaderboardKey, setSelectedLeaderboardKey: (value) => { selectedLeaderboardKey = value; }, getQuickAddDraft: () => quickAddDraft, setQuickAddDraft: (value) => { quickAddDraft = value; }, getPassingValidationLabel, isCountedPassing, getCompetitorPassings, getManualCorrectionSummary, formatTeamActiveMemberLabel, lastFinishAnnouncementSessionId: () => lastFinishAnnouncementSessionId, setLastFinishAnnouncementSessionId: (value) => { lastFinishAnnouncementSessionId = value; }, lastOverlayLeaderKeyBySession, lastOverlayTop3BySession, overlayEvents: () => overlayEvents, setOverlayEvents: (value) => { overlayEvents = value; } });
const renderJudging = () => renderJudgingView({ state, dom, t, escapeHtml, formatLap, renderTable, getActiveSession, ensureSessionResult, buildLeaderboard, getSessionTypeLabel, getJudgeFilteredRows, getJudgeFilteredLog, getCompetitorPassings, isCountedPassing, getPassingValidationLabel, undoJudgingAdjustment, applyCompetitorCorrection, invalidateCompetitorLastLap, restoreCompetitorLastInvalidLap, renderView, getSelectedJudgeKey: () => selectedJudgeKey, setSelectedJudgeKey: (value) => { selectedJudgeKey = value; }, getJudgingCompetitorFilter: () => judgingCompetitorFilter, setJudgingCompetitorFilter: (value) => { judgingCompetitorFilter = value; }, getJudgingLogFilter: () => judgingLogFilter, setJudgingLogFilter: (value) => { judgingLogFilter = value; } });
const normalizeTrainingState = (training) => normalizeTrainingStateLogic(training, { uid });
const syncTrainingLedger = () => {
@@ -210,7 +212,7 @@ const buildDriverActivePracticeStats = (trainingState, driverId, nowTs = Date.no
const buildTrainingRecommendation = (stats) => buildTrainingRecommendationLogic(stats);
const getTrainingGoalProgress = (goal, stats) => getTrainingGoalProgressLogic(goal, stats);
const getNextTrainingMilestone = (stats) => getNextTrainingMilestoneLogic(stats);
const renderTraining = () => renderTrainingView({ state, dom, t, escapeHtml, formatLap, formatRaceClock, uid, saveState, renderView, normalizeTrainingState, syncTrainingLedger, buildDriverTrainingStats, buildDriverActivePracticeStats, buildTrainingRecommendation, getTrainingGoalProgress, getNextTrainingMilestone, getSelectedDriverId: () => selectedTrainingDriverId, setSelectedDriverId: (value) => { selectedTrainingDriverId = value; updateViewUrl("training", value, selectedTrainingClassId); }, getSelectedClassId: () => selectedTrainingClassId, setSelectedClassId: (value) => { selectedTrainingClassId = value; updateViewUrl("training", selectedTrainingDriverId, value); } });
const renderTraining = () => renderTrainingView({ state, dom, t, escapeHtml, formatLap, formatRaceClock, uid, saveState, renderView, normalizeTrainingState, syncTrainingLedger, buildDriverTrainingStats, buildDriverTimingChannelStats: buildDriverTimingChannelStatsLogic, buildDriverActivePracticeStats, buildTrainingRecommendation, getTrainingGoalProgress, getNextTrainingMilestone, getSelectedDriverId: () => selectedTrainingDriverId, setSelectedDriverId: (value) => { selectedTrainingDriverId = value; updateViewUrl("training", value, selectedTrainingClassId); }, getSelectedClassId: () => selectedTrainingClassId, setSelectedClassId: (value) => { selectedTrainingClassId = value; updateViewUrl("training", selectedTrainingDriverId, value); } });
const applyCompetitorCorrection = (session, row, options = {}) => applyCompetitorCorrectionLogic(session, row, options, { ensureSessionResult, uid, t, formatLap, saveState });
const recalculateCompetitorFromPassings = (session, rowKey) => recalculateCompetitorFromPassingsLogic(session, rowKey, { ensureSessionResult, getCompetitorPassings, isCountedPassing });
const invalidateCompetitorLastLap = (session, row) => invalidateCompetitorLastLapLogic(session, row, { ensureSessionResult, getCompetitorPassings, isCountedPassing, recalculateCompetitorFromPassings, uid, t, formatLap, saveState });
@@ -343,7 +345,15 @@ const TRANSLATIONS = {
"training.no_activity": "Ingen aktivitet ännu",
"training.history": "Träningshistorik per dag",
"training.detailed_stats": "Mer statistik",
"training.detailed_stats_hint": "Snitt, varvserie och historik per träningsdag.",
"training.detailed_stats_hint": "Snitt, varvserie, mellantider, depåstopp och historik per träningsdag.",
"training.split_passings": "Mellantidspasseringar",
"training.best_split": "Bästa mellantid",
"training.average_split": "Snitt mellantid",
"training.pit_stops": "Depåstopp",
"training.total_pit_time": "Total depåtid",
"training.average_pit_stop": "Snitt depåstopp",
"training.best_pit_stop": "Bästa depåstopp",
"training.recent_pit_stops": "Senaste depåstopp",
"training.no_history": "Ingen träningshistorik finns ännu.",
"training.date": "Datum",
"training.laps": "Varv",
@@ -793,6 +803,19 @@ const TRANSLATIONS = {
"timing.pit_entry_decoder": "Depå in",
"timing.pit_exit_decoder": "Depå ut",
"timing.pit_active": "I DEPÅ",
"timing.channel_analysis": "Mellantider och depåstopp",
"timing.channel_analysis_hint": "Mellantider räknar inte varv. Depåtiden mäts från Depå in till Depå ut och kopplas till föraren och bilen vid infarten.",
"timing.channel_analysis_race_hint": "Sammanställning och full depålogg per heat. Använd detta exempelvis för att granska alla stopp i ett 45-minutersrace.",
"timing.split_count": "Antal split",
"timing.best_split": "Bästa split",
"timing.average_split": "Snitt split",
"timing.pit_stops": "Depåstopp",
"timing.total_pit": "Total depåtid",
"timing.average_pit": "Snitt depåstopp",
"timing.best_pit": "Bästa depåstopp",
"timing.pit_stop_log": "Depåstoppslogg",
"timing.pit_in": "Depå in",
"timing.pit_out": "Depå ut",
"timing.control": "Sessionkontroll",
"timing.speaker_panel": "Speaker-panel",
"timing.speaker_panel_hint": "Dessa växlar slår av/på cues direkt för pågående session och overlay utan att lämna Tidtagning.",
@@ -1218,6 +1241,8 @@ const TRANSLATIONS = {
"guide.decoder_roles_6": "6. I Inställningar aktiverar du endast de extrakanaler som finns fysiskt, sparar Managed AMMC och klickar Starta AMMC. Använd sedan serverns WS-url och Anslut decoder.",
"guide.decoder_roles_7": "7. Öppna TCP 9000 och de valfria portarna 9001-9003 i backend-hostens brandvägg om webbläsaren kör på en annan maskin. Port 8081 används för webb/API.",
"guide.decoder_roles_8": "8. Kontrollera i Tidtagning att varje aktiverad roll står som Ansluten. Om main är offline räknas inga varv; om en extrakanal är offline fortsätter vanlig varvräkning.",
"guide.decoder_roles_9": "9. Tidtagning visar live-sammanställning och depåstoppslogg. Race Setup visar samma data per heat, och resultat-PDF inkluderar stoppantal, total depåtid samt varje depå in/ut.",
"guide.decoder_roles_10": "10. Träningsstatistik visar mellantid och depåstopp per personlig förarprofil och vald klass. Vid förarbyte i depån tillhör stoppet föraren och bilen som registrerades vid Depå in.",
"guide.host_1": "1. AMMC körs alltid på samma maskin som backend körs på (`npm.cmd start` i Windows eller `npm start` i Linux).",
"guide.host_2": "2. Om du bara surfar in från en laptop/webbläsare startas ingen process där. Webbläsaren styr bara backend via HTTP.",
"guide.host_3": "3. Kör backend på Linux-servern -> Linux-binären används: `AMMC/linux_x86-64/ammc-amb`.",
@@ -1352,7 +1377,15 @@ const TRANSLATIONS = {
"training.no_activity": "No activity yet",
"training.history": "Daily practice history",
"training.detailed_stats": "More statistics",
"training.detailed_stats_hint": "Averages, lap streak and daily practice history.",
"training.detailed_stats_hint": "Averages, lap streak, split timing, pit stops, and daily practice history.",
"training.split_passings": "Split passings",
"training.best_split": "Best split",
"training.average_split": "Average split",
"training.pit_stops": "Pit stops",
"training.total_pit_time": "Total pit time",
"training.average_pit_stop": "Average pit stop",
"training.best_pit_stop": "Best pit stop",
"training.recent_pit_stops": "Recent pit stops",
"training.no_history": "No practice history is available yet.",
"training.date": "Date",
"training.laps": "Laps",
@@ -1802,6 +1835,19 @@ const TRANSLATIONS = {
"timing.pit_entry_decoder": "Pit entry",
"timing.pit_exit_decoder": "Pit exit",
"timing.pit_active": "IN PIT",
"timing.channel_analysis": "Split timing and pit stops",
"timing.channel_analysis_hint": "Split timing does not count laps. Pit time is measured from Pit entry to Pit exit and attributed to the driver and car at entry.",
"timing.channel_analysis_race_hint": "Summary and complete pit log per heat. Use it, for example, to review every stop in a 45-minute race.",
"timing.split_count": "Split count",
"timing.best_split": "Best split",
"timing.average_split": "Average split",
"timing.pit_stops": "Pit stops",
"timing.total_pit": "Total pit time",
"timing.average_pit": "Average pit stop",
"timing.best_pit": "Best pit stop",
"timing.pit_stop_log": "Pit stop log",
"timing.pit_in": "Pit entry",
"timing.pit_out": "Pit exit",
"timing.control": "Session Control",
"timing.speaker_panel": "Speaker panel",
"timing.speaker_panel_hint": "These toggles enable or disable cues live for the current session and overlay without leaving Timing.",
@@ -2227,6 +2273,8 @@ const TRANSLATIONS = {
"guide.decoder_roles_6": "6. In Settings, enable only physically installed extra channels, save Managed AMMC, and click Start AMMC. Then use the server WS URLs and Connect decoder.",
"guide.decoder_roles_7": "7. Allow TCP 9000 and optional ports 9001-9003 through the backend host firewall when the browser runs on another machine. Port 8081 serves the web UI/API.",
"guide.decoder_roles_8": "8. Verify in Timing that every enabled role is Connected. If main is offline no laps are counted; an offline optional channel does not stop normal lap counting.",
"guide.decoder_roles_9": "9. Timing shows a live summary and pit-stop log. Race Setup shows the same data per heat, and result PDFs include stop count, total pit time, and every pit entry/exit.",
"guide.decoder_roles_10": "10. Training statistics show split and pit data per personal driver profile and selected class. During a pit driver change, the stop remains attributed to the driver and car recorded at Pit entry.",
"guide.host_1": "1. AMMC always runs on the backend host (`npm.cmd start` on Windows or `npm start` on Linux).",
"guide.host_2": "2. If you only browse from a laptop/browser, no process is started there. The browser only controls the backend over HTTP.",
"guide.host_3": "3. Run the backend on Linux -> the Linux binary is used: `AMMC/linux_x86-64/ammc-amb`.",
@@ -3428,6 +3476,7 @@ function renderEventManager(eventId) {
buildQualifyingStandings,
buildFinalStandings,
renderTeamRaceStandings,
renderSessionChannelAnalysis,
renderFinalMatrix,
getModeLabel,
renderTable,
@@ -3788,6 +3837,7 @@ async function exportRaceStartListsPdf(event) {
async function exportRaceResultsPdf(event) {
const branding = resolveEventBranding(event);
const channelSections = getSessionsForEvent(event.id).flatMap((session) => buildSessionChannelPdfSections(session));
await requestPdfExport({
filename: `${event.name.replaceAll(/\s+/g, "_")}_results.pdf`,
title: event.name,
@@ -3820,35 +3870,92 @@ async function exportRaceResultsPdf(event) {
...buildTeamRaceStandings(event).map(({ session, rows }) =>
buildPdfSection(
`${t("events.team_standings")} • ${session.name}`,
[t("table.pos"), t("events.team_name"), t("table.laps"), t("table.result"), t("table.best_lap")],
rows.map((row, index) => [
String(index + 1),
row.displayName || row.driverName || "-",
String(row.laps || 0),
row.resultDisplay || "-",
formatLap(row.bestLapMs),
])
[t("table.pos"), t("events.team_name"), t("table.laps"), t("table.result"), t("table.best_lap"), t("timing.pit_stops"), t("timing.total_pit")],
rows.map((row, index) => {
const channel = buildSessionChannelStats(session).competitors.find((item) => item.competitorKey === row.key) || {};
return [
String(index + 1),
row.displayName || row.driverName || "-",
String(row.laps || 0),
row.resultDisplay || "-",
formatLap(row.bestLapMs),
String(channel.pitStopCount || 0),
channel.pitStopCount ? formatRaceClock(channel.totalPitMs) : "-",
];
})
)
),
...channelSections,
],
});
}
function buildSessionChannelPdfSections(session) {
const stats = buildSessionChannelStats(session);
if (!stats.splitCount && !stats.pitStopCount) {
return [];
}
const sections = [
buildPdfSection(
`${timingSessionPdfTitle(session)} • ${t("timing.channel_analysis")}`,
[t("table.driver"), t("timing.split_count"), t("timing.best_split"), t("timing.average_split"), t("timing.pit_stops"), t("timing.total_pit"), t("timing.average_pit"), t("timing.best_pit")],
stats.competitors.map((row) => [
row.displayName || "-",
String(row.splitCount || 0),
row.splitCount ? formatLap(row.bestSplitMs) : "-",
row.splitCount ? formatLap(row.averageSplitMs) : "-",
String(row.pitStopCount || 0),
row.pitStopCount ? formatRaceClock(row.totalPitMs) : "-",
row.pitStopCount ? formatLap(row.averagePitStopMs) : "-",
row.pitStopCount ? formatLap(row.bestPitStopMs) : "-",
])
),
];
if (stats.pitStops.length) {
sections.push(
buildPdfSection(
`${timingSessionPdfTitle(session)} • ${t("timing.pit_stop_log")}`,
["#", t("events.team_name"), t("table.driver"), t("table.car"), t("timing.pit_in"), t("timing.pit_out"), t("table.duration")],
stats.pitStops.map((stop) => [
String(stop.index),
stop.displayName || "-",
stop.driverName || "-",
stop.carName || "-",
stop.enteredAt ? new Date(stop.enteredAt).toLocaleTimeString() : "-",
stop.exitedAt ? new Date(stop.exitedAt).toLocaleTimeString() : "-",
formatLap(stop.pitStopMs),
])
)
);
}
return sections;
}
function timingSessionPdfTitle(session) {
return `${session.name} • ${getSessionTypeLabel(session.type)}`;
}
async function exportTeamRaceResultsPdf(event) {
const branding = resolveEventBranding(event);
const sections = [];
buildTeamRaceStandings(event).forEach(({ session, rows }) => {
const channelRows = new Map(buildSessionChannelStats(session).competitors.map((row) => [row.competitorKey, row]));
sections.push(
buildPdfSection(
`${t("events.team_report")} • ${session.name}`,
[t("table.pos"), t("events.team_name"), t("table.laps"), t("table.result"), t("table.best_lap")],
rows.map((row, index) => [
String(index + 1),
row.displayName || row.driverName || "-",
String(row.laps || 0),
row.resultDisplay || "-",
formatLap(row.bestLapMs),
])
[t("table.pos"), t("events.team_name"), t("table.laps"), t("table.result"), t("table.best_lap"), t("timing.pit_stops"), t("timing.total_pit")],
rows.map((row, index) => {
const channel = channelRows.get(row.key) || {};
return [
String(index + 1),
row.displayName || row.driverName || "-",
String(row.laps || 0),
row.resultDisplay || "-",
formatLap(row.bestLapMs),
String(channel.pitStopCount || 0),
channel.pitStopCount ? formatRaceClock(channel.totalPitMs) : "-",
];
})
)
);
@@ -3869,6 +3976,7 @@ async function exportTeamRaceResultsPdf(event) {
)
);
});
sections.push(...buildSessionChannelPdfSections(session));
});
await requestPdfExport({