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:
+83
-24
@@ -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;
|
||||
}[];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user