(() => { const menuController = window.WarpBoxUI?.bindMenuBar?.() || { close() {} }; const rowsNode = document.getElementById("settings-rows"); const searchInput = document.getElementById("settingsSearch"); const categoryButtons = Array.from(document.querySelectorAll(".settings-category-button")); const groups = Array.from(document.querySelectorAll(".settings-group")); const saveButton = document.getElementById("saveButton"); const exportButton = document.getElementById("exportButton"); const importButton = document.getElementById("importButton"); const resetButton = document.getElementById("resetButton"); const importInput = document.getElementById("settingsImportInput"); const dirtyChip = document.getElementById("dirtyChip"); const actionSummary = document.getElementById("actionSummary"); const visibleCount = document.getElementById("visibleCount"); const editableCount = document.getElementById("editableCount"); const unsavedCount = document.getElementById("unsavedCount"); const lockedCount = document.getElementById("lockedCount"); const statusLeft = document.getElementById("statusLeft"); const statusMiddle = document.getElementById("statusMiddle"); const statusRight = document.getElementById("statusRight"); const popupClose = document.getElementById("doc-popup-close"); const toastTarget = document.getElementById("toast"); if (!rowsNode || !searchInput || !saveButton) return; const state = { currentCategory: "all", showChangedOnly: false, showLockedOnly: false }; function parseRows() { try { return JSON.parse(rowsNode.textContent || "[]"); } catch (_) { return []; } } const rowData = parseRows().reduce((map, row) => { map[row.key] = row; return map; }, {}); const rows = Array.from(document.querySelectorAll(".setting-row")).map((row) => ({ element: row, input: row.querySelector(".setting-input"), hint: row.querySelector('[data-role="hint"]'), badge: row.querySelector('[data-role="source-badge"]'), key: row.dataset.key, label: row.dataset.label, category: row.dataset.category, envName: row.dataset.envName, type: row.dataset.type, minimum: Number(row.dataset.minimum || 0), locked: row.classList.contains("is-locked") })); function showToast(message, type = "info", duration = 2400) { window.WarpBoxUI?.toast?.(message, type, { target: toastTarget, duration }); } function escapeHtml(value) { return window.WarpBoxUI?.htmlEscape?.(value) || String(value ?? ""); } function currentValue(row) { if (!row.input) return row.element.dataset.original || ""; return String(row.input.value ?? "").trim(); } function isDirty(row) { return !row.locked && currentValue(row) !== (row.element.dataset.original || ""); } function validateRow(row) { if (row.locked || !row.input) { row.element.classList.remove("is-invalid"); return true; } const value = currentValue(row); let valid = true; if (row.type === "int" || row.type === "int64") { if (!/^\d+$/.test(value)) valid = false; else if (Number(value) < row.minimum) valid = false; } else if (row.type === "bool") { valid = value === "true" || value === "false"; } row.element.classList.toggle("is-invalid", !valid); return valid; } function rowMatchesSearch(row) { const query = searchInput.value.trim().toLowerCase(); if (!query) return true; const data = [ row.label, row.envName, row.element.dataset.description, row.key ].join(" ").toLowerCase(); return data.includes(query); } function applyFilters() { let visible = 0; groups.forEach((group) => { let groupVisible = 0; group.querySelectorAll(".setting-row").forEach((node) => { const row = rows.find((item) => item.element === node); const categoryMatch = state.currentCategory === "all" || row.category === state.currentCategory; const searchMatch = rowMatchesSearch(row); const changedMatch = !state.showChangedOnly || isDirty(row); const lockedMatch = !state.showLockedOnly || row.locked; const show = categoryMatch && searchMatch && changedMatch && lockedMatch; node.classList.toggle("is-hidden", !show); if (show) { visible += 1; groupVisible += 1; } }); group.hidden = groupVisible === 0; }); visibleCount.textContent = String(visible); statusMiddle.textContent = `category: ${state.currentCategory}`; } function updateStats() { let dirty = 0; let editable = 0; let locked = 0; let invalid = 0; rows.forEach((row) => { if (row.locked) locked += 1; else editable += 1; if (isDirty(row)) dirty += 1; if (!validateRow(row)) invalid += 1; }); editableCount.textContent = String(editable); lockedCount.textContent = String(locked); unsavedCount.textContent = String(dirty); dirtyChip.textContent = `${dirty} unsaved`; dirtyChip.classList.toggle("is-dirty", dirty > 0); saveButton.disabled = dirty === 0 || invalid > 0; if (invalid > 0) { actionSummary.textContent = `${invalid} invalid setting value(s) must be fixed before save.`; statusLeft.textContent = "Invalid values"; statusRight.textContent = "fix before save"; } else if (dirty > 0) { actionSummary.textContent = `${dirty} unsaved change(s) ready to save or export.`; statusLeft.textContent = "Unsaved changes"; statusRight.textContent = "draft ready"; } else { actionSummary.textContent = "No unsaved changes."; statusLeft.textContent = "No unsaved changes"; statusRight.textContent = "admin only"; } } function updateView() { updateStats(); applyFilters(); } function setCategory(category) { state.currentCategory = category; categoryButtons.forEach((button) => button.classList.toggle("is-active", button.dataset.category === category)); applyFilters(); } function draftValues() { const values = {}; rows.forEach((row) => { if (!row.locked) values[row.key] = currentValue(row); }); return values; } function updateRowFromPayload(payload) { const row = rows.find((item) => item.key === payload.key); if (!row) return; row.element.dataset.original = payload.value; row.element.dataset.default = payload.default_value || ""; row.element.dataset.source = payload.source || "default"; row.element.dataset.sourceBadge = payload.source_badge || payload.source || "default"; row.element.dataset.description = payload.description || ""; row.element.dataset.minimum = String(payload.minimum || 0); row.element.classList.toggle("is-locked", Boolean(payload.locked)); row.locked = Boolean(payload.locked); row.minimum = Number(payload.minimum || 0); if (row.input) { row.input.value = payload.value ?? ""; row.input.disabled = Boolean(payload.locked); } if (row.hint) { row.hint.textContent = payload.locked ? "Locked by environment or hard runtime implication." : (payload.default_value ? `Default: ${payload.default_value}` : ""); } if (row.badge) { row.badge.textContent = payload.source_badge || payload.source || "default"; row.badge.className = `settings-badge ${badgeClass(payload.source_badge || payload.source || "default")}`; } rowData[payload.key] = payload; } function badgeClass(source) { if (source === "default") return "badge-default"; if (source === "environment") return "badge-env"; if (source === "db override") return "badge-db"; return "badge-hard"; } function hydrateRows(payloadRows) { if (!Array.isArray(payloadRows)) return; payloadRows.forEach(updateRowFromPayload); updateView(); } async function postJSON(url, body) { const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); const payload = await response.json().catch(() => ({})); if (!response.ok) { throw new Error(payload.error || "Request failed"); } return payload; } async function saveChanges() { try { const payload = await postJSON("/admin/settings/save", { values: draftValues() }); hydrateRows(payload.rows); showToast(payload.message || "Settings saved", payload.warnings?.length ? "warning" : "success"); } catch (error) { showToast(error.message, "error", 3200); } } async function resetDefaults() { if (!window.confirm("Reset all editable settings to built-in defaults?")) return; try { const payload = await postJSON("/admin/settings/reset", {}); hydrateRows(payload.rows); showToast(payload.message || "Defaults restored", "success"); } catch (error) { showToast(error.message, "error", 3200); } } async function exportSettings() { try { const response = await fetch("/admin/settings/export"); if (!response.ok) throw new Error("Could not export settings"); const payload = await response.json(); const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json;charset=utf-8" }); const url = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = url; anchor.download = `warpbox-settings-${new Date().toISOString().replaceAll(":", "-")}.json`; anchor.click(); URL.revokeObjectURL(url); showToast("Settings JSON exported"); } catch (error) { showToast(error.message, "error", 3200); } } async function importSettingsFile(file) { if (!file) return; try { const text = await file.text(); const payload = JSON.parse(text); const result = await postJSON("/admin/settings/import", payload); hydrateRows(result.rows); showToast(result.message || "Settings imported", result.warnings?.length ? "warning" : "success", 3200); } catch (error) { showToast(error.message || "Could not import settings JSON", "error", 3200); } finally { importInput.value = ""; } } function discardUnsaved() { rows.forEach((row) => { if (!row.input) return; row.input.value = row.element.dataset.original || ""; }); updateView(); showToast("Unsaved changes discarded"); } function explainSources() { window.WarpBoxUI?.openPopup?.( "Setting Sources", `
Reset defaults writes built-in WarpBox defaults as admin overrides for editable settings.
Environment-only settings stay locked and unchanged.
` ); } function showRowInfo(row) { window.WarpBoxUI?.openPopup?.( row.label, `Environment variable: ${escapeHtml(row.envName || "n/a")}
Current source: ${escapeHtml(row.badge?.textContent || row.element.dataset.sourceBadge || "default")}
Description: ${escapeHtml(row.element.dataset.description || "No description available.")}
${row.element.dataset.default ? `Default value: ${escapeHtml(row.element.dataset.default)}
` : ""} ` ); } async function runCommand(command) { switch (command) { case "save": await saveChanges(); return; case "export": await exportSettings(); return; case "import": importInput.click(); return; case "discard": discardUnsaved(); return; case "show-all": state.showChangedOnly = false; state.showLockedOnly = false; applyFilters(); showToast("Showing all matching settings"); return; case "show-changed": state.showChangedOnly = !state.showChangedOnly; if (state.showChangedOnly) state.showLockedOnly = false; applyFilters(); showToast(state.showChangedOnly ? "Showing changed settings only" : "Showing all matching settings"); return; case "show-locked": state.showLockedOnly = !state.showLockedOnly; if (state.showLockedOnly) state.showChangedOnly = false; applyFilters(); showToast(state.showLockedOnly ? "Showing locked settings only" : "Showing all matching settings"); return; case "reset-defaults": await resetDefaults(); return; case "reload": window.location.reload(); return; case "legend": explainSources(); return; case "reset-help": explainReset(); return; default: showToast(`Unknown command: ${command}`, "warning"); } } rows.forEach((row) => { row.input?.addEventListener(row.input.tagName === "SELECT" ? "change" : "input", updateView); row.element.querySelector(".row-reset")?.addEventListener("click", () => { if (row.locked || !row.input) return; row.input.value = row.element.dataset.default || row.element.dataset.original || ""; updateView(); }); row.element.querySelector(".row-info")?.addEventListener("click", () => showRowInfo(row)); }); searchInput.addEventListener("input", applyFilters); categoryButtons.forEach((button) => button.addEventListener("click", () => setCategory(button.dataset.category))); saveButton.addEventListener("click", saveChanges); exportButton.addEventListener("click", exportSettings); importButton.addEventListener("click", () => importInput.click()); resetButton.addEventListener("click", resetDefaults); importInput.addEventListener("change", (event) => importSettingsFile(event.target.files?.[0])); popupClose?.addEventListener("click", () => window.WarpBoxUI?.closePopup?.()); document.getElementById("modal-backdrop")?.addEventListener("click", () => window.WarpBoxUI?.closePopup?.()); document.querySelectorAll("[data-command]").forEach((button) => { button.addEventListener("click", async () => { menuController.close(); await runCommand(button.dataset.command); }); }); document.addEventListener("keydown", async (event) => { if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "s") { event.preventDefault(); await saveChanges(); } if (event.key === "F5") { event.preventDefault(); window.location.reload(); } if (event.key === "Escape") { menuController.close(); window.WarpBoxUI?.closePopup?.(); } }); updateView(); })();