fix team lag

This commit is contained in:
larssand
2026-03-15 14:38:51 +01:00
parent 3dd2b8cbfd
commit a6e1f4c89c
2 changed files with 258 additions and 5 deletions

View File

@@ -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>