Add persistent driver training history and progression
This commit is contained in:
56
src/app.js
56
src/app.js
@@ -17,7 +17,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, 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 { normalizeTrainingState as normalizeTrainingStateLogic, syncTrainingLedger as syncTrainingLedgerLogic, buildDriverTrainingStats as buildDriverTrainingStatsLogic, buildTrainingRecommendation as buildTrainingRecommendationLogic, getTrainingGoalProgress as getTrainingGoalProgressLogic, getNextTrainingMilestone as getNextTrainingMilestoneLogic } from "./training_logic.js";
|
||||
import { renderTrainingView } from "./training_views.js";
|
||||
|
||||
|
||||
@@ -198,10 +198,15 @@ const renderOverlay = () => renderOverlayPageView({ state, dom, t, escapeHtml, f
|
||||
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 syncTrainingLedger = () => {
|
||||
state.training = syncTrainingLedgerLogic(state.training, state, { uid });
|
||||
return state.training;
|
||||
};
|
||||
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 getNextTrainingMilestone = (stats) => getNextTrainingMilestoneLogic(stats);
|
||||
const renderTraining = () => renderTrainingView({ state, dom, t, escapeHtml, formatLap, uid, saveState, renderView, normalizeTrainingState, syncTrainingLedger, buildDriverTrainingStats, buildTrainingRecommendation, getTrainingGoalProgress, getNextTrainingMilestone, 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 });
|
||||
@@ -314,6 +319,21 @@ const TRANSLATIONS = {
|
||||
"training.goal_reached": "Målet är nått",
|
||||
"training.goal_claimed": "Belöning utdelad",
|
||||
"training.claim_reward": "Markera belöning utdelad",
|
||||
"training.training_days": "Träningsdagar",
|
||||
"training.sessions_count": "{sessions} sparade sessioner",
|
||||
"training.next_level": "Nästa automatiska nivå",
|
||||
"training.next_level_target": "Nå {laps} varv",
|
||||
"training.average_lap": "Snittvarv",
|
||||
"training.recent_average": "Snitt senaste 30",
|
||||
"training.controlled_streak": "Kontrollerad serie",
|
||||
"training.lap_count": "{laps} varv",
|
||||
"training.last_activity": "Senaste aktivitet",
|
||||
"training.no_activity": "Ingen aktivitet ännu",
|
||||
"training.history": "Träningshistorik per dag",
|
||||
"training.no_history": "Ingen träningshistorik finns ännu.",
|
||||
"training.date": "Datum",
|
||||
"training.laps": "Varv",
|
||||
"training.pb_improvement": "Senaste 30: {lap} snabbare",
|
||||
"mode.track": "Sponsor Event",
|
||||
"mode.race": "Race",
|
||||
"dashboard.events": "Event",
|
||||
@@ -936,6 +956,7 @@ const TRANSLATIONS = {
|
||||
"guide.card_sponsor_blurb": "Delade bilar, roterande förare och heat/finaler för sponsor- och prova-på-event.",
|
||||
"guide.card_race_blurb": "Personliga transpondrar, kval/finaler, raceformat och seedning för riktiga tävlingsrace.",
|
||||
"guide.card_team_blurb": "Lagrace och endurance med team, stintlogg och summerade varv över lång tid.",
|
||||
"guide.card_training_blurb": "Förarutveckling, beständig varvhistorik, träningsmål och belöningar i mobilvänlig vy.",
|
||||
"guide.card_decoder_blurb": "AMMC, backend, WebSocket, Windows/Linux och lokal SQLite-lagring.",
|
||||
"guide.sponsor_title": "Skapa Sponsor Event: 10 personer, 4 bilar",
|
||||
"guide.sponsor_1": "1. Lägg upp 4 bilar i sidan Bilar med unikt transponder-ID. Lägg gärna också in märke/modell i brandfältet.",
|
||||
@@ -1007,6 +1028,13 @@ const TRANSLATIONS = {
|
||||
"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.",
|
||||
"guide.training_title": "Träning, utveckling och belöningar",
|
||||
"guide.training_1": "1. Registrera föraren och koppla den personliga transpondern under Förare. Endast passeringar med ett registrerat förar-ID kan räknas i träningsprofilen.",
|
||||
"guide.training_2": "2. Kör Free Practice, Open Practice eller en vanlig racesession. Giltiga varv kopieras automatiskt till en beständig träningslogg; korta/långa ogiltiga varv räknas inte.",
|
||||
"guide.training_3": "3. Öppna Träning och välj förare. Sidan visar totalvarv, träningsdagar, personbästa, snitt, senaste 30 varv, kontrollerad serie och historik per dag.",
|
||||
"guide.training_4": "4. Nästa automatiska nivå räknas utan konfiguration. Skapa egna delmål om klubben eller en förälder vill koppla en fysisk belöning till exempelvis 500, 1000 eller 1500 varv.",
|
||||
"guide.training_5": "5. När målet nåtts kan belöningen markeras som utdelad. Direktlänken `/?view=training&driver=<id>` kan bokmärkas på förarens mobil.",
|
||||
"guide.training_6": "6. Träningsloggen uppdateras av domarkorrigeringar medan sessionen finns, men behålls när sessionen senare raderas. Full dataexport i Inställningar inkluderar både logg och mål.",
|
||||
"overlay.title": "Overlay",
|
||||
"overlay.subtitle": "Extern leaderboard-skärm",
|
||||
"overlay.no_active": "Ingen aktiv session vald.",
|
||||
@@ -1163,6 +1191,21 @@ const TRANSLATIONS = {
|
||||
"training.goal_reached": "Milestone reached",
|
||||
"training.goal_claimed": "Reward delivered",
|
||||
"training.claim_reward": "Mark reward delivered",
|
||||
"training.training_days": "Practice days",
|
||||
"training.sessions_count": "{sessions} recorded sessions",
|
||||
"training.next_level": "Next automatic level",
|
||||
"training.next_level_target": "Reach {laps} laps",
|
||||
"training.average_lap": "Average lap",
|
||||
"training.recent_average": "Latest 30 average",
|
||||
"training.controlled_streak": "Controlled streak",
|
||||
"training.lap_count": "{laps} laps",
|
||||
"training.last_activity": "Latest activity",
|
||||
"training.no_activity": "No activity yet",
|
||||
"training.history": "Daily practice history",
|
||||
"training.no_history": "No practice history is available yet.",
|
||||
"training.date": "Date",
|
||||
"training.laps": "Laps",
|
||||
"training.pb_improvement": "Latest 30: {lap} faster",
|
||||
"mode.track": "Track Event",
|
||||
"mode.race": "Race",
|
||||
"dashboard.events": "Events",
|
||||
@@ -1785,6 +1828,7 @@ const TRANSLATIONS = {
|
||||
"guide.card_sponsor_blurb": "Shared cars, rotating drivers and heat/final flow for sponsor or try-out events.",
|
||||
"guide.card_race_blurb": "Personal transponders, qualifying/finals, race format and seeding for competition racing.",
|
||||
"guide.card_team_blurb": "Team race and endurance with teams, stint logs and total laps over long sessions.",
|
||||
"guide.card_training_blurb": "Driver development, durable lap history, practice goals and rewards in a mobile-friendly view.",
|
||||
"guide.card_decoder_blurb": "AMMC, backend, WebSocket, Windows/Linux and local SQLite storage.",
|
||||
"guide.sponsor_title": "Create Sponsor Event: 10 drivers, 4 cars",
|
||||
"guide.sponsor_1": "1. Add 4 cars in Cars with unique transponder IDs. You can also store brand/model in the brand field.",
|
||||
@@ -1856,6 +1900,13 @@ const TRANSLATIONS = {
|
||||
"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.",
|
||||
"guide.training_title": "Practice, development and rewards",
|
||||
"guide.training_1": "1. Register the driver and personal transponder under Drivers. Only passings linked to a registered driver ID count toward the practice profile.",
|
||||
"guide.training_2": "2. Run Free Practice, Open Practice or a regular race session. Valid laps are copied automatically to a durable training ledger; invalid short or long laps do not count.",
|
||||
"guide.training_3": "3. Open Training and select a driver. The page shows total laps, practice days, personal best, averages, latest 30 laps, controlled streak and daily history.",
|
||||
"guide.training_4": "4. The next automatic level needs no configuration. Add custom milestones when the club or a parent wants to attach a physical reward to targets such as 500, 1000 or 1500 laps.",
|
||||
"guide.training_5": "5. When a target is reached, mark its reward as delivered. Bookmark `/?view=training&driver=<id>` on the driver's phone for direct access.",
|
||||
"guide.training_6": "6. The training ledger follows judging corrections while the source session exists, but remains when that session is later deleted. Full data export in Settings includes both ledger and goals.",
|
||||
"overlay.title": "Overlay",
|
||||
"overlay.subtitle": "External leaderboard screen",
|
||||
"overlay.no_active": "No active session selected.",
|
||||
@@ -2472,6 +2523,7 @@ function setupLanguageControl() {
|
||||
}
|
||||
|
||||
function buildPersistableState() {
|
||||
syncTrainingLedger();
|
||||
return {
|
||||
classes: state.classes,
|
||||
drivers: state.drivers.map((driver) => normalizeDriver(driver)),
|
||||
|
||||
Reference in New Issue
Block a user