143 lines
4.0 KiB
JavaScript
143 lines
4.0 KiB
JavaScript
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,
|
|
};
|
|
}
|