From f1605470ff40daf1df65e5f669099cd1446b643b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=AE=D1=80=D0=B8=D0=B9=20=D0=A7=D0=B5=D1=80=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=BA=D0=BE?= Date: Mon, 24 Aug 2026 17:22:02 +0300 Subject: [PATCH] test 5 --- app.py | 2 +- hockey_data/agent_bridge.py | 2 +- tests/test_build107_pause_change_countdown.py | 46 ++++++++++++++++++ ui_builder/static/app.js | 48 +++++++++++-------- 4 files changed, 75 insertions(+), 23 deletions(-) create mode 100644 tests/test_build107_pause_change_countdown.py diff --git a/app.py b/app.py index f0c9908..ceac762 100644 --- a/app.py +++ b/app.py @@ -29,7 +29,7 @@ from ui_builder import install_ui_builder from khl_site.khl_data_center import APP as khl_site_app BASE_DIR = Path(__file__).resolve().parent -BUILD_VERSION = "2026.08.24.9" +BUILD_VERSION = "2026.08.24.10" # compatibility: BUILD_VERSION = "2026.08.24.8" # compatibility: BUILD_VERSION = "2026.08.24.7" # compatibility: BUILD_VERSION = "2026.08.24.6" diff --git a/hockey_data/agent_bridge.py b/hockey_data/agent_bridge.py index 62123a6..7fb0c20 100644 --- a/hockey_data/agent_bridge.py +++ b/hockey_data/agent_bridge.py @@ -1539,7 +1539,7 @@ class VmixAgentHub: if no_ack_delivery: # BUILD106: realtime operator traffic bypasses Mapping's ACK queue. The new # timer-fast-ordered mode deliberately sends each timer command as its own WebSocket - # frame in strict order (Stop -> Set -> Start) and never waits for Agent/vMix ACK. + # frame in strict caller order and never waits for Agent/vMix ACK. # Legacy timer-fast keeps vmix.batch compatibility. The shared realtime lock prevents # interleaving with F-key traffic without reintroducing the slow Mapping lock. async with self._device_vmix_shortcut_send_lock(target_device_id): diff --git a/tests/test_build107_pause_change_countdown.py b/tests/test_build107_pause_change_countdown.py new file mode 100644 index 0000000..baf4ed7 --- /dev/null +++ b/tests/test_build107_pause_change_countdown.py @@ -0,0 +1,46 @@ +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +APP_JS = (ROOT / "ui_builder" / "static" / "app.js").read_text(encoding="utf-8") +APP_PY = (ROOT / "app.py").read_text(encoding="utf-8") + + +def helper_block(): + start = APP_JS.index("function vmixCountdownSyncCommands") + end = APP_JS.index("function buildShortcutRuntimeContext", start) + return APP_JS[start:end] + + +def test_pause_uses_pause_and_change_without_stop_reset(): + block = helper_block() + pause_start = block.index('if (action === "pause" || action === "stop")') + set_start = block.index('if (action === "set")', pause_start) + pause_branch = block[pause_start:set_start] + assert 'Function: "PauseCountdown"' in pause_branch + assert 'Function: "ChangeCountdown"' in pause_branch + assert 'Function: "StopCountdown"' not in pause_branch + assert pause_branch.index('Function: "PauseCountdown"') < pause_branch.index('Function: "ChangeCountdown"') + + +def test_start_uses_change_then_start_without_stop_or_set_duration(): + block = helper_block() + start_branch = block[block.index("// Start/Resume:"):] + assert 'Function: "ChangeCountdown"' in start_branch + assert 'Function: "StartCountdown"' in start_branch + assert 'Function: "StopCountdown"' not in start_branch + assert 'Function: "SetCountdown"' not in start_branch + assert start_branch.index('Function: "ChangeCountdown"') < start_branch.index('Function: "StartCountdown"') + + +def test_set_only_changes_current_position_and_does_not_toggle_pause(): + block = helper_block() + set_start = block.index('if (action === "set")') + start_branch = block.index("// Start/Resume:", set_start) + set_branch = block[set_start:start_branch] + assert 'Function: "ChangeCountdown"' in set_branch + assert 'Function: "PauseCountdown"' not in set_branch + assert 'Function: "StopCountdown"' not in set_branch + + +def test_runtime_version(): + assert 'BUILD_VERSION = "2026.08.24.10"' in APP_PY diff --git a/ui_builder/static/app.js b/ui_builder/static/app.js index 748eb39..1397c87 100644 --- a/ui_builder/static/app.js +++ b/ui_builder/static/app.js @@ -4153,12 +4153,10 @@ function startCustomTooltips() { } function vmixCountdownSyncCommands(input, selectedName, millisecondsProvider, action = "start") { - // BUILD106: Runtime is the ONLY source of truth for game/penalty time. - // Important vMix detail: SetCountdown changes the countdown Duration; it does not - // reliably replace the current position of a countdown that was previously started - // or suspended. Therefore every operator state change performs a hard reseed: - // freeze title rendering -> reset native countdown -> set Runtime value -> unfreeze. - // Start/Resume then starts from that freshly seeded Runtime value. + // BUILD107: Runtime remains the source of truth, but normal operator control no + // longer uses StopCountdown because vMix defines it as Stop + Reset. + // ChangeCountdown updates the CURRENT countdown position; SetCountdown only changes + // Duration. PauseCountdown is used only for an actual Runtime pause transition. const target = { Input: String(input || "").trim(), SelectedName: String(selectedName || "").trim() }; const renderTarget = { Input: target.Input }; const currentValue = () => { @@ -4167,24 +4165,32 @@ function startCustomTooltips() { }; if (!target.Input || !target.SelectedName) return []; - const reseedStopped = [ - { Function: "PauseRender", ...renderTarget }, - { Function: "StopCountdown", ...target }, - { Function: "SetCountdown", ...target, Value: currentValue }, - { Function: "ResumeRender", ...renderTarget }, - ]; - - if (action === "stop" || action === "pause" || action === "set") { - // Do not trust the native vMix current position on Pause. Freeze at the exact - // Runtime value and leave the native countdown stopped. The next Resume will - // hard-reseed again before StartCountdown. - return reseedStopped; + if (action === "pause" || action === "stop") { + // Space pause: toggle the native countdown into Pause, then pin its current value + // to the exact Runtime time. No StopCountdown reset is allowed in this path. + return [ + { Function: "PauseRender", ...renderTarget }, + { Function: "PauseCountdown", ...target }, + { Function: "ChangeCountdown", ...target, Value: currentValue }, + { Function: "ResumeRender", ...renderTarget }, + ]; } + if (action === "set") { + // Scoreboard/F1 pre-sync while Runtime is not running: update only the current + // vMix position. Do not toggle PauseCountdown because it is a Pause/Resume toggle. + return [ + { Function: "PauseRender", ...renderTarget }, + { Function: "ChangeCountdown", ...target, Value: currentValue }, + { Function: "ResumeRender", ...renderTarget }, + ]; + } + + // Start/Resume: set the native CURRENT position from Runtime and then run it. + // This avoids both StopCountdown reset and SetCountdown duration-only semantics. return [ { Function: "PauseRender", ...renderTarget }, - { Function: "StopCountdown", ...target }, - { Function: "SetCountdown", ...target, Value: currentValue }, + { Function: "ChangeCountdown", ...target, Value: currentValue }, { Function: "ResumeRender", ...renderTarget }, { Function: "StartCountdown", ...target }, ]; @@ -13915,7 +13921,7 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
${timerFinishActionRows(step)}
${escapeHtml(shortcutInventoryLabel())}Для каждого countdown теперь обязательно выбирается конкретный Text / SelectedName. Это исключает отправку времени в первый текстовый элемент по умолчанию.
-

