синхронизация событий со всеми браузерами, тест 1

This commit is contained in:
2026-05-25 14:32:27 +03:00
parent 998774bf1c
commit 495121a8b7
2 changed files with 58 additions and 11 deletions

12
app.py
View File

@@ -1222,6 +1222,18 @@ def api_clear_events(session_token: str):
match_id = session_row[1] match_id = session_row[1]
clear_events(match_id) clear_events(match_id)
# Если события очищены в одном браузере, сбрасываем общий серверный таймер тоже.
# Иначе другой браузер с запущенным таймером может снова подтянуть старое время.
MATCH_CLOCK_STATES[match_id] = {
"currentPeriod": None,
"matchClockSeconds": 0,
"timerRunning": False,
"pausedAccumulatedSeconds": 0,
"serverSavedAt": time.time(),
"sourceClientId": "events_clear",
}
return {"ok": True} return {"ok": True}

View File

@@ -74,6 +74,10 @@ let lastClockServerSyncAt = 0;
let clockServerSyncInFlight = false; let clockServerSyncInFlight = false;
let applyingRemoteClockState = false; let applyingRemoteClockState = false;
let clockEditorIsOpen = false; let clockEditorIsOpen = false;
let eventsServerSyncInFlight = false;
let applyingRemoteEventsState = false;
let lastEventsSnapshot = "";
const formationState = { const formationState = {
@@ -326,7 +330,6 @@ function saveMatchState() {
}; };
localStorage.setItem(STORAGE_KEY, JSON.stringify(payload)); localStorage.setItem(STORAGE_KEY, JSON.stringify(payload));
syncClockStateToServer(false);
} catch (e) { } catch (e) {
console.warn("Не удалось сохранить состояние матча", e); console.warn("Не удалось сохранить состояние матча", e);
} }
@@ -860,6 +863,7 @@ function setMatchPeriod(period) {
startTimer(); startTimer();
saveMatchState(); saveMatchState();
syncClockStateToServer(true); syncClockStateToServer(true);
syncClockStateToServer(true);
updatePeriodButtons(); updatePeriodButtons();
return; return;
} }
@@ -1107,7 +1111,7 @@ function stepExtraTime(step) {
input.value = value; input.value = value;
} }
function clearMatchEvents() { async function clearMatchEvents() {
if (timerId) { if (timerId) {
clearInterval(timerId); clearInterval(timerId);
timerId = null; timerId = null;
@@ -1136,8 +1140,8 @@ function clearMatchEvents() {
renderEvents(); renderEvents();
saveMatchState(); saveMatchState();
syncClockStateToServer(true); await clearEventsOnServer();
clearEventsOnServer(); await syncClockStateToServer(true);
updateCoachCardButtons(); updateCoachCardButtons();
updatePeriodButtons(); updatePeriodButtons();
triggerVmixTimerAction("clear_match"); triggerVmixTimerAction("clear_match");
@@ -1680,18 +1684,39 @@ function restoreEventsClickOrder(serverEvents, persistedEvents) {
async function loadEventsFromServer() { async function loadEventsFromServer() {
if (!SESSION_TOKEN) return; if (!SESSION_TOKEN || applyingRemoteEventsState || clockEditorIsOpen) return;
if (eventsServerSyncInFlight) return;
eventsServerSyncInFlight = true;
try { try {
const persisted = getPersistedState(); const persisted = getPersistedState();
const persistedEvents = Array.isArray(persisted?.matchEvents) ? persisted.matchEvents : []; const persistedEvents = Array.isArray(persisted?.matchEvents) ? persisted.matchEvents : [];
const res = await fetch(`/admin/session/${SESSION_TOKEN}/events`); const res = await fetch(`/admin/session/${SESSION_TOKEN}/events`, { cache: "no-store" });
if (!res.ok) return; if (!res.ok) return;
const data = await res.json(); const data = await res.json();
if (!Array.isArray(data)) return; if (!Array.isArray(data)) return;
const snapshot = JSON.stringify(data.map((event) => ({
id: event.id ?? null,
side: event.side ?? "",
type: event.type ?? "",
player_name: event.player_name ?? "",
minute: event.minute ?? null,
seconds: event.seconds ?? 0,
meta: event.meta ?? null,
player_id: event.player_id ?? null,
player_out_id: event.player_out_id ?? null,
player_in_id: event.player_in_id ?? null
})));
if (snapshot === lastEventsSnapshot) return;
lastEventsSnapshot = snapshot;
applyingRemoteEventsState = true;
matchEvents = data.map((event) => { matchEvents = data.map((event) => {
const fallback = persistedEvents.find((p) => const fallback = persistedEvents.find((p) =>
String(p.type || "") === String(event.type || "") && String(p.type || "") === String(event.type || "") &&
@@ -1709,8 +1734,6 @@ async function loadEventsFromServer() {
minute: typeof event.minute === "string" && event.minute.includes("'") minute: typeof event.minute === "string" && event.minute.includes("'")
? event.minute ? event.minute
: (fallback?.minute || getEventDisplayMinuteBySeconds(event.seconds || 0, event.type)), : (fallback?.minute || getEventDisplayMinuteBySeconds(event.seconds || 0, event.type)),
player_id: event.player_id ?? fallback?.player_id ?? null, player_id: event.player_id ?? fallback?.player_id ?? null,
player_out_id: event.player_out_id ?? fallback?.player_out_id ?? null, player_out_id: event.player_out_id ?? fallback?.player_out_id ?? null,
player_in_id: event.player_in_id ?? fallback?.player_in_id ?? null player_in_id: event.player_in_id ?? fallback?.player_in_id ?? null
@@ -1719,8 +1742,6 @@ async function loadEventsFromServer() {
matchEvents = restoreEventsClickOrder(matchEvents, persistedEvents); matchEvents = restoreEventsClickOrder(matchEvents, persistedEvents);
scoreState = { home: 0, away: 0 }; scoreState = { home: 0, away: 0 };
matchEvents.forEach((event) => { matchEvents.forEach((event) => {
if (["goal", "penalty", "own_goal"].includes(event.type)) { if (["goal", "penalty", "own_goal"].includes(event.type)) {
@@ -1728,6 +1749,12 @@ async function loadEventsFromServer() {
} }
}); });
if (!matchEvents.length) {
activeSubstitution = null;
clearBenchSelection();
restoreOriginalLineupRows();
}
updateScoreUI(); updateScoreUI();
renderEvents(); renderEvents();
rebuildLineupsFromEvents(); rebuildLineupsFromEvents();
@@ -1736,9 +1763,17 @@ async function loadEventsFromServer() {
updatePeriodButtons(); updatePeriodButtons();
} catch (e) { } catch (e) {
console.warn("Не удалось загрузить события из БД", e); console.warn("Не удалось загрузить события из БД", e);
} finally {
applyingRemoteEventsState = false;
eventsServerSyncInFlight = false;
} }
} }
function initEventsStateSync() {
loadEventsFromServer();
setInterval(loadEventsFromServer, 2000);
}
async function sendDirectVmixCommands(commands) { async function sendDirectVmixCommands(commands) {
const items = Array.isArray(commands) const items = Array.isArray(commands)
? commands.map(x => String(x || "").trim()).filter(Boolean) ? commands.map(x => String(x || "").trim()).filter(Boolean)
@@ -2530,7 +2565,7 @@ if (!restored) {
saveMatchState(); saveMatchState();
} }
loadEventsFromServer(); initEventsStateSync();
initClockStateSync(); initClockStateSync();
window.addEventListener("beforeunload", saveMatchState); window.addEventListener("beforeunload", saveMatchState);
window.addEventListener("pagehide", saveMatchState); window.addEventListener("pagehide", saveMatchState);