const STATUS_CLASS = { pending: "pending", current: "current", running: "current", done: "done", error: "error", }; function normalizeStatus(status) { const key = (status || "pending").toString().toLowerCase(); return STATUS_CLASS[key] || "pending"; } function currentStepLabel(steps = [], fallback = "") { const current = steps.find((step) => { const status = typeof step === "string" ? "pending" : step.status; return ["current", "running", "error"].includes(status); }); if (current) { return typeof current === "string" ? current : current.label; } const first = steps.find((step) => (typeof step === "string" ? step : step.label)); if (first) return typeof first === "string" ? first : first.label; return fallback || ""; } function renderSteps(stepsEl, steps = []) { if (!stepsEl) return; stepsEl.innerHTML = ""; steps.forEach((step) => { const li = document.createElement("li"); const status = normalizeStatus(step.status); li.className = `firstboot-step ${status}`; const dot = document.createElement("span"); dot.className = "step-dot"; dot.setAttribute("aria-hidden", "true"); const label = document.createElement("span"); label.className = "step-label"; label.textContent = step.label || ""; li.appendChild(dot); li.appendChild(label); stepsEl.appendChild(li); }); } function setLogText(logEl, text) { if (!logEl) return; const value = text && text.trim().length ? text : "Waiting for setup logs..."; logEl.textContent = value; logEl.scrollTop = logEl.scrollHeight; } function wireCopyButton(btn, getText, showToast) { if (!btn) return; btn.addEventListener("click", async () => { const text = getText(); if (!text) return; try { if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(text); } else { const ta = document.createElement("textarea"); ta.value = text; ta.style.position = "fixed"; ta.style.opacity = "0"; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); document.body.removeChild(ta); } showToast?.("Copied error log", "success"); } catch (err) { showToast?.("Copy failed", "error"); } }); } export function createFirstbootUI({ overlay, stepsEl, currentStepEl, logEl, logNoteEl, errorModal, errorLogEl, errorCloseBtn, errorCopyBtn, errorShowRecoveryBtn, recoveryEl, showToast, }) { let lastErrorText = ""; if (errorModal) { errorModal.addEventListener("click", (e) => { if (e.target === errorModal) errorModal.classList.add("hidden"); }); } errorCloseBtn?.addEventListener("click", () => errorModal?.classList.add("hidden")); errorShowRecoveryBtn?.addEventListener("click", () => recoveryEl?.classList.toggle("hidden")); wireCopyButton(errorCopyBtn, () => lastErrorText, showToast); function update(data) { if (!data) return; const steps = Array.isArray(data.steps) ? data.steps : []; const current = data.current_step || currentStepLabel(steps); renderSteps( stepsEl, steps.map((step) => { const label = typeof step === "string" ? step : step.label || ""; const status = typeof step === "string" ? "pending" : step.status; return { label, status: normalizeStatus(status) }; }) ); if (currentStepEl) { currentStepEl.textContent = current ? `Current step: ${current}` : "Current step: preparing"; } setLogText(logEl, data.log_tail || ""); if (logNoteEl) logNoteEl.textContent = "If this stalls for more than 10 minutes, refresh the page or check SSH."; } function showOverlay(show) { if (!overlay) return; overlay.classList.toggle("hidden", !show); } function showError(text) { lastErrorText = text || ""; if (errorLogEl) { errorLogEl.textContent = lastErrorText || "(no error log found)"; } if (errorModal) errorModal.classList.remove("hidden"); } return { update, showOverlay, showError, }; }