Add per-driver live stats for timed practice sessions

This commit is contained in:
larssand
2026-08-24 17:36:03 +02:00
parent 40d0c3fa90
commit aa74da0bdf
7 changed files with 114 additions and 9 deletions

View File

@@ -284,7 +284,9 @@ app.get(["/public-overlay", "/public-overlay/:mode"], (req, res) => {
app.get("/public-training/api/:shareToken", (req, res) => {
try {
const stored = readStoredAppState();
const profile = stored ? buildPublicTrainingProfile(stored.state, String(req.params.shareToken || ""), stored.updatedAt) : null;
const sinceTimestamp = normalizePublicTrainingSince(req.query.since, req.query.startedAgoMs);
const windowMs = normalizePublicTrainingWindow(req.query.windowMs);
const profile = stored ? buildPublicTrainingProfile(stored.state, String(req.params.shareToken || ""), stored.updatedAt, sinceTimestamp, windowMs) : null;
if (!profile) {
res.status(404).json({ error: "Profile not found" });
return;
@@ -384,7 +386,7 @@ function hasPublicTrainingShare(state, shareToken) {
return Boolean(findPublicTrainingShare(state, shareToken));
}
function buildPublicTrainingProfile(state, shareToken, updatedAt) {
function buildPublicTrainingProfile(state, shareToken, updatedAt, sinceTimestamp = 0, windowMs = 0) {
const share = findPublicTrainingShare(state, shareToken);
if (!share) return null;
const driverId = String(share.driverId || "");
@@ -410,6 +412,9 @@ function buildPublicTrainingProfile(state, shareToken, updatedAt) {
const rankedTotals = getRankedTrainingTotals(ledger, new Set(drivers.map((item) => String(item?.id || "")).filter(Boolean)));
const rankIndex = rankedTotals.findIndex(([id]) => id === driverId);
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 || ""),
@@ -429,6 +434,33 @@ function buildPublicTrainingProfile(state, shareToken, updatedAt) {
nextMilestone: getPublicTrainingMilestone(stats.totalLaps),
recommendation: getPublicTrainingRecommendation(stats),
goals,
currentSession,
};
}
function normalizePublicTrainingSince(value, startedAgoValue) {
const now = Date.now();
if (startedAgoValue !== undefined) {
const startedAgoMs = Math.round(Number(startedAgoValue));
if (Number.isFinite(startedAgoMs) && startedAgoMs >= 0 && startedAgoMs <= 24 * 60 * 60 * 1000) {
return now - startedAgoMs;
}
}
const timestamp = Math.round(Number(value || 0) || 0);
return timestamp >= now - 24 * 60 * 60 * 1000 && timestamp <= now + 60 * 1000 ? timestamp : 0;
}
function normalizePublicTrainingWindow(value) {
const windowMs = Math.round(Number(value || 0) || 0);
return Math.min(24 * 60 * 60 * 1000, Math.max(0, windowMs));
}
function buildPublicTrainingWindowStats(laps) {
const lapTimes = laps.map((lap) => Number(lap.lapMs || 0)).filter((lapMs) => lapMs > 500);
return {
laps: lapTimes.length,
bestLapMs: lapTimes.length ? Math.min(...lapTimes) : 0,
averageLapMs: lapTimes.length ? Math.round(lapTimes.reduce((sum, lapMs) => sum + lapMs, 0) / lapTimes.length) : 0,
};
}