(function () { const LOG = "[WespKioskPairing]"; const API = { status: "/api/kiosk/status", accessLink: "/api/kiosk/access-link", setupAvailable: "/api/kiosk/setup-available", }; const LOCALHOST_HOSTS = new Set(["localhost", "127.0.0.1", "::1"]); function isLocalhostContext() { return LOCALHOST_HOSTS.has((window.location.hostname || "").toLowerCase()); } function ensureModalStyles() { const criticalId = "wespKioskPairCriticalCss"; if (!document.getElementById(criticalId)) { const style = document.createElement("style"); style.id = criticalId; style.textContent = "#kioskPairModal{display:none;position:fixed;inset:0;z-index:5000;padding:8px;background:rgba(0,0,0,.55);overflow:hidden}" + "#kioskPairModal.kiosk-pair-open{display:flex;align-items:stretch;justify-content:center;height:100vh;height:100dvh}" + "body.kiosk-pair-modal-open{overflow:hidden!important}" + "#kioskPairModal .kiosk-pair-panel{display:flex;flex-direction:column;width:min(480px,100%);height:100%;max-height:100%;min-height:0;overflow:hidden;box-sizing:border-box}" + "#kioskPairModal .kiosk-pair-body{flex:1 1 auto;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch}" + "#kioskPairModal .kiosk-pair-actions{flex:0 0 auto;display:flex;flex-wrap:wrap;gap:6px}"; document.head.appendChild(style); } if (document.getElementById("wespKioskModalsCss")) return; if (window.WespKioskTheme && typeof window.WespKioskTheme.ensureModalStyles === "function") { window.WespKioskTheme.ensureModalStyles(); return; } const link = document.createElement("link"); link.id = "wespKioskModalsCss"; link.rel = "stylesheet"; link.href = "/static/css/kiosk-modals.css"; document.head.appendChild(link); } function createModal() { ensureModalStyles(); const modal = document.createElement("div"); modal.id = "kioskPairModal"; modal.innerHTML = ` `; document.body.appendChild(modal); return modal; } function createBlockOverlay() { const el = document.createElement("div"); el.id = "kioskBlockedOverlay"; el.style.cssText = "display:none;position:fixed;inset:0;background:rgba(10,10,10,.78);z-index:4500;color:#fff;align-items:center;justify-content:center;text-align:center;padding:20px;"; el.innerHTML = `

Терминал не привязан

Обратитесь к администратору для настройки терминала.

`; document.body.appendChild(el); return el; } async function fetchJson(url, options) { const response = await fetch(url, options); const data = await response.json().catch(() => ({})); if (!response.ok) { console.warn(LOG, url, response.status, data.message || data); throw new Error(data.message || `HTTP ${response.status}`); } return data; } function setup(opts) { const options = opts || {}; const modal = createModal(); const block = createBlockOverlay(); const qrImg = modal.querySelector("#kioskPairQr"); const qrWrap = modal.querySelector(".kiosk-pair-qr-wrap"); const loadingEl = modal.querySelector("#kioskPairLoading"); const urlEl = modal.querySelector("#kioskPairUrl"); const statusEl = modal.querySelector("#kioskPairStatus"); const copyBtn = modal.querySelector("#kioskPairCopy"); const refreshBtn = modal.querySelector("#kioskPairRefresh"); const closeBtn = modal.querySelector("#kioskPairClose"); let currentStartUrl = ""; let canIssueAccessLink = isLocalhostContext(); function isEnforcePairedOnly(data) { if (!data || typeof data !== "object") return true; return data.enforce_paired_only !== false; } async function refreshSetupAvailability() { if (isLocalhostContext()) { canIssueAccessLink = true; return true; } try { const data = await fetchJson(API.setupAvailable); canIssueAccessLink = !!data.can_issue_access_link; } catch (e) { console.warn(LOG, "setup-available error", e); canIssueAccessLink = false; } if (options.menuLinkSelector) { const menuLink = document.querySelector(options.menuLinkSelector); if (menuLink) { menuLink.style.display = canIssueAccessLink ? "" : "none"; } } return canIssueAccessLink; } async function checkStatus() { if (isLocalhostContext()) { block.style.display = "none"; if (typeof options.onPaired === "function") options.onPaired({ paired: true, localhost: true }); return true; } try { const data = await fetchJson(API.status); const enforce = isEnforcePairedOnly(data); const paired = !!data.paired || !enforce; console.debug(LOG, "/api/kiosk/status", { paired: !!data.paired, enforce_paired_only: enforce, device_id: data.device_id, status: data.status, }); block.style.display = paired ? "none" : "flex"; if (paired && typeof options.onPaired === "function") options.onPaired(data); if (!paired && typeof options.onUnpaired === "function") options.onUnpaired(data); return paired; } catch (e) { console.warn(LOG, "checkStatus error", e); block.style.display = "flex"; if (typeof options.onUnpaired === "function") options.onUnpaired({}); return false; } } async function loadAccessLink(refresh) { if (!canIssueAccessLink) { statusEl.textContent = "Выдача Start URL доступна только администратору или с localhost."; statusEl.style.color = "#ff9f9f"; return; } loadingEl.style.display = "flex"; qrWrap.classList.remove("kiosk-pair-qr-visible"); qrImg.removeAttribute("src"); statusEl.textContent = refresh ? "Обновление ссылки..." : "Загрузка ссылки..."; try { const data = await fetchJson(API.accessLink, { method: refresh ? "POST" : "GET", headers: refresh ? { "Content-Type": "application/json" } : undefined, body: refresh ? JSON.stringify({ refresh: true }) : undefined, }); currentStartUrl = data.start_url || data.pair_url || ""; console.info(LOG, "access-link OK", { start_url: currentStartUrl.slice(0, 80) }); const qrSrc = data.qr_image_url || ""; await new Promise((resolve) => { const done = () => { qrImg.onload = null; qrImg.onerror = null; resolve(); }; qrImg.onload = done; qrImg.onerror = done; qrImg.src = qrSrc; }); loadingEl.style.display = "none"; qrWrap.classList.add("kiosk-pair-qr-visible"); qrImg.style.display = "block"; urlEl.textContent = currentStartUrl; statusEl.textContent = "Постоянная ссылка. Вставьте её в Fully Kiosk → Start URL."; statusEl.style.color = ""; } catch (e) { loadingEl.style.display = "none"; statusEl.textContent = globalThis.WespUserMessages?.userFacingMessage?.(e.message, "Не удалось получить ссылку") || "Не удалось получить ссылку"; statusEl.style.color = "#ff9f9f"; } } async function openModal() { if (!canIssueAccessLink) { window.alert("Выдача Start URL доступна только администратору или при открытии с localhost."); return; } modal.classList.add("kiosk-pair-open"); document.body.classList.add("kiosk-pair-modal-open"); loadAccessLink(false); } function closeModal() { modal.classList.remove("kiosk-pair-open"); document.body.classList.remove("kiosk-pair-modal-open"); } async function copyStartUrl() { if (!currentStartUrl) { statusEl.textContent = "Сначала дождитесь загрузки ссылки"; return; } try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(currentStartUrl); } else { const ta = document.createElement("textarea"); ta.value = currentStartUrl; ta.style.position = "fixed"; ta.style.left = "-9999px"; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); document.body.removeChild(ta); } statusEl.textContent = "Ссылка скопирована"; statusEl.style.color = "#9fd89f"; } catch (e) { statusEl.textContent = "Не удалось скопировать — выделите ссылку вручную"; statusEl.style.color = "#ff9f9f"; } } async function refreshLink() { if (!window.confirm("Старая ссылка перестанет работать. Обновить Start URL?")) { return; } await loadAccessLink(true); } copyBtn.addEventListener("click", copyStartUrl); refreshBtn.addEventListener("click", refreshLink); closeBtn.addEventListener("click", closeModal); modal.addEventListener("click", (e) => { if (e.target === modal) closeModal(); }); if (options.menuLinkSelector) { const menuLink = document.querySelector(options.menuLinkSelector); if (menuLink) { menuLink.addEventListener("click", (e) => { e.preventDefault(); openModal(); }); } } refreshSetupAvailability().then(function () { checkStatus(); }); return { checkStatus, openModal }; } window.WespKioskPairing = { setup }; })();