Add hub settings tabs and account password change UI.

Restructure manual-trading-hub settings with CSS tabs matching instance layout; add password API writing HUB_USERNAME/HUB_PASSWORD to hub .env.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-08 22:01:26 +08:00
parent d339971c26
commit 88f7f0cfca
5 changed files with 494 additions and 202 deletions
+68 -16
View File
@@ -4168,6 +4168,8 @@
const parts = [];
if (m.password_required) parts.push("已启用用户名+密码登录");
else parts.push("未设 HUB_PASSWORD(反代公网暴露时建议设置 HUB_USERNAME + HUB_PASSWORD");
const userEl = document.getElementById("hub-pwd-current-user");
if (userEl && m.default_username) userEl.textContent = m.default_username;
if (m.hub_bridge_token_set) parts.push("中控已配置 HUB_BRIDGE_TOKEN");
else parts.push("中控未设 HUB_BRIDGE_TOKEN(实例需 APP_AUTH_DISABLED 或同令牌)");
if (m.public_origin) parts.push("浏览器外链基址: " + m.public_origin);
@@ -4258,21 +4260,6 @@
}
function initSettingsSectionFolds() {
document.querySelectorAll(".settings-section[data-settings-section]").forEach((el) => {
applySettingsSectionFold(el);
if (el.dataset.foldBound === "1") return;
el.dataset.foldBound = "1";
const foldBtn = el.querySelector(":scope > .settings-section-head > .settings-section-fold");
if (foldBtn) {
foldBtn.addEventListener("click", () => {
const section = el.dataset.settingsSection;
const collapsed = !el.classList.contains("is-collapsed");
el.classList.toggle("is-collapsed", collapsed);
foldBtn.setAttribute("aria-expanded", collapsed ? "false" : "true");
setSettingsFoldState(section, collapsed);
});
}
});
document.querySelectorAll(".settings-section-save").forEach((btn) => {
if (btn.dataset.saveBound === "1") return;
btn.dataset.saveBound = "1";
@@ -4290,12 +4277,76 @@
? "交易监管"
: section === "exchanges"
? "交易所账户"
: "设置";
: section === "backup"
? "备份设置"
: "设置";
if (section === "backup") return;
void saveSettingsSection(section, { label });
});
});
}
async function waitHubHealth() {
const deadline = Date.now() + 90000;
while (Date.now() < deadline) {
await new Promise((r) => setTimeout(r, 2000));
try {
const r = await fetch("/api/admin/health", { credentials: "same-origin" });
if (r.ok) return;
} catch (_) {}
}
throw new Error("重启后中控未在预期时间内恢复");
}
async function saveHubPassword() {
const status = document.getElementById("hub-pwd-save-status");
const setStatus = (msg, err) => {
if (!status) return;
status.textContent = msg || "";
status.classList.toggle("is-err", !!err);
};
setStatus("保存中…");
try {
const r = await apiFetch("/api/settings/password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
old_password: (document.getElementById("hub-pwd-old") || {}).value || "",
new_username: (document.getElementById("hub-pwd-new-username") || {}).value || "",
new_password: (document.getElementById("hub-pwd-new") || {}).value || "",
confirm_password: (document.getElementById("hub-pwd-confirm") || {}).value || "",
}),
});
const j = await r.json();
if (!r.ok) throw new Error(j.detail || j.msg || "保存失败");
if (!j.restart_required) {
setStatus("未修改(与当前配置相同)");
showToast("未修改");
return;
}
setStatus("密码已保存,正在重启中控…");
await apiFetch("/api/admin/restart", { method: "POST" });
await waitHubHealth();
setStatus("密码已更新,请用新密码重新登录");
showToast("密码已更新,请重新登录");
setTimeout(() => {
location.href = "/login";
}, 1200);
} catch (e) {
setStatus(String(e.message || e), true);
showToast(String(e.message || e), true);
}
}
function initHubPasswordSettings() {
const btn = document.getElementById("hub-pwd-save-btn");
if (!btn || btn.dataset.bound === "1") return;
btn.dataset.bound = "1";
btn.addEventListener("click", () => {
void saveHubPassword();
});
}
function macroDatetimeLocalToApi(v) {
if (!v) return "";
return String(v).trim().replace("T", " ").slice(0, 16);
@@ -4439,6 +4490,7 @@
function loadSettingsUI() {
loadSettingsMetaLine();
initMacroCalendarSettings();
initHubPasswordSettings();
loadMacroCalendarUI();
loadSettings().then((data) => {
syncDisplayPrefsUI(data);