feat(backup): add database backup/restore with scheduled retention and admin dashboard

- Add BackupRecord model for tracking backup archives
- Add migration 021_add_backup_records
- Add backup configuration settings (backup_enabled, backup_dir, backup_remote_destination, backup_remote_folder, backup_retain_hourly/daily/weekly)
- Add backup_tasks.py with create_backup, cleanup_old_backups, and helpers
- Register hourly/daily/weekly Celery beat schedules
- Add /api/admin/backup/* REST endpoints (list, create, download, restore, delete, cleanup)
- Add /admin/backup dashboard view and template
- Add backup link to admin dropdown navigation in base.html
- Add backup settings to SETTING_METADATA in settings_service.py
- Add comprehensive test suite (39 tests passing)

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-07 22:03:27 +00:00
parent f4a607df05
commit 2dd1ca0197
13 changed files with 1935 additions and 0 deletions
+58
View File
@@ -0,0 +1,58 @@
"""
Backup management dashboard view admin only.
"""
import logging
from fastapi import Depends, HTTPException, Request, status
from sqlalchemy.orm import Session
from app.config import settings
from app.models import BackupRecord
from app.views.base import APIRouter, get_db, require_login, templates
from app.views.settings import require_admin_access
logger = logging.getLogger(__name__)
router = APIRouter()
@router.get("/admin/backup")
@require_login
@require_admin_access
async def backup_dashboard(request: Request, db: Session = Depends(get_db)):
"""Backup management dashboard admin only."""
try:
records = db.query(BackupRecord).order_by(BackupRecord.created_at.desc()).limit(500).all()
# Summarise counts per tier
counts: dict[str, int] = {"hourly": 0, "daily": 0, "weekly": 0}
for r in records:
if r.backup_type in counts:
counts[r.backup_type] += 1
# Compute total local size
import os
total_size = sum(r.size_bytes for r in records if r.local_path and os.path.exists(r.local_path))
return templates.TemplateResponse(
"backup.html",
{
"request": request,
"records": records,
"counts": counts,
"total_size": total_size,
"backup_enabled": getattr(settings, "backup_enabled", True),
"backup_remote_destination": getattr(settings, "backup_remote_destination", None),
"backup_retain_hourly": getattr(settings, "backup_retain_hourly", 96),
"backup_retain_daily": getattr(settings, "backup_retain_daily", 21),
"backup_retain_weekly": getattr(settings, "backup_retain_weekly", 13),
"app_version": settings.version,
},
)
except Exception as e:
logger.error(f"Error loading backup dashboard: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to load backup dashboard",
)