286 lines
11 KiB
JavaScript
286 lines
11 KiB
JavaScript
(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 = `
|
|
<div class="kiosk-pair-panel" role="dialog" aria-modal="true" aria-labelledby="kioskPairTitle">
|
|
<div class="kiosk-pair-header">
|
|
<h3 id="kioskPairTitle">Привязать терминал</h3>
|
|
<p class="kiosk-pair-subtitle">Start URL для Fully Kiosk Browser</p>
|
|
</div>
|
|
<div class="kiosk-pair-body">
|
|
<div class="kiosk-pair-steps">
|
|
1) Скопируйте ссылку или отсканируйте QR.<br/>
|
|
2) Fully Kiosk → Settings → Start URL → вставьте ссылку → Save.<br/>
|
|
3) Перезапустите Fully Kiosk.
|
|
</div>
|
|
<div id="kioskPairLoading" class="kiosk-pair-loading">
|
|
<span class="kiosk-pair-spinner"></span>
|
|
<span>Загрузка ссылки...</span>
|
|
</div>
|
|
<div class="kiosk-pair-qr-wrap">
|
|
<img id="kioskPairQr" class="kiosk-pair-qr" alt="QR Start URL" />
|
|
</div>
|
|
<p id="kioskPairUrl" class="kiosk-pair-url"></p>
|
|
<p id="kioskPairStatus" class="kiosk-pair-status"></p>
|
|
</div>
|
|
<div class="kiosk-pair-actions">
|
|
<button id="kioskPairCopy" type="button" class="kiosk-pair-btn kiosk-pair-btn--primary">Скопировать ссылку</button>
|
|
<button id="kioskPairRefresh" type="button" class="kiosk-pair-btn kiosk-pair-btn--secondary">Обновить ссылку</button>
|
|
<button id="kioskPairClose" type="button" class="kiosk-pair-btn kiosk-pair-btn--ghost">Закрыть</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
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 = `
|
|
<div style="max-width:520px">
|
|
<h2 style="margin:0 0 10px 0">Терминал не привязан</h2>
|
|
<p style="margin:0;line-height:1.4">Обратитесь к администратору для настройки терминала.</p>
|
|
</div>
|
|
`;
|
|
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 };
|
|
})();
|