diff --git a/app.py b/app.py index f4e05aa..72d6748 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.09.14.1" +BUILD_VERSION = "2026.09.15.1" # compatibility: BUILD_VERSION = "2026.08.26.1" # compatibility: BUILD_VERSION = "2026.08.25.1" # compatibility: BUILD_VERSION = "2026.08.24.15" diff --git a/hockey_data/static/admin-directories.css b/hockey_data/static/admin-directories.css index d6c69eb..07bb872 100644 --- a/hockey_data/static/admin-directories.css +++ b/hockey_data/static/admin-directories.css @@ -1771,3 +1771,370 @@ .hockey-team-color-chip.is-enabled>code{color:#bed2e2} @media(max-width:1180px){.hockey-team-color-field-head{align-items:flex-start;flex-direction:column}.hockey-team-color-field-head>small{text-align:left}} @media(max-width:720px){.hockey-team-color-control{grid-template-columns:42px minmax(140px,1fr) 34px}.hockey-team-color-value>input{min-width:140px}} + +/* BUILD133 — full-screen Settings workspace + always-visible Mapping actions */ +body.hockey-admin-settings-open { overflow: hidden; } + +.hockey-directory-backdrop { + place-items: stretch; + padding: 0; + background: #07111d; + backdrop-filter: none; +} + +.hockey-directory-modal, +.hockey-directory-modal.is-mapping, +.hockey-directory-modal.is-khl-site { + width: 100vw; + height: 100dvh; + max-height: none; + border: 0; + border-radius: 0; + box-shadow: none; + display: grid; + grid-template-columns: 224px minmax(0, 1fr); + grid-template-rows: 68px minmax(0, 1fr); + overflow: hidden; +} + +.hockey-directory-modal > header, +.hockey-directory-modal.is-mapping > header, +.hockey-directory-modal.is-khl-site > header { + grid-column: 1 / -1; + grid-row: 1; + min-height: 68px; + padding: 10px 18px; + z-index: 4; +} + +.hockey-directory-modal > nav.hockey-settings-nav { + grid-column: 1; + grid-row: 2; + min-width: 0; + min-height: 0; + display: flex; + flex-direction: column; + align-items: stretch; + gap: 16px; + padding: 16px 12px 20px; + overflow-x: hidden; + overflow-y: auto; + border-right: 1px solid #24394f; + border-bottom: 0; + background: #0a1523; +} + +.hockey-settings-nav-group { + display: grid; + gap: 5px; +} + +.hockey-settings-nav-group > span { + padding: 0 9px 5px; + color: #60758d; + font-size: 8px; + font-weight: 950; + letter-spacing: .15em; +} + +.hockey-directory-modal > nav.hockey-settings-nav .hockey-settings-nav-group > button { + width: 100%; + min-height: 38px; + display: flex; + align-items: center; + justify-content: flex-start; + padding: 0 10px; + border: 1px solid transparent; + border-radius: 9px; + color: #8fa4ba; + background: transparent; + text-align: left; + white-space: normal; +} + +.hockey-directory-modal > nav.hockey-settings-nav .hockey-settings-nav-group > button:hover { + color: #d8e8f7; + border-color: #263d53; + background: #101f30; +} + +.hockey-directory-modal > nav.hockey-settings-nav .hockey-settings-nav-group > button.is-active { + color: #eaf7ff; + border-color: #2e695f; + background: linear-gradient(90deg, rgba(72,223,189,.13), rgba(18,34,51,.86)); + box-shadow: inset 3px 0 0 #48dfbd; +} + +.hockey-directory-content, +.hockey-directory-modal.is-khl-site .hockey-directory-content, +.hockey-directory-modal.is-mapping .hockey-directory-content { + grid-column: 2; + grid-row: 2; + min-width: 0; + min-height: 0; + max-height: none; + overflow: auto; + padding: 18px 20px 26px; +} + +.hockey-directory-modal.is-mapping .hockey-directory-content { + overflow: hidden; + padding: 14px; +} + +.hockey-directory-form { + scroll-margin-bottom: 86px; +} + +.hockey-directory-form > .hockey-directory-form-actions { + position: sticky; + bottom: -1px; + z-index: 4; + margin: 14px -2px -2px; + padding: 10px 2px 2px; + background: linear-gradient(180deg, rgba(11,22,37,0), #0b1625 28%); +} + +/* Mapping: keep config creation and save actions visible at all times. */ +.hockey-mapping-layout.is-visual { + height: 100%; + min-height: 0; + grid-template-columns: 320px minmax(0, 1fr); + gap: 14px; +} + +.hockey-mapping-layout.is-visual .hockey-mapping-sidebar { + min-height: 0; + overflow: hidden; + display: flex; + flex-direction: column; + gap: 10px; + padding-right: 3px; +} + +.hockey-mapping-layout.is-visual .hockey-mapping-devices { + flex: 0 0 auto; + max-height: 190px; + overflow: auto; +} + +.hockey-mapping-layout.is-visual .hockey-mapping-profiles { + flex: 1 1 auto; + min-height: 110px; + max-height: none; + overflow: auto; + padding-bottom: 6px; +} + +.hockey-mapping-sidebar-title { + flex: 0 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-top: 2px; +} + +.hockey-mapping-sidebar-title h3 { margin: 0; } + +.hockey-map-new-profile, +.hockey-map-empty-profile button, +.hockey-map-create-footer button { + min-height: 34px; + padding: 0 11px; + border: 1px solid #35506a; + border-radius: 8px; + background: #132235; + color: #c8d7e7; + font-size: 10px; + font-weight: 900; + cursor: pointer; +} + +.hockey-map-new-profile { + flex: 0 0 auto; + border-color: #2d7d6d; + background: #12332f; + color: #75e7cf; +} + +.hockey-map-new-profile:hover, +.hockey-map-new-profile.is-active { + border-color: #48dfbd; + background: #16443c; + color: #a2f7e5; +} + +.hockey-map-empty-profile { + height: 100%; + box-sizing: border-box; + display: grid; + place-content: center; + justify-items: center; + gap: 8px; + text-align: center; +} + +.hockey-map-empty-profile strong { color: #e8f2fb; font-size: 16px; } +.hockey-map-empty-profile span { color: #8298ae; } + +.hockey-map-create-workspace { + height: 100%; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; + border: 1px solid #30445a; + border-radius: 14px; + background: #0d1723; +} + +.hockey-map-create-head { + flex: 0 0 auto; + padding: 18px 20px; + border-bottom: 1px solid #2a3d52; + background: linear-gradient(110deg, rgba(72,223,189,.08), rgba(13,23,35,0) 48%); +} + +.hockey-map-create-head > div { display: grid; gap: 4px; } +.hockey-map-create-head span { color: #48dfbd; font-size: 9px; font-weight: 950; letter-spacing: .14em; } +.hockey-map-create-head strong { color: #eef6ff; font-size: 19px; } +.hockey-map-create-head small { color: #8297ad; line-height: 1.45; } + +.hockey-map-create-body { + flex: 1 1 auto; + min-height: 0; + overflow: auto; + display: grid; + align-content: start; + gap: 14px; + padding: 20px; +} + +.hockey-map-create-body label { + width: min(760px, 100%); + display: grid; + gap: 6px; + color: #91a6bc; + font-size: 11px; +} + +.hockey-map-create-body label > span { + color: #c4d3e2; + font-weight: 850; +} + +.hockey-map-create-body input, +.hockey-map-create-body select, +.hockey-map-create-body textarea { + width: 100%; + box-sizing: border-box; + border: 1px solid #37506a; + border-radius: 9px; + background: #0a141f; + color: #edf6ff; + padding: 8px 10px; + font: inherit; +} + +.hockey-map-create-body input, +.hockey-map-create-body select { min-height: 40px; } +.hockey-map-create-body textarea { min-height: 100px; resize: vertical; } +.hockey-map-create-body small { color: #6f859b; line-height: 1.4; } + +.hockey-map-create-footer { + flex: 0 0 auto; + display: flex; + justify-content: flex-end; + gap: 9px; + padding: 12px 16px; + border-top: 1px solid #2a3d52; + background: #0b1521; +} + +.hockey-map-create-footer button.is-accent, +.hockey-map-empty-profile button.is-accent { + border-color: #238f79; + background: #17433a; + color: #83f0d7; +} + +.hockey-map-create-footer button:disabled { opacity: .45; cursor: not-allowed; } + +.hockey-mapping-layout.is-visual .hockey-mapping-workspace { + min-width: 0; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.hockey-mapping-savebar.is-visual { + flex: 0 0 auto; + position: relative; + z-index: 10; + margin-top: 10px; + padding: 10px 0 0; + border-top: 1px solid #2e3b4e; + background: #0b1625; +} + +.hockey-mapping-savebar.is-visual [data-map-save] { + min-width: 170px; + min-height: 38px; + border-color: #238f79; + background: #17433a; + color: #8df2db; + font-weight: 950; +} + +@media (max-width: 900px) { + .hockey-directory-modal, + .hockey-directory-modal.is-mapping, + .hockey-directory-modal.is-khl-site { + grid-template-columns: minmax(0,1fr); + grid-template-rows: 64px auto minmax(0,1fr); + } + .hockey-directory-modal > header, + .hockey-directory-modal.is-mapping > header, + .hockey-directory-modal.is-khl-site > header { + grid-column: 1; + grid-row: 1; + min-height: 64px; + } + .hockey-directory-modal > nav.hockey-settings-nav { + grid-column: 1; + grid-row: 2; + flex-direction: row; + gap: 8px; + padding: 8px 10px; + overflow-x: auto; + overflow-y: hidden; + border-right: 0; + border-bottom: 1px solid #24394f; + } + .hockey-settings-nav-group { + display: flex; + gap: 4px; + align-items: center; + } + .hockey-settings-nav-group > span { display: none; } + .hockey-directory-modal > nav.hockey-settings-nav .hockey-settings-nav-group > button { + width: auto; + min-height: 34px; + padding: 0 10px; + white-space: nowrap; + } + .hockey-directory-content, + .hockey-directory-modal.is-khl-site .hockey-directory-content, + .hockey-directory-modal.is-mapping .hockey-directory-content { + grid-column: 1; + grid-row: 3; + } + .hockey-mapping-layout.is-visual { grid-template-columns: 1fr; overflow: auto; } + .hockey-mapping-layout.is-visual .hockey-mapping-sidebar { overflow: visible; } + .hockey-mapping-layout.is-visual .hockey-mapping-profiles { max-height: 220px; flex: 0 0 auto; } + .hockey-mapping-layout.is-visual .hockey-mapping-workspace { min-height: 680px; overflow: visible; } + .hockey-map-visual-workspace { grid-template-columns: 1fr; overflow: visible; } + .hockey-map-data-browser, .hockey-map-vmix-browser { min-height: 520px; } +} diff --git a/hockey_data/static/admin-directories.js b/hockey_data/static/admin-directories.js index e27e57f..9ab8d93 100644 --- a/hockey_data/static/admin-directories.js +++ b/hockey_data/static/admin-directories.js @@ -20,6 +20,7 @@ mappingProfiles: null, mappingActiveProfile: null, mappingCreateDeviceId: "", + mappingCreateOpen: false, mappingSelectedInputKey: "", mappingTargetField: "", mappingSelectedSourceKey: "", @@ -326,19 +327,28 @@ × - - Команды - Игроки - Тренеры - Судьи - Страны - Таймеры и составы - Удаления - vMix JSON - Mapping - Переменные - SQL источники - ${isKhlTournamentSelected() ? `КХЛ сайт` : ""} + + + СПРАВОЧНИКИ + Команды + Игроки + Тренеры + Судьи + Страны + + + ИГРА + Таймеры и составы + Удаления + + + ИНТЕГРАЦИИ + vMix JSON + Mapping + Переменные + SQL источники + ${isKhlTournamentSelected() ? `КХЛ сайт` : ""} + ${content} @@ -2262,6 +2272,7 @@ const profiles = state.mappingProfiles?.profiles || []; const profile = state.mappingActiveProfile; const usableDevices = devices.filter((item) => item.project_fingerprint); + const creatingProfile = state.mappingWorkspaceTab === "links" && state.mappingCreateOpen; const deviceRows = devices.length ? devices.map((item) => { const agentVersion = String(item.agent_version || "").trim(); const tooOldForMapping = Boolean(agentVersion && item.mapping_supported === false); @@ -2304,8 +2315,26 @@ editor = mappingContextPanel(); } else if (state.mappingWorkspaceTab === "sql") { editor = mappingSqlPanel(); + } else if (creatingProfile) { + const selectedCreateDevice = state.mappingCreateDeviceId || selectedDeviceId || usableDevices[0]?.device_id || ""; + state.mappingCreateDeviceId = selectedCreateDevice; + editor = ` + + + НОВЫЙ MAPPINGСоздать конфиг из текущего vMixСначала создаём профиль и считываем структуру Input/полей. После создания он сразу откроется для связывания данных. + + + Agent / текущий vMixВыберите устройство${usableDevices.map((item) => `${escapeHtml(item.name || item.device_id)} · ${Number(item.input_count || 0)} Inputs · ${Number(item.field_count || 0)} полей`).join("")}${usableDevices.length ? "Выберите Agent, из которого будет считана структура графического проекта." : "Нет Agent с доступной структурой vMix. Подключите Agent и дождитесь чтения проекта."} + Название конфига + Описание + + + `; } else if (!profile) { - editor = `Выберите профиль слева или создайте новый из подключённого vMix.`; + editor = `Mapping-конфиг не выбранВыберите профиль слева или нажмите «+ Новый конфиг».+ Новый конфиг`; } else { const currentInput = mappingCurrentInput(profile); const profileId = Number(profile.id || 0); @@ -2357,18 +2386,9 @@ ${statusMarkup()}${mappingDiagnostics}${editor} `); @@ -2627,7 +2647,30 @@ renderMapping(); })); + modal.querySelectorAll("[data-map-new-profile]").forEach((button) => button.addEventListener("click", () => { + state.mappingCreateOpen = true; + state.mappingWorkspaceTab = "links"; + state.section = "mapping"; + const selectedId = String(localStorage.getItem("hockey.vmix.selected_device") || "").trim(); + const candidates = state.mappingDevices?.devices || []; + state.mappingCreateDeviceId = candidates.some((item) => item.device_id === selectedId && item.project_fingerprint) + ? selectedId + : (candidates.find((item) => item.project_fingerprint)?.device_id || ""); + setStatus(""); + renderMapping(); + setTimeout(() => ensureModal().querySelector('[data-mapping-create] input[name="name"]')?.focus(), 0); + })); + modal.querySelector("[data-map-create-cancel]")?.addEventListener("click", () => { + state.mappingCreateOpen = false; + setStatus(""); + renderMapping(); + }); + modal.querySelector("[data-map-create-device]")?.addEventListener("change", (event) => { + state.mappingCreateDeviceId = event.target.value || ""; + }); + modal.querySelectorAll("[data-mapping-profile]").forEach((button) => button.addEventListener("click", async () => { + state.mappingCreateOpen = false; try { state.mappingActiveProfile = await request(`/api/hockey/admin/vmix-mapping/profiles/${button.dataset.mappingProfile}`); state.mappingSelectedInputKey = ""; @@ -2640,14 +2683,25 @@ })); modal.querySelector("[data-mapping-create]")?.addEventListener("submit", async (event) => { event.preventDefault(); + const submit = event.currentTarget.querySelector('button[type="submit"]'); const form = new FormData(event.currentTarget); + const deviceId = String(form.get("device_id") || "").trim(); + const name = String(form.get("name") || "").trim(); + if (!deviceId) return setStatus("Выберите Agent / vMix для нового Mapping-конфига.", true), renderMapping(); + if (!name) return setStatus("Введите название нового Mapping-конфига.", true), renderMapping(); + if (submit) { submit.disabled = true; submit.textContent = "Создаю…"; } try { - const created = await request("/api/hockey/admin/vmix-mapping/profiles", { method: "POST", body: JSON.stringify({ device_id: form.get("device_id"), name: form.get("name"), description: form.get("description") }) }); + const created = await request("/api/hockey/admin/vmix-mapping/profiles", { method: "POST", body: JSON.stringify({ device_id: deviceId, name, description: form.get("description") }) }); + state.mappingCreateOpen = false; await loadMapping(created.id); state.mappingSelectedInputKey = ""; + state.mappingTargetField = ""; + state.mappingSelectedSourceKey = ""; state.mappingVmixScrollTop = 0; - setStatus(`Mapping «${created.name}» создан.`); - } catch (error) { setStatus(error.message, true); } + setStatus(`✓ Mapping «${created.name}» создан и открыт. Свяжите поля и нажмите «Сохранить конфиг».`); + } catch (error) { + setStatus(error.message, true); + } renderMapping(); }); @@ -3056,6 +3110,7 @@ modal.querySelectorAll("[data-directory-section]").forEach((button) => { button.addEventListener("click", async () => { state.section = button.dataset.directorySection; + if (state.section !== "mapping") state.mappingCreateOpen = false; if (isMappingSection()) state.mappingWorkspaceTab = mappingTabFromSection(); state.editingTeam = null; state.editingPenalty = null;