feat(m5d): demand-driven deep backfill + per-user ETA

Per-user opt-in to full-history (deep) backfill so a new user's unique
channels no longer trigger a big shared-quota burst.

- migration 0007: Subscription.deep_requested (default false); seed admins'
  existing subscriptions to preserve today's global-backfill behaviour
- run_deep_backfill is now demand-driven: only channels at least one user has
  requested are deep-backfilled; recent backfill stays unconditional (cheap)
- estimate_deep_backfill ETA helper (quota-bound) surfaced in /api/sync/my-status
- POST /api/sync/deep-all to opt all my channels in; PATCH channels accepts
  deep_requested
- UI: per-channel Full history toggle, Backfill everything action, deep
  progress + ETA in Channels header and Settings - Sync
This commit is contained in:
npeter83 2026-06-11 23:07:09 +02:00
parent bc8db45323
commit 5f60b3e9fe
9 changed files with 256 additions and 18 deletions

View file

@ -34,6 +34,19 @@ export function formatDuration(sec: number | null): string {
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${m}:${pad(s)}`;
}
/** Coarse, human ETA for a remaining duration in seconds ("~3 hours", "~2 days"). */
export function formatEta(seconds: number): string {
if (seconds <= 0) return "done";
const hours = seconds / 3600;
if (hours < 1) return "< 1 hour";
if (hours < 24) {
const h = Math.round(hours);
return `~${h} hour${h === 1 ? "" : "s"}`;
}
const d = Math.round(hours / 24);
return `~${d} day${d === 1 ? "" : "s"}`;
}
export function formatViews(n: number | null): string {
if (n == null) return "";
if (n >= 1e9) return `${(n / 1e9).toFixed(1).replace(/\.0$/, "")}B`;