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
+3
View File
@@ -24,6 +24,9 @@ class Settings(BaseSettings):
admin_password: str = Field(default="admin123", alias="ADMIN_PASSWORD")
env_file: str = Field(default=".env", alias="ENV_FILE")
backup_dir: str = Field(default="/root/market_intel_backups", alias="BACKUP_DIR")
backup_auto_enabled: bool = Field(default=True, alias="BACKUP_AUTO_ENABLED")
backup_interval_hours: float = Field(default=24.0, alias="BACKUP_INTERVAL_HOURS", ge=1, le=168)
backup_keep_count: int = Field(default=30, alias="BACKUP_KEEP_COUNT", ge=5, le=200)
# OKX
okx_api_key: str = Field(default="", alias="OKX_API_KEY")
+68 -1
View File
@@ -2,13 +2,17 @@
from __future__ import annotations
import logging
import re
import sqlite3
from datetime import datetime
from pathlib import Path
from zoneinfo import ZoneInfo
log = logging.getLogger("backup")
_BACKUP_NAME_RE = re.compile(r"^market_intel_\d{8}_\d{6}\.db$")
DISPLAY_LIMIT = 5
def _now_stamp(tz: str = "Asia/Shanghai") -> str:
@@ -69,7 +73,7 @@ def restore_db(backup_path: Path, db_path: Path) -> None:
src.close()
def list_backups(backup_dir: Path) -> list[dict]:
def list_backups(backup_dir: Path, limit: int | None = None) -> list[dict]:
if not backup_dir.is_dir():
return []
items: list[dict] = []
@@ -84,4 +88,67 @@ def list_backups(backup_dir: Path) -> list[dict]:
"modified_ms": int(st.st_mtime * 1000),
}
)
if limit is not None and len(items) >= limit:
break
return items
def prune_backups(backup_dir: Path, keep: int) -> int:
"""删除超出保留数量的旧备份,返回删除数。"""
if keep < 1:
return 0
if not backup_dir.is_dir():
return 0
files = sorted(
[
p
for p in backup_dir.glob("market_intel_*.db")
if p.is_file() and is_valid_backup_name(p.name)
],
key=lambda p: p.stat().st_mtime,
reverse=True,
)
removed = 0
for p in files[keep:]:
try:
p.unlink()
removed += 1
except OSError as e:
log.warning("failed to prune %s: %s", p, e)
return removed
def latest_backup_mtime_ms(backup_dir: Path) -> int | None:
items = list_backups(backup_dir, limit=1)
if not items:
return None
return int(items[0]["modified_ms"])
def should_auto_backup(
backup_dir: Path,
*,
enabled: bool,
interval_hours: float,
) -> bool:
if not enabled:
return False
if interval_hours <= 0:
return False
latest = latest_backup_mtime_ms(backup_dir)
if latest is None:
return True
age_ms = int(datetime.now().timestamp() * 1000) - latest
return age_ms >= interval_hours * 3600 * 1000
def run_auto_backup(
db_path: Path,
backup_dir: Path,
*,
tz: str = "Asia/Shanghai",
keep: int = 30,
) -> Path:
dest = backup_db(db_path, backup_dir, tz=tz)
prune_backups(backup_dir, keep)
return dest