(() => { const body = document.body; const media = window.matchMedia("(max-width: 900px)"); const sidebarPreferenceKey = "lumi-sidebar-collapsed"; if (!media.matches && window.localStorage.getItem(sidebarPreferenceKey) === "true") { body.classList.add("sidebar-collapsed"); } document.querySelectorAll("[data-sidebar-toggle]").forEach((button) => { button.addEventListener("click", () => { if (media.matches) { body.classList.toggle("sidebar-open"); } else { body.classList.toggle("sidebar-collapsed"); window.localStorage.setItem( sidebarPreferenceKey, body.classList.contains("sidebar-collapsed") ? "true" : "false" ); } }); }); document.querySelector("[data-sidebar-dismiss]")?.addEventListener("click", () => { body.classList.remove("sidebar-open"); }); document.querySelectorAll(".nav-link").forEach((link) => { link.addEventListener("click", () => { if (body.classList.contains("sidebar-open")) { body.classList.remove("sidebar-open"); } }); }); document.querySelectorAll(".nav-section").forEach((section) => { const summary = section.querySelector("summary"); summary?.setAttribute("aria-expanded", section.open ? "true" : "false"); section.addEventListener("toggle", () => { summary?.setAttribute("aria-expanded", section.open ? "true" : "false"); if (!section.open) return; document.querySelectorAll(".nav-section[open]").forEach((other) => { if (other !== section) { other.open = false; other.querySelector("summary")?.setAttribute("aria-expanded", "false"); } }); }); }); media.addEventListener?.("change", () => { body.classList.remove("sidebar-open"); if (media.matches) { body.classList.remove("sidebar-collapsed"); } else if (window.localStorage.getItem(sidebarPreferenceKey) === "true") { body.classList.add("sidebar-collapsed"); } }); const editToggles = Array.from( document.querySelectorAll("[data-edit-toggle]") ); if (editToggles.length) { const editRows = Array.from(document.querySelectorAll("[data-edit-row]")); const updateToggleStates = () => { editToggles.forEach((button) => { const key = button.dataset.editToggle; const row = editRows.find((item) => item.dataset.editRow === key); const isOpen = row?.classList.contains("is-open"); button.setAttribute("aria-expanded", isOpen ? "true" : "false"); }); }; editToggles.forEach((button) => { button.addEventListener("click", () => { const key = button.dataset.editToggle; const target = editRows.find((item) => item.dataset.editRow === key); const willOpen = target ? !target.classList.contains("is-open") : false; editRows.forEach((row) => { row.classList.remove("is-open"); }); if (target && willOpen) { target.classList.add("is-open"); } updateToggleStates(); }); }); } document.querySelectorAll("[data-table]").forEach((table) => { const tbody = table.tBodies[0]; if (!tbody) { return; } let rows = Array.from(tbody.rows); const tableId = table.getAttribute("data-table"); const isCommandTable = tableId === "commands"; const isPageable = table.dataset.pageable !== undefined && table.dataset.pageable !== "false"; const pageSizes = (table.dataset.pageSizes || "25,50,100,250") .split(",") .map((value) => Number(value.trim())) .filter((value) => Number.isFinite(value) && value > 0); const defaultSize = Number(table.dataset.pageSize) || pageSizes[0] || 25; const sizeSelect = document.querySelector(`[data-table-size="${tableId}"]`); const pagination = document.querySelector( `[data-table-pagination="${tableId}"]` ); const prevButton = pagination?.querySelector("[data-page-prev]"); const nextButton = pagination?.querySelector("[data-page-next]"); const pageLabel = pagination?.querySelector("[data-page-label]"); let currentPage = 1; let currentPageSize = defaultSize; const buildCommandGroups = () => { const groupMap = new Map(); rows.forEach((row) => { const key = row.dataset.commandRoot; if (!key) { return; } groupMap.set(key, { root: row, subRows: [] }); }); rows.forEach((row) => { const parent = row.dataset.commandParent; if (!parent) { return; } const group = groupMap.get(parent); if (group) { group.subRows.push(row); } }); return groupMap; }; const commandGroups = isCommandTable ? buildCommandGroups() : null; let highlightTimeout = null; const setGroupExpanded = (group, expanded) => { if (!group || !group.root) { return; } group.root.dataset.expanded = expanded ? "true" : "false"; group.root.classList.toggle("is-expanded", expanded); group.subRows.forEach((row) => { row.classList.toggle("is-visible", expanded); }); const toggle = group.root.querySelector("[data-command-toggle]"); if (toggle) { toggle.setAttribute("aria-expanded", expanded ? "true" : "false"); } }; const clearCommandHighlights = () => { tbody.querySelectorAll(".command-highlight").forEach((row) => { row.classList.remove("command-highlight"); }); }; const highlightCommandRow = (row) => { if (!row || !tbody.contains(row)) { return; } clearCommandHighlights(); row.classList.add("command-highlight"); if (highlightTimeout) { window.clearTimeout(highlightTimeout); } highlightTimeout = window.setTimeout(() => { row.classList.remove("command-highlight"); }, 2200); }; const revealAnchorRow = () => { if (!isCommandTable || !commandGroups) { return; } const anchor = window.location.hash.slice(1); if (!anchor) { return; } const target = document.getElementById(anchor); if (!target || !tbody.contains(target)) { return; } if (target.dataset.commandParent) { const group = commandGroups.get(target.dataset.commandParent); if (group) { setGroupExpanded(group, true); } } highlightCommandRow(target); }; if (isCommandTable && commandGroups) { commandGroups.forEach((group) => { setGroupExpanded(group, false); }); tbody.querySelectorAll("[data-command-toggle]").forEach((button) => { button.addEventListener("click", (event) => { event.preventDefault(); const rootRow = button.closest("tr"); if (!rootRow) { return; } const key = rootRow.dataset.commandRoot; const group = key ? commandGroups.get(key) : null; if (!group) { return; } const expanded = rootRow.dataset.expanded === "true"; setGroupExpanded(group, !expanded); }); }); revealAnchorRow(); window.addEventListener("hashchange", () => { revealAnchorRow(); }); } const filterInput = document.querySelector( `[data-table-filter="${tableId}"]` ); const filterSelect = document.querySelector( `[data-table-filter-select="${tableId}"]` ); const filterKey = filterSelect?.dataset.filterKey || "filter"; const refreshRows = () => { rows = Array.from(tbody.rows); }; const getFilteredRows = () => { const term = (filterInput?.value || "").trim().toLowerCase(); const filterValue = (filterSelect?.value || "").trim().toLowerCase(); return rows.filter((row) => { const haystack = (row.dataset.search || row.textContent || "") .toLowerCase() .trim(); const matchesTerm = !term || haystack.includes(term); const rowFilter = (row.dataset[filterKey] || "").toLowerCase(); const matchesFilter = !filterValue || rowFilter === filterValue; return matchesTerm && matchesFilter; }); }; const applyPagination = () => { if (!isPageable || isCommandTable) { return; } refreshRows(); const filtered = getFilteredRows(); const totalPages = Math.max( 1, Math.ceil(filtered.length / currentPageSize) ); currentPage = Math.min(currentPage, totalPages); const start = (currentPage - 1) * currentPageSize; const end = start + currentPageSize; const visible = new Set(filtered.slice(start, end)); rows.forEach((row) => { row.style.display = visible.has(row) ? "" : "none"; }); if (pageLabel) { pageLabel.textContent = `Page ${currentPage} of ${totalPages}`; } if (prevButton) { prevButton.disabled = currentPage <= 1; } if (nextButton) { nextButton.disabled = currentPage >= totalPages; } }; if (filterInput) { filterInput.addEventListener("input", () => { const term = filterInput.value.trim().toLowerCase(); const filterValue = (filterSelect?.value || "").trim().toLowerCase(); if (!isCommandTable || !commandGroups) { if (isPageable) { currentPage = 1; applyPagination(); return; } rows.forEach((row) => { const haystack = (row.dataset.search || row.textContent || "") .toLowerCase() .trim(); const rowFilter = (row.dataset[filterKey] || "").toLowerCase(); const matchesTerm = !term || haystack.includes(term); const matchesFilter = !filterValue || rowFilter === filterValue; row.style.display = matchesTerm && matchesFilter ? "" : "none"; }); return; } commandGroups.forEach((group) => { const root = group.root; const rootHaystack = (root.dataset.search || root.textContent || "") .toLowerCase() .trim(); const rootMatches = rootHaystack.includes(term); const subMatches = group.subRows.filter((row) => { const haystack = (row.dataset.search || row.textContent || "") .toLowerCase() .trim(); return haystack.includes(term); }); const showGroup = !term || rootMatches || subMatches.length > 0; root.style.display = showGroup ? "" : "none"; if (!term) { const expanded = root.dataset.expanded === "true"; group.subRows.forEach((row) => { row.style.display = expanded ? "" : "none"; }); return; } if (subMatches.length > 0) { setGroupExpanded(group, true); } group.subRows.forEach((row) => { row.style.display = subMatches.includes(row) ? "" : "none"; }); }); }); } if (filterSelect) { filterSelect.addEventListener("change", () => { if (isPageable) { currentPage = 1; applyPagination(); } else if (filterInput) { filterInput.dispatchEvent(new Event("input")); } }); } const headers = table.querySelectorAll("th[data-sort]"); headers.forEach((header) => { header.addEventListener("click", () => { const key = header.dataset.sort; const currentKey = table.dataset.sortKey; const currentDir = table.dataset.sortDir || "asc"; const nextDir = currentKey === key && currentDir === "asc" ? "desc" : "asc"; table.dataset.sortKey = key; table.dataset.sortDir = nextDir; const compare = (a, b) => { const aValue = (a.dataset[key] || "").toString(); const bValue = (b.dataset[key] || "").toString(); const aNumber = Number(aValue); const bNumber = Number(bValue); if (!Number.isNaN(aNumber) && !Number.isNaN(bNumber)) { return aNumber - bNumber; } return aValue.localeCompare(bValue); }; if (isCommandTable && commandGroups) { const roots = Array.from(commandGroups.values()).map((group) => group.root); const sorted = roots.slice().sort(compare); if (nextDir === "desc") { sorted.reverse(); } sorted.forEach((root) => { const group = commandGroups.get(root.dataset.commandRoot); tbody.appendChild(root); group?.subRows.forEach((row) => tbody.appendChild(row)); }); return; } const sorted = rows.slice().sort(compare); if (nextDir === "desc") { sorted.reverse(); } sorted.forEach((row) => tbody.appendChild(row)); refreshRows(); if (isPageable) { currentPage = 1; applyPagination(); } }); }); if (isPageable && sizeSelect) { sizeSelect.value = currentPageSize.toString(); sizeSelect.addEventListener("change", () => { const nextSize = Number(sizeSelect.value); if (Number.isFinite(nextSize) && nextSize > 0) { currentPageSize = nextSize; currentPage = 1; applyPagination(); } }); } if (isPageable && prevButton && nextButton) { prevButton.addEventListener("click", () => { if (currentPage > 1) { currentPage -= 1; applyPagination(); } }); nextButton.addEventListener("click", () => { currentPage += 1; applyPagination(); }); } if (isPageable) { applyPagination(); } }); const logList = document.querySelector("[data-log-list]"); if (logList) { const entries = Array.from(logList.querySelectorAll("[data-log-entry]")); const searchInput = document.querySelector("[data-log-search]"); const levelSelect = document.querySelector("[data-log-level]"); const rangeSelect = document.querySelector("[data-log-range]"); const limitSelect = document.querySelector("[data-log-limit]"); const applyLogFilters = () => { const term = (searchInput?.value || "").trim().toLowerCase(); entries.forEach((entry) => { const haystack = (entry.dataset.search || entry.textContent || "") .toLowerCase() .trim(); const matchesTerm = !term || haystack.includes(term); entry.style.display = matchesTerm ? "" : "none"; }); }; searchInput?.addEventListener("input", applyLogFilters); applyLogFilters(); const reloadLogView = () => { const url = new URL(window.location.href); const rangeValue = rangeSelect?.value || "all"; const levelValue = levelSelect?.value || "all"; const limitValue = limitSelect?.value || "50"; url.searchParams.set("range", rangeValue); url.searchParams.set("level", levelValue); url.searchParams.set("limit", limitValue); window.location.assign(url.toString()); }; levelSelect?.addEventListener("change", reloadLogView); rangeSelect?.addEventListener("change", reloadLogView); limitSelect?.addEventListener("change", reloadLogView); } const logModal = document.querySelector("[data-log-modal]"); const logModalOpen = document.querySelector("[data-log-download]"); if (logModal && logModalOpen) { const closeButtons = logModal.querySelectorAll("[data-modal-close]"); const closeModal = () => { logModal.classList.remove("is-open"); logModal.setAttribute("aria-hidden", "true"); }; const openModal = () => { logModal.classList.add("is-open"); logModal.setAttribute("aria-hidden", "false"); }; logModalOpen.addEventListener("click", openModal); closeButtons.forEach((button) => { button.addEventListener("click", closeModal); }); logModal.addEventListener("click", (event) => { if (event.target === logModal) { closeModal(); } }); window.addEventListener("keydown", (event) => { if (event.key === "Escape" && logModal.classList.contains("is-open")) { closeModal(); } }); } const compareToggle = document.querySelector("[data-compare-toggle]"); if (compareToggle) { const defaultLabel = compareToggle.textContent.trim(); const altLabel = compareToggle.getAttribute("data-compare-label") || "Back"; compareToggle.addEventListener("click", () => { const active = document.body.classList.toggle("stats-compare-mode"); compareToggle.textContent = active ? altLabel : defaultLabel; }); } const healthEndpoint = "/health"; let connectionLost = false; const checkConnection = async () => { try { const response = await fetch(healthEndpoint, { cache: "no-store" }); if (response.ok) { if (connectionLost) { window.LumiInteractions?.showEventNotice?.({ message: "Connection restored. Refresh manually if you need newer page data." }, "info"); } connectionLost = false; } else { connectionLost = true; } } catch { connectionLost = true; } }; window.addEventListener("online", () => { checkConnection(); }); window.addEventListener("offline", () => { connectionLost = true; }); window.setInterval(checkConnection, 5000); const destructivePattern = /(?:^|\/)(?:delete|remove|clear|reset|renew|uninstall|cleanup|archive|revoke|unlink|unset)(?:\/|$)/i; const highImpactPattern = /(?:\/plugins\/[^/]+\/uninstall|\/models\/[^/]+\/delete|\/storage\/cleanup|\/navigation\/reset|\/updates\/|\/remove-data)/i; const destructiveModal = document.querySelector("[data-destructive-modal]"); const destructiveTitle = destructiveModal?.querySelector("[data-destructive-title]"); const destructiveDescription = destructiveModal?.querySelector("[data-destructive-description]"); const destructiveConfirm = destructiveModal?.querySelector("[data-destructive-confirm]"); const destructiveStates = new WeakMap(); let activeDestructive = null; let activeCallbackConfirm = null; const destructiveAction = (form, submitter = null) => { try { const action = submitter?.formAction || form.action; return new URL(action, window.location.origin).pathname; } catch { return ""; } }; const actionCopy = (action) => { const normalized = String(action || "").toLowerCase(); if (normalized.includes("/delete")) return { title: "Confirm deletion", label: "Delete" }; if (normalized.includes("/uninstall")) return { title: "Confirm uninstall", label: "Uninstall" }; if (normalized.includes("/cleanup")) return { title: "Confirm cleanup", label: "Clean selected" }; if (normalized.includes("/reset")) return { title: "Confirm reset", label: "Reset" }; if (normalized.includes("/remove")) return { title: "Confirm removal", label: "Remove" }; if (normalized.includes("/update")) return { title: "Confirm update", label: "Update" }; if (normalized.includes("/restart")) return { title: "Confirm restart", label: "Restart" }; return { title: "Confirm action", label: "Confirm" }; }; const isDestructiveForm = (form, submitter = null) => { if (!form || form.dataset.noDestructiveConfirm !== undefined) return false; if (form.dataset.updateAction !== undefined) return false; return String(form.method || "get").toLowerCase() === "post" && destructivePattern.test(destructiveAction(form, submitter)); }; const resetDestructive = (form) => { const state = destructiveStates.get(form); if (state?.timer) window.clearInterval(state.timer); if (state?.expiryTimer) window.clearTimeout(state.expiryTimer); state?.inline?.remove(); form.querySelector('input[name="confirmation_token"]')?.remove(); destructiveStates.delete(form); if (activeDestructive?.form === form) { destructiveModal?.classList.remove("is-open"); destructiveModal?.setAttribute("aria-hidden", "true"); activeDestructive = null; } if (form.dataset.syntheticConfirmation === "true") form.remove(); }; const resetCallbackConfirm = (result = false) => { const active = activeCallbackConfirm; if (!active) return; activeCallbackConfirm = null; destructiveModal?.classList.remove("is-open"); destructiveModal?.setAttribute("aria-hidden", "true"); destructiveConfirm?.removeEventListener("click", active.onConfirm); active.resolve(result); active.returnFocus?.focus?.(); }; window.LumiConfirm = { destructive({ title = "Confirm action", text = "This action cannot be undone.", label = "Confirm", danger = true } = {}) { if (!destructiveModal || !destructiveConfirm) { return Promise.resolve(window.confirm(text)); } if (activeDestructive?.form) resetDestructive(activeDestructive.form); if (activeCallbackConfirm) resetCallbackConfirm(false); return new Promise((resolve) => { const returnFocus = document.activeElement; destructiveTitle.textContent = title; destructiveDescription.textContent = text; destructiveConfirm.disabled = false; destructiveConfirm.textContent = label; destructiveConfirm.classList.toggle("danger", danger); destructiveModal.classList.add("is-open"); destructiveModal.setAttribute("aria-hidden", "false"); const onConfirm = () => resetCallbackConfirm(true); activeCallbackConfirm = { resolve, returnFocus, onConfirm }; destructiveConfirm.addEventListener("click", onConfirm); destructiveConfirm.focus(); }); } }; const submitDestructive = (form, submitter, token) => { let tokenField = form.querySelector('input[name="confirmation_token"]'); if (!tokenField) { tokenField = document.createElement("input"); tokenField.type = "hidden"; tokenField.name = "confirmation_token"; form.append(tokenField); } tokenField.value = token; const state = destructiveStates.get(form) || {}; state.confirmed = true; destructiveStates.set(form, state); form.requestSubmit(submitter?.form === form ? submitter : undefined); }; const confirmLabel = (form, submitter = null) => form.dataset.confirmLabel || submitter?.dataset?.confirmLabel || actionCopy(destructiveAction(form, submitter)).label; const startCountdown = ({ form, button, token, notBefore, expiresAt, submitter }) => { const state = destructiveStates.get(form) || {}; const update = () => { const remaining = Math.max(0, Math.ceil((notBefore - Date.now()) / 1000)); button.disabled = remaining > 0; button.textContent = remaining > 0 ? `${confirmLabel(form, submitter)} in ${remaining}` : confirmLabel(form, submitter); if (!remaining && state.timer) { window.clearInterval(state.timer); state.timer = null; } }; update(); state.timer = window.setInterval(update, 200); state.expiryTimer = window.setTimeout(() => resetDestructive(form), Math.max(0, expiresAt - Date.now())); button.addEventListener("click", () => { if (!button.disabled) submitDestructive(form, submitter, token); }, { once: true }); destructiveStates.set(form, state); }; const issueDestructiveConfirmation = async (form, submitter) => { if (destructiveStates.has(form)) return; const action = destructiveAction(form, submitter); const state = { confirmed: false, inline: null, timer: null, expiryTimer: null }; destructiveStates.set(form, state); const copy = actionCopy(action); const message = submitter?.dataset?.confirmText || form.dataset.confirmText || form.dataset.confirmForm || "This action cannot be undone."; const mode = form.dataset.confirmMode || (highImpactPattern.test(action) ? "modal" : "inline"); let confirmButton; if (mode === "modal" && destructiveModal && destructiveConfirm) { if (activeDestructive?.form) resetDestructive(activeDestructive.form); activeDestructive = { form }; destructiveTitle.textContent = submitter?.dataset?.confirmTitle || form.dataset.confirmTitle || copy.title; destructiveDescription.textContent = message; destructiveConfirm.disabled = true; destructiveConfirm.classList.add("danger"); destructiveConfirm.textContent = "Preparing..."; destructiveModal.classList.add("is-open"); destructiveModal.setAttribute("aria-hidden", "false"); confirmButton = destructiveConfirm; } else { const inline = document.createElement("span"); inline.className = "destructive-inline-confirm"; const label = document.createElement("span"); label.textContent = message; const cancel = document.createElement("button"); cancel.type = "button"; cancel.className = "button subtle"; cancel.textContent = "Cancel"; cancel.addEventListener("click", () => resetDestructive(form)); confirmButton = document.createElement("button"); confirmButton.type = "button"; confirmButton.className = "button danger"; confirmButton.disabled = true; confirmButton.textContent = "Preparing..."; inline.append(label, cancel, confirmButton); form.insertAdjacentElement("afterend", inline); state.inline = inline; } try { const response = await fetch("/api/destructive-confirmations", { method: "POST", headers: { "Content-Type": "application/json", "Accept": "application/json" }, body: JSON.stringify({ action }) }); const payload = await response.json(); if (!response.ok) throw new Error(payload.error || "Unable to prepare confirmation."); if (destructiveStates.get(form) !== state) return; startCountdown({ form, button: confirmButton, token: payload.token, notBefore: payload.not_before, expiresAt: payload.expires_at, submitter }); } catch (error) { if (destructiveStates.get(form) !== state) return; confirmButton.disabled = true; confirmButton.textContent = "Confirmation unavailable"; confirmButton.title = error.message; } }; document.addEventListener("submit", (event) => { const form = event.target; if (!(form instanceof HTMLFormElement) || !isDestructiveForm(form, event.submitter)) return; const state = destructiveStates.get(form); if (state?.confirmed) { state.confirmed = false; return; } event.preventDefault(); issueDestructiveConfirmation(form, event.submitter); }, true); document.addEventListener("click", (event) => { const button = event.target.closest("[data-confirm-action]"); if (!button) return; const action = button.dataset.confirmAction || ""; if (!destructivePattern.test(action)) return; event.preventDefault(); event.stopImmediatePropagation(); const form = document.createElement("form"); form.method = "post"; form.action = action; form.hidden = true; form.dataset.confirmMode = "modal"; form.dataset.syntheticConfirmation = "true"; form.dataset.confirmTitle = button.dataset.confirmTitle || "Confirm destructive action"; form.dataset.confirmText = button.dataset.confirmText || "This action cannot be undone."; form.dataset.confirmLabel = button.dataset.confirmLabel || "Confirm"; document.body.append(form); issueDestructiveConfirmation(form, null); }, true); document.querySelectorAll("[data-destructive-cancel]").forEach((button) => { button.addEventListener("click", () => { if (activeDestructive?.form) resetDestructive(activeDestructive.form); else resetCallbackConfirm(false); }); }); destructiveModal?.addEventListener("click", (event) => { if (event.target === destructiveModal && activeDestructive?.form) { resetDestructive(activeDestructive.form); } else if (event.target === destructiveModal) { resetCallbackConfirm(false); } }); window.addEventListener("keydown", (event) => { if (event.key === "Escape" && activeDestructive?.form) { resetDestructive(activeDestructive.form); } else if (event.key === "Escape") { resetCallbackConfirm(false); } }); const updateLog = document.querySelector("[data-update-progress-log]"); if (updateLog) { const appendUpdateLog = (message, level = "info") => { const row = document.createElement("div"); row.className = `update-progress-entry ${level}`; row.textContent = message; updateLog.prepend(row); }; try { const source = new EventSource("/admin/updates/events"); [ "update:queued", "update:checking", "update:metadata", "update:snapshot", "update:recovery_marker", "update:download", "update:apply", "update:verify", "update:restart_required", "update:complete", "update:failed", "update:revert", "recovery:plugin_disabled", "recovery:retry_startup" ].forEach((eventName) => { source.addEventListener(eventName, (event) => { const payload = JSON.parse(event.data || "{}"); const target = payload.plugin_id ? `plugin ${payload.plugin_id}` : payload.target || "recovery"; appendUpdateLog(`${eventName.replace("update:", "").replace("recovery:", "recovery ")}: ${target}`, eventName.includes("failed") ? "danger" : "info"); }); }); } catch { appendUpdateLog("Live update stream is unavailable.", "danger"); } document.querySelectorAll("form[data-update-action]").forEach((form) => { form.addEventListener("submit", async (event) => { event.preventDefault(); const submitter = event.submitter || form.querySelector("button[type='submit']"); const confirmed = form.dataset.confirmMode === "modal" ? await window.LumiConfirm?.destructive?.({ title: form.dataset.confirmTitle || "Confirm update action", text: form.dataset.confirmText || "This update action will change local files.", label: form.dataset.confirmLabel || submitter?.textContent || "Confirm" }) : true; if (!confirmed) { window.LumiStateButton?.reset?.(submitter); return; } const originalText = submitter?.textContent; const isStateButton = submitter?.matches?.("[data-lumi-state-button]"); if (isStateButton) { window.LumiStateButton?.setState?.(submitter, "loading", { busy: true }); } else if (submitter) { submitter.disabled = true; submitter.textContent = "Working..."; } appendUpdateLog(`Started ${submitter?.textContent?.trim() || "update action"}.`); try { const response = await fetch(form.action, { method: form.method || "POST", body: new FormData(form), headers: { Accept: "application/json" } }); const result = await response.json(); if (!response.ok || result.ok === false) throw new Error(result.error || "Update action failed."); if (isStateButton) window.LumiStateButton?.success?.(submitter); else if (submitter) submitter.textContent = "Done"; appendUpdateLog(result.message || "Update action completed.", "success"); if (result.refresh_after_ms) { appendUpdateLog(`Lumi will refresh in ${Math.round(result.refresh_after_ms / 1000)} seconds.`, "success"); window.setTimeout(() => window.location.reload(), Number(result.refresh_after_ms)); } } catch (error) { if (isStateButton) window.LumiStateButton?.error?.(submitter); else if (submitter) submitter.textContent = "Failed"; appendUpdateLog(error.message, "danger"); } finally { if (!isStateButton && submitter) { window.setTimeout(() => { submitter.disabled = false; submitter.textContent = originalText; }, 2500); } } }); }); } document.querySelectorAll("[data-copy]").forEach((button) => { button.addEventListener("click", async () => { const text = button.getAttribute("data-copy") || ""; if (!text) { return; } const label = button.querySelector("[data-copy-label]"); const originalLabel = label ? label.textContent : ""; const markCopied = () => { button.classList.add("copied"); if (label) { label.textContent = "Copied"; } window.setTimeout(() => { button.classList.remove("copied"); if (label) { label.textContent = originalLabel; } }, 1200); }; try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(text); markCopied(); return; } } catch { // Fall back to legacy copy. } const tempInput = document.createElement("input"); tempInput.value = text; document.body.appendChild(tempInput); tempInput.select(); try { document.execCommand("copy"); markCopied(); } catch { // Ignore copy errors. } finally { tempInput.remove(); } }); }); })();