Files
crypto_monitor/lib/common/static/instance_settings_prefs.js
T
dekun 1ee9eb02c3 Add settings page tabs and show env API keys with last four digits.
System settings now use the same CSS tab layout as env config; sensitive env fields display masked values from .env while keeping save-on-empty behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 21:31:14 +08:00

393 lines
14 KiB
JavaScript

/**
* 实例:导航显示、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 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 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 = '<span class="err">' + (e.message || "加载失败") + "</span>";
}
}
async function saveDisplayPrefs() {
const status = document.getElementById("display-prefs-status");
const root = displayPrefsRoot();
const display = {};
(root ? 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 = '<div class="env-config-loading muted">加载配置中…</div>';
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 = '<span class="err">' + (e.message || "加载失败") + "</span>";
}
}
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() {
await fetchJson("/api/admin/restart", { method: "POST" });
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 bindEvents() {
const dSave = document.getElementById("display-prefs-save");
if (dSave) dSave.addEventListener("click", saveDisplayPrefs);
const eSave = document.getElementById("env-config-save");
if (eSave) eSave.addEventListener("click", () => saveEnvConfig(false));
const eRestart = document.getElementById("env-config-save-restart");
if (eRestart) eRestart.addEventListener("click", () => saveEnvConfig(true));
const eReload = document.getElementById("env-config-reload");
if (eReload) eReload.addEventListener("click", () => loadEnvConfig(true));
const pSave = document.getElementById("pwd-save-btn");
if (pSave) pSave.addEventListener("click", savePassword);
}
function initPage() {
bindEvents();
loadDisplayPrefsForm(false);
loadEnvConfig(false);
if (global.__INSTANCE_DISPLAY__) applyDisplayToNav(global.__INSTANCE_DISPLAY__);
}
global.InstanceSettingsPrefs = {
pageNavAllowed,
applyDisplayToNav,
loadDisplayPrefsForm,
loadEnvConfig,
bindEnvTabs,
restartInstance,
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initPage);
} else {
initPage();
}
})(typeof window !== "undefined" ? window : globalThis);