const status = document.querySelector("#status"); const driverCount = document.querySelector("#driver-count"); const trackName = document.querySelector("#track-name"); const sessionType = document.querySelector("#session-type"); const timeLeft = document.querySelector("#time-left"); const weather = document.querySelector("#weather"); const lastUpdate = document.querySelector("#last-update"); const leaderboardNote = document.querySelector("#leaderboard-note"); const leaderboardBody = document.querySelector("#leaderboard-body"); function escapeHtml(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """); } function fmt(value, fallback = "-") { if (value === undefined || value === null || value === "" || Number.isNaN(value)) { return `${fallback}`; } return escapeHtml(value); } function fmtTime(seconds) { if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds < 0) { return "-"; } const minutes = Math.floor(seconds / 60); const remainder = seconds - minutes * 60; return `${minutes}:${remainder.toFixed(3).padStart(6, "0")}`; } function fmtSplit(split) { if (!split || typeof split.delta !== "number" || !Number.isFinite(split.delta)) { return "-"; } if (split.delta === 0) { return "0"; } const sign = split.delta > 0 ? "+" : "-"; return `${sign}${Math.abs(split.delta).toFixed(3)}`; } function sectorSplitForRow(row, session) { const sessionType = String(session?.sessionType || "").toLowerCase(); const usePersonalBest = sessionType === "practice" || sessionType === "qualifying"; return usePersonalBest ? row?.sectorTimeSplit || row?.checkpointSplit : row?.checkpointSplit || row?.sectorTimeSplit; } function positionChange(change) { if (!change?.direction) { return ''; } const isUp = change.direction === "up"; const label = `${isUp ? "Moved up" : "Moved down"} ${change.places ?? 1}`; return ` ${isUp ? "▲" : "▼"}${change.places && change.places > 1 ? escapeHtml(change.places) : ""} `; } function nationalityBadge(nationality) { if (!nationality?.code) { return '-'; } const title = `${nationality.country || nationality.code}${nationality.source === "override" ? " (name override)" : ""}`; const flag = flagSvg(nationality.code); return `${flag || escapeHtml(nationality.code)}`; } function carShortName(row) { if (!row?.vehicleShortName) { return '-'; } return `${escapeHtml(row.vehicleShortName)}`; } function statusBadge(row) { const status = row?.status; const label = status?.label || (row?.active === false ? "Inactive" : "Active"); const tone = status?.tone || (row?.active === false ? "warn" : "good"); return `${escapeHtml(label)}`; } function raceFlagDetails(row) { const flag = row?.flag; if (!flag || row.highestFlag === undefined || row.highestFlag === null) { return `flag ${fmt(row?.highestFlag)}`; } const label = flag.observed ? ` ${escapeHtml(flag.observed)}` : ""; return `flag ${fmt(row.highestFlag)} c${fmt(flag.colour)} r${fmt(flag.reason)}${label}`; } function flagSvg(code) { const iso2ByIso3 = { DEU: "DE", FRA: "FR", GBR: "GB", JPN: "JP", USA: "US" }; const iso2 = iso2ByIso3[String(code).toUpperCase()]; if (!iso2) return ""; const flags = { DE: ``, FR: ``, GB: ``, JP: ``, US: `` }; const svg = flags[iso2]; return svg ? `${escapeHtml(iso2)} flag` : ""; } function clock(value) { if (!value) return "-"; return new Date(value).toLocaleTimeString(); } function renderLeaderboard(leaderboard) { const rows = leaderboard?.rows ?? []; const session = leaderboard?.session ?? {}; driverCount.textContent = String(rows.length); leaderboardNote.textContent = leaderboard?.note ?? "Waiting for participant or timing packets"; trackName.textContent = session.trackName || "-"; sessionType.textContent = session.sessionType || "-"; timeLeft.textContent = fmtTime(session.timeLeft); weather.textContent = session.weather || "-"; lastUpdate.textContent = clock(session.lastUpdate); leaderboardBody.innerHTML = rows.length ? rows.map((row) => ` ${(() => { const sectorSplit = sectorSplitForRow(row, session); return ` ${fmt(row.position)}${positionChange(row.positionChange)} ${fmt(row.driver)} slot ${fmt(row.slot)} / id ${fmt(row.id)} ${nationalityBadge(row.nationality)} ${carShortName(row)} ${fmt(row.lap)} ${fmt(row.sector)} ${fmtTime(row.currentTime)} ${fmtTime(row.lastLapTime)} ${fmtSplit(row.latestLapSplit)} ${row.latestLapSplit?.comparedTo ? `vs ${fmt(row.latestLapSplit.comparedTo)}` : ""} ${fmtTime(row.fastestLapTime)} ${fmtSplit(row.bestLapSplit)} ${row.bestLapSplit?.comparedTo ? `vs ${fmt(row.bestLapSplit.comparedTo)}` : ""} ${fmtSplit(sectorSplit)} ${fmt(sectorSplit?.checkpoint)}${sectorSplit?.comparedTo ? ` vs ${fmt(sectorSplit.comparedTo)}` : ""} ${fmtTime(sectorSplit?.time)} ${statusBadge(row)} pit ${fmt(row.pitModeSchedule)} / ${raceFlagDetails(row)} `; })()} `).join("") : `No leaderboard data received yet`; } fetch("/api/leaderboard") .then((response) => response.json()) .then(renderLeaderboard) .catch(() => { status.textContent = "Could not load leaderboard state"; status.className = "warn"; }); const events = new EventSource("/events"); events.addEventListener("state", (event) => { const payload = JSON.parse(event.data); renderLeaderboard(payload.leaderboard); }); events.addEventListener("leaderboard", (event) => { const leaderboard = JSON.parse(event.data); status.textContent = `Receiving packets. Last update ${clock(leaderboard?.session?.lastUpdate)}`; status.className = "good"; renderLeaderboard(leaderboard); }); events.onerror = () => { status.textContent = "Connection to local telemetry server lost"; status.className = "warn"; };