Add class-specific driver training profiles and metadata

This commit is contained in:
larssand
2026-08-24 22:05:50 +02:00
parent 3aeb77a042
commit 6f02e955a5
14 changed files with 559 additions and 128 deletions

View File

@@ -8,55 +8,100 @@ function hasPublicTrainingShare(state, shareToken) {
return Boolean(findPublicTrainingShare(state, shareToken));
}
function getPublicDriverProfileId(driver) {
const explicitId = String(driver?.profileId || "").trim();
if (explicitId) return explicitId;
const name = String(driver?.name || "").trim().toLocaleLowerCase().replace(/\s+/g, " ");
return name ? `legacy:${name}` : `driver:${String(driver?.id || "unknown")}`;
}
function getPublicDriverProfile(drivers, reference) {
const normalizedReference = String(reference || "");
const anchor = drivers.find((driver) => (
String(driver?.id || "") === normalizedReference
|| getPublicDriverProfileId(driver) === normalizedReference
));
if (!anchor) return null;
const profileId = getPublicDriverProfileId(anchor);
return {
id: profileId,
drivers: drivers.filter((driver) => getPublicDriverProfileId(driver) === profileId),
};
}
function buildPublicTrainingProfile(state, shareToken, updatedAt, sinceTimestamp = 0, windowMs = 0) {
const share = findPublicTrainingShare(state, shareToken);
if (!share) return null;
const driverId = String(share.driverId || "");
const profileReference = String(share.driverId || "");
const drivers = Array.isArray(state?.drivers) ? state.drivers : [];
const driver = drivers.find((item) => String(item?.id || "") === driverId);
if (!driver) return null;
const driverProfile = getPublicDriverProfile(drivers, profileReference);
if (!driverProfile?.drivers.length) return null;
const driver = driverProfile.drivers[0];
const classes = Array.isArray(state?.classes) ? state.classes : [];
const driverClass = classes.find((item) => String(item?.id || "") === String(driver.classId || ""));
const ledger = Array.isArray(state?.training?.ledger) ? state.training.ledger : [];
const driverLaps = ledger
.filter((entry) => String(entry?.driverId || "") === driverId && Number(entry?.lapMs || 0) > 500)
.map((entry) => ({
timestamp: Math.max(0, Number(entry.timestamp || 0) || 0),
lapMs: Math.max(0, Math.round(Number(entry.lapMs || 0) || 0)),
sessionId: String(entry.sessionId || ""),
}))
.sort((left, right) => left.timestamp - right.timestamp);
const stats = buildPublicTrainingStats(driverLaps);
const goals = (Array.isArray(state?.training?.goals) ? state.training.goals : [])
.filter((goal) => String(goal?.driverId || "") === driverId)
.map((goal) => buildPublicTrainingGoal(goal, stats.totalLaps));
const rankedTotals = getRankedTrainingTotals(ledger, new Set(drivers.map((item) => String(item?.id || "")).filter(Boolean)));
const rankIndex = rankedTotals.findIndex(([id]) => id === driverId);
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) => {
const classDrivers = driverProfile.drivers.filter((item) => String(item.classId || "") === classId);
const classDriverIds = new Set(classDrivers.map((item) => String(item.id || "")));
const classLaps = ledger
.filter((entry) => classDriverIds.has(String(entry?.driverId || "")) && Number(entry?.lapMs || 0) > 500)
.map((entry) => ({
timestamp: Math.max(0, Number(entry.timestamp || 0) || 0),
lapMs: Math.max(0, Math.round(Number(entry.lapMs || 0) || 0)),
sessionId: String(entry.sessionId || ""),
}))
.sort((left, right) => left.timestamp - right.timestamp);
const stats = buildPublicTrainingStats(classLaps);
const rankedTotals = getRankedTrainingTotals(ledger, drivers, classId);
const rankIndex = rankedTotals.findIndex(([profileId]) => profileId === driverProfile.id);
const classGoals = (Array.isArray(state?.training?.goals) ? state.training.goals : [])
.filter((goal) => profileReferences.has(String(goal?.driverId || "")) && (!goal?.classId || String(goal.classId) === classId))
.map((goal) => buildPublicTrainingGoal(goal, stats.totalLaps));
const brands = [...new Set(classDrivers.map((item) => String(item.brand || "").trim()).filter(Boolean))];
return {
classId,
className: String(classes.find((item) => String(item?.id || "") === classId)?.name || "").trim(),
brand: brands.join(" / "),
clubRank: rankIndex >= 0 ? rankIndex + 1 : 0,
rankedDrivers: rankedTotals.length,
stats,
nextMilestone: getPublicTrainingMilestone(stats.totalLaps),
recommendation: getPublicTrainingRecommendation(stats),
goals: classGoals,
currentSession: sinceTimestamp
? buildPublicTrainingWindowStats(classLaps.filter((lap) => lap.timestamp >= sinceTimestamp && (!windowMs || lap.timestamp <= sinceTimestamp + windowMs)))
: null,
};
});
const activeClass = classProfiles[0] || {
classId: "", className: "", brand: "", clubRank: 0, rankedDrivers: 0,
stats: buildPublicTrainingStats([]), nextMilestone: getPublicTrainingMilestone(0),
recommendation: getPublicTrainingRecommendation({ totalLaps: 0 }), goals: [], currentSession: null,
};
const settings = state?.settings && typeof state.settings === "object" ? state.settings : {};
const currentSession = sinceTimestamp
? buildPublicTrainingWindowStats(driverLaps.filter((lap) => lap.timestamp >= sinceTimestamp && (!windowMs || lap.timestamp <= sinceTimestamp + windowMs)))
: null;
return {
updatedAt: String(updatedAt || ""),
profile: {
name: String(driver.name || "Driver").trim(),
brand: String(driver.brand || "").trim(),
className: String(driverClass?.name || "").trim(),
clubRank: rankIndex >= 0 ? rankIndex + 1 : 0,
rankedDrivers: rankedTotals.length,
brand: activeClass.brand,
className: activeClass.className,
clubRank: activeClass.clubRank,
rankedDrivers: activeClass.rankedDrivers,
},
branding: {
clubName: String(settings.clubName || "JMK RB RaceController").trim(),
clubTagline: String(settings.clubTagline || "RC Timing System").trim(),
logoDataUrl: normalizePublicLogoDataUrl(settings.logoDataUrl),
},
stats,
nextMilestone: getPublicTrainingMilestone(stats.totalLaps),
recommendation: getPublicTrainingRecommendation(stats),
goals,
currentSession,
classProfiles,
stats: activeClass.stats,
nextMilestone: activeClass.nextMilestone,
recommendation: activeClass.recommendation,
goals: activeClass.goals,
currentSession: activeClass.currentSession,
};
}
@@ -100,12 +145,16 @@ function buildPublicTrainingGoal(goal, currentLaps) {
};
}
function getRankedTrainingTotals(ledger, registeredDriverIds) {
function getRankedTrainingTotals(ledger, drivers, classId = "") {
const profileByDriverId = new Map(drivers
.filter((driver) => !classId || String(driver?.classId || "") === String(classId))
.map((driver) => [String(driver?.id || ""), getPublicDriverProfileId(driver)]));
const totals = new Map();
ledger.forEach((entry) => {
const driverId = String(entry?.driverId || "");
if (!driverId || !registeredDriverIds.has(driverId) || Number(entry?.lapMs || 0) <= 500) return;
totals.set(driverId, (totals.get(driverId) || 0) + 1);
const profileId = profileByDriverId.get(driverId);
if (!profileId || Number(entry?.lapMs || 0) <= 500) return;
totals.set(profileId, (totals.get(profileId) || 0) + 1);
});
return [...totals.entries()].sort((left, right) => right[1] - left[1]);
}