diff --git a/hockey_data/static/admin-directories.css b/hockey_data/static/admin-directories.css index ad9a823..2f2cc80 100644 --- a/hockey_data/static/admin-directories.css +++ b/hockey_data/static/admin-directories.css @@ -1457,7 +1457,7 @@ .hockey-map-target-hint{flex:0 0 auto;display:grid;gap:2px;margin:10px 10px 0;padding:9px 10px;border:1px dashed #3a4b61;border-radius:10px;background:#101923}.hockey-map-target-hint.is-active{border-style:solid;border-color:#39bfa2;background:rgba(72,223,189,.07)}.hockey-map-target-hint span{font-size:9px;font-weight:900;letter-spacing:.08em;color:#7f93aa}.hockey-map-target-hint strong{font-size:12px;color:#e6f0fb}.hockey-map-target-hint small{font-size:10px;color:#7f93aa} .hockey-map-data-search{flex:0 0 auto;display:flex;align-items:center;gap:7px;margin:10px;padding:0 10px;height:38px;border:1px solid #33475f;border-radius:9px;background:#0b141f;color:#48dfbd}.hockey-map-data-search input{width:100%;min-width:0;border:0;outline:0;background:transparent;color:#eaf4ff;font-size:12px}.hockey-map-data-search input::placeholder{color:#667d96} .hockey-map-data-scroll,.hockey-map-vmix-scroll{min-height:0;flex:1;overflow:auto;padding:0 9px 10px} -.hockey-map-data-group{border-bottom:1px solid #24354a}.hockey-map-data-group summary{list-style:none;cursor:pointer;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 4px;color:#b9c8d9;font-size:11px;font-weight:900;text-transform:uppercase;letter-spacing:.04em}.hockey-map-data-group summary::-webkit-details-marker{display:none}.hockey-map-data-group summary b{min-width:23px;padding:3px 6px;border-radius:999px;background:#1d3047;color:#8ea6c0;font-size:9px;text-align:center}.hockey-map-data-group>div{display:grid;gap:6px;padding:0 0 9px} +.hockey-map-data-group{border-bottom:1px solid #24354a}.hockey-map-data-group summary{list-style:none;cursor:pointer;display:flex;align-items:center;gap:8px;padding:10px 4px;color:#b9c8d9;font-size:11px;font-weight:900;text-transform:uppercase;letter-spacing:.04em}.hockey-map-data-group summary::-webkit-details-marker{display:none}.hockey-map-data-group summary>span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.hockey-map-data-group summary b{margin-left:auto;min-width:23px;padding:3px 6px;border-radius:999px;background:#1d3047;color:#8ea6c0;font-size:9px;text-align:center;flex:0 0 auto}.hockey-map-data-group summary::after{width:14px;flex:0 0 14px;text-align:center}.hockey-map-data-group>div{display:grid;gap:6px;padding:0 0 9px} .hockey-map-data-item{width:100%;display:grid;gap:3px;text-align:left;padding:8px 9px;border:1px solid #293c52;border-radius:9px;background:#121d2a;color:#dce8f5;cursor:pointer}.hockey-map-data-item:hover,.hockey-map-data-item.is-selected{border-color:#48dfbd;background:#142a2b}.hockey-map-data-main{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:8px}.hockey-map-data-main strong{font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.hockey-map-data-main em{max-width:150px;font-style:normal;font-size:11px;font-weight:900;color:#48dfbd;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.hockey-map-data-item small{font:9px/1.2 Consolas,monospace;color:#667b93;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .hockey-map-vmix-browser>header>div:first-child{display:grid;gap:4px;min-width:0;flex:1}.hockey-map-vmix-browser>header select{width:min(540px,100%);min-height:34px;border:1px solid #39495f;border-radius:8px;background:#0d1520;color:#edf5ff;padding:5px 8px;font-weight:750}.hockey-map-input-progress{display:flex;align-items:baseline;gap:4px;white-space:nowrap}.hockey-map-input-progress b{font-size:20px;color:#48dfbd}.hockey-map-input-progress span{font-size:10px;color:#8094aa} .hockey-map-vmix-scroll{display:grid;align-content:start;gap:8px;padding:10px} diff --git a/hockey_data/static/admin-directories.js b/hockey_data/static/admin-directories.js index f951000..8e0e8ca 100644 --- a/hockey_data/static/admin-directories.js +++ b/hockey_data/static/admin-directories.js @@ -1338,16 +1338,10 @@ } function mappingInventoryInputs(profile) { - const inputs = Array.isArray(profile?.inventory?.inputs) ? [...profile.inventory.inputs] : []; - const numberValue = (item) => { - const match = String(item?.number ?? "").match(/\d+/); - return match ? Number(match[0]) : Number.MAX_SAFE_INTEGER; - }; - return inputs.sort((a, b) => { - const diff = numberValue(a) - numberValue(b); - if (diff) return diff; - return String(a?.title || a?.key || "").localeCompare(String(b?.title || b?.key || ""), "ru", { numeric: true, sensitivity: "base" }); - }); + // 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] : []; } function mappingInputByKey(profile, key, title = "") { diff --git a/tests/test_build79_mapping_alignment_input_order.py b/tests/test_build79_mapping_alignment_input_order.py new file mode 100644 index 0000000..1dfab4b --- /dev/null +++ b/tests/test_build79_mapping_alignment_input_order.py @@ -0,0 +1,28 @@ +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +JS = (ROOT / "hockey_data/static/admin-directories.js").read_text(encoding="utf-8") +CSS = (ROOT / "hockey_data/static/admin-directories.css").read_text(encoding="utf-8") +AGENT = (ROOT / "hockey_data/agent_bridge.py").read_text(encoding="utf-8") + + +def test_mapping_data_group_count_is_right_aligned_before_fixed_arrow(): + assert ".hockey-map-data-group summary b{margin-left:auto" in CSS + assert ".hockey-map-data-group summary::after{width:14px;flex:0 0 14px;text-align:center}" in CSS + assert ".hockey-map-data-group summary>span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}" in CSS + + +def test_mapping_vmix_input_selector_preserves_inventory_api_order(): + start = JS.index("function mappingInventoryInputs(profile)") + end = JS.index("function mappingInputByKey", start) + block = JS[start:end] + assert "return Array.isArray(profile?.inventory?.inputs) ? [...profile.inventory.inputs] : [];" in block + assert ".sort(" not in block + assert "numberValue" not in block + + +def test_server_preserves_agent_inventory_array_order(): + # receive_inventory iterates the Agent-provided inputs and appends in that order. + assert "for raw_input in inputs:" in AGENT + assert "clean_inputs.append({" in AGENT + assert '"inputs": clean_inputs' in AGENT diff --git a/tests/test_mapping_admin_visibility.py b/tests/test_mapping_admin_visibility.py index a26abb5..bb79f39 100644 --- a/tests/test_mapping_admin_visibility.py +++ b/tests/test_mapping_admin_visibility.py @@ -17,13 +17,13 @@ def test_mapping_load_is_resilient_per_endpoint(): assert 'Часть Mapping API недоступна' in JS -def test_visual_mapping_has_sql_table_cell_picker_and_searchable_ordered_inputs(): +def test_visual_mapping_has_sql_table_cell_picker_and_searchable_api_order_inputs(): assert 'data-map-table-source' in JS assert 'data-map-table-row-search' in JS assert '.row.${row.index}.' in JS assert 'data-map-input-search' in JS assert 'mappingFilteredInputs' in JS - assert 'return inputs.sort' in JS + assert 'return Array.isArray(profile?.inventory?.inputs) ? [...profile.inventory.inputs] : [];' in JS def test_visual_mapping_has_explicit_sql_cell_dropdowns_and_apply_input():