import { mountWespLogoLoader } from "/static/js/wesp-logo-loader.js"; import { createSetupOrchestrator } from "/static/js/setup-orchestrator.js"; import { SetupTouchKeyboard } from "/static/js/setup-touch-keyboard.js"; import { DEFAULT_PASSWORD_MIN_LEN, validateNewUserCredentials, } from "/static/js/wesp-user-credentials.js"; const API = { status: "/api/setup/status", deviceRole: "/api/setup/device-role", network: "/api/setup/network", sync: "/api/setup/sync", syncProgress: "/api/setup/sync/progress", users: "/api/setup/users", hardware: "/api/setup/hardware/check", kioskPrepare: "/api/setup/kiosk/prepare", kioskSetupAvailable: "/api/kiosk/setup-available", complete: "/api/setup/complete", kioskAccessLink: "/api/kiosk/access-link", kioskStatus: "/api/kiosk/status", }; const THEME_STORAGE_KEY = "theme"; const STEP_LABELS = { theme: "Тема", welcome: "Начало", device: "Устройство", network: "Сеть", sync: "Сервер", users: "Пользователи", hardware: "Весы", kiosk: "Терминал", bootstrap: "Синхронизация", done: "Готово", }; function passwordMinLen(statusData) { const n = Number(statusData?.user_rules?.password_min_len); return Number.isFinite(n) && n > 0 ? n : DEFAULT_PASSWORD_MIN_LEN; } async function fetchJson(url, options) { const response = await fetch(url, options); const data = await response.json().catch(() => ({})); if (!response.ok) { const msg = globalThis.WespUserMessages?.messageFromResponseBody?.( data, "Не удалось выполнить запрос" ) || "Не удалось выполнить запрос"; throw new Error(msg); } return data; } function setupUserError(message, fallback) { return globalThis.WespUserMessages?.userFacingMessage?.(message, fallback) || fallback || "Не удалось выполнить операцию."; } async function canIssueKioskAccessLink() { try { const data = await fetchJson(API.kioskSetupAvailable); return !!data.can_issue_access_link; } catch { return false; } } function el(tag, className, text) { const node = document.createElement(tag); if (className) node.className = className; if (text != null) node.textContent = text; return node; } function field(label, input) { const wrap = el("div", "wesp-setup__field"); wrap.appendChild(el("label", "wesp-setup__label", label)); wrap.appendChild(input); return wrap; } function fieldWithSuffix(label, input, suffixText) { const wrap = el("div", "wesp-setup__field"); wrap.appendChild(el("label", "wesp-setup__label", label)); const group = el("div", "wesp-setup__input-group"); group.appendChild(input); group.appendChild(el("span", "wesp-setup__input-suffix", suffixText)); wrap.appendChild(group); return wrap; } function normalizeHostnameLabel(value) { return String(value || "").trim().toLowerCase().replace(/\.local$/i, ""); } function formatSyncServerDisplay(serverUrl) { const raw = String(serverUrl || "").trim().replace(/\/+$/, ""); if (raw) return raw; return "http://komton_srv_1.local"; } function buildSyncServerUrl(raw) { const value = String(raw || "").trim().replace(/\/+$/, ""); if (!value) return ""; if (/^https?:\/\//i.test(value)) return value; if (/^\d+\.\d+\.\d+\.\d+(:\d+)?$/i.test(value)) return `http://${value}`; const host = normalizeHostnameLabel(value); return host ? `http://${host}.local` : ""; } function readSetupTheme() { try { const t = (localStorage.getItem(THEME_STORAGE_KEY) || "").trim().toLowerCase(); return t === "light" ? "light" : "dark"; } catch { return "dark"; } } function applySetupTheme(theme) { const next = theme === "dark" ? "dark" : "light"; document.documentElement.setAttribute("data-theme", next); try { localStorage.setItem(THEME_STORAGE_KEY, next); } catch { /* ignore */ } } function stepIndicatorLabel(step) { return STEP_LABELS[step] || step || ""; } function textInput(id, placeholder, type = "text") { const input = document.createElement("input"); input.className = "wesp-setup__input"; input.id = id; input.type = type; input.placeholder = placeholder; return input; } function postSetupLandingPath(statusData) { const role = statusData?.setup?.device_role; const scaleHw = Boolean(statusData?.hardware?.scale_hardware_platform); if (role === "client" && scaleHw) return "/scales"; return "/login"; } export async function mountSetupWizard(root) { const orchestrator = createSetupOrchestrator(); let statusData = null; let bootstrapTimer = 0; let activeTouchKeyboard = null; let introStarted = false; const intro = el("div", "wesp-setup__intro"); intro.setAttribute("aria-hidden", "false"); root.appendChild(intro); const panel = el("div", "wesp-setup__panel wesp-setup__panel--hidden"); panel.setAttribute("role", "dialog"); panel.setAttribute("aria-live", "polite"); root.appendChild(panel); const statusPromise = loadStatusEarly(); async function loadStatusEarly() { statusData = await fetchJson(API.status); if (statusData.setup?.setup_completed) { window.location.href = postSetupLandingPath(statusData); return statusData; } if (statusData.setup?.device_role === "client") { orchestrator.setDeviceRole("client"); } else { orchestrator.setDeviceRole("server"); } return statusData; } async function playIntroAnimation() { if (introStarted) return; introStarted = true; await mountWespLogoLoader(intro, { loop: false, embedded: true, transparentBackground: true, knockoutBackground: true, darkTheme: readSetupTheme() === "dark", hideChrome: true, onComplete: () => revealWelcome(), }); } async function revealWelcome() { intro.classList.add("wesp-setup__intro--done"); intro.setAttribute("aria-hidden", "true"); await statusPromise; if (statusData?.setup?.setup_completed) return; panel.classList.remove("wesp-setup__panel--hidden"); renderTheme(); window.setTimeout(() => { if (intro.isConnected) intro.remove(); }, 480); } playIntroAnimation(); function setPanelBusy(busy, label = "Сохранение…") { let overlay = panel.querySelector(".wesp-setup__busy"); if (busy) { if (!overlay) { overlay = el("div", "wesp-setup__busy"); overlay.appendChild(el("span", "wesp-setup__spinner")); overlay.appendChild(el("span", "wesp-setup__busy-label", label)); panel.appendChild(overlay); } else { overlay.querySelector(".wesp-setup__busy-label").textContent = label; overlay.hidden = false; } panel.classList.add("wesp-setup__panel--busy"); panel.setAttribute("aria-busy", "true"); return; } overlay?.remove(); panel.classList.remove("wesp-setup__panel--busy"); panel.removeAttribute("aria-busy"); } async function withStepLoading(label, fn) { setPanelBusy(true, label); try { return await fn(); } finally { setPanelBusy(false); } } function renderShell(title, lead) { activeTouchKeyboard?.destroy(); activeTouchKeyboard = null; panel.replaceChildren(); panel.classList.remove("wesp-setup__panel--busy"); panel.removeAttribute("aria-busy"); const header = el("div", "wesp-setup__header"); header.appendChild( el("div", "wesp-setup__step-indicator", `Шаг · ${stepIndicatorLabel(orchestrator.currentStep)}`), ); header.appendChild(el("h1", "wesp-setup__title", title)); if (lead) header.appendChild(el("p", "wesp-setup__lead", lead)); panel.appendChild(header); const content = el("div", "wesp-setup__content"); panel.appendChild(content); return content; } function populateActionRow(row, ...buttons) { row.replaceChildren(); let primaryIndex = -1; buttons.forEach((b, i) => { if (b.classList.contains("wesp-setup__btn--primary")) primaryIndex = i; }); buttons.forEach((b, i) => { if (!b.classList.contains("wesp-setup__btn--primary")) { b.classList.add("wesp-setup__btn--ghost"); } if (i === primaryIndex && primaryIndex > 0) { row.appendChild(el("div", "wesp-setup__actions-fill")); } row.appendChild(b); }); } function actions(...buttons) { const footer = el("div", "wesp-setup__footer"); const row = el("div", "wesp-setup__actions"); populateActionRow(row, ...buttons); footer.appendChild(row); panel.appendChild(footer); return row; } function msg(text, kind, target) { const m = el("p", `wesp-setup__msg${kind ? ` wesp-setup__msg--${kind}` : ""}`, text || ""); (target || panel.querySelector(".wesp-setup__content") || panel).appendChild(m); return m; } function btn(label, className, onClick) { const b = el("button", `wesp-setup__btn${className ? ` ${className}` : ""}`, label); b.type = "button"; b.addEventListener("click", onClick); return b; } async function loadStatus() { statusData = await fetchJson(API.status); if (statusData.setup?.setup_completed) { window.location.href = postSetupLandingPath(statusData); return statusData; } if (statusData.setup?.device_role === "client") { orchestrator.setDeviceRole("client"); } else { orchestrator.setDeviceRole("server"); } return statusData; } function shouldShowBootstrap() { return !!statusData?.setup?.sync_server_connected; } function goAfterKiosk() { if (shouldShowBootstrap()) renderBootstrap(); else goComplete(); } async function goComplete() { orchestrator.setStep("done"); const data = await withStepLoading("Завершение…", () => fetchJson(API.complete, { method: "POST" }), ); const fallbackRedirect = postSetupLandingPath(statusData); const redirect = data.redirect || fallbackRedirect; const opensScales = redirect === "/scales"; renderShell( "Готово", opensScales ? "Система настроена. Сейчас откроется экран весов." : "Система настроена. Сейчас откроется страница входа.", ); msg(data.message, "ok"); actions(btn("Открыть", "wesp-setup__btn--primary", () => { window.location.href = redirect; })); window.setTimeout(() => { window.location.href = redirect; }, 2200); } function renderTheme() { orchestrator.setStep("theme"); const content = renderShell( "Оформление", "Выберите тему. Её можно сменить позже в меню терминала.", ); let selected = readSetupTheme(); applySetupTheme(selected); const grid = el("div", "wesp-setup__role-grid wesp-setup__role-grid--cards wesp-setup__theme-grid"); const options = [ ["light", "Светлая", "Светлый фон и контрастные элементы."], ["dark", "Тёмная", "Удобнее при слабом освещении."], ]; const buttons = []; options.forEach(([value, title, hint]) => { const b = el("button", `wesp-setup__role-btn wesp-setup__role-card wesp-setup__theme-card wesp-setup__theme-card--${value}`, ""); b.type = "button"; b.innerHTML = `${title}${hint}`; if (selected === value) b.classList.add("is-selected"); b.addEventListener("click", () => { selected = value; applySetupTheme(selected); buttons.forEach((x) => x.classList.toggle("is-selected", x === b)); }); buttons.push(b); grid.appendChild(b); }); content.appendChild(grid); actions(btn("Далее", "wesp-setup__btn--primary", () => renderWelcome())); } function renderWelcome() { orchestrator.setStep("welcome"); renderShell( "Добро пожаловать", "Мастер поможет выбрать роль устройства и базовые параметры.", ); const health = statusData?.health; if (health?.ok) { msg("Система готова к настройке.", "ok"); } else { msg("Проверка health… при ошибках миграций обратитесь к установщику.", "error"); } actions(btn("Начать", "wesp-setup__btn--primary", () => renderDevice())); } function renderDevice() { orchestrator.setStep("device"); const content = renderShell("Тип устройства", "Где будет работать это устройство?"); const grid = el("div", "wesp-setup__role-grid wesp-setup__role-grid--cards"); const roles = [ ["server", "Главный сервер"], ["client", "Весовой терминал"], ]; let selected = orchestrator.deviceRole === "client" ? "client" : "server"; const buttons = []; roles.forEach(([value, title]) => { const b = el("button", "wesp-setup__role-btn wesp-setup__role-card", ""); b.type = "button"; b.innerHTML = `${title}`; if (selected === value) b.classList.add("is-selected"); b.addEventListener("click", () => { selected = value; buttons.forEach((x) => x.classList.toggle("is-selected", x === b)); }); buttons.push(b); grid.appendChild(b); }); content.appendChild(grid); const statusEl = msg(""); actions( btn("Назад", "", () => renderWelcome()), btn("Далее", "wesp-setup__btn--primary", async () => { try { const data = await withStepLoading("Сохранение…", () => fetchJson(API.deviceRole, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ device_role: selected }), }), ); if (data?.connection && statusData) { statusData.connection = data.connection; } orchestrator.setDeviceRole(selected); if (selected === "server") renderNetwork(); else renderSync(); } catch (e) { statusEl.textContent = setupUserError(e.message, "Не удалось выполнить шаг настройки."); statusEl.className = "wesp-setup__msg wesp-setup__msg--error"; } }), ); } function renderNetwork() { orchestrator.setStep("network"); const content = renderShell( "Сеть", "Как этот сервер будет называться в вашей Wi‑Fi сети.", ); const net = statusData?.network || {}; const hostDefault = normalizeHostnameLabel( net.local_hostname || net.default_local_hostname || "komton_srv_1", ); const hostLocked = !!net.local_hostname_locked_by_env; const hostInput = textInput("setupHost", hostDefault); hostInput.placeholder = "komton_srv_1"; hostInput.disabled = hostLocked; hostInput.autocomplete = "off"; hostInput.spellcheck = false; hostInput.addEventListener("input", () => { const cleaned = normalizeHostnameLabel(hostInput.value); if (hostInput.value !== cleaned) hostInput.value = cleaned; }); content.appendChild(fieldWithSuffix("Имя сервера", hostInput, ".local")); content.appendChild( el( "p", "wesp-setup__hint", "Латиница и цифры, без пробелов. Суффикс .local добавится автоматически.", ), ); if (net.detected?.lan_ip) { content.appendChild(el("p", "wesp-setup__hint", `IP в сети: ${net.detected.lan_ip}`)); } const statusEl = msg(""); actions( btn("Назад", "", () => renderDevice()), btn("Далее", "wesp-setup__btn--primary", async () => { try { const body = { local_hostname: normalizeHostnameLabel(hostInput.value) }; if (!net.mdns_enabled_locked_by_env) { body.mdns_enabled = true; } await withStepLoading("Сохранение…", async () => { await fetchJson(API.network, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); await loadStatus(); }); renderUsers(); } catch (e) { statusEl.textContent = setupUserError(e.message, "Не удалось выполнить шаг настройки."); statusEl.className = "wesp-setup__msg wesp-setup__msg--error"; } }), ); } function renderUsers() { orchestrator.setStep("users"); const content = renderShell( "Учётные записи", "Создайте учётную запись зоотехника для работы с рецептами.", ); const zootechUsers = statusData?.zootech_users || []; if (zootechUsers.length) { content.appendChild( el( "p", "wesp-setup__info", `Уже есть: ${zootechUsers.join(", ")}. Тот же логин — обновит пароль, новый — добавит ещё одного зоотехника.`, ), ); } const zootechLogin = textInput("zootechLogin", zootechUsers[0] || "zootech"); const zootechPass = textInput("zootechPass", "Пароль", "password"); const zootechConfirm = textInput("zootechConfirm", "Повтор пароля", "password"); content.appendChild(field("Логин", zootechLogin)); content.appendChild(field("Пароль", zootechPass)); content.appendChild(field("Подтверждение", zootechConfirm)); const statusEl = msg(""); actions( btn("Назад", "", () => renderNetwork()), btn("Далее", "wesp-setup__btn--primary", async () => { const validationError = validateNewUserCredentials({ login: zootechLogin.value, password: zootechPass.value, confirmPassword: zootechConfirm.value, passwordMinLen: passwordMinLen(statusData), label: "Зоотехник", }); if (validationError) { statusEl.textContent = validationError; statusEl.className = "wesp-setup__msg wesp-setup__msg--error"; return; } try { const data = await withStepLoading("Проверка…", () => fetchJson(API.users, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ zootech: { login: zootechLogin.value.trim(), password: zootechPass.value, confirm: zootechConfirm.value, }, }), }), ); await loadStatus(); statusEl.textContent = data.message || "Сохранено."; statusEl.className = "wesp-setup__msg wesp-setup__msg--ok"; window.setTimeout(() => goComplete(), 350); } catch (e) { statusEl.textContent = setupUserError(e.message, "Не удалось выполнить шаг настройки."); statusEl.className = "wesp-setup__msg wesp-setup__msg--error"; } }), ); } function renderHardware() { orchestrator.setStep("hardware"); const content = renderShell( "Весы", "Проверьте подключение весов или выполните калибровку.", ); const platform = statusData?.hardware || {}; if (platform.scale_hardware_platform) { content.appendChild( el( "p", "wesp-setup__hint", "Платформа ARM — можно подключить весы HX711 к Raspberry Pi.", ), ); } else { content.appendChild( el( "p", "wesp-setup__info wesp-setup__info--warn", platform.message || "Весы не найдены", ), ); } const statusEl = msg(""); actions( btn("Назад", "", () => renderSync()), btn("Проверить весы", "", async () => { if (!platform.scale_hardware_platform) { statusEl.textContent = platform.message || "Весы не найдены"; statusEl.className = "wesp-setup__msg wesp-setup__msg--error"; return; } try { const data = await withStepLoading("Проверка весов…", () => fetchJson(API.hardware, { method: "POST" }), ); statusEl.textContent = data.ok ? `OK · вес ${data.hardware?.current_weight_kg ?? "—"} кг` : "HX711 не отвечает — проверьте подключение или включите симуляцию в админке."; statusEl.className = `wesp-setup__msg ${data.ok ? "wesp-setup__msg--ok" : "wesp-setup__msg--error"}`; } catch (e) { statusEl.textContent = setupUserError(e.message, "Не удалось выполнить шаг настройки."); statusEl.className = "wesp-setup__msg wesp-setup__msg--error"; } }), btn("Калибровка", "", () => { if (window.WespKioskCalibration?.openModal) { window.WespKioskCalibration.openModal({ zIndex: 10050, theme: readSetupTheme() }); return; } window.open("/calibration?embed=1", "_blank", "noopener,noreferrer"); }), btn("Далее", "wesp-setup__btn--primary", () => renderKiosk()), ); } async function renderKiosk() { orchestrator.setStep("kiosk"); const localSetup = await canIssueKioskAccessLink(); if (!localSetup) { const content = renderShell( "Настройка терминала", "Этот шаг выполняется на самом терминале.", ); content.appendChild(el("p", "wesp-setup__info", "Настройка производится на терминале.")); actions( btn("Назад", "", () => renderHardware()), btn("Далее", "wesp-setup__btn--primary", () => goAfterKiosk()), ); return; } const content = renderShell( "Настройка терминала", "Привязка планшета тракториста по QR-коду.", ); content.appendChild( el( "p", "wesp-setup__info", "Дополнительные планшеты можно привязать позже через меню ☰ → Привязать терминал.", ), ); const qrWrap = el("div", "wesp-setup__qr-wrap"); const qr = el("img", "wesp-setup__qr"); qr.alt = "QR Start URL"; const urlEl = el("p", "wesp-setup__url"); qrWrap.appendChild(qr); content.appendChild(qrWrap); content.appendChild(urlEl); const statusEl = msg(""); try { await withStepLoading("Подготовка QR…", async () => { await fetchJson(API.kioskPrepare, { method: "POST" }); const data = await fetchJson(API.kioskAccessLink); qr.src = data.qr_image_url || ""; urlEl.textContent = data.start_url || ""; }); statusEl.textContent = "Вставьте ссылку в Fully Kiosk → Start URL."; statusEl.className = "wesp-setup__msg wesp-setup__msg--ok"; } catch (e) { statusEl.textContent = setupUserError(e.message, "Не удалось выполнить шаг настройки."); statusEl.className = "wesp-setup__msg wesp-setup__msg--error"; } actions( btn("Назад", "", () => renderHardware()), btn("Проверить статус", "", async () => { try { const st = await withStepLoading("Проверка…", () => fetchJson(API.kioskStatus)); statusEl.textContent = st.paired ? "Терминал привязан." : "Ожидание привязки…"; statusEl.className = `wesp-setup__msg ${st.paired ? "wesp-setup__msg--ok" : ""}`; } catch (e) { statusEl.textContent = setupUserError(e.message, "Не удалось выполнить шаг настройки."); statusEl.className = "wesp-setup__msg wesp-setup__msg--error"; } }), btn("Пропустить", "", () => goAfterKiosk()), btn("Далее", "wesp-setup__btn--primary", () => goAfterKiosk()), ); } function renderSync() { orchestrator.setStep("sync"); const content = renderShell( "Подключение к серверу", "Укажите адрес главного сервера и имя этого терминала.", ); const conn = statusData?.connection || {}; const serverLocked = !!conn.server_url_locked_by_env; const serverInput = textInput( "syncServer", serverLocked ? conn.server_url || conn.env_server_url || "" : formatSyncServerDisplay(conn.server_url), ); serverInput.placeholder = "komton_srv_1.local"; serverInput.disabled = serverLocked; content.appendChild(field("Адрес сервера", serverInput)); if (serverLocked) { content.appendChild( el( "p", "wesp-setup__hint", "Адрес задан в настройках системы (WESP_SYNC_SERVER_URL).", ), ); } else { content.appendChild( el( "p", "wesp-setup__hint", "Имя сервера (komton_srv_1.local) или IP, например http://192.168.0.10.", ), ); } const nameDefault = conn.client_name || normalizeHostnameLabel(statusData?.network?.local_hostname || "") || statusData?.network?.detected?.os_hostname || "vesy_1"; const nameInput = textInput("syncName", nameDefault); nameInput.placeholder = "vesy_1"; content.appendChild(field("Название терминала", nameInput)); const statusEl = msg(""); let actionsRow = null; async function submitSync(offline) { const serverUrl = buildSyncServerUrl(serverInput.value); if (!serverUrl && !serverLocked) { statusEl.textContent = "Укажите адрес сервера."; statusEl.className = "wesp-setup__msg wesp-setup__msg--error"; return; } const body = { client_name: nameInput.value.trim(), offline: !!offline, }; if (!serverLocked) { body.server_url = serverUrl; } await withStepLoading(offline ? "Сохранение…" : "Подключение…", () => fetchJson(API.sync, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }), ); await loadStatus(); renderHardware(); } function showSyncFailureChoices(errorMessage) { statusEl.textContent = setupUserError(errorMessage, "Не удалось подключиться к серверу."); statusEl.className = "wesp-setup__msg wesp-setup__msg--error"; if (!actionsRow) return; populateActionRow( actionsRow, btn("Исправить адрес", "", () => { serverInput.focus(); statusEl.textContent = ""; statusEl.className = "wesp-setup__msg"; populateActionRow( actionsRow, btn("Назад", "", () => renderDevice()), btn("Далее", "wesp-setup__btn--primary", onNext), ); }), btn("Повторить", "wesp-setup__btn--primary", onNext), btn("Продолжить без сервера", "", async () => { try { await submitSync(true); } catch (e) { statusEl.textContent = setupUserError(e.message, "Не удалось выполнить шаг настройки."); statusEl.className = "wesp-setup__msg wesp-setup__msg--error"; } }), ); } async function onNext() { try { await submitSync(false); } catch (e) { showSyncFailureChoices(e.message); } } actionsRow = actions( btn("Назад", "", () => renderDevice()), btn("Далее", "wesp-setup__btn--primary", onNext), ); activeTouchKeyboard = new SetupTouchKeyboard(panel); if (!serverInput.disabled) { activeTouchKeyboard.attach(serverInput, { layout: "url", maxLength: 256 }); } activeTouchKeyboard.attach(nameInput, { layout: "hostname", maxLength: 200 }); nameInput.addEventListener("input", () => { const cleaned = normalizeHostnameLabel(nameInput.value); if (nameInput.value !== cleaned) nameInput.value = cleaned; }); window.setTimeout(() => { if (!serverInput.disabled) serverInput.focus(); else nameInput.focus(); }, 80); } function renderBootstrap() { if (!shouldShowBootstrap()) { goComplete(); return; } orchestrator.setStep("bootstrap"); const content = renderShell( "Первая синхронизация", "Дождитесь загрузки данных с сервера.", ); const bar = el("div", "wesp-setup__progress-bar"); const fill = el("div", "wesp-setup__progress-fill"); bar.appendChild(fill); content.appendChild(bar); const statusEl = msg("Ожидание синхронизации…"); function stopPoll() { if (bootstrapTimer) window.clearInterval(bootstrapTimer); bootstrapTimer = 0; } async function poll() { try { const data = await fetchJson(API.syncProgress); const p = data.initial_sync_progress || {}; const percent = p.percent != null ? Number(p.percent) : data.first_bootstrap_done ? 100 : 10; fill.style.width = `${Math.min(100, percent)}%`; orchestrator.setBootstrapProgress(percent / 100); if (data.first_bootstrap_done) { stopPoll(); statusEl.textContent = "Синхронизация завершена."; statusEl.className = "wesp-setup__msg wesp-setup__msg--ok"; } else if (p.active) { statusEl.textContent = `Синхронизация… ${Math.round(percent)}%`; } } catch (e) { statusEl.textContent = setupUserError(e.message, "Не удалось выполнить шаг настройки."); statusEl.className = "wesp-setup__msg wesp-setup__msg--error"; } } poll(); bootstrapTimer = window.setInterval(poll, 2000); actions( btn("Назад", "", () => { stopPoll(); renderKiosk(); }), btn("Далее", "wesp-setup__btn--primary", () => { stopPoll(); goComplete(); }), ); } await statusPromise; } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => { const root = document.getElementById("wespSetupRoot"); if (root) mountSetupWizard(root); }); } else { const root = document.getElementById("wespSetupRoot"); if (root) mountSetupWizard(root); }