/** * 实例:导航显示,env 配置,改密,PM2 重启. */ (function (global) { const DISPLAY = () => global.__INSTANCE_DISPLAY__ || {}; function setStatus(el, text, isErr) { if (!el) return; el.textContent = text || ""; el.classList.toggle("err", !!isErr); } async function fetchJson(url, opts) { const res = await fetch(url, Object.assign({ credentials: "same-origin" }, opts || {})); const data = await res.json().catch(() => ({})); if (!res.ok) { throw new Error(data.msg || res.statusText || "请求失败"); } return data; } function applyDisplayToNav(display) { const map = { strategy: "show_nav_strategy", strategy_records: "show_nav_strategy_records", records: "show_nav_records", stats: "show_nav_stats", options: "show_nav_options", risk_policy: "show_nav_risk_policy", env_config: "show_nav_env_config", }; document.querySelectorAll(".embed-top-nav [data-embed-tab], .top-nav a[href^='/']").forEach((a) => { const tab = a.getAttribute("data-embed-tab") || (a.getAttribute("href") || "").replace(/^\//, "").split("?")[0]; const key = map[tab]; if (!key) return; const show = display[key] !== false; a.classList.toggle("nav-hidden", !show); a.style.display = show ? "" : "none"; }); global.__INSTANCE_DISPLAY__ = display; } function pageNavAllowed(tab) { const d = DISPLAY(); const map = { strategy: "show_nav_strategy", strategy_records: "show_nav_strategy_records", records: "show_nav_records", stats: "show_nav_stats", options: "show_nav_options", risk_policy: "show_nav_risk_policy", env_config: "show_nav_env_config", }; const key = map[tab]; if (!key) return true; return d[key] !== false; } function displayPrefsRoot() { const settingsPane = document.querySelector('.embed-tab-pane[data-embed-pane="settings"]'); if (settingsPane) { const inSettings = settingsPane.querySelector("#display-prefs-form"); if (inSettings) return inSettings; } const pane = document.querySelector(".embed-tab-pane.is-active-pane"); if (pane) { const inPane = pane.querySelector("#display-prefs-form"); if (inPane) return inPane; } return document.getElementById("display-prefs-form"); } function displayPrefsStatusEl() { const card = document.getElementById("display-prefs-card"); if (card) { const el = card.querySelector("#display-prefs-status"); if (el) return el; } return document.getElementById("display-prefs-status"); } function envConfigRoot() { const activePane = document.querySelector(".embed-tab-pane.is-active-pane"); if (activePane) { return activePane.querySelector(".env-config-page"); } return document.querySelector(".env-config-page"); } function bindEnvTabs() { /* Tab 切换由 CSS radio+label 实现 */ } async function loadDisplayPrefsForm(force) { const root = displayPrefsRoot(); if (!root) return; if (!force && root.getAttribute("data-prefs-ssr") === "1" && root.querySelector("[data-pref-key]")) { return; } return loadDisplayPrefsFormIn(root); } async function loadDisplayPrefsFormIn(root) { try { const data = await fetchJson("/api/settings/display"); const display = data.display || {}; const meta = data.meta || []; root.innerHTML = ""; meta.forEach((group) => { const section = document.createElement("div"); section.className = "display-prefs-group"; const title = document.createElement("h3"); title.className = "settings-subcard-title"; title.textContent = group.group; section.appendChild(title); const grid = document.createElement("div"); grid.className = "display-prefs-checks"; (group.entries || []).forEach((item) => { const label = document.createElement("label"); label.className = "chk-label"; const cb = document.createElement("input"); cb.type = "checkbox"; cb.dataset.prefKey = item.key; cb.checked = display[item.key] !== false; label.appendChild(cb); label.appendChild(document.createTextNode(" " + item.label)); grid.appendChild(label); }); section.appendChild(grid); root.appendChild(section); }); root.setAttribute("data-prefs-ssr", "1"); } catch (e) { root.innerHTML = '' + (e.message || "加载失败") + ""; } } async function saveDisplayPrefs() { const status = displayPrefsStatusEl(); const root = displayPrefsRoot(); if (!root) { setStatus(status, "未找到导航设置表单", true); return; } const display = {}; root.querySelectorAll("input[data-pref-key]").forEach((cb) => { display[cb.dataset.prefKey] = !!cb.checked; }); try { const data = await fetchJson("/api/settings/display", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ display }), }); applyDisplayToNav(data.display || display); setStatus(status, "已保存,导航已更新"); } catch (e) { setStatus(status, e.message || "保存失败", true); } } let envSchemaGroups = []; function renderEnvFieldRow(field) { const row = document.createElement("div"); row.className = "env-field-row" + (field.restart_required ? " env-field-row--restart" : ""); const label = document.createElement("label"); label.className = "env-field-label"; label.htmlFor = "env-f-" + field.key; label.textContent = field.label || field.key; if (field.restart_required) { const mark = document.createElement("span"); mark.className = "env-restart-mark"; mark.title = "需重启"; mark.textContent = "*"; label.appendChild(mark); } row.appendChild(label); if (field.note) { const note = document.createElement("div"); note.className = "env-field-note muted"; note.textContent = field.note; row.appendChild(note); } let input; if (field.type === "bool") { input = document.createElement("select"); input.id = "env-f-" + field.key; [["true", "开启"], ["false", "关闭"]].forEach(([v, text]) => { const o = document.createElement("option"); o.value = v; o.textContent = text; input.appendChild(o); }); const cur = (field.current || field.default || "false").toLowerCase(); input.value = cur === "true" || cur === "1" ? "true" : "false"; } else { input = document.createElement("input"); input.id = "env-f-" + field.key; input.type = "password"; input.autocomplete = "off"; if (field.sensitive) { if (field.has_value) { const cur = document.createElement("div"); cur.className = "env-sensitive-current muted"; const labelSpan = document.createElement("span"); labelSpan.textContent = "已配置 "; const masked = document.createElement("span"); masked.className = "env-masked-value"; masked.textContent = field.masked || ""; cur.appendChild(labelSpan); cur.appendChild(masked); row.appendChild(cur); } input.placeholder = field.has_value ? "修改时填写新值,留空不修改" : "请输入"; } else { input.type = "text"; input.value = field.current || field.default || ""; } } input.dataset.envKey = field.key; input.className = "env-field-input"; row.appendChild(input); return row; } function renderEnvConfigBody(groups) { const body = document.createElement("div"); body.className = "env-config-body card"; body.id = "env-config-body"; body.setAttribute("data-env-ssr", "1"); groups.forEach((_group, idx) => { const radio = document.createElement("input"); radio.type = "radio"; radio.name = "env-section"; radio.id = "env-sec-" + idx; radio.className = "env-tab-radio"; if (idx === 0) radio.checked = true; body.appendChild(radio); }); const tabBar = document.createElement("div"); tabBar.className = "env-config-tabs"; tabBar.setAttribute("role", "tablist"); const panelsWrap = document.createElement("div"); panelsWrap.className = "env-config-panels"; panelsWrap.id = "env-config-grid"; groups.forEach((group, idx) => { const label = document.createElement("label"); label.className = "env-tab-btn"; label.htmlFor = "env-sec-" + idx; label.setAttribute("role", "tab"); label.textContent = group.title || "其他"; tabBar.appendChild(label); const panel = document.createElement("section"); panel.className = "env-panel env-panel--" + idx; panel.setAttribute("role", "tabpanel"); if (group.has_restart) { const hint = document.createElement("p"); hint.className = "env-panel-hint"; hint.textContent = "本组含需重启项,修改后请点「保存并重启」."; panel.appendChild(hint); } const grid = document.createElement("div"); grid.className = "env-form-grid"; (group.fields || []).forEach((field) => grid.appendChild(renderEnvFieldRow(field))); panel.appendChild(grid); panelsWrap.appendChild(panel); }); body.appendChild(tabBar); body.appendChild(panelsWrap); return body; } async function loadEnvConfig(force) { const root = envConfigRoot(); const body = root && root.querySelector("#env-config-body"); if (!force && body && body.getAttribute("data-env-ssr") === "1" && body.querySelector("[data-env-key]")) { return; } return loadEnvConfigIn(root); } async function loadEnvConfigIn(root) { const page = root || envConfigRoot() || document.querySelector(".env-config-page"); if (!page) return; const loading = document.createElement("div"); loading.className = "env-config-loading-wrap card"; loading.id = "env-config-body"; loading.innerHTML = '
加载配置中…
'; const oldBody = page.querySelector("#env-config-body"); const oldGrid = page.querySelector("#env-config-grid.env-config-loading-wrap"); if (oldBody) oldBody.replaceWith(loading); else if (oldGrid) oldGrid.replaceWith(loading); try { const data = await fetchJson("/api/settings/env"); envSchemaGroups = data.groups || []; loading.replaceWith(renderEnvConfigBody(envSchemaGroups)); } catch (e) { loading.innerHTML = '' + (e.message || "加载失败") + ""; } } function collectEnvValues() { const root = envConfigRoot(); const values = {}; const scope = root || document; scope.querySelectorAll(".env-field-input[data-env-key]").forEach((el) => { values[el.dataset.envKey] = el.value; }); return values; } async function saveEnvConfig(restartAfter) { const status = document.getElementById("env-config-status"); setStatus(status, "保存中…"); try { const data = await fetchJson("/api/settings/env", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ values: collectEnvValues() }), }); const needRestart = restartAfter || data.restart_required; if (needRestart) { setStatus(status, "已保存,正在重启实例…"); await restartInstance(); setStatus(status, "保存并重启完成"); await loadEnvConfig(); } else { setStatus(status, "已保存(即时生效项已应用)"); await loadEnvConfig(); } } catch (e) { setStatus(status, e.message || "保存失败", true); } } async function restartInstance() { try { await fetchJson("/api/admin/restart", { method: "POST" }); } catch (_) { // 重启会中断当前 HTTP 连接;只要后续 health 恢复即视为成功. } const deadline = Date.now() + 90000; while (Date.now() < deadline) { await new Promise((r) => setTimeout(r, 2000)); try { const h = await fetch("/api/admin/health", { credentials: "same-origin" }); if (h.ok) return; } catch (_) {} } throw new Error("重启后服务未在预期时间内恢复"); } async function savePassword() { const status = document.getElementById("pwd-save-status"); const body = { old_password: (document.getElementById("pwd-old") || {}).value || "", new_username: (document.getElementById("pwd-new-username") || {}).value || "", new_password: (document.getElementById("pwd-new") || {}).value || "", confirm_password: (document.getElementById("pwd-confirm") || {}).value || "", }; try { const data = await fetchJson("/api/settings/password", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (data.restart_required) { setStatus(status, "密码已保存,正在重启…"); await restartInstance(); setStatus(status, "密码已更新,请用新密码登录"); } else { setStatus(status, "密码已更新"); } } catch (e) { setStatus(status, e.message || "保存失败", true); } } function installDelegatedHandlers() { if (document.documentElement.dataset.prefsDelegateBound === "1") return; document.documentElement.dataset.prefsDelegateBound = "1"; document.addEventListener("click", (ev) => { const target = ev.target; if (!(target instanceof Element)) return; if (target.closest("#display-prefs-save")) { ev.preventDefault(); void saveDisplayPrefs(); return; } if (target.closest("#env-config-save")) { ev.preventDefault(); void saveEnvConfig(false); return; } if (target.closest("#env-config-save-restart")) { ev.preventDefault(); void saveEnvConfig(true); return; } if (target.closest("#env-config-reload")) { ev.preventDefault(); void loadEnvConfig(true); return; } if (target.closest("#pwd-save-btn")) { ev.preventDefault(); void savePassword(); } }); } function bindClickOnce(id, handler) { const el = document.getElementById(id); if (!el || el.dataset.bound === "1") return; el.dataset.bound = "1"; el.addEventListener("click", handler); } function bindEvents() { bindClickOnce("display-prefs-save", saveDisplayPrefs); bindClickOnce("env-config-save", () => saveEnvConfig(false)); bindClickOnce("env-config-save-restart", () => saveEnvConfig(true)); bindClickOnce("env-config-reload", () => loadEnvConfig(true)); bindClickOnce("pwd-save-btn", savePassword); } function initPage() { installDelegatedHandlers(); bindEvents(); loadDisplayPrefsForm(false); loadEnvConfig(false); if (global.__INSTANCE_DISPLAY__) applyDisplayToNav(global.__INSTANCE_DISPLAY__); } global.InstanceSettingsPrefs = { pageNavAllowed, applyDisplayToNav, loadDisplayPrefsForm, loadEnvConfig, bindEnvTabs, bindEvents, restartInstance, }; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initPage); } else { initPage(); } })(typeof window !== "undefined" ? window : globalThis);