Add DB+.env auto backup with download and upload restore in Settings.

Backups land under /root/eth_hedge_backups; restore accepts zip body for new-server migration and restarts the process.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-26 23:05:58 +08:00
parent 9d3b36826c
commit 2b348854fe
9 changed files with 946 additions and 25 deletions
+83 -24
View File
@@ -299,28 +299,87 @@ export type RuntimeSettings = {
sim: boolean;
};
export type StrategySettings = {
fee_rate: number;
exit_move_pct?: number;
exit_mode: "fixed_usdt" | "premium_multiple";
net_profit_target: number;
premium_exit_multiple: number;
rest_seconds: number;
live_order_interval_sec?: number;
skip_weekends: boolean;
initial_equity: number;
leverage: number;
min_option_hours: number;
min_option_leverage: number;
atm_open_offset_enabled: boolean;
max_atm_open_offset: number;
close_bid_mark_max_pct: number;
perp_qty_eth: number;
option_qty_eth: number;
show_manual_trade_buttons?: boolean;
exchange: "okx" | "binance";
perp_inst_id?: string;
option_inst_family?: string;
index_inst_id?: string;
ledger: { equity: number; available: number };
export async function downloadBackup(name: string): Promise<void> {
await ensureFreshToken();
const token = getToken();
const res = await fetch(
`${getApiBase()}/api/backup/download/${encodeURIComponent(name)}`,
{
headers: token ? { Authorization: `Bearer ${token}` } : {},
},
);
if (res.status === 401) {
clearSession();
throw new Error("unauthorized");
}
if (!res.ok) {
const text = await res.text();
let detail = res.statusText;
try {
const j = JSON.parse(text) as { detail?: string };
if (j.detail) detail = String(j.detail);
} catch {
if (text) detail = text;
}
throw new Error(detail || `HTTP ${res.status}`);
}
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = name;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
export async function uploadRestoreBackup(
file: File,
confirmPhrase: string,
): Promise<{ detail?: string; restart_required?: boolean }> {
await ensureFreshToken();
const token = getToken();
const res = await fetch(`${getApiBase()}/api/backup/restore`, {
method: "POST",
headers: {
...(token ? { Authorization: `Bearer ${token}` } : {}),
"X-Confirm-Phrase": confirmPhrase,
"Content-Type": "application/zip",
},
body: file,
});
if (res.status === 401) {
clearSession();
throw new Error("unauthorized");
}
const text = await res.text();
let data: unknown = null;
try {
data = text ? JSON.parse(text) : null;
} catch {
data = { detail: text };
}
if (!res.ok) {
const detail =
typeof data === "object" && data && "detail" in data
? String((data as { detail: unknown }).detail)
: res.statusText;
throw new Error(detail || `HTTP ${res.status}`);
}
return data as { detail?: string; restart_required?: boolean };
}
export type BackupStatus = {
auto_enabled: boolean;
interval_hours: number;
keep_count: number;
last_at_ms: number | null;
backup_dir: string;
items: {
name: string;
path: string;
size_bytes: number;
mtime_ms: number;
}[];
};