Files
crypto_monitor/manual_trading_hub/static/backup.js
T
dekun bfa3352122 feat: 系统设置增加备份恢复与默认登录 admin
支持手动/每日自动备份四所数据库、K线库与 env,上传 zip 一键恢复;中控默认账号 admin/admin123。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 16:39:46 +08:00

251 lines
8.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 系统设置 · 备份与恢复
*/
(function () {
const page = document.getElementById("page-settings");
if (!page) return;
const elAuto = document.getElementById("backup-auto-enabled");
const elHour = document.getElementById("backup-auto-hour");
const elRetention = document.getElementById("backup-retention-days");
const elIncludeEnv = document.getElementById("backup-include-env");
const elIncludeImages = document.getElementById("backup-include-images");
const elRoot = document.getElementById("backup-root");
const elStatus = document.getElementById("backup-status-line");
const elList = document.getElementById("backup-list");
const elRun = document.getElementById("backup-run-now");
const elRestoreFile = document.getElementById("backup-restore-file");
const elRestoreBtn = document.getElementById("backup-restore-upload-btn");
let settingsCache = null;
let statusCache = null;
function fmtBytes(n) {
const v = Number(n);
if (!Number.isFinite(v) || v < 0) return "—";
if (v < 1024) return v + " B";
if (v < 1024 * 1024) return (v / 1024).toFixed(1) + " KB";
return (v / (1024 * 1024)).toFixed(2) + " MB";
}
function setStatus(msg, isErr) {
if (!elStatus) return;
elStatus.textContent = msg || "";
elStatus.className = "backup-status-line" + (isErr ? " err" : "");
}
function collectBackupFromUI() {
return {
auto_enabled: !!(elAuto && elAuto.checked),
auto_hour: Math.max(0, Math.min(23, parseInt(elHour && elHour.value, 10) || 0)),
retention_days: Math.max(1, Math.min(365, parseInt(elRetention && elRetention.value, 10) || 30)),
include_env: !!(elIncludeEnv && elIncludeEnv.checked),
include_exchange_images: !!(elIncludeImages && elIncludeImages.checked),
backup_root: (elRoot && elRoot.value || "").trim(),
};
}
function syncBackupUI(data) {
const b = (data && data.backup) || {};
if (elAuto) elAuto.checked = b.auto_enabled !== false;
if (elHour) elHour.value = b.auto_hour != null ? b.auto_hour : 0;
if (elRetention) elRetention.value = b.retention_days != null ? b.retention_days : 30;
if (elIncludeEnv) elIncludeEnv.checked = b.include_env !== false;
if (elIncludeImages) elIncludeImages.checked = !!b.include_exchange_images;
if (elRoot) elRoot.value = b.backup_root || "";
}
function renderBackupList(status) {
if (!elList) return;
const rows = (status && status.backups) || [];
const state = (status && status.state) || {};
const root = (status && status.backup_root) || "";
let html = '<div class="backup-meta">';
html += '<div>目录:<code>' + esc(root) + '</code></div>';
if (state.last_backup_at) {
html += '<div>上次备份:' + esc(state.last_backup_at) + '' + esc(state.last_trigger || "") + "</div>";
}
if (state.last_auto_at) {
html += '<div>上次自动:' + esc(state.last_auto_at) + "</div>";
}
if (state.last_restore_at) {
html += '<div>上次恢复:' + esc(state.last_restore_at) + " ← " + esc(state.last_restore_from || "") + "</div>";
}
html += "</div>";
if (!rows.length) {
html += '<p class="backup-empty">暂无备份文件</p>';
elList.innerHTML = html;
return;
}
html += '<table class="backup-table"><thead><tr><th>文件</th><th>大小</th><th>时间</th><th></th></tr></thead><tbody>';
rows.forEach(function (row) {
html +=
"<tr><td>" +
esc(row.name) +
"</td><td>" +
fmtBytes(row.size) +
"</td><td>" +
esc(row.modified_at || "") +
'</td><td class="backup-row-actions">' +
'<a class="ghost" href="/api/backup/download/' +
encodeURIComponent(row.name) +
'" download>下载</a> ' +
'<button type="button" class="danger backup-restore-local" data-name="' +
escAttr(row.name) +
'">恢复</button></td></tr>';
});
html += "</tbody></table>";
elList.innerHTML = html;
elList.querySelectorAll(".backup-restore-local").forEach(function (btn) {
btn.addEventListener("click", function () {
restoreLocal(btn.getAttribute("data-name"));
});
});
}
function esc(s) {
return String(s || "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/"/g, "&quot;");
}
function escAttr(s) {
return esc(s).replace(/'/g, "&#39;");
}
async function loadSettingsData() {
const r = await fetch("/api/settings", { credentials: "same-origin" });
if (!r.ok) throw new Error("加载设置失败");
settingsCache = await r.json();
syncBackupUI(settingsCache);
}
async function loadBackupStatus() {
const r = await fetch("/api/backup/status", { credentials: "same-origin" });
if (!r.ok) throw new Error("加载备份状态失败");
statusCache = await r.json();
renderBackupList(statusCache);
}
async function saveBackupSettings() {
if (!settingsCache) await loadSettingsData();
const body = {
exchanges: settingsCache.exchanges || [],
display: settingsCache.display,
supervisor: settingsCache.supervisor,
backup: collectBackupFromUI(),
};
const r = await fetch("/api/settings", {
method: "POST",
credentials: "same-origin",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!r.ok) throw new Error("保存失败");
settingsCache = (await r.json()).settings || settingsCache;
syncBackupUI(settingsCache);
await loadBackupStatus();
if (typeof showToast === "function") showToast("备份设置已保存");
}
async function runBackupNow() {
setStatus("备份中…");
const r = await fetch("/api/backup/run", { method: "POST", credentials: "same-origin" });
const data = await r.json().catch(function () {
return {};
});
if (!r.ok) {
setStatus(data.detail || "备份失败", true);
return;
}
setStatus("完成:" + (data.file || "") + "" + fmtBytes(data.size) + "");
await loadBackupStatus();
if (typeof showToast === "function") showToast("备份完成");
}
async function restoreLocal(name) {
if (!name) return;
if (!window.confirm("确认从服务器备份 " + name + " 恢复?\n恢复前会自动做 pre-restore 快照并重启 PM2。")) return;
if (window.prompt('请输入 RESTORE 确认恢复') !== "RESTORE") return;
setStatus("恢复中…");
const r = await fetch("/api/backup/restore-local", {
method: "POST",
credentials: "same-origin",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: name, confirm: "RESTORE" }),
});
const data = await r.json().catch(function () {
return {};
});
if (!r.ok) {
setStatus(data.detail || "恢复失败", true);
return;
}
setStatus("恢复完成,已恢复 " + ((data.restored && data.restored.length) || 0) + " 个文件");
await loadBackupStatus();
if (typeof showToast === "function") showToast("恢复完成,请刷新页面");
}
async function restoreUpload() {
const file = elRestoreFile && elRestoreFile.files && elRestoreFile.files[0];
if (!file) {
setStatus("请选择 .zip 备份文件", true);
return;
}
if (!window.confirm("确认上传并恢复 " + file.name + "\n恢复前会自动做 pre-restore 快照并重启 PM2。")) return;
if (window.prompt('请输入 RESTORE 确认恢复') !== "RESTORE") return;
setStatus("上传并恢复中…");
const fd = new FormData();
fd.append("file", file);
fd.append("confirm", "RESTORE");
const r = await fetch("/api/backup/restore", {
method: "POST",
credentials: "same-origin",
body: fd,
});
const data = await r.json().catch(function () {
return {};
});
if (!r.ok) {
setStatus(data.detail || "恢复失败", true);
return;
}
setStatus("恢复完成,已恢复 " + ((data.restored && data.restored.length) || 0) + " 个文件");
if (elRestoreFile) elRestoreFile.value = "";
await loadBackupStatus();
if (typeof showToast === "function") showToast("恢复完成,请刷新页面");
}
window.initBackupSettingsUI = async function () {
try {
await loadSettingsData();
await loadBackupStatus();
setStatus("");
} catch (e) {
setStatus(e.message || String(e), true);
}
};
if (elRun) elRun.addEventListener("click", function () {
runBackupNow().catch(function (e) {
setStatus(e.message || String(e), true);
});
});
if (elRestoreBtn) elRestoreBtn.addEventListener("click", function () {
restoreUpload().catch(function (e) {
setStatus(e.message || String(e), true);
});
});
page.addEventListener("click", function (ev) {
const btn = ev.target.closest(".settings-section-save[data-settings-section='backup']");
if (!btn) return;
ev.preventDefault();
saveBackupSettings().catch(function (e) {
setStatus(e.message || String(e), true);
});
});
})();