diff --git a/app.py b/app.py index fe7a85c..6ed0bcf 100644 --- a/app.py +++ b/app.py @@ -1222,6 +1222,18 @@ def api_clear_events(session_token: str): match_id = session_row[1] 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} diff --git a/static/script.js b/static/script.js index 2c1146c..3aa2d08 100644 --- a/static/script.js +++ b/static/script.js @@ -74,6 +74,10 @@ let lastClockServerSyncAt = 0; let clockServerSyncInFlight = false; let applyingRemoteClockState = false; let clockEditorIsOpen = false; +let eventsServerSyncInFlight = false; +let applyingRemoteEventsState = false; +let lastEventsSnapshot = ""; + const formationState = { @@ -326,7 +330,6 @@ function saveMatchState() { }; localStorage.setItem(STORAGE_KEY, JSON.stringify(payload)); - syncClockStateToServer(false); } catch (e) { console.warn("Не удалось сохранить состояние матча", e); } @@ -860,6 +863,7 @@ function setMatchPeriod(period) { startTimer(); saveMatchState(); syncClockStateToServer(true); + syncClockStateToServer(true); updatePeriodButtons(); return; } @@ -1107,7 +1111,7 @@ function stepExtraTime(step) { input.value = value; } -function clearMatchEvents() { +async function clearMatchEvents() { if (timerId) { clearInterval(timerId); timerId = null; @@ -1136,8 +1140,8 @@ function clearMatchEvents() { renderEvents(); saveMatchState(); - syncClockStateToServer(true); - clearEventsOnServer(); + await clearEventsOnServer(); + await syncClockStateToServer(true); updateCoachCardButtons(); updatePeriodButtons(); triggerVmixTimerAction("clear_match"); @@ -1680,18 +1684,39 @@ function restoreEventsClickOrder(serverEvents, persistedEvents) { async function loadEventsFromServer() { - if (!SESSION_TOKEN) return; + if (!SESSION_TOKEN || applyingRemoteEventsState || clockEditorIsOpen) return; + if (eventsServerSyncInFlight) return; + + eventsServerSyncInFlight = true; try { const persisted = getPersistedState(); 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; const data = await res.json(); 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) => { const fallback = persistedEvents.find((p) => String(p.type || "") === String(event.type || "") && @@ -1709,8 +1734,6 @@ async function loadEventsFromServer() { minute: typeof event.minute === "string" && event.minute.includes("'") ? event.minute : (fallback?.minute || getEventDisplayMinuteBySeconds(event.seconds || 0, event.type)), - - player_id: event.player_id ?? fallback?.player_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 @@ -1719,8 +1742,6 @@ async function loadEventsFromServer() { matchEvents = restoreEventsClickOrder(matchEvents, persistedEvents); - - scoreState = { home: 0, away: 0 }; matchEvents.forEach((event) => { if (["goal", "penalty", "own_goal"].includes(event.type)) { @@ -1728,6 +1749,12 @@ async function loadEventsFromServer() { } }); + if (!matchEvents.length) { + activeSubstitution = null; + clearBenchSelection(); + restoreOriginalLineupRows(); + } + updateScoreUI(); renderEvents(); rebuildLineupsFromEvents(); @@ -1736,9 +1763,17 @@ async function loadEventsFromServer() { updatePeriodButtons(); } catch (e) { console.warn("Не удалось загрузить события из БД", e); + } finally { + applyingRemoteEventsState = false; + eventsServerSyncInFlight = false; } } +function initEventsStateSync() { + loadEventsFromServer(); + setInterval(loadEventsFromServer, 2000); +} + async function sendDirectVmixCommands(commands) { const items = Array.isArray(commands) ? commands.map(x => String(x || "").trim()).filter(Boolean) @@ -2530,7 +2565,7 @@ if (!restored) { saveMatchState(); } -loadEventsFromServer(); +initEventsStateSync(); initClockStateSync(); window.addEventListener("beforeunload", saveMatchState); window.addEventListener("pagehide", saveMatchState);