feat: add settings tabs, auto backup, and show latest 5 backups

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-02 09:15:21 +08:00
parent 52ebbfbeae
commit 458cc42dd5
12 changed files with 621 additions and 190 deletions
+65 -3
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import re
import time
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
@@ -10,7 +11,16 @@ from pydantic import BaseModel, Field
from apps.api.auth import auth_disabled, require_auth, verify_credentials
from packages.config import get_settings, reload_settings
from packages.config.env_file import update_env_file
from packages.db.backup import backup_db, ensure_backup_dir, list_backups, resolve_backup_file, restore_db
from packages.db.backup import (
DISPLAY_LIMIT,
backup_db,
ensure_backup_dir,
latest_backup_mtime_ms,
list_backups,
prune_backups,
resolve_backup_file,
restore_db,
)
router = APIRouter(
prefix="/settings",
@@ -36,6 +46,12 @@ class RestoreBody(BaseModel):
backup_name: str = Field(min_length=1, max_length=128)
class AutoBackupBody(BaseModel):
current_password: str = Field(min_length=1, max_length=256)
enabled: bool
interval_hours: float = Field(default=24, ge=1, le=168)
def _verify_password_or_401(current_password: str) -> None:
s = get_settings()
if auth_disabled():
@@ -44,6 +60,25 @@ def _verify_password_or_401(current_password: str) -> None:
raise HTTPException(status_code=401, detail="current password incorrect")
def _backup_meta(s) -> dict:
backup_dir = s.backup_dir_path
latest_ms = latest_backup_mtime_ms(backup_dir)
next_due_ms = None
if s.backup_auto_enabled and s.backup_interval_hours > 0:
if latest_ms is None:
next_due_ms = int(time.time() * 1000)
else:
next_due_ms = latest_ms + int(s.backup_interval_hours * 3600 * 1000)
return {
"auto_enabled": s.backup_auto_enabled,
"interval_hours": s.backup_interval_hours,
"keep_count": s.backup_keep_count,
"display_limit": DISPLAY_LIMIT,
"latest_backup_ms": latest_ms,
"next_due_ms": next_due_ms,
}
@router.get("/backup")
def get_backup_info() -> dict:
s = get_settings()
@@ -55,9 +90,9 @@ def get_backup_info() -> dict:
probe.write_text("", encoding="utf-8")
probe.unlink(missing_ok=True)
writable = True
items = list_backups(backup_dir)
items = list_backups(backup_dir, limit=DISPLAY_LIMIT)
except OSError as e:
items = list_backups(backup_dir) if backup_dir.is_dir() else []
items = list_backups(backup_dir, limit=DISPLAY_LIMIT) if backup_dir.is_dir() else []
if not items:
raise HTTPException(status_code=500, detail=f"backup dir error: {e}") from e
return {
@@ -65,6 +100,32 @@ def get_backup_info() -> dict:
"db_path": str(s.db_path),
"writable": writable,
"backups": items,
**_backup_meta(s),
}
@router.put("/backup/auto")
def update_auto_backup(body: AutoBackupBody) -> dict:
_verify_password_or_401(body.current_password)
s = get_settings()
updates = {
"BACKUP_AUTO_ENABLED": "1" if body.enabled else "0",
"BACKUP_INTERVAL_HOURS": str(
int(body.interval_hours)
if float(body.interval_hours).is_integer()
else body.interval_hours
),
}
try:
update_env_file(s.env_file_path, updates)
except OSError as e:
raise HTTPException(status_code=500, detail=f"failed to write {s.env_file_path}: {e}") from e
reload_settings()
info = get_backup_info()
return {
"ok": True,
"message": "自动备份设置已保存",
**info,
}
@@ -74,6 +135,7 @@ def create_backup(body: BackupActionBody) -> dict:
s = get_settings()
try:
dest = backup_db(s.db_path, s.backup_dir_path, tz=s.tz)
prune_backups(s.backup_dir_path, s.backup_keep_count)
except FileNotFoundError as e:
raise HTTPException(status_code=404, detail=str(e)) from e
except OSError as e:
+30 -3
View File
@@ -1,4 +1,4 @@
"""Worker 入口:周期回填到期结算指数。"""
"""Worker 入口:周期回填到期结算指数 + 自动备份"""
from __future__ import annotations
@@ -10,8 +10,9 @@ from typing import Any
from apps.collector.okx_rest import OkxRestClient
from apps.worker.settle import backfill_settlements
from packages.config import get_settings
from packages.config import get_settings, reload_settings
from packages.db import Repository
from packages.db.backup import run_auto_backup, should_auto_backup
logging.basicConfig(
level=logging.INFO,
@@ -29,13 +30,35 @@ def _handle_signal(signum: int, _frame: Any) -> None:
_STOP = True
def _maybe_auto_backup() -> None:
s = reload_settings()
if not should_auto_backup(
s.backup_dir_path,
enabled=s.backup_auto_enabled,
interval_hours=s.backup_interval_hours,
):
return
try:
dest = run_auto_backup(
s.db_path,
s.backup_dir_path,
tz=s.tz,
keep=s.backup_keep_count,
)
log.info("auto backup ok: %s", dest.name)
except Exception as e: # noqa: BLE001
log.exception("auto backup failed: %s", e)
def run() -> int:
settings = get_settings()
interval = max(60, int(settings.settle_backfill_interval_sec))
log.info(
"start settle backfill interval=%ss db=%s",
"start settle backfill interval=%ss db=%s backup_auto=%s every=%sh",
interval,
settings.db_path,
settings.backup_auto_enabled,
settings.backup_interval_hours,
)
repo = Repository(settings.db_path)
client = OkxRestClient(
@@ -43,6 +66,8 @@ def run() -> int:
proxy=settings.okx_proxy or None,
)
try:
# 启动时先尝试一次自动备份(若到期)
_maybe_auto_backup()
while not _STOP:
try:
result = backfill_settlements(
@@ -62,6 +87,8 @@ def run() -> int:
except Exception as e: # noqa: BLE001
log.exception("backfill loop failed: %s", e)
_maybe_auto_backup()
end = time.monotonic() + interval
while not _STOP and time.monotonic() < end:
time.sleep(min(1.0, end - time.monotonic()))