fix team lag
This commit is contained in:
206
src/app.js
206
src/app.js
@@ -220,6 +220,7 @@ const TRANSLATIONS = {
|
||||
"events.no_teams": "Inga lag skapade ännu.",
|
||||
"events.team_standings": "Lagställning",
|
||||
"events.no_team_results": "Inga teamresultat ännu.",
|
||||
"events.edit_team": "Redigera lag",
|
||||
"events.add_session": "Lägg till session",
|
||||
"events.set_active": "Sätt aktiv",
|
||||
"events.assignments": "Tilldelningar",
|
||||
@@ -277,6 +278,7 @@ const TRANSLATIONS = {
|
||||
"timing.open_speaker_overlay": "Speaker overlay",
|
||||
"timing.open_results_overlay": "Result overlay",
|
||||
"timing.open_tv_overlay": "TV overlay",
|
||||
"timing.open_team_overlay": "Team overlay",
|
||||
"timing.close_details": "Stang",
|
||||
"timing.detail_title": "Leaderboard-detaljer",
|
||||
"timing.lap_history": "Varvhistorik",
|
||||
@@ -479,12 +481,16 @@ const TRANSLATIONS = {
|
||||
"overlay.mode_speaker": "Speaker",
|
||||
"overlay.mode_results": "Resultat",
|
||||
"overlay.mode_tv": "TV",
|
||||
"overlay.mode_team": "Team",
|
||||
"overlay.fastest_lap": "Snabbaste varv",
|
||||
"overlay.fullscreen": "Fullscreen",
|
||||
"overlay.leaderboard_live": "Live leaderboard",
|
||||
"overlay.rotating_panel": "Displaypanel",
|
||||
"overlay.next_predicted_lap": "Nästa varv",
|
||||
"overlay.event_markers": "Eventmarkörer",
|
||||
"overlay.team_battle": "Lagkamp",
|
||||
"overlay.active_member": "Aktiv förare/bil",
|
||||
"overlay.top_three": "Topp 3",
|
||||
"guide.host_title": "Hur Managed AMMC körs",
|
||||
"guide.host_1": "1. AMMC körs alltid på samma maskin som `npm start` eller `node server.js` körs på.",
|
||||
"guide.host_2": "2. Om du bara surfar in från en laptop/webbläsare startas ingen process där. Webbläsaren styr bara backend via HTTP.",
|
||||
@@ -722,6 +728,7 @@ const TRANSLATIONS = {
|
||||
"events.no_teams": "No teams created yet.",
|
||||
"events.team_standings": "Team standings",
|
||||
"events.no_team_results": "No team results yet.",
|
||||
"events.edit_team": "Edit team",
|
||||
"events.add_session": "Add Session",
|
||||
"events.set_active": "Set Active",
|
||||
"events.assignments": "Assignments",
|
||||
@@ -779,6 +786,7 @@ const TRANSLATIONS = {
|
||||
"timing.open_speaker_overlay": "Speaker overlay",
|
||||
"timing.open_results_overlay": "Results overlay",
|
||||
"timing.open_tv_overlay": "TV overlay",
|
||||
"timing.open_team_overlay": "Team overlay",
|
||||
"timing.close_details": "Close",
|
||||
"timing.detail_title": "Leaderboard details",
|
||||
"timing.lap_history": "Lap history",
|
||||
@@ -981,12 +989,16 @@ const TRANSLATIONS = {
|
||||
"overlay.mode_speaker": "Speaker",
|
||||
"overlay.mode_results": "Results",
|
||||
"overlay.mode_tv": "TV",
|
||||
"overlay.mode_team": "Team",
|
||||
"overlay.fastest_lap": "Fastest Lap",
|
||||
"overlay.fullscreen": "Fullscreen",
|
||||
"overlay.leaderboard_live": "Live leaderboard",
|
||||
"overlay.rotating_panel": "Display panel",
|
||||
"overlay.next_predicted_lap": "Next lap",
|
||||
"overlay.event_markers": "Event markers",
|
||||
"overlay.team_battle": "Team battle",
|
||||
"overlay.active_member": "Active driver/car",
|
||||
"overlay.top_three": "Top 3",
|
||||
"guide.host_title": "How Managed AMMC Runs",
|
||||
"guide.host_1": "1. AMMC always runs on the same machine where `npm start` or `node server.js` is running.",
|
||||
"guide.host_2": "2. If you only browse from a laptop/browser, no process is started there. The browser only controls the backend over HTTP.",
|
||||
@@ -1024,7 +1036,7 @@ const TRANSLATIONS = {
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const overlayMode = urlParams.get("view") === "overlay";
|
||||
const overlayViewMode = ["leaderboard", "speaker", "results", "tv"].includes(String(urlParams.get("overlayMode") || "").toLowerCase())
|
||||
const overlayViewMode = ["leaderboard", "speaker", "results", "tv", "team"].includes(String(urlParams.get("overlayMode") || "").toLowerCase())
|
||||
? String(urlParams.get("overlayMode")).toLowerCase()
|
||||
: "leaderboard";
|
||||
const state = loadState();
|
||||
@@ -1041,6 +1053,7 @@ let selectedDriverEditId = null;
|
||||
let selectedCarEditId = null;
|
||||
let selectedEventEditId = null;
|
||||
let selectedSessionEditId = null;
|
||||
let selectedTeamEditId = null;
|
||||
let quickAddDraft = null;
|
||||
let overlaySyncTimer = null;
|
||||
let overlayRotationTimer = null;
|
||||
@@ -2749,6 +2762,10 @@ function renderEventManager(eventId) {
|
||||
.join("");
|
||||
const raceDrivers = event.mode === "race" ? state.drivers.filter((driver) => !event.classId || driver.classId === event.classId) : [];
|
||||
const raceTeams = event.mode === "race" ? getEventTeams(event) : [];
|
||||
if (selectedTeamEditId && !raceTeams.some((team) => team.id === selectedTeamEditId)) {
|
||||
selectedTeamEditId = null;
|
||||
}
|
||||
const editingTeam = event.mode === "race" ? raceTeams.find((team) => team.id === selectedTeamEditId) || null : null;
|
||||
const carOptions = state.cars
|
||||
.map((c) => `<option value="${c.id}">${escapeHtml(c.name)} (${escapeHtml(c.transponder)})</option>`)
|
||||
.join("");
|
||||
@@ -2992,7 +3009,10 @@ function renderEventManager(eventId) {
|
||||
.join(", ") || "-"
|
||||
)}</div>
|
||||
</div>
|
||||
<button id="team-delete-${team.id}" class="btn btn-danger" type="button">${t("common.delete")}</button>
|
||||
<div class="actions-inline">
|
||||
<button id="team-edit-${team.id}" class="btn" type="button">${t("events.edit_team")}</button>
|
||||
<button id="team-delete-${team.id}" class="btn btn-danger" type="button">${t("common.delete")}</button>
|
||||
</div>
|
||||
</article>
|
||||
`
|
||||
)
|
||||
@@ -3163,6 +3183,59 @@ function renderEventManager(eventId) {
|
||||
: ""
|
||||
}
|
||||
|
||||
${
|
||||
editingTeam
|
||||
? `
|
||||
<div class="modal-overlay" id="teamEditModalOverlay">
|
||||
<div class="modal-card">
|
||||
<div class="panel-header">
|
||||
<h3>${t("events.edit_team")}</h3>
|
||||
<button class="btn" id="teamEditCancel">${t("common.cancel")}</button>
|
||||
</div>
|
||||
<form id="teamEditForm" class="panel-body form-grid cols-2">
|
||||
<input name="teamName" required value="${escapeHtml(editingTeam.name)}" placeholder="${t("events.team_name")}" />
|
||||
<p class="form-error" id="teamEditError" hidden></p>
|
||||
<div>
|
||||
<h4>${t("events.team_drivers")}</h4>
|
||||
<div class="check-grid">
|
||||
${raceDrivers
|
||||
.map(
|
||||
(driver) => `
|
||||
<label class="check-card">
|
||||
<input type="checkbox" name="teamDriverIds" value="${driver.id}" ${editingTeam.driverIds.includes(driver.id) ? "checked" : ""} />
|
||||
<span>${escapeHtml(driver.name)}${driver.transponder ? ` (${escapeHtml(driver.transponder)})` : ""}</span>
|
||||
</label>
|
||||
`
|
||||
)
|
||||
.join("")}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4>${t("events.team_cars")}</h4>
|
||||
<div class="check-grid">
|
||||
${state.cars
|
||||
.map(
|
||||
(car) => `
|
||||
<label class="check-card">
|
||||
<input type="checkbox" name="teamCarIds" value="${car.id}" ${editingTeam.carIds.includes(car.id) ? "checked" : ""} />
|
||||
<span>${escapeHtml(car.name)} (${escapeHtml(car.transponder || "-")})</span>
|
||||
</label>
|
||||
`
|
||||
)
|
||||
.join("")}
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions-inline">
|
||||
<button class="btn btn-primary" type="submit">${t("common.save")}</button>
|
||||
<button class="btn" id="teamEditCancelFooter" type="button">${t("common.cancel")}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
${
|
||||
editingSession
|
||||
? `
|
||||
@@ -3490,13 +3563,69 @@ function renderEventManager(eventId) {
|
||||
});
|
||||
|
||||
raceTeams.forEach((team) => {
|
||||
document.getElementById(`team-edit-${team.id}`)?.addEventListener("click", () => {
|
||||
selectedTeamEditId = team.id;
|
||||
renderEventManager(eventId);
|
||||
});
|
||||
|
||||
document.getElementById(`team-delete-${team.id}`)?.addEventListener("click", () => {
|
||||
event.raceConfig.teams = getEventTeams(event).filter((item) => item.id !== team.id);
|
||||
if (selectedTeamEditId === team.id) {
|
||||
selectedTeamEditId = null;
|
||||
}
|
||||
saveState();
|
||||
renderEventManager(eventId);
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById("teamEditCancel")?.addEventListener("click", () => {
|
||||
selectedTeamEditId = null;
|
||||
renderEventManager(eventId);
|
||||
});
|
||||
|
||||
document.getElementById("teamEditCancelFooter")?.addEventListener("click", () => {
|
||||
selectedTeamEditId = null;
|
||||
renderEventManager(eventId);
|
||||
});
|
||||
|
||||
document.getElementById("teamEditModalOverlay")?.addEventListener("click", (modalEvent) => {
|
||||
if (modalEvent.target?.id === "teamEditModalOverlay") {
|
||||
selectedTeamEditId = null;
|
||||
renderEventManager(eventId);
|
||||
}
|
||||
});
|
||||
|
||||
bindModalShell("teamEditModalOverlay", () => {
|
||||
selectedTeamEditId = null;
|
||||
renderEventManager(eventId);
|
||||
});
|
||||
|
||||
document.getElementById("teamEditForm")?.addEventListener("submit", (submitEvent) => {
|
||||
submitEvent.preventDefault();
|
||||
if (!editingTeam) {
|
||||
return;
|
||||
}
|
||||
const form = new FormData(submitEvent.currentTarget);
|
||||
const name = String(form.get("teamName") || "").trim();
|
||||
const driverIds = form.getAll("teamDriverIds").map(String).filter(Boolean);
|
||||
const carIds = form.getAll("teamCarIds").map(String).filter(Boolean);
|
||||
if (!name) {
|
||||
setFormError("teamEditError", t("validation.required_name"));
|
||||
return;
|
||||
}
|
||||
if (!driverIds.length && !carIds.length) {
|
||||
setFormError("teamEditError", t("validation.invalid_selection"));
|
||||
return;
|
||||
}
|
||||
setFormError("teamEditError", "");
|
||||
event.raceConfig.teams = getEventTeams(event).map((team) =>
|
||||
team.id === editingTeam.id ? normalizeRaceTeam({ ...team, name, driverIds, carIds }) : team
|
||||
);
|
||||
selectedTeamEditId = null;
|
||||
saveState();
|
||||
renderEventManager(eventId);
|
||||
});
|
||||
|
||||
document.getElementById("raceFormatForm")?.addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
const form = new FormData(e.currentTarget);
|
||||
@@ -3764,6 +3893,7 @@ function renderTiming() {
|
||||
<button id="openSpeakerOverlay" class="btn" type="button">${t("timing.open_speaker_overlay")}</button>
|
||||
<button id="openResultsOverlay" class="btn" type="button">${t("timing.open_results_overlay")}</button>
|
||||
<button id="openTvOverlay" class="btn" type="button">${t("timing.open_tv_overlay")}</button>
|
||||
<button id="openTeamOverlay" class="btn" type="button">${t("timing.open_team_overlay")}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4004,6 +4134,7 @@ function renderTiming() {
|
||||
document.getElementById("openSpeakerOverlay")?.addEventListener("click", () => openOverlayWindow("speaker"));
|
||||
document.getElementById("openResultsOverlay")?.addEventListener("click", () => openOverlayWindow("results"));
|
||||
document.getElementById("openTvOverlay")?.addEventListener("click", () => openOverlayWindow("tv"));
|
||||
document.getElementById("openTeamOverlay")?.addEventListener("click", () => openOverlayWindow("team"));
|
||||
document.querySelectorAll("[data-speaker-setting]").forEach((node) => {
|
||||
node.addEventListener("change", (event) => {
|
||||
const input = event.currentTarget;
|
||||
@@ -4318,6 +4449,8 @@ function renderOverlay() {
|
||||
</div>
|
||||
</section>
|
||||
`
|
||||
: overlayViewMode === "team"
|
||||
? renderTeamOverlay(leaderboard, result, sessionTiming)
|
||||
: overlayViewMode === "tv"
|
||||
? `
|
||||
<section class="overlay-board overlay-board-tv">
|
||||
@@ -4592,7 +4725,10 @@ function renderLeaderboard(rows) {
|
||||
return `
|
||||
<tr>
|
||||
<td><span class="pos-pill ${posClass}">${idx + 1}</span></td>
|
||||
<td>${escapeHtml(row.displayName || row.driverName)}</td>
|
||||
<td>
|
||||
<div class="table-primary">${escapeHtml(row.displayName || row.driverName)}</div>
|
||||
${row.teamId && row.subLabel ? `<div class="table-subnote">${t("overlay.active_member")}: ${escapeHtml(row.subLabel)}</div>` : ""}
|
||||
</td>
|
||||
<td>${escapeHtml(row.subLabel || row.carName)}</td>
|
||||
<td>${escapeHtml(row.transponder)}</td>
|
||||
<td>${row.laps}</td>
|
||||
@@ -4626,7 +4762,7 @@ function renderOverlayLeaderboard(rows) {
|
||||
</div>
|
||||
<div class="overlay-race-driver">
|
||||
<strong>${escapeHtml(row.displayName || row.driverName)}</strong>
|
||||
<span>${escapeHtml(row.subLabel || row.transponder || "-")}</span>
|
||||
<span>${escapeHtml(row.teamId && row.subLabel ? `${t("overlay.active_member")}: ${row.subLabel}` : row.subLabel || row.transponder || "-")}</span>
|
||||
<div class="overlay-prediction">
|
||||
<div class="overlay-prediction-meta">
|
||||
<label>${t("overlay.next_predicted_lap")}</label>
|
||||
@@ -4661,6 +4797,63 @@ function renderOverlayLeaderboard(rows) {
|
||||
`;
|
||||
}
|
||||
|
||||
function renderTeamOverlay(rows, result, sessionTiming) {
|
||||
const topThree = rows.slice(0, 3);
|
||||
return `
|
||||
<section class="overlay-team-layout">
|
||||
<section class="overlay-team-podium">
|
||||
<div class="overlay-section-head">
|
||||
<h3>${t("overlay.top_three")}</h3>
|
||||
<span class="pill">${t("overlay.team_battle")}</span>
|
||||
</div>
|
||||
<div class="overlay-team-podium-grid">
|
||||
${topThree
|
||||
.map(
|
||||
(row, index) => `
|
||||
<article class="overlay-team-card overlay-team-card-${index + 1}">
|
||||
<span class="pos-pill pos-${Math.min(index + 1, 3)}">${index + 1}</span>
|
||||
<strong>${escapeHtml(row.displayName || row.driverName)}</strong>
|
||||
<p>${escapeHtml(row.resultDisplay || "-")}</p>
|
||||
<small>${t("overlay.active_member")}: ${escapeHtml(row.subLabel || "-")}</small>
|
||||
</article>
|
||||
`
|
||||
)
|
||||
.join("")}
|
||||
</div>
|
||||
</section>
|
||||
<section class="overlay-board overlay-board-tv">
|
||||
<div class="overlay-table-wrap overlay-display-wrap">
|
||||
<section class="overlay-stats-row">
|
||||
<article class="overlay-stat-card">
|
||||
<span>${t("table.laps")}</span>
|
||||
<strong>${rows[0]?.laps || 0}</strong>
|
||||
<small>${escapeHtml(rows[0]?.displayName || rows[0]?.driverName || "-")}</small>
|
||||
</article>
|
||||
<article class="overlay-stat-card">
|
||||
<span>${t("timing.total_passings")}</span>
|
||||
<strong>${result?.passings.length || 0}</strong>
|
||||
<small>${sessionTiming?.untimed ? t("timing.elapsed") : t("timing.remaining")}</small>
|
||||
</article>
|
||||
<article class="overlay-stat-card">
|
||||
<span>${t("overlay.fastest_lap")}</span>
|
||||
<strong>${formatLap([...rows].filter((row) => Number.isFinite(row.bestLapMs)).sort((a, b) => a.bestLapMs - b.bestLapMs)[0]?.bestLapMs)}</strong>
|
||||
<small>${escapeHtml(
|
||||
[...rows].filter((row) => Number.isFinite(row.bestLapMs)).sort((a, b) => a.bestLapMs - b.bestLapMs)[0]?.displayName || "-"
|
||||
)}</small>
|
||||
</article>
|
||||
</section>
|
||||
<div class="overlay-leaderboard-card overlay-leaderboard-card-tv">
|
||||
<div class="overlay-section-head">
|
||||
<h3>${t("events.team_standings")}</h3>
|
||||
</div>
|
||||
${renderOverlayLeaderboard(rows)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderRecentPassings(session) {
|
||||
if (!session) {
|
||||
return `<p>${t("timing.no_session_selected")}</p>`;
|
||||
@@ -5913,7 +6106,10 @@ function renderTeamRaceStandings(event) {
|
||||
(row, index) => `
|
||||
<tr>
|
||||
<td>${index + 1}</td>
|
||||
<td>${escapeHtml(row.displayName || row.driverName)}</td>
|
||||
<td>
|
||||
<div class="table-primary">${escapeHtml(row.displayName || row.driverName)}</div>
|
||||
${row.subLabel ? `<div class="table-subnote">${t("overlay.active_member")}: ${escapeHtml(row.subLabel)}</div>` : ""}
|
||||
</td>
|
||||
<td>${row.laps}</td>
|
||||
<td>${escapeHtml(row.resultDisplay)}</td>
|
||||
<td>${formatLap(row.bestLapMs)}</td>
|
||||
|
||||
Reference in New Issue
Block a user