Режим Countdown vMix: веб-таймер является единственным источником времени. Перед Start/Resume vMix выполняет StopCountdown → SetCountdown(время Runtime) → StartCountdown; при Pause/Stop vMix выполняет StopCountdown → SetCountdown(время Runtime) и остаётся остановленным на точном веб-времени. Команды идут в Agent без ACK отдельными кадрами строго по порядку. Каждую секунду значение в vMix не передаётся. Text mirror оставлен только для совместимости со старыми титрами. Для верхнего счёта используется одна penalty-плашка: при реальном большинстве она показывает ближайшее изменение численного состава; при чистом равном обоюдном удалении сама по себе не появляется.

+

Режим Countdown vMix: веб-таймер является источником времени. Перед Start/Resume vMix выполняет ChangeCountdown(время Runtime) → StartCountdown; при Pause/Stop выполняет PauseCountdown → ChangeCountdown(время Runtime). StopCountdown в обычном управлении не используется, потому что он сбрасывает countdown. Команды идут в Agent без ACK отдельными кадрами строго по порядку. Каждую секунду значение в vMix не передаётся. Text mirror оставлен только для совместимости со старыми титрами. Для верхнего счёта используется одна penalty-плашка: при реальном большинстве она показывает ближайшее изменение численного состава; при чистом равном обоюдном удалении сама по себе не появляется.

`; } else if (step.type === "delay") { body.innerHTML = `
`;