feat(downloads): live speeds and active/completed split on the download panel (DL-2)

- GET /download-tasks/board?state=active|completed: per-torrent dl/up speed,
  ETA, seeds/leechs, downloader total speed (transfer/info), counts for both
  groups, latest organize result (status, reason code, open decision)
- live qB data cached per instance for 2 s with in-flight coalescing
- panel: total speed line, 进行中 / 已完成 segments remembered locally,
  4 s polling while the page is visible, mobile layout, "—" for zero speed

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-29 21:44:34 +08:00
1 parent 8086250ec2
commit 3b340745bf
17 files changed
+1113 -93

No files matched your search

@@ -90,6 +90,12 @@ export interface QbTorrent {
eta: number | null;
ratio: number | null;
seedingTimeSec: number | null;
/** 已连接的做种 / 下载用户数(num_seeds / num_leechs) */
numSeeds: number;
numLeechs: number;
/** Tracker 报告的做种 / 下载总数(num_complete / num_incomplete;未知为 null) */
numComplete: number | null;
numIncomplete: number | null;
}
export interface QbFile {
@@ -226,6 +232,11 @@ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
function num(v: unknown): number | null {
return typeof v === "number" && Number.isFinite(v) ? v : null;
}
/** qB 用 -1 表示未知(num_complete 等) */
function nonNeg(v: unknown): number | null {
const n = num(v);
return n !== null && n >= 0 ? n : null;
}
function str(v: unknown): string | null {
return typeof v === "string" && v !== "" ? v : null;
}
@@ -282,6 +293,10 @@ export function normalizeTorrent(instanceId: string, r: Record<string, unknown>)
eta: num(r.eta),
ratio: num(r.ratio),
seedingTimeSec: num(r.seeding_time),
numSeeds: num(r.num_seeds) ?? 0,
numLeechs: num(r.num_leechs) ?? 0,
numComplete: nonNeg(r.num_complete),
numIncomplete: nonNeg(r.num_incomplete),
};
}