синхронизация текущего времени матча во всех браузерах, тест 1

This commit is contained in:
2026-05-25 14:22:47 +03:00
parent 64cd3891ea
commit 998774bf1c
2 changed files with 194 additions and 0 deletions

View File

@@ -64,6 +64,17 @@ let originalLineupState = null;
const SESSION_TOKEN = MATCH_DATA.sessionToken ?? null;
const MATCH_ID = MATCH_DATA.matchId ?? null;
const STORAGE_KEY = `match_admin_state_match_${MATCH_ID}`;
const CLOCK_CLIENT_ID_KEY = `match_admin_clock_client_${MATCH_ID}`;
let CLOCK_CLIENT_ID = localStorage.getItem(CLOCK_CLIENT_ID_KEY);
if (!CLOCK_CLIENT_ID) {
CLOCK_CLIENT_ID = `${Date.now()}_${Math.random().toString(16).slice(2)}`;
localStorage.setItem(CLOCK_CLIENT_ID_KEY, CLOCK_CLIENT_ID);
}
let lastClockServerSyncAt = 0;
let clockServerSyncInFlight = false;
let applyingRemoteClockState = false;
let clockEditorIsOpen = false;
const formationState = {
home: Array.isArray(MATCH_DATA.homeFormations) ? MATCH_DATA.homeFormations : [],
@@ -315,11 +326,106 @@ function saveMatchState() {
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(payload));
syncClockStateToServer(false);
} catch (e) {
console.warn("Не удалось сохранить состояние матча", e);
}
}
async function syncClockStateToServer(force = false) {
if (!SESSION_TOKEN || applyingRemoteClockState) return;
const now = Date.now();
if (!force && now - lastClockServerSyncAt < 1500) return;
if (clockServerSyncInFlight) return;
lastClockServerSyncAt = now;
clockServerSyncInFlight = true;
try {
const liveSeconds = getLiveClockSeconds();
await fetch(`/admin/session/${SESSION_TOKEN}/clock-state`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
currentPeriod,
matchClockSeconds: liveSeconds,
timerRunning: !!timerId,
pausedAccumulatedSeconds: liveSeconds,
sourceClientId: CLOCK_CLIENT_ID
})
});
} catch (e) {
console.warn("Не удалось синхронизировать время с сервером", e);
} finally {
clockServerSyncInFlight = false;
}
}
async function loadClockStateFromServer() {
if (!SESSION_TOKEN || clockEditorIsOpen) return false;
try {
const res = await fetch(`/admin/session/${SESSION_TOKEN}/clock-state`, {
cache: "no-store"
});
if (!res.ok) return false;
const state = await res.json();
if (!state || !state.exists) return false;
// Если это только что отправил этот же браузер — не дёргаем локальный таймер лишний раз.
if (state.sourceClientId && state.sourceClientId === CLOCK_CLIENT_ID) {
return true;
}
applyingRemoteClockState = true;
currentPeriod = state.currentPeriod ?? currentPeriod;
const seconds = Math.max(0, Number(state.matchClockSeconds) || 0);
const shouldRun = !!state.timerRunning && (currentPeriod === "1H" || currentPeriod === "2H");
if (timerId) {
clearInterval(timerId);
timerId = null;
}
pausedAccumulatedSeconds = seconds;
matchClockSeconds = seconds;
periodStartedAt = shouldRun ? Date.now() : null;
if (shouldRun) {
timerId = setInterval(() => {
updateTimerUI();
saveMatchState();
}, 1000);
}
updateTimerUI();
updatePeriodBadge(
currentPeriod === "1H" ? (extraTimeMinutes.first ? `1-й тайм +${extraTimeMinutes.first}` : "1-й тайм") :
currentPeriod === "HT" ? "Перерыв" :
currentPeriod === "2H" ? (extraTimeMinutes.second ? `2-й тайм +${extraTimeMinutes.second}` : "2-й тайм") :
currentPeriod === "FT" ? "Матч завершён" :
"Матч не начат"
);
saveMatchState();
updatePeriodButtons();
return true;
} catch (e) {
console.warn("Не удалось получить время с сервера", e);
return false;
} finally {
applyingRemoteClockState = false;
}
}
function initClockStateSync() {
loadClockStateFromServer();
setInterval(loadClockStateFromServer, 2000);
}
function restoreMatchState() {
const state = getPersistedState();
if (!state) return false;
@@ -407,12 +513,15 @@ async function syncEditedClockToVmix(totalSeconds, shouldRun) {
}
function closeClockEditor() {
clockEditorIsOpen = false;
const modal = document.getElementById("clockEditorModal");
if (modal) modal.classList.remove("show");
}
function openClockEditor() {
clockEditorIsOpen = true;
if (!(currentPeriod === "1H" || currentPeriod === "2H")) {
clockEditorIsOpen = false;
alert("Редактировать время можно после запуска 1-го или 2-го тайма");
return;
}
@@ -472,6 +581,7 @@ async function applyClockEditor() {
updateTimerUI();
saveMatchState();
await syncClockStateToServer(true);
closeClockEditor();
await syncEditedClockToVmix(newSeconds, wasRunning);
}
@@ -710,6 +820,7 @@ function setMatchPeriod(period) {
addPeriodMarker("period_start_1h", "Начало 1-го тайма", 0);
triggerVmixTimerAction("first_half_start");
saveMatchState();
syncClockStateToServer(true);
clearExtraTimeInput();
updatePeriodButtons();
return;
@@ -725,6 +836,7 @@ function setMatchPeriod(period) {
addPeriodMarker("period_ht", "Конец 1-го тайма", actualEndSeconds);
triggerVmixTimerAction("halftime");
saveMatchState();
syncClockStateToServer(true);
clearExtraTimeInput();
updatePeriodButtons();
return;
@@ -747,6 +859,7 @@ function setMatchPeriod(period) {
triggerVmixTimerAction("second_half_start");
startTimer();
saveMatchState();
syncClockStateToServer(true);
updatePeriodButtons();
return;
}
@@ -757,6 +870,7 @@ function setMatchPeriod(period) {
addPeriodMarker("period_ft", "Конец 2-го тайма", getLiveClockSeconds());
triggerVmixTimerAction("full_time");
saveMatchState();
syncClockStateToServer(true);
clearExtraTimeInput();
updatePeriodButtons();
}
@@ -1022,6 +1136,7 @@ function clearMatchEvents() {
renderEvents();
saveMatchState();
syncClockStateToServer(true);
clearEventsOnServer();
updateCoachCardButtons();
updatePeriodButtons();
@@ -2416,6 +2531,7 @@ if (!restored) {
}
loadEventsFromServer();
initClockStateSync();
window.addEventListener("beforeunload", saveMatchState);
window.addEventListener("pagehide", saveMatchState);
document.addEventListener("visibilitychange", () => {