"Add Windows installer and driver training rewards"
This commit is contained in:
140
src/app.js
140
src/app.js
@@ -17,6 +17,8 @@ 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, speakTextHelper, announcePassingHelper, announceRaceFinishedHelper, handleSessionTimerTickHelper, tickClockHelper } from "./runtime_services.js";
|
||||
import { connectDecoderHelper, disconnectDecoderHelper, processDecoderMessageHelper } from "./decoder_runtime.js";
|
||||
import { normalizeTrainingState as normalizeTrainingStateLogic, buildDriverTrainingStats as buildDriverTrainingStatsLogic, buildTrainingRecommendation as buildTrainingRecommendationLogic, getTrainingGoalProgress as getTrainingGoalProgressLogic } from "./training_logic.js";
|
||||
import { renderTrainingView } from "./training_views.js";
|
||||
|
||||
|
||||
import { renderTimingView, renderJudgingView } from "./timing_views.js";
|
||||
@@ -122,7 +124,7 @@ const saveAmmcConfigToBackend = (config) => saveAmmcConfigToBackendHelper(config
|
||||
const refreshAmmcStatus = () => refreshAmmcStatusHelper({ ammc, t, getBackendUrl });
|
||||
const startManagedAmmc = () => startManagedAmmcHelper({ ammc, t, getBackendUrl });
|
||||
const stopManagedAmmc = () => stopManagedAmmcHelper({ ammc, t, getBackendUrl });
|
||||
const applyPersistedState = (persisted) => applyPersistedStateHelper(persisted, { state, backend, t, getDefaultBackendUrl, DEFAULT_LANGUAGE, DEFAULT_THEME, AVAILABLE_THEMES, normalizeDriver, normalizeCar, normalizeEvent, normalizeSession, normalizeStoredRacePreset, normalizeObsOverlaySettings, applyTheme });
|
||||
const applyPersistedState = (persisted) => applyPersistedStateHelper(persisted, { state, backend, t, getDefaultBackendUrl, DEFAULT_LANGUAGE, DEFAULT_THEME, AVAILABLE_THEMES, normalizeDriver, normalizeCar, normalizeEvent, normalizeSession, normalizeStoredRacePreset, normalizeObsOverlaySettings, normalizeTrainingState, applyTheme });
|
||||
const hydrateFromBackend = () => hydrateFromBackendHelper({ backend, t, getBackendUrl, applyPersistedState, saveState });
|
||||
const scheduleBackendSync = () => scheduleBackendSyncHelper({ getBackendSyncTimer: () => backendSyncTimer, setBackendSyncTimer: (value) => { backendSyncTimer = value; }, syncStateToBackend });
|
||||
const syncStateToBackend = () => syncStateToBackendHelper({ backend, t, getBackendUrl, buildPersistableState, getLocalStateVersion: () => localStateVersion, getSyncedStateVersion: () => syncedStateVersion, setSyncedStateVersion: (value) => { syncedStateVersion = value; }, getLocalStateDirty: () => localStateDirty, setLocalStateDirty: (value) => { localStateDirty = value; } });
|
||||
@@ -195,6 +197,11 @@ 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, pushOverlayEvent, 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 buildDriverTrainingStats = (trainingState, driverId) => buildDriverTrainingStatsLogic(trainingState, driverId);
|
||||
const buildTrainingRecommendation = (stats) => buildTrainingRecommendationLogic(stats);
|
||||
const getTrainingGoalProgress = (goal, stats) => getTrainingGoalProgressLogic(goal, stats);
|
||||
const renderTraining = () => renderTrainingView({ state, dom, t, escapeHtml, formatLap, uid, saveState, renderView, normalizeTrainingState, buildDriverTrainingStats, buildTrainingRecommendation, getTrainingGoalProgress, getSelectedDriverId: () => selectedTrainingDriverId, setSelectedDriverId: (value) => { selectedTrainingDriverId = value; updateViewUrl("training", 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 });
|
||||
@@ -212,6 +219,7 @@ const NAV_ITEMS = [
|
||||
{ id: "classes", titleKey: "nav.classes", subtitleKey: "nav.classes_sub" },
|
||||
{ id: "drivers", titleKey: "nav.drivers", subtitleKey: "nav.drivers_sub" },
|
||||
{ id: "cars", titleKey: "nav.cars", subtitleKey: "nav.cars_sub" },
|
||||
{ id: "training", titleKey: "nav.training", subtitleKey: "nav.training_sub" },
|
||||
{ id: "timing", titleKey: "nav.timing", subtitleKey: "nav.timing_sub" },
|
||||
{ id: "judging", titleKey: "nav.judging", subtitleKey: "nav.judging_sub" },
|
||||
{ id: "settings", titleKey: "nav.settings", subtitleKey: "nav.settings_sub" },
|
||||
@@ -227,7 +235,7 @@ const STORAGE_KEY = "rc_timing_control_v1";
|
||||
const DEFAULT_LANGUAGE = "sv";
|
||||
const DEFAULT_THEME = "dark";
|
||||
const AVAILABLE_THEMES = ["dark", "nord", "light"];
|
||||
const EXPORT_SCHEMA_VERSION = 1;
|
||||
const EXPORT_SCHEMA_VERSION = 2;
|
||||
|
||||
const TRANSLATIONS = {
|
||||
sv: {
|
||||
@@ -245,6 +253,8 @@ const TRANSLATIONS = {
|
||||
"nav.drivers_sub": "Förare och personliga transpondrar",
|
||||
"nav.cars": "Bilar",
|
||||
"nav.cars_sub": "Bilar med fasta transpondrar",
|
||||
"nav.training": "Träning",
|
||||
"nav.training_sub": "Utveckling, delmål och belöningar",
|
||||
"nav.timing": "Tidtagning",
|
||||
"nav.timing_sub": "Live timing-board",
|
||||
"nav.judging": "Domare",
|
||||
@@ -264,6 +274,46 @@ const TRANSLATIONS = {
|
||||
"ui.event": "Event",
|
||||
"ui.decoder_online": "Decoder online",
|
||||
"ui.decoder_offline": "Decoder offline",
|
||||
"training.eyebrow": "Förarutveckling",
|
||||
"training.intro": "Följ träning över flera sessioner och gör utvecklingen synlig mellan tävlingarna.",
|
||||
"training.driver": "Förare",
|
||||
"training.no_drivers_title": "Ingen förare registrerad",
|
||||
"training.no_drivers_body": "Skapa en förare först. Träningsstatistik kräver en registrerad förare på passeringarna.",
|
||||
"training.total_laps": "Totala varv",
|
||||
"training.valid_laps": "Giltiga registrerade varv",
|
||||
"training.sessions": "Sessioner",
|
||||
"training.recorded_sessions": "Med registrerade förarvarv",
|
||||
"training.best_lap": "Bästa varv",
|
||||
"training.personal_best": "Personbästa i sparad data",
|
||||
"training.consistency": "Spridning",
|
||||
"training.last_30_laps": "P10-P90, senaste 30 varven",
|
||||
"training.recommendation": "Rekommenderat träningspass",
|
||||
"training.plan_baseline_title": "Bygg en baslinje",
|
||||
"training.plan_baseline_body": "Kör lugnt och jämnt tills minst 10 rena varv är registrerade. Fokusera på samma linje och bromspunkt varje varv.",
|
||||
"training.plan_consistency_title": "Höj din lägstanivå",
|
||||
"training.plan_consistency_body": "Spridningen är hög. Kör först 10 minuter nära max, sedan 10 minuter på 80-90 procent där rena, repeterbara varv är viktigare än ett enstaka snabbt varv.",
|
||||
"training.plan_progression_title": "Balansera fart och kontroll",
|
||||
"training.plan_progression_body": "Du har en stabil grund. Kör 10 minuter med maxfokus och avsluta med 10 minuter kontrollerad körning där målet är en obruten serie jämna varv.",
|
||||
"training.plan_target": "Kontrollmål omkring {lap}",
|
||||
"training.plan_no_target": "Registrera fler varv för tidsmål",
|
||||
"training.block_push": "Maxfokus: hitta gränsen",
|
||||
"training.block_control": "80-90%: höj lägstanivån",
|
||||
"training.new_goal": "Nytt delmål",
|
||||
"training.target_laps": "Antal varv",
|
||||
"training.quick_targets": "Snabbval för antal varv",
|
||||
"training.goal_name": "Namn på delmålet",
|
||||
"training.goal_name_placeholder": "Till exempel Bronsnivå",
|
||||
"training.reward": "Belöning",
|
||||
"training.reward_placeholder": "Till exempel ny kaross eller klubbmärke",
|
||||
"training.add_goal": "Lägg till delmål",
|
||||
"training.goals": "Delmål och rewards",
|
||||
"training.no_goals": "Inga delmål finns för den här föraren ännu.",
|
||||
"training.default_goal_title": "Nå {laps} varv",
|
||||
"training.goal_progress": "Framsteg mot delmål",
|
||||
"training.goal_remaining": "{laps} varv kvar",
|
||||
"training.goal_reached": "Målet är nått",
|
||||
"training.goal_claimed": "Belöning utdelad",
|
||||
"training.claim_reward": "Markera belöning utdelad",
|
||||
"mode.track": "Sponsor Event",
|
||||
"mode.race": "Race",
|
||||
"dashboard.events": "Event",
|
||||
@@ -1008,11 +1058,11 @@ const TRANSLATIONS = {
|
||||
"guide.host_7": "7. OBS overlay finns nu som publik URL: `/public-overlay/obs` och kan konfigureras från Overlay-menyn.",
|
||||
"guide.host_8": "8. Sätt `PUBLIC_OVERLAY_TOKEN` på servern om du vill kräva `?token=...` för publika overlay-länkar.",
|
||||
"guide.windows_title": "Windows + AMMC + npm",
|
||||
"guide.windows_1": "1. Installera Node.js LTS och Visual C++ Runtime 2015-2022 på hosten som ska köra `live_event`.",
|
||||
"guide.windows_2": "2. Standardbinär för Managed AMMC på Windows-host: `AMMC/windows64/ammc-amb.exe`.",
|
||||
"guide.windows_3": "3. Kör `npm start` på Windows-hosten. Då är det där AMMC startas om du använder Managed AMMC.",
|
||||
"guide.windows_4": "4. I Settings: `Decoder IP / host` = decoderns IP, t.ex. `192.168.1.11`.",
|
||||
"guide.windows_5": "5. I appen: `Backend URL` = http://<windows-host>:8081, `WebSocket URL` = ws://<windows-host>:9000.",
|
||||
"guide.windows_1": "1. Använd 64-bitars Windows 11. Dubbelklicka på `windows/install.bat`; skriptet installerar vid behov Node.js LTS med winget och kör `npm ci`.",
|
||||
"guide.windows_2": "2. Installationen kräver Node 22 eller 24 LTS och verifierar automatiskt Express, SQLite samt `AMMC/windows64`.",
|
||||
"guide.windows_3": "3. Starta i bakgrunden med `npm start` eller `windows/start_backend.bat`. Kontrollera med `npm run status` och stoppa med `npm stop`.",
|
||||
"guide.windows_4": "4. I Inställningar: `Decoder IP / host` = decoderns IP, t.ex. `192.168.1.10`. Managed AMMC körs på Windows-hosten.",
|
||||
"guide.windows_5": "5. Från andra enheter används `http://<windows-host>:8081` och `ws://<windows-host>:9000`. Se `windows/README.md` för brandvägg och felsökning.",
|
||||
"guide.linux_title": "Linux + npm",
|
||||
"guide.linux_1": "1. Installera Node 22 LTS, build-essential och python3 på Linux-hosten.",
|
||||
"guide.linux_2": "2. Standardbinär för Managed AMMC på Linux-host: `AMMC/linux_x86-64/ammc-amb`.",
|
||||
@@ -1052,6 +1102,8 @@ const TRANSLATIONS = {
|
||||
"nav.drivers_sub": "Drivers and personal transponders",
|
||||
"nav.cars": "Cars",
|
||||
"nav.cars_sub": "Track cars with fixed transponders",
|
||||
"nav.training": "Training",
|
||||
"nav.training_sub": "Development, milestones and rewards",
|
||||
"nav.timing": "Timing",
|
||||
"nav.timing_sub": "Live timing board",
|
||||
"nav.judging": "Judging",
|
||||
@@ -1071,6 +1123,46 @@ const TRANSLATIONS = {
|
||||
"ui.event": "Event",
|
||||
"ui.decoder_online": "Decoder Online",
|
||||
"ui.decoder_offline": "Decoder Offline",
|
||||
"training.eyebrow": "Driver development",
|
||||
"training.intro": "Track practice across sessions and make progress visible between race days.",
|
||||
"training.driver": "Driver",
|
||||
"training.no_drivers_title": "No driver registered",
|
||||
"training.no_drivers_body": "Create a driver first. Training statistics require passings linked to a registered driver.",
|
||||
"training.total_laps": "Total laps",
|
||||
"training.valid_laps": "Valid recorded laps",
|
||||
"training.sessions": "Sessions",
|
||||
"training.recorded_sessions": "With recorded driver laps",
|
||||
"training.best_lap": "Best lap",
|
||||
"training.personal_best": "Personal best in stored data",
|
||||
"training.consistency": "Spread",
|
||||
"training.last_30_laps": "P10-P90, latest 30 laps",
|
||||
"training.recommendation": "Recommended practice session",
|
||||
"training.plan_baseline_title": "Build a baseline",
|
||||
"training.plan_baseline_body": "Drive calmly and consistently until at least 10 clean laps are recorded. Repeat the same line and braking point every lap.",
|
||||
"training.plan_consistency_title": "Raise your minimum level",
|
||||
"training.plan_consistency_body": "Lap spread is high. Run 10 minutes near maximum pace, then 10 minutes at 80-90 percent where clean, repeatable laps matter more than one fast lap.",
|
||||
"training.plan_progression_title": "Balance pace and control",
|
||||
"training.plan_progression_body": "You have a stable baseline. Run 10 minutes at maximum focus and finish with 10 controlled minutes aiming for an unbroken sequence of consistent laps.",
|
||||
"training.plan_target": "Control target around {lap}",
|
||||
"training.plan_no_target": "Record more laps to calculate a target",
|
||||
"training.block_push": "Maximum focus: find the limit",
|
||||
"training.block_control": "80-90%: raise the floor",
|
||||
"training.new_goal": "New milestone",
|
||||
"training.target_laps": "Target laps",
|
||||
"training.quick_targets": "Quick lap targets",
|
||||
"training.goal_name": "Milestone name",
|
||||
"training.goal_name_placeholder": "For example Bronze level",
|
||||
"training.reward": "Reward",
|
||||
"training.reward_placeholder": "For example a new body or club badge",
|
||||
"training.add_goal": "Add milestone",
|
||||
"training.goals": "Milestones and rewards",
|
||||
"training.no_goals": "This driver has no milestones yet.",
|
||||
"training.default_goal_title": "Reach {laps} laps",
|
||||
"training.goal_progress": "Milestone progress",
|
||||
"training.goal_remaining": "{laps} laps remaining",
|
||||
"training.goal_reached": "Milestone reached",
|
||||
"training.goal_claimed": "Reward delivered",
|
||||
"training.claim_reward": "Mark reward delivered",
|
||||
"mode.track": "Track Event",
|
||||
"mode.race": "Race",
|
||||
"dashboard.events": "Events",
|
||||
@@ -1812,11 +1904,11 @@ const TRANSLATIONS = {
|
||||
"guide.host_4": "4. Run the backend on Windows -> the Windows binary is used: `AMMC/windows64/ammc-amb.exe`.",
|
||||
"guide.host_5": "5. The `AMMC executable` field in Settings is a path on the backend host, not on the client laptop.",
|
||||
"guide.windows_title": "Windows + AMMC + npm",
|
||||
"guide.windows_1": "1. Install Node.js LTS and Visual C++ Runtime 2015-2022 on the host that will run `live_event`.",
|
||||
"guide.windows_2": "2. Default Managed AMMC binary on a Windows host: `AMMC/windows64/ammc-amb.exe`.",
|
||||
"guide.windows_3": "3. Run `npm start` on the Windows host. Managed AMMC starts there if enabled.",
|
||||
"guide.windows_4": "4. In Settings: `Decoder IP / host` = decoder IP, for example `192.168.1.11`.",
|
||||
"guide.windows_5": "5. In app: `Backend URL` = http://<windows-host>:8081, `WebSocket URL` = ws://<windows-host>:9000.",
|
||||
"guide.windows_1": "1. Use 64-bit Windows 11. Double-click `windows/install.bat`; it installs Node.js LTS with winget when needed and runs `npm ci`.",
|
||||
"guide.windows_2": "2. Setup requires Node 22 or 24 LTS and automatically verifies Express, SQLite, and `AMMC/windows64`.",
|
||||
"guide.windows_3": "3. Start in the background with `npm start` or `windows/start_backend.bat`. Check with `npm run status` and stop with `npm stop`.",
|
||||
"guide.windows_4": "4. In Settings, `Decoder IP / host` is the decoder address, for example `192.168.1.10`. Managed AMMC runs on the Windows host.",
|
||||
"guide.windows_5": "5. Other devices use `http://<windows-host>:8081` and `ws://<windows-host>:9000`. See `windows/README.md` for firewall and troubleshooting.",
|
||||
"guide.linux_title": "Linux + npm",
|
||||
"guide.linux_1": "1. Install Node 22 LTS, build-essential and python3 on the Linux host.",
|
||||
"guide.linux_2": "2. Default Managed AMMC binary on a Linux host: `AMMC/linux_x86-64/ammc-amb`.",
|
||||
@@ -1856,7 +1948,8 @@ const overlayViewMode = allowedOverlayModes.includes(routeOverlayMode)
|
||||
: "leaderboard";
|
||||
const publicOverlayMode = Boolean(publicOverlayMatch);
|
||||
const state = loadState();
|
||||
let currentView = overlayMode ? "overlay" : "dashboard";
|
||||
const requestedView = String(urlParams.get("view") || "").toLowerCase();
|
||||
let currentView = overlayMode ? "overlay" : NAV_ITEMS.some((item) => item.id === requestedView) ? requestedView : "dashboard";
|
||||
let wsClient = null;
|
||||
let reconnectTimer = null;
|
||||
let backendSyncTimer = null;
|
||||
@@ -1875,6 +1968,7 @@ let selectedEventEditId = null;
|
||||
let selectedSessionEditId = null;
|
||||
let selectedTeamEditId = null;
|
||||
let selectedJudgeKey = null;
|
||||
let selectedTrainingDriverId = String(urlParams.get("driver") || "");
|
||||
let judgingCompetitorFilter = "all";
|
||||
let judgingLogFilter = "all";
|
||||
let quickAddDraft = null;
|
||||
@@ -2202,6 +2296,7 @@ function seedDefaultData() {
|
||||
state.cars = state.cars.map((car) => normalizeCar(car)).filter((car) => car.name);
|
||||
state.events = state.events.map((event) => normalizeEvent(event));
|
||||
state.sessions = state.sessions.map((session) => normalizeSession(session));
|
||||
state.training = normalizeTrainingState(state.training);
|
||||
|
||||
saveState({ skipBackend: true });
|
||||
}
|
||||
@@ -2218,6 +2313,7 @@ function loadState() {
|
||||
events: parsed.events || [],
|
||||
sessions: parsed.sessions || [],
|
||||
resultsBySession: parsed.resultsBySession || {},
|
||||
training: normalizeTrainingState(parsed.training),
|
||||
activeSessionId: parsed.activeSessionId || null,
|
||||
settings: {
|
||||
wsUrl: parsed.settings?.wsUrl || "ws://127.0.0.1:9000",
|
||||
@@ -2262,6 +2358,7 @@ function loadState() {
|
||||
events: [],
|
||||
sessions: [],
|
||||
resultsBySession: {},
|
||||
training: normalizeTrainingState(),
|
||||
activeSessionId: null,
|
||||
settings: {
|
||||
wsUrl: "ws://127.0.0.1:9000",
|
||||
@@ -2382,6 +2479,7 @@ function buildPersistableState() {
|
||||
events: state.events,
|
||||
sessions: state.sessions.map((session) => normalizeSession(session)),
|
||||
resultsBySession: state.resultsBySession,
|
||||
training: normalizeTrainingState(state.training),
|
||||
activeSessionId: state.activeSessionId,
|
||||
settings: state.settings,
|
||||
};
|
||||
@@ -2441,6 +2539,7 @@ function buildDataExportPayload() {
|
||||
events: state.events,
|
||||
sessions: state.sessions,
|
||||
resultsBySession: state.resultsBySession,
|
||||
training: normalizeTrainingState(state.training),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2601,6 +2700,7 @@ function importDataPayload(parsed, mode = "full") {
|
||||
state.events = incomingEvents.map((item) => normalizeEvent(item));
|
||||
state.sessions = incomingSessions.map((item) => normalizeSession(item));
|
||||
state.resultsBySession = parsed?.resultsBySession && typeof parsed.resultsBySession === "object" ? parsed.resultsBySession : {};
|
||||
state.training = normalizeTrainingState(parsed?.training);
|
||||
if (!state.sessions.some((session) => session.id === state.activeSessionId)) {
|
||||
state.activeSessionId = null;
|
||||
}
|
||||
@@ -2634,6 +2734,7 @@ function renderNav() {
|
||||
button.textContent = t(item.titleKey);
|
||||
button.addEventListener("click", () => {
|
||||
currentView = item.id;
|
||||
updateViewUrl(currentView);
|
||||
renderNav();
|
||||
renderView();
|
||||
});
|
||||
@@ -2641,6 +2742,16 @@ function renderNav() {
|
||||
});
|
||||
}
|
||||
|
||||
function updateViewUrl(view, driverId = "") {
|
||||
if (overlayMode) return;
|
||||
const nextUrl = new URL(window.location.href);
|
||||
if (view && view !== "dashboard") nextUrl.searchParams.set("view", view);
|
||||
else nextUrl.searchParams.delete("view");
|
||||
if (view === "training" && driverId) nextUrl.searchParams.set("driver", driverId);
|
||||
else nextUrl.searchParams.delete("driver");
|
||||
window.history.replaceState({}, "", nextUrl);
|
||||
}
|
||||
|
||||
function renderView() {
|
||||
clearModalEscapeHandler();
|
||||
const navMeta = NAV_ITEMS.find((x) => x.id === currentView);
|
||||
@@ -2675,6 +2786,9 @@ function renderView() {
|
||||
case "cars":
|
||||
renderCars();
|
||||
break;
|
||||
case "training":
|
||||
renderTraining();
|
||||
break;
|
||||
case "timing":
|
||||
renderTiming();
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user