diff --git a/app.py b/app.py
index be3d1f4..2f69d58 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.26.7"
+BUILD_VERSION = "2026.08.26.8"
# compatibility: BUILD_VERSION = "2026.08.26.1"
# compatibility: BUILD_VERSION = "2026.08.25.1"
# compatibility: BUILD_VERSION = "2026.08.24.15"
diff --git a/ui_builder/static/app.js b/ui_builder/static/app.js
index d073430..8aba320 100644
--- a/ui_builder/static/app.js
+++ b/ui_builder/static/app.js
@@ -2538,14 +2538,13 @@ function startCustomTooltips() {
prevent_default: sequence.prevent_default !== false,
allow_in_inputs: Boolean(sequence.allow_in_inputs),
toggle_all_overlays_on_repeat: Boolean(sequence.toggle_all_overlays_on_repeat),
- // BUILD117: keep the legacy toggle flag for config compatibility, but let the
- // operator choose exactly which vMix overlay channels are taken OUT on repeat.
- // Existing scoreboard configs had no list, so the safe default remains 1/2/3.
- // BUILD118 exposes vMix 29 Overlay channels 1..8 while leaving 4..8 untouched by default.
+ // BUILD121: overlay repeat-out targets are optional. Generic shortcuts do not
+ // necessarily control graphics, so an empty selection is valid and is the default.
+ // Existing saved selections are preserved exactly. The dedicated scoreboard preset
+ // still explicitly creates O1/O2/O3 because that preset is a graphics shortcut.
repeat_overlay_outs: (() => {
- const raw = Array.isArray(sequence.repeat_overlay_outs) ? sequence.repeat_overlay_outs : [1, 2, 3];
- const normalized = [...new Set(raw.map((value) => Number(value)).filter((value) => Number.isInteger(value) && value >= 1 && value <= 8))];
- return normalized.length ? normalized : [1, 2, 3];
+ const raw = Array.isArray(sequence.repeat_overlay_outs) ? sequence.repeat_overlay_outs : [];
+ return [...new Set(raw.map((value) => Number(value)).filter((value) => Number.isInteger(value) && value >= 1 && value <= 8))];
})(),
sync_hockey_team_states: sequence.sync_hockey_team_states === undefined
? Boolean(sequence.toggle_all_overlays_on_repeat)
@@ -5564,22 +5563,25 @@ function startCustomTooltips() {
// BUILD118: repeat OUT is configurable across vMix Overlay channels 1..8. Do not use
// OverlayInputAllOff: explicit OverlayInputNOut preserves each configured
// transition-out and avoids touching unrelated channels that are not selected.
- const repeatOverlayOuts = (Array.isArray(sequence.repeat_overlay_outs) ? sequence.repeat_overlay_outs : [1, 2, 3])
+ const selectedOverlayOuts = (Array.isArray(sequence.repeat_overlay_outs) ? sequence.repeat_overlay_outs : [])
.map((value) => Number(value))
.filter((value, index, values) => Number.isInteger(value) && value >= 1 && value <= 8 && values.indexOf(value) === index);
- const selectedOverlayOuts = repeatOverlayOuts.length ? repeatOverlayOuts : [1, 2, 3];
- await sendRuntimeVmixSequence(
- selectedOverlayOuts.map((number) => ({ Function: `OverlayInput${number}Out` })),
- execution,
- );
- if (sequence.sync_hockey_team_states) hockeyClearTeamStateOverlayTracking();
- state.shortcutSequenceOverlayState.set(sequence.id, false);
- state.quickPanelOnAirSequences.delete(String(sequence.id || ""));
- refreshQuickPanelOnAirClasses();
- window.dispatchEvent(new CustomEvent("ui-builder:shortcut-sequence-finish", { detail: { sequence: clone(sequence), meta: { ...meta, overlay_group_action: "off" } } }));
- if (meta.source === "quick-panel-button") toast(`Выполнено: ${sequence.name} · кнопка «${meta.button_label || sequence.name}»`);
- else if (String(meta.source || "").startsWith("keyboard")) toast(`Шорткат выполнен: ${sequence.name}`);
- return true;
+ // Empty is intentionally valid: a shortcut may have no graphics/Overlay action.
+ // In that case do not swallow the repeat press; execute its normal steps below.
+ if (selectedOverlayOuts.length) {
+ await sendRuntimeVmixSequence(
+ selectedOverlayOuts.map((number) => ({ Function: `OverlayInput${number}Out` })),
+ execution,
+ );
+ if (sequence.sync_hockey_team_states) hockeyClearTeamStateOverlayTracking();
+ state.shortcutSequenceOverlayState.set(sequence.id, false);
+ state.quickPanelOnAirSequences.delete(String(sequence.id || ""));
+ refreshQuickPanelOnAirClasses();
+ window.dispatchEvent(new CustomEvent("ui-builder:shortcut-sequence-finish", { detail: { sequence: clone(sequence), meta: { ...meta, overlay_group_action: "off" } } }));
+ if (meta.source === "quick-panel-button") toast(`Выполнено: ${sequence.name} · кнопка «${meta.button_label || sequence.name}»`);
+ else if (String(meta.source || "").startsWith("keyboard")) toast(`Шорткат выполнен: ${sequence.name}`);
+ return true;
+ }
}
if (sequence.is_scoreboard_sequence) {
await syncConfiguredScoreboardCountdownsToRuntime();
@@ -14391,7 +14393,7 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
снять:
- ${[1, 2, 3, 4, 5, 6, 7, 8].map((number) => ``).join("")}
+ ${[1, 2, 3, 4, 5, 6, 7, 8].map((number) => ``).join("")}
@@ -14430,11 +14432,8 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
const selected = [...card.querySelectorAll("[data-sequence-overlay-out]:checked")]
.map((item) => Number(item.dataset.sequenceOverlayOut))
.filter((value) => Number.isInteger(value) && value >= 1 && value <= 8);
- if (!selected.length) {
- event.target.checked = true;
- toast("Для повторного снятия выберите хотя бы один Overlay");
- return;
- }
+ // BUILD121: zero selected overlays is valid. Some shortcuts do not put any
+ // graphics on air, so there is nothing that must be taken OUT on repeat.
sequence.repeat_overlay_outs = selected;
}));
card.querySelector("[data-sequence-scoreboard]").addEventListener("change", (event) => {