feat(layouts): switch track layout mid-practice and view stats per layout
A club can run one Open Practice all season and re-lay the track now and then. Previously a session's layout was frozen at start and every training lap inherited it, so a season-long practice stayed on its first layout forever, and changing layout was hidden under Settings -> Appearance. - Each practice lap now counts toward the layout active when it was driven; a running practice session moves onto a new layout when one is added - "New layout" strip on Training stats, Race Day and the Dashboard, offered only around practice; a running race session blocks layout changes everywhere - "Stats for" selector (current / older layout / before layouts / all layouts) on Training stats and in the driver's mobile profile (?layout= on the public API) - A layout that has practice laps on it can no longer be deleted - Fix: the buildDriverTrainingStats wrapper in app.js dropped extra arguments Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -77,7 +77,17 @@ function filterPublicTrainingStats(stats, visibility) {
|
||||
return filtered;
|
||||
}
|
||||
|
||||
function buildPublicTrainingProfile(state, shareToken, updatedAt, sinceTimestamp = 0, windowMs = 0, evenLapsSince = 0, stepTrainingSince = 0) {
|
||||
// Which layout the pace/record/consistency numbers are for - mirrors
|
||||
// getDriverTrainingLapsForLayout in src/training_logic.js: "current", "all", "none"
|
||||
// (laps from before any layout existed) or a known layout id. Anything else -> "current".
|
||||
function normalizePublicTrainingLayout(value, state) {
|
||||
const layout = String(value || "current");
|
||||
if (["current", "all", "none"].includes(layout)) return layout;
|
||||
const layouts = Array.isArray(state?.trackLayouts) ? state.trackLayouts : [];
|
||||
return layouts.some((item) => String(item?.id || "") === layout) ? layout : "current";
|
||||
}
|
||||
|
||||
function buildPublicTrainingProfile(state, shareToken, updatedAt, sinceTimestamp = 0, windowMs = 0, evenLapsSince = 0, stepTrainingSince = 0, layout = "current") {
|
||||
const share = findPublicTrainingShare(state, shareToken);
|
||||
if (!share) return null;
|
||||
const visibility = normalizePublicTrainingVisibility(share.visibility);
|
||||
@@ -90,6 +100,8 @@ function buildPublicTrainingProfile(state, shareToken, updatedAt, sinceTimestamp
|
||||
const classes = Array.isArray(state?.classes) ? state.classes : [];
|
||||
const ledger = Array.isArray(state?.training?.ledger) ? state.training.ledger : [];
|
||||
const currentLayoutId = getCurrentTrackLayoutId(state);
|
||||
const selectedLayout = normalizePublicTrainingLayout(layout, state);
|
||||
const layoutFilterId = selectedLayout === "current" ? currentLayoutId : selectedLayout === "none" ? "" : selectedLayout;
|
||||
const profileReferences = new Set([driverProfile.id, ...driverProfile.drivers.map((item) => String(item.id || ""))]);
|
||||
const profileClassIds = [...new Set(driverProfile.drivers.map((item) => String(item.classId || "")).filter(Boolean))];
|
||||
const classProfiles = profileClassIds.map((classId) => {
|
||||
@@ -111,7 +123,7 @@ function buildPublicTrainingProfile(state, shareToken, updatedAt, sinceTimestamp
|
||||
// track rebuild doesn't erase past practice. Pace/record/consistency numbers instead
|
||||
// read only laps tagged with today's active layout, so a personal best from a track
|
||||
// that no longer exists can't be shown as current. See getCurrentTrackLayoutId above.
|
||||
const classLayoutLaps = classLaps.filter((lap) => lap.trackLayoutId === currentLayoutId);
|
||||
const classLayoutLaps = selectedLayout === "all" ? classLaps : classLaps.filter((lap) => lap.trackLayoutId === layoutFilterId);
|
||||
const stats = buildPublicTrainingStats(classLaps, classLayoutLaps);
|
||||
const classMarkers = (Array.isArray(state?.training?.markers) ? state.training.markers : []).filter((marker) => classDriverIds.has(String(marker?.driverId || "")) && String(marker?.classId || "") === classId);
|
||||
stats.paceBuckets = buildPublicTrainingPaceBuckets(classLaps, classMarkers);
|
||||
@@ -176,6 +188,15 @@ function buildPublicTrainingProfile(state, shareToken, updatedAt, sinceTimestamp
|
||||
logoDataUrl: normalizePublicLogoDataUrl(settings.logoDataUrl),
|
||||
},
|
||||
classProfiles,
|
||||
trackLayouts: {
|
||||
selected: selectedLayout,
|
||||
currentId: currentLayoutId,
|
||||
options: (Array.isArray(state?.trackLayouts) ? state.trackLayouts : [])
|
||||
.slice()
|
||||
.sort((a, b) => Number(a?.startedAt || 0) - Number(b?.startedAt || 0))
|
||||
.map((item) => ({ id: String(item?.id || ""), label: String(item?.label || "").trim() })),
|
||||
hasUntaggedLaps: ledger.some((entry) => profileReferences.has(String(entry?.driverId || "")) && !String(entry?.trackLayoutId || "")),
|
||||
},
|
||||
stats: activeClass.stats,
|
||||
channelStats: activeClass.channelStats,
|
||||
nextMilestone: activeClass.nextMilestone,
|
||||
@@ -827,6 +848,7 @@ function getPublicTrainingRecommendation(stats) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
normalizePublicTrainingLayout,
|
||||
buildPublicTrainingProfile,
|
||||
findPublicTrainingShare,
|
||||
hasPublicTrainingShare,
|
||||
|
||||
Reference in New Issue
Block a user