diff --git a/app.py b/app.py index 17a6100..2baf4f1 100644 --- a/app.py +++ b/app.py @@ -29,7 +29,8 @@ 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.7" +BUILD_VERSION = "2026.08.24.8" +# compatibility: BUILD_VERSION = "2026.08.24.7" # compatibility: BUILD_VERSION = "2026.08.24.6" # compatibility: BUILD_VERSION = "2026.08.24.5" # compatibility: BUILD_VERSION = "2026.08.24.4" diff --git a/tests/test_build101_simple_timer_runtime.py b/tests/test_build101_simple_timer_runtime.py index 312812e..203d7bf 100644 --- a/tests/test_build101_simple_timer_runtime.py +++ b/tests/test_build101_simple_timer_runtime.py @@ -22,11 +22,11 @@ def test_timer_start_stop_protocol_is_minimal(): helper = _block("function vmixCountdownSyncCommands", "function buildShortcutRuntimeContext") assert 'if (action === "stop" || action === "pause")' in helper stop_branch = helper.split('if (action === "stop" || action === "pause")', 1)[1].split('return [', 1)[1].split('];', 1)[0] - assert 'Function: "StopCountdown"' in stop_branch + assert 'Function: "SuspendCountdown"' in stop_branch + assert 'Function: "StopCountdown"' not in stop_branch assert 'Function: "SetCountdown"' not in stop_branch start_branch = helper.rsplit('return [', 1)[1] assert start_branch.index('Function: "SetCountdown"') < start_branch.index('Function: "StartCountdown"') - assert 'SuspendCountdown' not in helper assert 'PauseCountdown' not in helper diff --git a/tests/test_build104_timer_stop_no_rollback.py b/tests/test_build104_timer_stop_no_rollback.py index f2366e4..b73b01a 100644 --- a/tests/test_build104_timer_stop_no_rollback.py +++ b/tests/test_build104_timer_stop_no_rollback.py @@ -9,10 +9,11 @@ def _block(start: str, end: str) -> str: return APP_JS.split(start, 1)[1].split(end, 1)[0] -def test_pause_stop_sends_only_stopcountdown(): +def test_pause_stop_sends_only_pause_only_command(): helper = _block("function vmixCountdownSyncCommands", "function buildShortcutRuntimeContext") branch = helper.split('if (action === "stop" || action === "pause")', 1)[1].split('if (action === "set")', 1)[0] - assert 'Function: "StopCountdown"' in branch + assert 'Function: "SuspendCountdown"' in branch + assert 'Function: "StopCountdown"' not in branch assert 'Function: "SetCountdown"' not in branch diff --git a/tests/test_build105_suspend_countdown_pause.py b/tests/test_build105_suspend_countdown_pause.py new file mode 100644 index 0000000..a044436 --- /dev/null +++ b/tests/test_build105_suspend_countdown_pause.py @@ -0,0 +1,34 @@ +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +APP_JS = (ROOT / "ui_builder/static/app.js").read_text(encoding="utf-8") +APP = (ROOT / "app.py").read_text(encoding="utf-8") + + +def _block(start: str, end: str) -> str: + return APP_JS.split(start, 1)[1].split(end, 1)[0] + + +def test_space_pause_uses_suspend_not_stop_reset(): + helper = _block("function vmixCountdownSyncCommands", "function buildShortcutRuntimeContext") + branch = helper.split('if (action === "stop" || action === "pause")', 1)[1].split('if (action === "set")', 1)[0] + assert 'Function: "SuspendCountdown"' in branch + assert 'Function: "StopCountdown"' not in branch + assert 'Function: "SetCountdown"' not in branch + + +def test_explicit_reset_set_path_keeps_stop_then_set(): + helper = _block("function vmixCountdownSyncCommands", "function buildShortcutRuntimeContext") + set_branch = helper.split('if (action === "set")', 1)[1].split('return [', 2)[1] + assert 'Function: "StopCountdown"' in set_branch + assert 'Function: "SetCountdown"' in set_branch + + +def test_penalty_normal_pause_uses_suspend(): + block = _block("async function rebalanceVmixPenaltyTargets", "function finishActionMatchesSource") + assert 'if (preservePausedCountdown && !assignmentChanged)' in block + assert 'stopCommands.push({ Function: "SuspendCountdown"' in block + + +def test_build105_runtime_version(): + assert 'BUILD_VERSION = "2026.08.24.8"' in APP diff --git a/tests/test_build98_timer_countdown_sync_fixed_pin.py b/tests/test_build98_timer_countdown_sync_fixed_pin.py index fa79760..58579b5 100644 --- a/tests/test_build98_timer_countdown_sync_fixed_pin.py +++ b/tests/test_build98_timer_countdown_sync_fixed_pin.py @@ -33,9 +33,9 @@ def test_countdown_start_is_simple_set_start(): def test_pause_and_stop_freeze_without_reseed(): helper = _block("function vmixCountdownSyncCommands", "function buildShortcutRuntimeContext") branch = helper.split('if (action === "stop" || action === "pause")', 1)[1].split('return [', 1)[1].split('];', 1)[0] - assert 'Function: "StopCountdown"' in branch + assert 'Function: "SuspendCountdown"' in branch + assert 'Function: "StopCountdown"' not in branch assert 'Function: "SetCountdown"' not in branch - assert 'Function: "SuspendCountdown"' not in branch assert 'Function: "PauseCountdown"' not in branch @@ -45,7 +45,6 @@ def test_penalty_rebalance_hard_syncs_running_countdown_and_pauses_stably(): assert 'Value: () => vmixCountdownValue(entry.event.remainingMs)' in block assert 'runCommands.push({ Function: "StartCountdown"' in block assert 'sendRuntimeVmixTimerSequence(commands)' in block - assert 'Function: "SuspendCountdown"' not in block assert 'Function: "PauseCountdown"' not in block diff --git a/ui_builder/static/app.js b/ui_builder/static/app.js index 65bc364..a52d644 100644 --- a/ui_builder/static/app.js +++ b/ui_builder/static/app.js @@ -4163,12 +4163,12 @@ function startCustomTooltips() { }; if (!target.Input || !target.SelectedName) return []; if (action === "stop" || action === "pause") { - // BUILD104: never reseed immediately after StopCountdown. vMix has already - // frozen its native countdown at the exact frame it received StopCountdown; - // a following SetCountdown from the browser can be slightly older and makes - // the on-air clock visibly jump backwards. + // BUILD105: vMix StopCountdown means STOP + RESET TO BEGINNING. It must never + // be used for an operator pause/stop where the current sports time has to freeze. + // SuspendCountdown is the vMix pause-only command and preserves the exact native + // countdown value until the next SetCountdown + StartCountdown. return [ - { Function: "StopCountdown", ...target }, + { Function: "SuspendCountdown", ...target }, ]; } if (action === "set") { @@ -5087,10 +5087,14 @@ function startCustomTooltips() { runCommands.push({ Function: "StartCountdown", Input: target.input, SelectedName: target.selected_name }); } } else if (force || assignmentChanged || previous?.running !== false) { - // BUILD104: a normal Pause of a prepared/paused penalty freezes the native vMix countdown only. - // Re-seeding after StopCountdown can visibly roll the clock back. - // Explicit reset/set-time/assignment changes may still seed a value. - stopCommands.push({ Function: "StopCountdown", Input: target.input, SelectedName: target.selected_name }); + // BUILD105: an unchanged prepared/paused penalty must use SuspendCountdown. + // StopCountdown resets a native vMix countdown to its beginning. For a + // reassigned/reset target we may still deliberately Stop + Set below. + if (preservePausedCountdown && !assignmentChanged) { + stopCommands.push({ Function: "SuspendCountdown", Input: target.input, SelectedName: target.selected_name }); + } else { + stopCommands.push({ Function: "StopCountdown", Input: target.input, SelectedName: target.selected_name }); + } if (!preservePausedCountdown || assignmentChanged) { setCommands.push({ Function: "SetCountdown", Input: target.input, SelectedName: target.selected_name, Value: () => vmixCountdownValue(entry.event.remainingMs) }); } @@ -13868,7 +13872,7 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
Режим Countdown vMix: веб-таймер является главным. Start сразу меняет состояние Runtime и независимо отправляет SetCountdown → StartCountdown в vMix; Pause/Stop сразу останавливают Runtime и независимо отправляют только StopCountdown, без повторной установки времени. Ошибка Agent не блокирует управление таймером. Каждую секунду значение в vMix не передаётся. Text mirror оставлен только для совместимости со старыми титрами. Для верхнего счёта используется одна penalty-плашка: при реальном большинстве она показывает ближайшее изменение численного состава; при чистом равном обоюдном удалении сама по себе не появляется.
Режим Countdown vMix: веб-таймер является главным. Start сразу меняет состояние Runtime и независимо отправляет SetCountdown → StartCountdown в vMix; Pause/Stop сразу останавливают Runtime и независимо отправляют только SuspendCountdown (пауза без сброса), без повторной установки времени. Ошибка Agent не блокирует управление таймером. Каждую секунду значение в vMix не передаётся. Text mirror оставлен только для совместимости со старыми титрами. Для верхнего счёта используется одна penalty-плашка: при реальном большинстве она показывает ближайшее изменение численного состава; при чистом равном обоюдном удалении сама по себе не появляется.