удаления поправлены, сортировка элементов в титре поправлено, обоюдное удаление поправлено

This commit is contained in:
2026-08-20 12:48:11 +03:00
parent 81fd030846
commit 72d89ec02b
8 changed files with 142 additions and 38 deletions

View File

@@ -68,14 +68,14 @@ DEFAULT_CONTEXT_VARIABLES: tuple[dict[str, Any], ...] = (
{"key": "selected_penalty_team_penalty", "label": "Выбранное удаление командное (1/0)", "category": "Выделенные", "entity_type": "penalty", "scope": "match", "source_type": "selection", "value_type": "text"},
{"key": "selected_penalty_team_id", "label": "Команда выбранного удаления", "category": "Выделенные", "entity_type": "team", "scope": "match", "source_type": "selection"},
{"key": "selected_home_penalty_id", "label": "Удаление левой команды — ID удаления", "category": "Удаления — левая команда", "entity_type": "penalty", "scope": "match", "source_type": "selection"},
{"key": "selected_home_penalty_player_id", "label": "Удаление левой команды — игрок external ID", "category": "Удаления — левая команда", "entity_type": "player", "scope": "match", "source_type": "selection"},
{"key": "selected_home_penalty_player_db_id", "label": "Удаление левой команды — игрок DB ID", "category": "Удаления — левая команда", "entity_type": "player", "scope": "match", "source_type": "selection"},
{"key": "selected_home_penalty_team_penalty", "label": "Удаление левой команды — командное (1/0)", "category": "Удаления — левая команда", "entity_type": "penalty", "scope": "match", "source_type": "selection", "value_type": "text"},
{"key": "selected_away_penalty_id", "label": "Удаление правой команды — ID удаления", "category": "Удаления — правая команда", "entity_type": "penalty", "scope": "match", "source_type": "selection"},
{"key": "selected_away_penalty_player_id", "label": "Удаление правой команды — игрок external ID", "category": "Удаления — правая команда", "entity_type": "player", "scope": "match", "source_type": "selection"},
{"key": "selected_away_penalty_player_db_id", "label": "Удаление правой команды — игрок DB ID", "category": "Удаления — правая команда", "entity_type": "player", "scope": "match", "source_type": "selection"},
{"key": "selected_away_penalty_team_penalty", "label": "Удаление правой команды — командное (1/0)", "category": "Удаления — правая команда", "entity_type": "penalty", "scope": "match", "source_type": "selection", "value_type": "text"},
{"key": "selected_home_penalty_id", "label": "Удаление левой команды — ID удаления", "category": "Удаления — левая команда", "entity_type": "penalty", "scope": "match_shared", "source_type": "selection"},
{"key": "selected_home_penalty_player_id", "label": "Удаление левой команды — игрок external ID", "category": "Удаления — левая команда", "entity_type": "player", "scope": "match_shared", "source_type": "selection"},
{"key": "selected_home_penalty_player_db_id", "label": "Удаление левой команды — игрок DB ID", "category": "Удаления — левая команда", "entity_type": "player", "scope": "match_shared", "source_type": "selection"},
{"key": "selected_home_penalty_team_penalty", "label": "Удаление левой команды — командное (1/0)", "category": "Удаления — левая команда", "entity_type": "penalty", "scope": "match_shared", "source_type": "selection", "value_type": "text"},
{"key": "selected_away_penalty_id", "label": "Удаление правой команды — ID удаления", "category": "Удаления — правая команда", "entity_type": "penalty", "scope": "match_shared", "source_type": "selection"},
{"key": "selected_away_penalty_player_id", "label": "Удаление правой команды — игрок external ID", "category": "Удаления — правая команда", "entity_type": "player", "scope": "match_shared", "source_type": "selection"},
{"key": "selected_away_penalty_player_db_id", "label": "Удаление правой команды — игрок DB ID", "category": "Удаления — правая команда", "entity_type": "player", "scope": "match_shared", "source_type": "selection"},
{"key": "selected_away_penalty_team_penalty", "label": "Удаление правой команды — командное (1/0)", "category": "Удаления — правая команда", "entity_type": "penalty", "scope": "match_shared", "source_type": "selection", "value_type": "text"},
{"key": "selected_penalty_infraction_id", "label": "Нарушение выбранного удаления", "category": "Выделенные", "entity_type": "penalty", "scope": "match", "source_type": "selection"},
{"key": "selected_penalty_preset_id", "label": "Длительность выбранного удаления", "category": "Выделенные", "entity_type": "penalty", "scope": "match", "source_type": "selection"},
{"key": "selected_penalty_side", "label": "Сторона выбранного удаления", "category": "Выделенные", "entity_type": "penalty", "scope": "match", "source_type": "selection"},
@@ -92,7 +92,22 @@ DEFAULT_CONTEXT_VARIABLES: tuple[dict[str, Any], ...] = (
{"key": "compare_player2_id", "label": "Игрок сравнения 2", "category": "Сравнение", "entity_type": "player", "scope": "session", "source_type": "manual"},
{"key": "compare_team1_id", "label": "Команда сравнения 1", "category": "Сравнение", "entity_type": "team", "scope": "session", "source_type": "manual"},
{"key": "compare_team2_id", "label": "Команда сравнения 2", "category": "Сравнение", "entity_type": "team", "scope": "session", "source_type": "manual"},
)
)
# These values describe the live state of the match itself, not a private UI
# selection of one account. They must be visible to Mapping regardless of which
# operator/admin account opened the Mapping editor.
MATCH_SHARED_CONTEXT_KEYS: frozenset[str] = frozenset({
"selected_home_penalty_id",
"selected_home_penalty_player_id",
"selected_home_penalty_player_db_id",
"selected_home_penalty_team_penalty",
"selected_away_penalty_id",
"selected_away_penalty_player_id",
"selected_away_penalty_player_db_id",
"selected_away_penalty_team_penalty",
})
DEFAULT_SQL_SOURCES: tuple[dict[str, Any], ...] = (
@@ -211,7 +226,17 @@ class MappingDataService:
def ensure_defaults(self) -> None:
now = _utcnow()
with self.database.session() as session:
existing_vars = {row.key for row in session.scalars(select(MappingContextVariable))}
existing_var_rows = {row.key: row for row in session.scalars(select(MappingContextVariable))}
existing_vars = set(existing_var_rows)
# Build78 initially stored automatic HOME/AWAY penalty identifiers as
# account-scoped match values. Migrate existing databases in-place so
# an operator can set a penalty and an admin can immediately see it in Mapping.
for key in MATCH_SHARED_CONTEXT_KEYS:
row = existing_var_rows.get(key)
if row is not None and row.scope != "match_shared":
row.scope = "match_shared"
row.updated_by = "system"
row.updated_at = now
for index, item in enumerate(DEFAULT_CONTEXT_VARIABLES):
if item["key"] in existing_vars:
continue
@@ -505,6 +530,7 @@ class MappingDataService:
if scope == "global": return "global"
if scope == "account": return f"account:{user.id}"
if scope in {"session", "temporary"}: return f"session:{token or user.id}"
if scope in {"match_shared", "shared_match"}: return f"match:{game_id or '-'}"
return f"match:{game_id or '-'}:account:{user.id}"
def resolve_context(self, user: HockeyUser, supplied: dict[str, Any] | None = None) -> tuple[dict[str, str], list[dict[str, Any]]]:

View File

@@ -5537,8 +5537,13 @@ class HockeyDataService:
elif state_key not in {"5x5", ""}:
state_label = state_key.replace("x", " on " if language_key == "en" else " на ")
home_label = state_label if advantage_side == "home" else ""
away_label = state_label if advantage_side == "away" else ""
coincidental_penalties = bool(
home_penalties > 0
and away_penalties > 0
and home_skaters == away_skaters
)
home_label = state_label if advantage_side == "home" or coincidental_penalties else ""
away_label = state_label if advantage_side == "away" or coincidental_penalties else ""
phase_labels = {
"regulation": ("Основное время", "Regulation"),
"regular_overtime": ("Овертайм регулярки", "Regular OT"),

View File

@@ -1338,10 +1338,26 @@
}
function mappingInventoryInputs(profile) {
// Preserve the exact Input sequence received from the vMix API via Agent.
// The array order is presentation-only: stable Mapping identity still uses
// key -> unique title -> number, so changing Input positions does not break links.
return Array.isArray(profile?.inventory?.inputs) ? [...profile.inventory.inputs] : [];
// Mapping picker is operator-facing: show Inputs in vMix numeric order so a
// title can be found by its familiar # quickly. Stable links still use key/title.
const inputs = Array.isArray(profile?.inventory?.inputs) ? [...profile.inventory.inputs] : [];
return inputs.sort((left, right) => {
const leftNumber = Number(left?.number);
const rightNumber = Number(right?.number);
const leftRank = Number.isFinite(leftNumber) ? leftNumber : Number.MAX_SAFE_INTEGER;
const rightRank = Number.isFinite(rightNumber) ? rightNumber : Number.MAX_SAFE_INTEGER;
return leftRank - rightRank
|| String(left?.title || "").localeCompare(String(right?.title || ""), "ru", { numeric: true, sensitivity: "base" });
});
}
function mappingVmixFieldOrder(left, right) {
// Natural sort keeps Player2.Text before Player10.Text and makes long GT
// templates much easier to scan. Type suffix stays part of the comparison.
return String(left?.name || "").localeCompare(String(right?.name || ""), "ru", {
numeric: true,
sensitivity: "base",
});
}
function mappingInputByKey(profile, key, title = "") {
@@ -1939,7 +1955,7 @@
const visibleInputs = filteredInputs.some((item) => mappingInputValue(item) === mappingInputValue(currentInput))
? filteredInputs
: [currentInput, ...filteredInputs];
const allFields = currentInput.fields || [];
const allFields = [...(currentInput.fields || [])].sort(mappingVmixFieldOrder);
const filter = ["text", "image", "color"].includes(state.mappingFieldFilter) ? state.mappingFieldFilter : "text";
const counts = {
text: allFields.filter((field) => mappingFieldFilterMatch(field, "text")).length,