This commit is contained in:
2026-09-23 13:37:09 +03:00
parent 6e2f6a8dc1
commit 0c2d3f31f1
3 changed files with 87399 additions and 63975 deletions

File diff suppressed because it is too large Load Diff

2
app.py
View File

@@ -30,7 +30,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.09.23.2"
BUILD_VERSION = "2026.09.23.3"
# compatibility: BUILD_VERSION = "2026.08.26.1"
# compatibility: BUILD_VERSION = "2026.08.25.1"
# compatibility: BUILD_VERSION = "2026.08.24.15"

View File

@@ -101,6 +101,10 @@
vmixTimerMirrors: new Map(),
vmixPenaltyMirrors: new Map(),
activeHockeyVmixTimerSteps: new Set(),
// BUILD156: remember which native game-countdown targets have already received
// a Runtime value in this browser session. This lets the first Space seed the
// current web time exactly once, while later Pause/Resume remains native-only.
vmixGameCountdownSeededTargets: new Set(),
vmixPenaltyTargetAssignments: new Map(),
hockeyPenaltyAdvantageCycle: { hadAdvantage: false, lastAdvantageSide: "" },
hockeyPenaltyMappingContextSignature: "",
@@ -4198,6 +4202,19 @@ function startCustomTooltips() {
return `${padTimer(hours)}:${padTimer(minutes)}:${padTimer(seconds)}`;
}
function vmixGameCountdownTargetKey(input, selectedName) {
return `${String(input || "").trim()}\u0000${String(selectedName || "").trim()}`;
}
function markVmixGameCountdownSeeded(input, selectedName) {
const key = vmixGameCountdownTargetKey(input, selectedName);
if (key !== "\u0000") state.vmixGameCountdownSeededTargets.add(key);
}
function isVmixGameCountdownSeeded(input, selectedName) {
return state.vmixGameCountdownSeededTargets.has(vmixGameCountdownTargetKey(input, selectedName));
}
function vmixCountdownSyncCommands(input, selectedName, millisecondsProvider, action = "start") {
// BUILD107: Runtime remains the source of truth, but normal operator control no
// longer uses StopCountdown because vMix defines it as Stop + Reset.
@@ -4926,6 +4943,7 @@ function startCustomTooltips() {
commands.push(...vmixGameCountdownControlCommands(input, selectedName, valueProvider, "start"));
} else if (eventName === "timer_restart") {
commands.push(...vmixGameCountdownControlCommands(input, selectedName, valueProvider, "set"));
markVmixGameCountdownSeeded(input, selectedName);
commands.push(...vmixGameCountdownControlCommands(input, selectedName, valueProvider, "start"));
} else if (eventName === "timer_pause") {
commands.push(...vmixGameCountdownControlCommands(input, selectedName, valueProvider, "pause"));
@@ -4933,6 +4951,7 @@ function startCustomTooltips() {
commands.push(...vmixGameCountdownControlCommands(input, selectedName, valueProvider, "stop"));
} else if (["timer_reset", "timer_set_time", "timer_add_time", "timer_subtract_time"].includes(eventName)) {
commands.push(...vmixGameCountdownControlCommands(input, selectedName, valueProvider, "set"));
markVmixGameCountdownSeeded(input, selectedName);
}
}
if (!commands.length) return false;
@@ -4971,6 +4990,7 @@ function startCustomTooltips() {
commands.push(...vmixGameCountdownControlCommands(
input, selectedName, () => timerState.currentMs, "set"
));
markVmixGameCountdownSeeded(input, selectedName);
}
});
if (!commands.length) return { ok: true, applied: 0, requested: 0 };
@@ -5508,8 +5528,16 @@ function startCustomTooltips() {
step.game_vmix_input, step.game_vmix_selected_name, () => gameTimerState.currentMs, "pause"
));
} else {
// BUILD108 Space start/resume: native start only. The countdown was seeded
// when the operator set/reset the period time (or by F1 before first start).
// BUILD156: the first Start for this exact Input + SelectedName must also
// transfer the current Runtime value. This fixes a fresh vMix title where
// StartCountdown arrived but the configured web time never did. After the
// target is seeded, normal Resume is StartCountdown-only (BUILD108 rule).
if (!isVmixGameCountdownSeeded(step.game_vmix_input, step.game_vmix_selected_name)) {
commands.push(...vmixGameCountdownControlCommands(
step.game_vmix_input, step.game_vmix_selected_name, () => gameTimerState.currentMs, "set"
));
markVmixGameCountdownSeeded(step.game_vmix_input, step.game_vmix_selected_name);
}
commands.push(...vmixGameCountdownControlCommands(
step.game_vmix_input, step.game_vmix_selected_name, () => gameTimerState.currentMs, "start"
));
@@ -14718,7 +14746,7 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
<div class="shortcut-finish-action-list">${timerFinishActionRows(step)}</div>
<div class="shortcut-vmix-inventory-note"><span>${escapeHtml(shortcutInventoryLabel())}</span><small>Для каждого countdown теперь обязательно выбирается конкретный Text / SelectedName. Это исключает отправку времени в первый текстовый элемент по умолчанию.</small></div>
<p class="shortcut-step-note">Режим <b>Countdown vMix</b>: время передаётся в vMix только при явной установке/сбросе (например, 20:00 или 05:00). После этого Space не синхронизирует значение: Start/Resume отправляет только <code>StartCountdown</code>, Pause/Stop — только <code>PauseCountdown</code>. Это исключает скачки времени при паузе. <code>StopCountdown</code> в обычном управлении не используется. Команды идут в Agent без ACK. <b>Text mirror</b> оставлен только для совместимости со старыми титрами. Для верхнего счёта используется одна penalty-плашка: при реальном большинстве она показывает ближайшее изменение численного состава; при чистом равном обоюдном удалении сама по себе не появляется.</p>
<p class="shortcut-step-note">Режим <b>Countdown vMix</b>: при первом Start для выбранного Input + SelectedName текущее веб-время один раз передаётся через <code>ChangeCountdown</code>, затем отправляется <code>StartCountdown</code>. После этого Space не пересылает значение: Resume отправляет только <code>StartCountdown</code>, Pause/Stop — только <code>PauseCountdown</code>. Явная установка/сброс времени также обновляет vMix через <code>ChangeCountdown</code>. Это исключает скачки времени при паузе. <code>StopCountdown</code> в обычном управлении не используется. Команды идут в Agent без ACK. <b>Text mirror</b> оставлен только для совместимости со старыми титрами. Для верхнего счёта используется одна penalty-плашка: при реальном большинстве она показывает ближайшее изменение численного состава; при чистом равном обоюдном удалении сама по себе не появляется.</p>
</div>`;
} else if (step.type === "delay") {
body.innerHTML = `<div class="shortcut-step-grid"><label>Задержка, мс<input type="number" min="0" max="10000" step="10" data-step-field="milliseconds" value="${Number(step.milliseconds) || 0}"></label></div>`;