Add motivational training challenges and shared club goals

This commit is contained in:
larssand
2026-08-30 09:16:11 +02:00
parent 27dcbd92aa
commit 694aba5f3d
11 changed files with 799 additions and 13 deletions

View File

@@ -18,7 +18,7 @@ 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, buildDriverTimingChannelStats as buildDriverTimingChannelStatsLogic, buildDriverActivePracticeStats as buildDriverActivePracticeStatsLogic, buildTrainingRecommendation as buildTrainingRecommendationLogic, getTrainingGoalProgress as getTrainingGoalProgressLogic, getNextTrainingMilestone as getNextTrainingMilestoneLogic, buildTrainingPaceBuckets as buildTrainingPaceBucketsLogic, buildTrainingWeekdayStats as buildTrainingWeekdayStatsLogic, buildTrainingStintAnalysis as buildTrainingStintAnalysisLogic, buildTrainingMarkerComparisons as buildTrainingMarkerComparisonsLogic } 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, buildTrainingMissions as buildTrainingMissionsLogic, buildTrainingHighlights as buildTrainingHighlightsLogic, getClubTrainingGoalProgress as getClubTrainingGoalProgressLogic, buildTrainingPaceBuckets as buildTrainingPaceBucketsLogic, buildTrainingWeekdayStats as buildTrainingWeekdayStatsLogic, buildTrainingStintAnalysis as buildTrainingStintAnalysisLogic, buildTrainingMarkerComparisons as buildTrainingMarkerComparisonsLogic } from "./training_logic.js";
import { renderTrainingView } from "./training_views.js";
import { importDriversCsv, importCarsCsv } from "./csv_import.js";
@@ -293,7 +293,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, buildDriverTimingChannelStats: buildDriverTimingChannelStatsLogic, buildDriverActivePracticeStats, buildTrainingRecommendation, getTrainingGoalProgress, getNextTrainingMilestone, buildTrainingPaceBuckets: buildTrainingPaceBucketsLogic, buildTrainingWeekdayStats: buildTrainingWeekdayStatsLogic, buildTrainingStintAnalysis: buildTrainingStintAnalysisLogic, buildTrainingMarkerComparisons: buildTrainingMarkerComparisonsLogic, 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, buildTrainingMissions: buildTrainingMissionsLogic, buildTrainingHighlights: buildTrainingHighlightsLogic, getClubTrainingGoalProgress: getClubTrainingGoalProgressLogic, buildTrainingPaceBuckets: buildTrainingPaceBucketsLogic, buildTrainingWeekdayStats: buildTrainingWeekdayStatsLogic, buildTrainingStintAnalysis: buildTrainingStintAnalysisLogic, buildTrainingMarkerComparisons: buildTrainingMarkerComparisonsLogic, 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 = {}) => {
const result = applyCompetitorCorrectionLogic(session, row, options, { ensureSessionResult, uid, t, formatLap, saveState });
if (!options.reset && ((Number(options.timeMsDelta || 0) > 0) || (Number(options.lapDelta || 0) < 0))) {
@@ -543,13 +543,55 @@ const TRANSLATIONS = {
"training.achievement_empty": "Kör första passet för att börja samla achievements.",
"training.achievement_first_session": "Första träningspasset",
"training.achievement_laps_100": "100 träningsvarv",
"training.achievement_laps_250": "250 träningsvarv",
"training.achievement_laps_500": "500 träningsvarv",
"training.achievement_laps_1000": "1 000 träningsvarv",
"training.achievement_laps_1500": "1 500 träningsvarv",
"training.achievement_days_3": "3 träningsdagar",
"training.achievement_days_10": "10 träningsdagar",
"training.achievement_sessions_5": "5 fullföljda träningspass",
"training.achievement_controlled_5": "5 kontrollerade varv i rad",
"training.achievement_controlled_10": "10 kontrollerade varv i rad",
"training.achievement_five_minute": "Fullföljd femminutersstint",
"training.achievement_five_minutes_3": "3 fullföljda femminutersstintar",
"training.achievement_consistency_5": "Jämnhet under 5 procent",
"training.achievement_pace_gain": "Förbättrat snittempo",
"training.credit_eyebrow": "Din träning räknas",
"training.credit_today_title": "Bra jobbat, {driver}!",
"training.credit_today_body": "Varje genomfört varv bygger erfarenhet. Här är det du ska ta med dig från dagens körning.",
"training.credit_ready_title": "Redo när du är, {driver}",
"training.credit_ready_body": "Du behöver inte slå någon annan. Börja med ett lugnt varv och bygg vidare därifrån.",
"training.highlight_today_laps": "{laps} giltiga varv idag",
"training.highlight_personal_best": "Personbästat förbättrat med {lap}",
"training.highlight_pace_gain": "Snittempot förbättrat med {lap} per varv",
"training.highlight_consistency_gain": "{value} procentenheter jämnare körning",
"training.highlight_controlled_streak": "{laps} kontrollerade varv i rad idag",
"training.highlight_five_minute": "{count} fullföljd femminutersstint idag",
"training.highlight_keep_building": "{laps} varv har redan byggt din erfarenhet",
"training.highlight_ready": "Första giltiga varvet startar din utvecklingsresa",
"training.optional_challenges": "Valfritt, ingen press",
"training.daily_missions": "Dagens utmaningar",
"training.daily_missions_hint": "Tre små mål som nollställs varje dag. De ger kredit för mängd, kontroll och uthållighet i vald klass.",
"training.mission_laps_today": "Samla 20 varv",
"training.mission_controlled_today": "Kör 5 kontrollerade varv i rad",
"training.mission_five_minute_today": "Fullfölj en femminutersstint",
"training.mission_progress": "{current} / {target}",
"training.together": "Alla bidrar",
"training.club_goal": "Klubbens gemensamma mål",
"training.club_goal_active": "Gemensam utmaning",
"training.club_goal_reached": "Målet är nått",
"training.club_goal_celebrated": "Målet är firat",
"training.club_goal_default": "Tillsammans till {laps} varv",
"training.club_goal_remaining": "{laps} klubbvarv kvar",
"training.club_goal_empty": "Starta ett gemensamt mål där varje giltigt träningsvarv från alla förare räknas.",
"training.club_goal_create": "Skapa klubbmål",
"training.club_goal_new": "Starta ett nytt klubbmål",
"training.club_goal_name_placeholder": "Till exempel Klubben mot 10 000",
"training.club_goal_reward_placeholder": "Till exempel gemensam pizzakväll",
"training.club_goal_start": "Starta klubbmålet",
"training.club_goal_baseline_hint": "Målet börjar på noll när det sparas. Tidigare träningsvarv ligger kvar i statistiken men räknas inte i det nya klubbmålet.",
"training.club_goal_mark_celebrated": "Markera som firat",
"training.club_goal_delete": "Ta bort klubbmålet",
"training.progress_feed": "Klubbens senaste framsteg",
"training.progress_not_positions": "Utveckling, inte placering",
"training.feed_personal_best": "nytt PB, {lap} förbättring",
@@ -1441,6 +1483,9 @@ const TRANSLATIONS = {
"guide.training_18": "18. Före en biländring väljer du Ny setup, däck, dämpare, bil eller egen notering och klickar Markera ändring nu. Markören sparas endast för vald personlig profil och klass.",
"guide.training_19": "19. När det finns minst tre giltiga varv inom 30 minuter både före och efter markören visas förbättringen automatiskt. Jämför bara pass med liknande bana och förhållanden.",
"guide.training_20": "20. Stintanalysen använder glapp över två minuter som gräns. Normaltempo är trimmat snitt, misstag är varv över 15 procent långsammare än stintens median och Race pace 10 kräver tio sammanhängande varv.",
"guide.training_21": "21. Dagens utmaningar är frivilliga och nollställs per kalenderdag och vald klass: 20 giltiga varv, 5 kontrollerade varv i rad och en fullföljd femminutersstint. De påverkar aldrig raceresultat eller timing.",
"guide.training_22": "22. Träningskortet ger positiv kredit för dagens varv, PB, förbättrat snittempo, jämnhet och uthållighet. Kortet jämför bara föraren med den egna historiken inom vald klass.",
"guide.training_23": "23. Skapa ett klubbmål från Träningsöversikten. Målet börjar på noll när det sparas och summerar därefter alla giltiga registrerade träningsvarv. När målet nås kan det markeras som firat och ersättas med ett nytt.",
"overlay.title": "Overlay",
"overlay.subtitle": "Extern leaderboard-skärm",
"overlay.no_active": "Ingen aktiv session vald.",
@@ -1744,13 +1789,55 @@ const TRANSLATIONS = {
"training.achievement_empty": "Complete the first practice session to start collecting achievements.",
"training.achievement_first_session": "First practice session",
"training.achievement_laps_100": "100 practice laps",
"training.achievement_laps_250": "250 practice laps",
"training.achievement_laps_500": "500 practice laps",
"training.achievement_laps_1000": "1,000 practice laps",
"training.achievement_laps_1500": "1,500 practice laps",
"training.achievement_days_3": "3 practice days",
"training.achievement_days_10": "10 practice days",
"training.achievement_sessions_5": "5 completed practice sessions",
"training.achievement_controlled_5": "5 controlled laps in a row",
"training.achievement_controlled_10": "10 controlled laps in a row",
"training.achievement_five_minute": "Completed five-minute stint",
"training.achievement_five_minutes_3": "3 completed five-minute stints",
"training.achievement_consistency_5": "Consistency below 5 percent",
"training.achievement_pace_gain": "Improved average pace",
"training.credit_eyebrow": "Your practice counts",
"training.credit_today_title": "Good work, {driver}!",
"training.credit_today_body": "Every completed lap builds experience. These are today's wins.",
"training.credit_ready_title": "Ready when you are, {driver}",
"training.credit_ready_body": "You do not need to beat anyone else. Start with one calm lap and build from there.",
"training.highlight_today_laps": "{laps} valid laps today",
"training.highlight_personal_best": "Personal best improved by {lap}",
"training.highlight_pace_gain": "Average pace improved by {lap} per lap",
"training.highlight_consistency_gain": "{value} percentage points more consistent",
"training.highlight_controlled_streak": "{laps} controlled laps in a row today",
"training.highlight_five_minute": "{count} completed five-minute stint today",
"training.highlight_keep_building": "{laps} laps have already built your experience",
"training.highlight_ready": "Your first valid lap starts the development journey",
"training.optional_challenges": "Optional, no pressure",
"training.daily_missions": "Today's challenges",
"training.daily_missions_hint": "Three small goals reset every day. They recognize volume, control, and endurance in the selected class.",
"training.mission_laps_today": "Collect 20 laps",
"training.mission_controlled_today": "Run 5 controlled laps in a row",
"training.mission_five_minute_today": "Complete a five-minute stint",
"training.mission_progress": "{current} / {target}",
"training.together": "Everyone contributes",
"training.club_goal": "Shared club goal",
"training.club_goal_active": "Club challenge",
"training.club_goal_reached": "Goal reached",
"training.club_goal_celebrated": "Goal celebrated",
"training.club_goal_default": "Together to {laps} laps",
"training.club_goal_remaining": "{laps} club laps remaining",
"training.club_goal_empty": "Start a shared target where every valid practice lap from every driver contributes.",
"training.club_goal_create": "Create club goal",
"training.club_goal_new": "Start a new club goal",
"training.club_goal_name_placeholder": "For example Club to 10,000",
"training.club_goal_reward_placeholder": "For example a shared pizza night",
"training.club_goal_start": "Start club goal",
"training.club_goal_baseline_hint": "The target starts at zero when saved. Earlier practice laps remain in statistics but do not count toward the new club goal.",
"training.club_goal_mark_celebrated": "Mark as celebrated",
"training.club_goal_delete": "Delete club goal",
"training.progress_feed": "Latest club progress",
"training.progress_not_positions": "Development, not position",
"training.feed_personal_best": "new PB, improved by {lap}",
@@ -2642,6 +2729,9 @@ const TRANSLATIONS = {
"guide.training_18": "18. Before changing the car, select New setup, tires, suspension, car, or a custom note and click Mark change now. The marker is stored only for the selected personal profile and class.",
"guide.training_19": "19. Once at least three valid laps exist within 30 minutes both before and after the marker, the change is compared automatically. Only compare runs with similar track conditions.",
"guide.training_20": "20. Stint analysis uses gaps longer than two minutes as boundaries. Normal pace is a trimmed average, mistakes are laps over 15 percent slower than the stint median, and Race pace 10 requires ten consecutive laps.",
"guide.training_21": "21. Today's challenges are optional and reset per calendar day and selected class: 20 valid laps, 5 controlled laps in a row, and one completed five-minute stint. They never affect race results or timing.",
"guide.training_22": "22. The motivation card recognizes today's laps, personal bests, improved average pace, consistency, and endurance. It only compares the driver with their own history in the selected class.",
"guide.training_23": "23. Create a club goal from Practice overview. The target starts at zero when saved and then aggregates every valid registered practice lap. Once reached, it can be marked as celebrated and replaced with a new goal.",
"overlay.title": "Overlay",
"overlay.subtitle": "External leaderboard screen",
"overlay.no_active": "No active session selected.",