// Минимальный клиент wizard'а: рендерит страницы, ловит события из SSE, // отправляет POST'ы на backend для перехода между стадиями. let state = { stage: "welcome", precheck: [], config: {}, steps: [], errorMsg: "", }; const STAGE_ORDER = ["welcome", "precheck", "config", "installing", "done"]; const STEP_ICONS = { pending: "○", running: "◐", done: "✓", skipped: "—", failed: "✗", }; function $(sel) { return document.querySelector(sel); } function $$(sel) { return [...document.querySelectorAll(sel)]; } function render() { // stepper $$("#stepper span").forEach(el => { el.classList.remove("active", "done"); const stage = el.dataset.stage; if (stage === state.stage) el.classList.add("active"); if (STAGE_ORDER.indexOf(stage) < STAGE_ORDER.indexOf(state.stage)) el.classList.add("done"); }); // pages $$(".page").forEach(p => p.classList.toggle("active", p.dataset.stage === state.stage)); if (state.stage === "precheck") renderPrecheck(); if (state.stage === "installing" || state.stage === "done") renderSteps(); if (state.stage === "error") $("#error-message").textContent = state.errorMsg || "(нет деталей)"; if (state.stage === "done") { // подставляем хост машины в админскую ссылку const adminURL = window.location.protocol + "//" + window.location.hostname + ":8080/admin/setup"; $("#adminLink").href = adminURL; $("#adminLink").textContent = "Перейти в " + adminURL + " →"; } } function renderPrecheck() { const root = $("#precheck-results"); root.innerHTML = ""; let allOK = true; for (const r of state.precheck || []) { const div = document.createElement("div"); div.className = "check " + (r.ok ? "ok" : "bad"); div.innerHTML = ` ${r.ok ? "✓" : "✗"}
${escapeHTML(r.title)}
${r.message ? `
${escapeHTML(r.message)}
` : ""}
`; root.appendChild(div); if (!r.ok) allOK = false; } $("#goConfigBtn").disabled = !allOK; } function renderSteps() { const root = $("#step-list"); root.innerHTML = ""; let done = 0; for (const s of state.steps || []) { const li = document.createElement("li"); li.className = "step-" + s.status; li.innerHTML = ` ${STEP_ICONS[s.status] || "○"}
${escapeHTML(s.title)}
${s.message ? `
${escapeHTML(s.message)}
` : ""}
`; root.appendChild(li); if (s.status === "done" || s.status === "skipped") done++; } const total = state.steps.length; const pct = total ? Math.round(100 * done / total) : 0; $("#progress-bar").style.width = pct + "%"; } function escapeHTML(s) { return String(s).replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", "\"": """, "'": "'" }[c])); } // ------------- transitions ------------- async function startPrecheck() { await fetch("/api/precheck", { method: "POST" }); } function goWelcome() { state.stage = "welcome"; render(); } function goPrecheck() { state.stage = "precheck"; render(); } async function goConfig() { state.stage = "config"; render(); } async function startInstall() { const form = $("#config-form"); const data = Object.fromEntries(new FormData(form).entries()); await fetch("/api/config", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); await fetch("/api/install", { method: "POST" }); } async function resetWizard() { await fetch("/api/reset", { method: "POST" }); } // ------------- SSE ------------- function connectSSE() { const es = new EventSource("/api/events"); es.addEventListener("snapshot", e => { const snap = JSON.parse(e.data); state.stage = snap.stage; state.precheck = snap.precheck || []; state.config = snap.config || {}; state.steps = snap.steps || []; state.errorMsg = snap.errorMsg || ""; render(); }); es.addEventListener("stage", e => { state.stage = JSON.parse(e.data).stage; render(); }); es.addEventListener("precheck", e => { state.precheck = JSON.parse(e.data); render(); }); es.addEventListener("step", e => { const s = JSON.parse(e.data); const idx = state.steps.findIndex(x => x.id === s.id); if (idx >= 0) state.steps[idx] = s; render(); }); es.addEventListener("error", e => { state.errorMsg = JSON.parse(e.data).message; state.stage = "error"; render(); }); es.addEventListener("reset", () => { location.reload(); }); es.onerror = () => { // авто-реконнект делает EventSource сам, ничего не делаем }; } connectSSE();