поправлены удаления, и поправлено отображение в настройках SQL
This commit is contained in:
@@ -3878,6 +3878,10 @@ def create_hockey_agent_router(
|
|||||||
async def mapping_data_sources() -> dict[str, Any]:
|
async def mapping_data_sources() -> dict[str, Any]:
|
||||||
return hub.mapping_data.list_sources()
|
return hub.mapping_data.list_sources()
|
||||||
|
|
||||||
|
@router.get("/api/hockey/admin/mapping-data/database-schema", dependencies=admin)
|
||||||
|
async def mapping_database_schema() -> dict[str, Any]:
|
||||||
|
return hub.mapping_data.database_schema_reference()
|
||||||
|
|
||||||
@router.post("/api/hockey/admin/mapping-data/sources", dependencies=admin)
|
@router.post("/api/hockey/admin/mapping-data/sources", dependencies=admin)
|
||||||
async def mapping_data_source_create(payload: SqlDataSourcePayload, user: HockeyUser = Depends(auth_dependency)) -> dict[str, Any]:
|
async def mapping_data_source_create(payload: SqlDataSourcePayload, user: HockeyUser = Depends(auth_dependency)) -> dict[str, Any]:
|
||||||
return hub.mapping_data.create_source(payload, user)
|
return hub.mapping_data.create_source(payload, user)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from datetime import datetime
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from sqlalchemy import and_, delete, select, text
|
from sqlalchemy import and_, delete, inspect, select, text
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
from .auth_bridge import HockeyUser
|
from .auth_bridge import HockeyUser
|
||||||
@@ -229,6 +229,7 @@ class MappingDataService:
|
|||||||
def __init__(self, database: HockeyDatabase, settings: Any | None = None) -> None:
|
def __init__(self, database: HockeyDatabase, settings: Any | None = None) -> None:
|
||||||
self.database = database
|
self.database = database
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
|
self._database_schema_cache: dict[str, Any] | None = None
|
||||||
|
|
||||||
def _current_language(self, supplied: dict[str, Any] | None = None) -> str:
|
def _current_language(self, supplied: dict[str, Any] | None = None) -> str:
|
||||||
supplied = supplied or {}
|
supplied = supplied or {}
|
||||||
@@ -383,6 +384,44 @@ class MappingDataService:
|
|||||||
rows = list(session.scalars(select(MappingSqlDataSource).order_by(MappingSqlDataSource.sort_order, MappingSqlDataSource.id)))
|
rows = list(session.scalars(select(MappingSqlDataSource).order_by(MappingSqlDataSource.sort_order, MappingSqlDataSource.id)))
|
||||||
return {"sources": [self._source_payload(row) for row in rows]}
|
return {"sources": [self._source_payload(row) for row in rows]}
|
||||||
|
|
||||||
|
def database_schema_reference(self) -> dict[str, Any]:
|
||||||
|
"""Return a compact read-only reference of hockey database tables.
|
||||||
|
|
||||||
|
This is intentionally metadata-only: table/column names and SQL types,
|
||||||
|
never row data or connection credentials. The SQL editor uses it as an
|
||||||
|
expandable cheat sheet so operators do not have to inspect PostgreSQL
|
||||||
|
manually just to remember a table name.
|
||||||
|
"""
|
||||||
|
if self._database_schema_cache is not None:
|
||||||
|
return self._database_schema_cache
|
||||||
|
try:
|
||||||
|
inspector = inspect(self.database.engine)
|
||||||
|
table_names = sorted(
|
||||||
|
name for name in inspector.get_table_names()
|
||||||
|
if str(name).startswith("hockey_")
|
||||||
|
)
|
||||||
|
tables: list[dict[str, Any]] = []
|
||||||
|
for table_name in table_names:
|
||||||
|
columns = []
|
||||||
|
for column in inspector.get_columns(table_name):
|
||||||
|
columns.append({
|
||||||
|
"name": str(column.get("name") or ""),
|
||||||
|
"type": str(column.get("type") or ""),
|
||||||
|
"nullable": bool(column.get("nullable", True)),
|
||||||
|
})
|
||||||
|
tables.append({
|
||||||
|
"name": table_name,
|
||||||
|
"column_count": len(columns),
|
||||||
|
"columns": columns,
|
||||||
|
})
|
||||||
|
payload = {"tables": tables, "table_count": len(tables)}
|
||||||
|
self._database_schema_cache = payload
|
||||||
|
return payload
|
||||||
|
except SQLAlchemyError as exc:
|
||||||
|
return {"tables": [], "table_count": 0, "error": str(exc)}
|
||||||
|
except Exception as exc:
|
||||||
|
return {"tables": [], "table_count": 0, "error": str(exc)}
|
||||||
|
|
||||||
def create_source(self, payload: Any, user: HockeyUser) -> dict[str, Any]:
|
def create_source(self, payload: Any, user: HockeyUser) -> dict[str, Any]:
|
||||||
self.ensure_defaults()
|
self.ensure_defaults()
|
||||||
code = str(payload.code or "").strip()
|
code = str(payload.code or "").strip()
|
||||||
|
|||||||
@@ -1726,3 +1726,22 @@
|
|||||||
.hockey-map-sql-cell-quick:not([open]),.hockey-map-table-picker:not([open]){min-height:0!important;height:auto!important;display:block;padding:10px}
|
.hockey-map-sql-cell-quick:not([open]),.hockey-map-table-picker:not([open]){min-height:0!important;height:auto!important;display:block;padding:10px}
|
||||||
.hockey-map-sql-cell-quick:not([open])>summary,.hockey-map-table-picker:not([open])>summary{margin:0}
|
.hockey-map-sql-cell-quick:not([open])>summary,.hockey-map-table-picker:not([open])>summary{margin:0}
|
||||||
@media(max-width:1100px){.hockey-map-sql-cell-quick>summary{align-items:flex-start;flex-direction:column}.hockey-map-sql-cell-quick>summary small{max-width:none;text-align:left}}
|
@media(max-width:1100px){.hockey-map-sql-cell-quick>summary{align-items:flex-start;flex-direction:column}.hockey-map-sql-cell-quick>summary small{max-width:none;text-align:left}}
|
||||||
|
|
||||||
|
/* BUILD85 — collapsed SQL parameters + database table reference */
|
||||||
|
.hockey-map-sql-reference{margin:8px 0;border:1px solid #30465d;border-radius:10px;background:#0b141e;overflow:hidden}
|
||||||
|
.hockey-map-sql-reference>summary{display:flex;align-items:center;gap:9px;padding:8px 10px;cursor:pointer;list-style:none;color:#dbe8f5;user-select:none}
|
||||||
|
.hockey-map-sql-reference>summary::-webkit-details-marker{display:none}
|
||||||
|
.hockey-map-sql-reference>summary>span{display:grid;place-items:center;flex:0 0 24px;height:24px;border:1px solid #3a6c88;border-radius:7px;background:#122639;color:#6ad3ef;font-weight:950;font:900 11px Consolas,monospace}
|
||||||
|
.hockey-map-sql-reference>summary>div{display:grid;gap:1px;min-width:0}
|
||||||
|
.hockey-map-sql-reference>summary strong{font-size:10px;color:#dce9f6}
|
||||||
|
.hockey-map-sql-reference>summary small{font-size:8px;color:#7890a8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||||
|
.hockey-map-sql-reference>summary code{font:8px Consolas,monospace;color:#67dfc6}
|
||||||
|
.hockey-map-sql-reference-body{display:grid;gap:7px;padding:0 10px 10px}
|
||||||
|
.hockey-map-sql-reference .hockey-map-sql-params{margin:0;padding:0;border:0;background:transparent}
|
||||||
|
.hockey-map-sql-table-list{grid-template-columns:repeat(2,minmax(0,1fr))}
|
||||||
|
.hockey-map-sql-table-ref{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:3px 8px;min-width:0;padding:7px 8px;border:1px solid #2b4359;border-radius:8px;background:#101c29;color:#dce8f3;text-align:left;cursor:pointer}
|
||||||
|
.hockey-map-sql-table-ref:hover{border-color:#3b8ca3;background:#132535}
|
||||||
|
.hockey-map-sql-table-ref strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font:10px Consolas,monospace;color:#77dbc8}
|
||||||
|
.hockey-map-sql-table-ref span{font-size:8px;color:#7890a8;white-space:nowrap}
|
||||||
|
.hockey-map-sql-table-ref code{grid-column:1/-1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font:8px Consolas,monospace;color:#738da6}
|
||||||
|
@media(max-width:1050px){.hockey-map-sql-table-list{grid-template-columns:1fr}}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
mappingCatalog: null,
|
mappingCatalog: null,
|
||||||
mappingContextVariables: null,
|
mappingContextVariables: null,
|
||||||
mappingSqlSources: null,
|
mappingSqlSources: null,
|
||||||
|
mappingDatabaseSchema: null,
|
||||||
mappingSelectedSqlSourceId: null,
|
mappingSelectedSqlSourceId: null,
|
||||||
mappingSqlPreview: null,
|
mappingSqlPreview: null,
|
||||||
mappingSqlDraft: null,
|
mappingSqlDraft: null,
|
||||||
@@ -1316,16 +1317,18 @@
|
|||||||
try { return await request(url); }
|
try { return await request(url); }
|
||||||
catch (error) { state.mappingLoadErrors[key] = error.message || String(error); return fallback; }
|
catch (error) { state.mappingLoadErrors[key] = error.message || String(error); return fallback; }
|
||||||
};
|
};
|
||||||
const [devices, profiles, variables, sources] = await Promise.all([
|
const [devices, profiles, variables, sources, databaseSchema] = await Promise.all([
|
||||||
safe("devices", "/api/hockey/admin/vmix-mapping/devices", { devices: [] }),
|
safe("devices", "/api/hockey/admin/vmix-mapping/devices", { devices: [] }),
|
||||||
safe("profiles", "/api/hockey/admin/vmix-mapping/profiles", { profiles: [] }),
|
safe("profiles", "/api/hockey/admin/vmix-mapping/profiles", { profiles: [] }),
|
||||||
safe("variables", "/api/hockey/admin/mapping-context/variables", { variables: [] }),
|
safe("variables", "/api/hockey/admin/mapping-context/variables", { variables: [] }),
|
||||||
safe("sources", "/api/hockey/admin/mapping-data/sources", { sources: [] }),
|
safe("sources", "/api/hockey/admin/mapping-data/sources", { sources: [] }),
|
||||||
|
safe("databaseSchema", "/api/hockey/admin/mapping-data/database-schema", { tables: [], table_count: 0 }),
|
||||||
]);
|
]);
|
||||||
state.mappingDevices = devices;
|
state.mappingDevices = devices;
|
||||||
state.mappingProfiles = profiles;
|
state.mappingProfiles = profiles;
|
||||||
state.mappingContextVariables = variables;
|
state.mappingContextVariables = variables;
|
||||||
state.mappingSqlSources = sources;
|
state.mappingSqlSources = sources;
|
||||||
|
state.mappingDatabaseSchema = databaseSchema;
|
||||||
const id = profileId ?? state.mappingActiveProfile?.id ?? profiles.profiles?.[0]?.id ?? null;
|
const id = profileId ?? state.mappingActiveProfile?.id ?? profiles.profiles?.[0]?.id ?? null;
|
||||||
if (id) {
|
if (id) {
|
||||||
try { state.mappingActiveProfile = await request(`/api/hockey/admin/vmix-mapping/profiles/${id}`); }
|
try { state.mappingActiveProfile = await request(`/api/hockey/admin/vmix-mapping/profiles/${id}`); }
|
||||||
@@ -1334,7 +1337,7 @@
|
|||||||
if (!state.mappingSelectedSqlSourceId && sources.sources?.length) state.mappingSelectedSqlSourceId = sources.sources[0].id;
|
if (!state.mappingSelectedSqlSourceId && sources.sources?.length) state.mappingSelectedSqlSourceId = sources.sources[0].id;
|
||||||
await loadMappingCatalog();
|
await loadMappingCatalog();
|
||||||
if (state.mappingCatalog?.error) state.mappingLoadErrors.catalog = state.mappingCatalog.error;
|
if (state.mappingCatalog?.error) state.mappingLoadErrors.catalog = state.mappingCatalog.error;
|
||||||
return { devices, profiles, variables, sources };
|
return { devices, profiles, variables, sources, databaseSchema };
|
||||||
}
|
}
|
||||||
|
|
||||||
function mappingInventoryInputs(profile) {
|
function mappingInventoryInputs(profile) {
|
||||||
@@ -2072,6 +2075,30 @@
|
|||||||
</section>`;
|
</section>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mappingSqlDatabaseReference() {
|
||||||
|
const payload = state.mappingDatabaseSchema || {};
|
||||||
|
const tables = Array.isArray(payload.tables) ? payload.tables : [];
|
||||||
|
const count = Number(payload.table_count || tables.length || 0);
|
||||||
|
const rows = tables.map((table) => {
|
||||||
|
const columns = Array.isArray(table.columns) ? table.columns : [];
|
||||||
|
const columnNames = columns.map((column) => String(column.name || "")).filter(Boolean);
|
||||||
|
const compactColumns = columnNames.slice(0, 12).join(", ");
|
||||||
|
const tail = columnNames.length > 12 ? ` … +${columnNames.length - 12}` : "";
|
||||||
|
return `<button type="button" class="hockey-map-sql-table-ref" data-sql-table-name="${escapeHtml(table.name || "")}" title="Вставить ${escapeHtml(table.name || "")} в SQL">
|
||||||
|
<strong>${escapeHtml(table.name || "")}</strong>
|
||||||
|
<span>${Number(table.column_count || columns.length || 0)} колонок</span>
|
||||||
|
<code>${escapeHtml(compactColumns)}${escapeHtml(tail)}</code>
|
||||||
|
</button>`;
|
||||||
|
}).join("");
|
||||||
|
const body = payload.error
|
||||||
|
? `<div class="hockey-directory-empty is-error">Не удалось получить схему БД: ${escapeHtml(payload.error)}</div>`
|
||||||
|
: rows || `<div class="hockey-directory-empty">Таблицы не найдены.</div>`;
|
||||||
|
return `<details class="hockey-map-sql-reference hockey-map-sql-schema-ref">
|
||||||
|
<summary><span>▦</span><div><strong>Справочник таблиц БД</strong><small>${count} таблиц · клик по названию вставляет его в SQL</small></div></summary>
|
||||||
|
<div class="hockey-map-sql-reference-body hockey-map-sql-table-list">${body}</div>
|
||||||
|
</details>`;
|
||||||
|
}
|
||||||
|
|
||||||
function mappingSqlHelp() {
|
function mappingSqlHelp() {
|
||||||
const snippets = [
|
const snippets = [
|
||||||
["Текст", "CONCAT_WS", "CONCAT_WS(' ', last_name, first_name)", "Склеить строки через разделитель"],
|
["Текст", "CONCAT_WS", "CONCAT_WS(' ', last_name, first_name)", "Склеить строки через разделитель"],
|
||||||
@@ -2144,7 +2171,11 @@
|
|||||||
<label>Период, сек<input type="number" min="1" max="3600" step="1" data-sql-refresh-seconds value="${Math.max(1, Math.round(Number(draft.refresh_interval_ms || 1000) / 1000))}" ${draft.auto_refresh_enabled ? "" : "disabled"}></label>
|
<label>Период, сек<input type="number" min="1" max="3600" step="1" data-sql-refresh-seconds value="${Math.max(1, Math.round(Number(draft.refresh_interval_ms || 1000) / 1000))}" ${draft.auto_refresh_enabled ? "" : "disabled"}></label>
|
||||||
<small>Сервер повторяет только этот SQL. В vMix отправляются только связанные с ним поля и только если значение реально изменилось.</small>
|
<small>Сервер повторяет только этот SQL. В vMix отправляются только связанные с ним поля и только если значение реально изменилось.</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="hockey-map-sql-params"><span>Доступные параметры — клик вставляет в SQL</span><div>${variableChips}</div></div>
|
<details class="hockey-map-sql-reference hockey-map-sql-params-ref">
|
||||||
|
<summary><span>:</span><div><strong>Доступные параметры</strong><small>${(state.mappingCatalog?.variables || []).length} идентификаторов · клик вставляет <code>:параметр</code> в SQL</small></div></summary>
|
||||||
|
<div class="hockey-map-sql-reference-body"><div class="hockey-map-sql-params"><div>${variableChips}</div></div></div>
|
||||||
|
</details>
|
||||||
|
${mappingSqlDatabaseReference()}
|
||||||
${mappingSqlHelp()}
|
${mappingSqlHelp()}
|
||||||
<label class="hockey-map-sql-code"><span>SQL</span><textarea data-sql-text spellcheck="false">${escapeHtml(draft.sql_text || "")}</textarea></label>
|
<label class="hockey-map-sql-code"><span>SQL</span><textarea data-sql-text spellcheck="false">${escapeHtml(draft.sql_text || "")}</textarea></label>
|
||||||
<div class="hockey-map-sql-actions"><label class="hockey-directory-check"><input type="checkbox" data-sql-enabled ${draft.enabled !== false ? "checked" : ""}><span>Источник включён</span></label><button type="button" data-sql-preview>Проверить SQL</button><button type="button" class="is-accent" data-sql-save>${draft.id ? "Сохранить источник" : "Создать источник"}</button>${draft.id ? `<button type="button" class="danger" data-sql-delete>Удалить</button>` : ""}</div>
|
<div class="hockey-map-sql-actions"><label class="hockey-directory-check"><input type="checkbox" data-sql-enabled ${draft.enabled !== false ? "checked" : ""}><span>Источник включён</span></label><button type="button" data-sql-preview>Проверить SQL</button><button type="button" class="is-accent" data-sql-save>${draft.id ? "Сохранить источник" : "Создать источник"}</button>${draft.id ? `<button type="button" class="danger" data-sql-delete>Удалить</button>` : ""}</div>
|
||||||
@@ -2366,6 +2397,18 @@
|
|||||||
textarea.focus(); textarea.setSelectionRange(start + token.length, start + token.length);
|
textarea.focus(); textarea.setSelectionRange(start + token.length, start + token.length);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
modal.querySelectorAll("[data-sql-table-name]").forEach((button) => button.addEventListener("click", () => {
|
||||||
|
const textarea = modal.querySelector("[data-sql-text]");
|
||||||
|
if (!textarea) return;
|
||||||
|
const tableName = String(button.dataset.sqlTableName || "").trim();
|
||||||
|
if (!tableName) return;
|
||||||
|
const start = textarea.selectionStart ?? textarea.value.length;
|
||||||
|
const end = textarea.selectionEnd ?? start;
|
||||||
|
textarea.value = textarea.value.slice(0, start) + tableName + textarea.value.slice(end);
|
||||||
|
textarea.focus(); textarea.setSelectionRange(start + tableName.length, start + tableName.length);
|
||||||
|
state.mappingSqlDraft = null;
|
||||||
|
}));
|
||||||
|
|
||||||
modal.querySelectorAll("[data-sql-snippet]").forEach((button) => button.addEventListener("click", () => {
|
modal.querySelectorAll("[data-sql-snippet]").forEach((button) => button.addEventListener("click", () => {
|
||||||
const textarea = modal.querySelector("[data-sql-text]");
|
const textarea = modal.querySelector("[data-sql-text]");
|
||||||
if (!textarea) return;
|
if (!textarea) return;
|
||||||
|
|||||||
79
tests/test_build85_penalty_advantage_sql_reference.py
Normal file
79
tests/test_build85_penalty_advantage_sql_reference.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from hockey_data.mapping_context import MappingDataService
|
||||||
|
from hockey_data.service import HockeyDataService
|
||||||
|
from tests.support import LocalTestDatabase
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
APP_JS = (ROOT / "ui_builder/static/app.js").read_text(encoding="utf-8")
|
||||||
|
ADMIN_JS = (ROOT / "hockey_data/static/admin-directories.js").read_text(encoding="utf-8")
|
||||||
|
AGENT = (ROOT / "hockey_data/agent_bridge.py").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _penalty(side: str) -> dict:
|
||||||
|
return {
|
||||||
|
"side": side,
|
||||||
|
"infraction": {"code": "TEST"},
|
||||||
|
"preset": "2",
|
||||||
|
"durationMs": 120000,
|
||||||
|
"remainingMs": 90000,
|
||||||
|
"finished": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_strength_engine_reports_advantage_for_4x3_and_3x4():
|
||||||
|
settings = {"strength_regulation_skaters": 5, "strength_min_skaters": 3}
|
||||||
|
home_advantage = HockeyDataService._strength_payload(
|
||||||
|
settings,
|
||||||
|
stage="regular",
|
||||||
|
current_period="2",
|
||||||
|
timer_state={"penalty_board": {"penalties": [_penalty("home"), _penalty("away"), _penalty("away")]}},
|
||||||
|
language="ru",
|
||||||
|
)
|
||||||
|
assert home_advantage["strength_label"] == "4×3"
|
||||||
|
assert home_advantage["advantage_side"] == "home"
|
||||||
|
|
||||||
|
away_advantage = HockeyDataService._strength_payload(
|
||||||
|
settings,
|
||||||
|
stage="regular",
|
||||||
|
current_period="2",
|
||||||
|
timer_state={"penalty_board": {"penalties": [_penalty("home"), _penalty("home"), _penalty("away")]}},
|
||||||
|
language="ru",
|
||||||
|
)
|
||||||
|
assert away_advantage["strength_label"] == "3×4"
|
||||||
|
assert away_advantage["advantage_side"] == "away"
|
||||||
|
|
||||||
|
|
||||||
|
def test_penalty_plate_routing_uses_strength_advantage_side():
|
||||||
|
start = APP_JS.index("function penaltyDisplayEntriesByTargetSide")
|
||||||
|
end = APP_JS.index("function hockeyPenaltySideMappingDetail", start)
|
||||||
|
block = APP_JS[start:end]
|
||||||
|
assert 'getByPath(state.data, "hockey.game_control.strength")' in block
|
||||||
|
assert 'if (advantageSide === "home")' in block
|
||||||
|
assert 'return { home: take(away), away: [], routedToAdvantage: true, advantageSide: "home" };' in block
|
||||||
|
assert 'if (advantageSide === "away")' in block
|
||||||
|
assert 'return { home: [], away: take(home), routedToAdvantage: true, advantageSide: "away" };' in block
|
||||||
|
|
||||||
|
|
||||||
|
def test_database_schema_reference_lists_real_hockey_tables(tmp_path: Path):
|
||||||
|
database = LocalTestDatabase(tmp_path / "schema.sqlite3")
|
||||||
|
database.create_all()
|
||||||
|
service = MappingDataService(database) # type: ignore[arg-type]
|
||||||
|
payload = service.database_schema_reference()
|
||||||
|
names = {row["name"] for row in payload["tables"]}
|
||||||
|
assert "hockey_players" in names
|
||||||
|
assert "hockey_games" in names
|
||||||
|
players = next(row for row in payload["tables"] if row["name"] == "hockey_players")
|
||||||
|
column_names = {column["name"] for column in players["columns"]}
|
||||||
|
assert "id" in column_names
|
||||||
|
assert "external_id" in column_names
|
||||||
|
|
||||||
|
|
||||||
|
def test_sql_parameters_and_table_reference_are_collapsed_by_default():
|
||||||
|
assert 'class="hockey-map-sql-reference hockey-map-sql-params-ref"' in ADMIN_JS
|
||||||
|
assert 'class="hockey-map-sql-reference hockey-map-sql-schema-ref"' in ADMIN_JS
|
||||||
|
assert 'Справочник таблиц БД' in ADMIN_JS
|
||||||
|
assert 'data-sql-table-name=' in ADMIN_JS
|
||||||
|
assert '<details class="hockey-map-sql-reference hockey-map-sql-params-ref">' in ADMIN_JS
|
||||||
|
assert '<details class="hockey-map-sql-reference hockey-map-sql-schema-ref">' in ADMIN_JS
|
||||||
|
assert '/api/hockey/admin/mapping-data/database-schema' in AGENT
|
||||||
@@ -4143,6 +4143,8 @@ function startCustomTooltips() {
|
|||||||
const home = penalties.filter((item) => item.side === "home").map(penaltyRuntimeItem).sort(bySoonest);
|
const home = penalties.filter((item) => item.side === "home").map(penaltyRuntimeItem).sort(bySoonest);
|
||||||
const away = penalties.filter((item) => item.side === "away").map(penaltyRuntimeItem).sort(bySoonest);
|
const away = penalties.filter((item) => item.side === "away").map(penaltyRuntimeItem).sort(bySoonest);
|
||||||
const flags = getByPath(state.data, "hockey.game_control.flags") || {};
|
const flags = getByPath(state.data, "hockey.game_control.flags") || {};
|
||||||
|
// Legacy BUILD43 shortest-timer behaviour was allEntries.slice(0, 1);
|
||||||
|
// `take()` preserves that rule while routing the timer to the advantage side.
|
||||||
const strength = getByPath(state.data, "hockey.game_control.strength") || {};
|
const strength = getByPath(state.data, "hockey.game_control.strength") || {};
|
||||||
return {
|
return {
|
||||||
sequence: sequence ? clone(sequence) : null,
|
sequence: sequence ? clone(sequence) : null,
|
||||||
@@ -4635,32 +4637,43 @@ function startCustomTooltips() {
|
|||||||
|| Number(a.event.createdAt || 0) - Number(b.event.createdAt || 0));
|
|| Number(a.event.createdAt || 0) - Number(b.event.createdAt || 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
// BUILD83: HOME/AWAY penalty targets describe where the extra scorebug plate
|
// BUILD85: HOME/AWAY penalty targets describe the TEAM THAT HAS THE NUMERICAL
|
||||||
// is drawn, not which bench committed the penalty. While both teams have an
|
// ADVANTAGE, not the bench that committed the penalty. This matters not only
|
||||||
// active penalty we keep the traditional one-per-side display (coincidental
|
// for a normal 5x4/5x3 power play, but also for 4x3 and 3x4 when both benches
|
||||||
// penalties). As soon as only one team still has a penalty, its timer must
|
// still have active penalties. The authoritative advantage_side is calculated
|
||||||
// move to the OPPOSITE target because that is the team playing on the power
|
// by the hockey strength engine. When strength is equal (4x4 / 3x3), keep the
|
||||||
// play. Example: HOME 1:43 + HOME 2:00 + AWAY 1:43 -> after the coincidental
|
// traditional one-per-side coincidental display. If the strength payload has
|
||||||
// 1:43 pair expires, the remaining HOME 2:00 is shown through the AWAY input.
|
// not arrived yet, the single-penalty fallback still routes to the opposite
|
||||||
|
// target exactly as BUILD83 did.
|
||||||
function penaltyDisplayEntriesByTargetSide(step) {
|
function penaltyDisplayEntriesByTargetSide(step) {
|
||||||
const home = sortedPenaltyEntries("home");
|
const home = sortedPenaltyEntries("home");
|
||||||
const away = sortedPenaltyEntries("away");
|
const away = sortedPenaltyEntries("away");
|
||||||
const soonestOnly = String(step?.penalty_display_mode || "soonest") !== "all";
|
const soonestOnly = String(step?.penalty_display_mode || "soonest") !== "all";
|
||||||
// Legacy BUILD43 equivalent was `allEntries.slice(0, 1)`; routing is now
|
const strength = getByPath(state.data, "hockey.game_control.strength") || {};
|
||||||
// calculated for both sides together so the remaining penalty can move to
|
const advantageSide = String(strength.advantage_side || "").trim().toLowerCase();
|
||||||
// the power-play team target without losing the shortest-time behaviour.
|
const take = (items) => soonestOnly ? items.slice(0, 1) : items;
|
||||||
if (!soonestOnly) return { home, away, routedToAdvantage: false };
|
|
||||||
|
if (advantageSide === "home") {
|
||||||
|
// HOME has more skaters, therefore an AWAY penalty controls the PP clock.
|
||||||
|
return { home: take(away), away: [], routedToAdvantage: true, advantageSide: "home" };
|
||||||
|
}
|
||||||
|
if (advantageSide === "away") {
|
||||||
|
// AWAY has more skaters, therefore a HOME penalty controls the PP clock.
|
||||||
|
return { home: [], away: take(home), routedToAdvantage: true, advantageSide: "away" };
|
||||||
|
}
|
||||||
|
|
||||||
if (home.length && away.length) {
|
if (home.length && away.length) {
|
||||||
return { home: home.slice(0, 1), away: away.slice(0, 1), routedToAdvantage: false };
|
return { home: take(home), away: take(away), routedToAdvantage: false, advantageSide: "" };
|
||||||
}
|
}
|
||||||
if (home.length) {
|
if (home.length) {
|
||||||
return { home: [], away: home.slice(0, 1), routedToAdvantage: true };
|
if (soonestOnly) return { home: [], away: home.slice(0, 1), routedToAdvantage: true };
|
||||||
|
return { home: [], away: home, routedToAdvantage: true, advantageSide: "away" };
|
||||||
}
|
}
|
||||||
if (away.length) {
|
if (away.length) {
|
||||||
return { home: away.slice(0, 1), away: [], routedToAdvantage: true };
|
if (soonestOnly) return { home: away.slice(0, 1), away: [], routedToAdvantage: true };
|
||||||
|
return { home: away, away: [], routedToAdvantage: true, advantageSide: "home" };
|
||||||
}
|
}
|
||||||
return { home: [], away: [], routedToAdvantage: false };
|
return { home: [], away: [], routedToAdvantage: false, advantageSide: "" };
|
||||||
}
|
}
|
||||||
|
|
||||||
function hockeyPenaltySideMappingDetail(item, side) {
|
function hockeyPenaltySideMappingDetail(item, side) {
|
||||||
|
|||||||
Reference in New Issue
Block a user