diff --git a/app.py b/app.py index 609edca..6155c0c 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.20.01" +BUILD_VERSION = "2026.08.20.12" # compatibility: BUILD_VERSION = "2026.08.19.28" # compatibility: BUILD_VERSION = "2026.08.19.24" # compatibility: BUILD_VERSION = "2026.08.19.22" @@ -226,6 +226,7 @@ async def vmix_command( install_ui_builder( app, + build_version=BUILD_VERSION, settings_dir=BASE_DIR / "settings", data_providers={ "hockey_stat2tv": hockey_stat2tv_data, diff --git a/hockey_data/service.py b/hockey_data/service.py index d395d23..dd634a4 100644 --- a/hockey_data/service.py +++ b/hockey_data/service.py @@ -5537,13 +5537,22 @@ class HockeyDataService: elif state_key not in {"5x5", ""}: state_label = state_key.replace("x", " on " if language_key == "en" else " на ") - # BUILD89: television output has only one power-play plate. Equal - # coincidental strength never gets a side caption. As soon as a real - # numerical advantage exists (including 4x3 / 3x4 with penalties on both - # benches), expose the configured state label only on the advantage side. - # `state_label` itself remains available in Mapping for custom graphics. - home_label = state_label if advantage_side == "home" else "" - away_label = state_label if advantage_side == "away" else "" + # BUILD91: Mapping captions describe both sides independently. The + # numerical-advantage side keeps the configured PP/state caption, while + # a penalized side that is not the advantage side gets a plain Penalty + # caption. This does not control whether the television penalty plate is + # on air; that remains the single-plate logic in the runtime. + penalty_caption = "Penalty" if language_key == "en" else "Удаление" + home_label = ( + state_label if advantage_side == "home" + else penalty_caption if home_penalties > 0 + else "" + ) + away_label = ( + state_label if advantage_side == "away" + else penalty_caption if away_penalties > 0 + else "" + ) phase_labels = { "regulation": ("Основное время", "Regulation"), "regular_overtime": ("Овертайм регулярки", "Regular OT"), diff --git a/tests/test_build81_penalty_mapping_sort_and_mutual_labels.py b/tests/test_build81_penalty_mapping_sort_and_mutual_labels.py index 3a1b368..2121ee7 100644 --- a/tests/test_build81_penalty_mapping_sort_and_mutual_labels.py +++ b/tests/test_build81_penalty_mapping_sort_and_mutual_labels.py @@ -62,5 +62,5 @@ def test_coincidental_penalties_do_not_fill_side_powerplay_labels(): value = HockeyDataService._strength_payload(settings, stage="regular", current_period="1", timer_state=timer_state, language="ru") assert value["advantage_side"] == "" assert value["state_key"] == "4x4" - assert value["home_label"] == "" - assert value["away_label"] == "" + assert value["home_label"] == "Удаление" + assert value["away_label"] == "Удаление" diff --git a/tests/test_build88_complex_penalty_display.py b/tests/test_build88_complex_penalty_display.py index d8e9e81..20af892 100644 --- a/tests/test_build88_complex_penalty_display.py +++ b/tests/test_build88_complex_penalty_display.py @@ -32,7 +32,7 @@ def test_complex_two_vs_one_keeps_real_strength_and_outputs_caption_on_advantage ) assert value["strength_label"] == "3×4" assert value["advantage_side"] == "away" - assert value["home_label"] == "" + assert value["home_label"] == "Удаление" assert value["away_label"] == "Играют в большинстве" @@ -50,7 +50,7 @@ def test_single_remaining_penalty_outputs_pp_caption_on_opposite_team(): language="ru", ) assert value["advantage_side"] == "away" - assert value["home_label"] == "" + assert value["home_label"] == "Удаление" assert value["away_label"] == "Играют в большинстве" diff --git a/tests/test_build89_single_penalty_plate_transition.py b/tests/test_build89_single_penalty_plate_transition.py index 76fc53a..f823c4c 100644 --- a/tests/test_build89_single_penalty_plate_transition.py +++ b/tests/test_build89_single_penalty_plate_transition.py @@ -32,8 +32,8 @@ def test_equal_coincidental_penalties_have_no_side_caption(): ) assert payload["strength_label"] == "4×4" assert payload["advantage_side"] == "" - assert payload["home_label"] == "" - assert payload["away_label"] == "" + assert payload["home_label"] == "Удаление" + assert payload["away_label"] == "Удаление" def test_two_vs_one_penalties_caption_only_advantage_team(): @@ -55,7 +55,7 @@ def test_two_vs_one_penalties_caption_only_advantage_team(): ) assert payload["strength_label"] == "3×4" assert payload["advantage_side"] == "away" - assert payload["home_label"] == "" + assert payload["home_label"] == "Удаление" assert payload["away_label"] == "Играют в большинстве" diff --git a/tests/test_build91_runtime_version_pbp_penalty_labels.py b/tests/test_build91_runtime_version_pbp_penalty_labels.py new file mode 100644 index 0000000..b86842d --- /dev/null +++ b/tests/test_build91_runtime_version_pbp_penalty_labels.py @@ -0,0 +1,81 @@ +from pathlib import Path + +from hockey_data.service import HockeyDataService + +ROOT = Path(__file__).resolve().parents[1] +RUNTIME = (ROOT / "ui_builder/static/runtime.html").read_text(encoding="utf-8") +INDEX = (ROOT / "ui_builder/static/index.html").read_text(encoding="utf-8") +INTEGRATION = (ROOT / "ui_builder/integration.py").read_text(encoding="utf-8") +APP_JS = (ROOT / "ui_builder/static/app.js").read_text(encoding="utf-8") +APP = (ROOT / "app.py").read_text(encoding="utf-8") + + +def _penalty(side: str, remaining_ms: int = 120000): + return { + "side": side, + "infraction": {"id": "minor"}, + "preset": "2m", + "durationMs": 120000, + "remainingMs": remaining_ms, + "finished": False, + } + + +def _strength(home=0, away=0, language="ru"): + timer_state = { + "penalty_board": { + "penalties": [_penalty("home") for _ in range(home)] + [_penalty("away") for _ in range(away)] + } + } + settings = { + "strength_regulation_skaters": 5, + "strength_regular_overtime_skaters": 3, + "strength_playoff_overtime_skaters": 5, + "strength_min_skaters": 3, + "strength_state_labels": { + "regulation": { + "5x4": {"ru": "Играют в большинстве", "en": "Power play"}, + "4x4": {"ru": "4 на 4", "en": "4 on 4"}, + "4x3": {"ru": "Большинство 4 на 3", "en": "4 on 3 power play"}, + } + }, + } + return HockeyDataService._strength_payload( + settings, + stage="regular", + current_period="1", + timer_state=timer_state, + language=language, + ) + + +def test_runtime_badge_is_server_build_version_placeholder(): + assert "BUILD __UI_BUILDER_BUILD_VERSION__" in RUNTIME + assert "BUILD __UI_BUILDER_BUILD_VERSION__" in INDEX + assert '.replace("__UI_BUILDER_BUILD_VERSION__", str(build_version or asset_version))' in INTEGRATION + assert "build_version=BUILD_VERSION" in APP + assert 'BUILD_VERSION = "2026.08.20.12"' in APP + + +def test_play_by_play_defaults_collapsed_until_operator_opens_it(): + assert "Object.prototype.hasOwnProperty.call(state.formValues, collapsedKey)" in APP_JS + assert ": true;" in APP_JS + + +def test_penalized_side_gets_penalty_caption_and_advantage_side_keeps_pp_caption(): + payload = _strength(home=1, away=0) + assert payload["home_label"] == "Удаление" + assert payload["away_label"] == "Играют в большинстве" + + +def test_equal_coincidental_penalties_expose_penalty_caption_on_both_mapping_sides(): + payload = _strength(home=1, away=1) + assert payload["advantage_side"] == "" + assert payload["home_label"] == "Удаление" + assert payload["away_label"] == "Удаление" + + +def test_english_penalty_caption(): + payload = _strength(home=1, away=0, language="en") + assert payload["home_label"] == "Penalty" + assert payload["away_label"] == "Power play" diff --git a/ui_builder/integration.py b/ui_builder/integration.py index 78051b6..bea9ca1 100644 --- a/ui_builder/integration.py +++ b/ui_builder/integration.py @@ -24,6 +24,7 @@ def _seed_config(source: Path, destination: Path) -> None: def install_ui_builder( app: FastAPI, *, + build_version: str = "", settings_dir: Path, data_providers: dict[str, DataProvider], source_labels: dict[str, str] | None = None, @@ -108,6 +109,7 @@ def install_ui_builder( html = ( template.replace("__UI_BUILDER_ASSETS__", assets_url.rstrip("/")) .replace("__UI_BUILDER_ASSET_VERSION__", asset_version) + .replace("__UI_BUILDER_BUILD_VERSION__", str(build_version or asset_version)) .replace("__UI_BUILDER_API__", page_api) .replace("__UI_BUILDER_AUTH_API__", auth_api) .replace("__UI_BUILDER_EDITOR__", editor_url) diff --git a/ui_builder/static/app.js b/ui_builder/static/app.js index 2276e41..e390185 100644 --- a/ui_builder/static/app.js +++ b/ui_builder/static/app.js @@ -9574,7 +9574,9 @@ function renderStandaloneHockeyPlayByPlayWindow() { let selectedPeriod = String(state.formValues[stateKey] || "all"); if (!segments.includes(selectedPeriod)) selectedPeriod = "all"; rememberUiNavigationState("hockey_play_by_play", "period", selectedPeriod, hockeyStatisticsSegmentLabel(selectedPeriod, language), { emit: false }); - const collapsed = Boolean(state.formValues[collapsedKey]); + const collapsed = Object.prototype.hasOwnProperty.call(state.formValues, collapsedKey) + ? Boolean(state.formValues[collapsedKey]) + : true; const labels = hockeyEventCategoryLabels(language); const items = sourceItems .filter((item) => selectedPeriod === "all" || String(item?.period || "") === selectedPeriod) diff --git a/ui_builder/static/index.html b/ui_builder/static/index.html index 4131614..1d9c12f 100644 --- a/ui_builder/static/index.html +++ b/ui_builder/static/index.html @@ -15,6 +15,7 @@ editorUrl: "__UI_BUILDER_EDITOR__", runtimeUrl: "__UI_BUILDER_RUNTIME__", editorHotkey: "__UI_BUILDER_EDITOR_HOTKEY__", + buildVersion: "__UI_BUILDER_BUILD_VERSION__", mode: "__UI_BUILDER_MODE__" }; @@ -156,7 +157,7 @@
RUNTIME INTERFACE
-
ИнтерфейсBUILD 2026.08.19.28
+
ИнтерфейсBUILD __UI_BUILDER_BUILD_VERSION__
diff --git a/ui_builder/static/runtime.html b/ui_builder/static/runtime.html index 545c064..8cacbd0 100644 --- a/ui_builder/static/runtime.html +++ b/ui_builder/static/runtime.html @@ -15,6 +15,7 @@ editorUrl: "__UI_BUILDER_EDITOR__", runtimeUrl: "__UI_BUILDER_RUNTIME__", editorHotkey: "__UI_BUILDER_EDITOR_HOTKEY__", + buildVersion: "__UI_BUILDER_BUILD_VERSION__", mode: "runtime" }; @@ -24,7 +25,7 @@
RUNTIME INTERFACE
-
ИнтерфейсBUILD 2026.08.19.28
+
ИнтерфейсBUILD __UI_BUILDER_BUILD_VERSION__