feat(dashboard): revamp with at-a-glance strip, sparkline, modal, disk bar

Replace the four stat cards with a compact three-card strip: 7-day activity
with inline SVG sparkline (new ActivitySparkline), Running Now / Next Check
with per-source countdown list, and System with credential health + disk
usage bar.

- Add GET /downloads/activity-timeline — per-day completed/failed/files
  counts, pre-filled with zero buckets so the sparkline always has N points.
- Report filesystem-level usage via shutil.disk_usage in storage rollup,
  plus a live fallback in GET /settings so the capacity bar works before
  the first Celery rollup runs and for cached rows that predate the field.
- Extract Download Details into a reusable DownloadDetailsModal component
  and wire Recent Activity rows to open it (previously Downloads-page only).
- Compute next scheduled check per source from global schedule_interval;
  surface credential expiration/missing alerts scoped to platforms that
  actually have enabled sources.
- Two-speed polling (5s when downloads are active, 30s idle) with Page
  Visibility awareness so background tabs don't churn the API.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-20 19:32:59 -04:00
parent 86bf43c80a
commit 851df294f2
9 changed files with 1085 additions and 357 deletions
+25
View File
@@ -3,6 +3,7 @@
import asyncio
import logging
import os
import shutil
from datetime import datetime, timedelta
from pathlib import Path
@@ -38,6 +39,29 @@ def format_size(size_bytes: int) -> str:
return f"{size_bytes:.1f} PB"
def _disk_usage_stats(path: Path) -> dict:
"""Return filesystem-level usage for the volume hosting `path`.
Uses shutil.disk_usage so we report the actual mount's free space (the
number that matters for capacity planning), not just what our downloads
subtree consumes.
"""
try:
usage = shutil.disk_usage(path)
return {
"disk_total": usage.total,
"disk_used": usage.used,
"disk_free": usage.free,
"disk_total_formatted": format_size(usage.total),
"disk_used_formatted": format_size(usage.used),
"disk_free_formatted": format_size(usage.free),
"disk_percent_used": round(usage.used / usage.total * 100, 1) if usage.total else 0,
}
except OSError as e:
logger.warning(f"Could not read disk usage for {path}: {e}")
return {}
def calculate_storage_stats() -> dict:
"""Calculate storage statistics for the downloads directory.
@@ -80,6 +104,7 @@ def calculate_storage_stats() -> dict:
"total_size_formatted": format_size(total_size),
"file_count": file_count,
"calculated_at": datetime.utcnow().isoformat(),
**_disk_usage_stats(downloads_path),
}