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

@@ -1,4 +1,4 @@
import type { DownloadTask } from "@umt/contracts";
import type { DownloadLive, DownloadTask } from "@umt/contracts";
import { Bot, FolderInput, Pause, Play, ShieldAlert } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
@@ -23,6 +23,7 @@ import { relTime } from "~/components/workbench/release-fields.ts";
import { errorMessage } from "~/lib/api.ts";
import { planReasonText } from "~/lib/labels.ts";
import { formatBytes, formatDateTime } from "~/lib/utils.ts";
import { liveItems } from "./board-format.ts";
import { BusinessTimeline } from "./business-timeline.tsx";
const PLAN_PURPOSE_LABEL: Record<string, string> = { fill: "补缺", upgrade: "升级", replace_source: "换源" };
@@ -89,7 +90,7 @@ export function PauseResume({ t, size = "sm" }: { t: DownloadTask; size?: "sm" |
}
/** 下载任务详情:状态、阈值说明、业务时间线、文件、关联计划 */
export function DownloadDetail({ task }: { task: DownloadTask }) {
export function DownloadDetail({ task, live }: { task: DownloadTask; live?: DownloadLive | null }) {
const plan = usePlan(task.acquisitionPlanId);
const names = useDownloaderNames();
const trace = useDownloadTrace(task, plan.data);
@@ -141,13 +142,14 @@ export function DownloadDetail({ task }: { task: DownloadTask }) {
) : null}
<DetailSection title="进度">
<ProgressLine t={task} />
<ProgressLine t={live ? { ...task, progress: live.progress } : task} />
<KvList
className="mt-3"
items={[
{ label: "下载器", value: names.data?.get(task.instanceId) ?? task.instanceId.slice(0, 8) },
{ label: "大小", value: task.sizeBytes === null ? "未知" : formatBytes(task.sizeBytes, 2) },
{ label: "剩余", value: task.remainingBytes === null ? "未知" : formatBytes(task.remainingBytes, 2) },
...liveItems(live),
{ label: "分享率", value: task.ratio === null ? "—" : task.ratio.toFixed(2) },
{ label: "最近进展", value: relTime(task.lastProgressAt) },
{ label: "最近同步", value: relTime(task.lastSyncedAt) },