${escapeHtml(item.device_id)}diff --git a/app.py b/app.py
index 05cb89f..78871a0 100644
--- a/app.py
+++ b/app.py
@@ -29,7 +29,8 @@ 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.24.14"
+BUILD_VERSION = "2026.08.24.15"
+# compatibility: BUILD_VERSION = "2026.08.24.14"
# compatibility: BUILD_VERSION = "2026.08.24.13"
# compatibility: BUILD_VERSION = "2026.08.24.8"
# compatibility: BUILD_VERSION = "2026.08.24.7"
diff --git a/hockey_data/agent_bridge.py b/hockey_data/agent_bridge.py
index 750084a..72f71ca 100644
--- a/hockey_data/agent_bridge.py
+++ b/hockey_data/agent_bridge.py
@@ -354,11 +354,21 @@ class VmixAgentHub:
return key.split(".", 1)[0] if key else ""
@staticmethod
- def _agent_supports_batch(version: Any) -> bool:
+ def _agent_version_tuple(version: Any) -> tuple[int, int, int] | None:
match = re.search(r"(\d+)\.(\d+)\.(\d+)", str(version or ""))
if not match:
- return False
- return tuple(int(part) for part in match.groups()) >= (1, 4, 0)
+ return None
+ return tuple(int(part) for part in match.groups())
+
+ @classmethod
+ def _agent_supports_mapping_inventory(cls, version: Any) -> bool:
+ parsed = cls._agent_version_tuple(version)
+ return bool(parsed is not None and parsed >= (1, 3, 0))
+
+ @classmethod
+ def _agent_supports_batch(cls, version: Any) -> bool:
+ parsed = cls._agent_version_tuple(version)
+ return bool(parsed is not None and parsed >= (1, 4, 0))
@staticmethod
@@ -1068,6 +1078,14 @@ class VmixAgentHub:
if not assignment_id or not match_id:
return {"ok": False, "reason": "no_match_assignment", "device_id": device_id}
if not fingerprint:
+ if agent_version and not self._agent_supports_mapping_inventory(agent_version):
+ return {
+ "ok": False,
+ "reason": "agent_mapping_unsupported",
+ "device_id": device_id,
+ "agent_version": agent_version,
+ "required_agent_version": "1.3.0",
+ }
return {"ok": False, "reason": "no_project_inventory", "device_id": device_id}
if not vmix_connected:
return {"ok": False, "reason": "vmix_not_connected", "device_id": device_id}
@@ -2354,6 +2372,8 @@ class VmixAgentHub:
"device_id": row.device_uuid,
"name": row.name or row.hostname or row.device_uuid,
"hostname": row.hostname,
+ "agent_version": row.agent_version or "",
+ "mapping_supported": self._agent_supports_mapping_inventory(row.agent_version),
"online": row.device_uuid in self._live,
"vmix_connected": bool(row.vmix_connected),
"vmix_version": row.vmix_version,
diff --git a/hockey_data/static/admin-directories.js b/hockey_data/static/admin-directories.js
index b83d8cf..2001120 100644
--- a/hockey_data/static/admin-directories.js
+++ b/hockey_data/static/admin-directories.js
@@ -2205,13 +2205,22 @@
const profiles = state.mappingProfiles?.profiles || [];
const profile = state.mappingActiveProfile;
const usableDevices = devices.filter((item) => item.project_fingerprint);
- const deviceRows = devices.length ? devices.map((item) => `
+ const deviceRows = devices.length ? devices.map((item) => {
+ const agentVersion = String(item.agent_version || "").trim();
+ const tooOldForMapping = Boolean(agentVersion && item.mapping_supported === false);
+ const mappingState = tooOldForMapping
+ ? `Agent ${escapeHtml(agentVersion)} слишком старый для Mapping · нужен 1.3.0+`
+ : (item.mapping
+ ? `Mapping: ${escapeHtml(item.mapping.name)} · v${Number(item.mapping.version || 1)}`
+ : (item.project_fingerprint ? "Mapping не назначен" : "Ожидание структуры vMix"));
+ return `
${escapeHtml(item.device_id)}