Files
crypto_monitor/lib/common/static/instance_settings_prefs.js
T
dekun 10128d18bc Add OKX options review module with hedge plan entries.
Import closed OKX option history and closed hedge plans into one list for journaling, images, and stats without mixing contract reviews.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 09:35:19 +08:00

478 lines
16 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",
"options-review": "show_nav_options_review",
options_review: "show_nav_options_review",
"hedge-plan": "show_nav_hedge_plan",
hedge_plan: "show_nav_hedge_plan",
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",
"options-review": "show_nav_options_review",
options_review: "show_nav_options_review",
"hedge-plan": "show_nav_hedge_plan",
hedge_plan: "show_nav_hedge_plan",
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 = '<span class="err">' + (e.message || "加载失败") + "</span>";
}
}
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";
// 防止浏览器把登录密码自动填进 API Key/Secret(保存对冲开关时曾误写入密钥)
input.autocomplete = "new-password";
input.setAttribute("data-lpignore", "true");
input.setAttribute("data-1p-ignore", "true");
input.setAttribute("data-form-type", "other");
input.readOnly = true;
input.addEventListener("focus", function () {
input.readOnly = false;
});
if (field.sensitive) {
input.dataset.envSensitive = "1";
input.dataset.envDirty = "0";
input.addEventListener("input", function () {
input.dataset.envDirty = "1";
});
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.autocomplete = "off";
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) => {
if (el.dataset.envSensitive === "1" && el.dataset.envDirty !== "1") {
// 未改动过的敏感项不提交,避免浏览器自动填充覆盖已有密钥
return;
}
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);