Files
site/apps/web/public/wesp/js/pages/setup-wizard.js
T

884 lines
31 KiB
JavaScript

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 = `<strong>${title}</strong><span>${hint}</span>`;
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 = `<strong>${title}</strong>`;
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);
}