fix(web): expose organize recovery and file episode evidence
This commit is contained in:
1 parent
b7883eaf4f
commit
2b3ccb89e4
12 files changed
+390
-72
No files matched your search
@@ -101,3 +101,16 @@ describe("下载面板展示换算(DL-2)", () => {
|
||||
expect(liveStateLabel(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("入库状态不能遮住最近一次部分整理或处理中结果", () => {
|
||||
expect(
|
||||
organizeState({ status: "imported", organize: rec("partial", { reason: "excluded_unresolved" }) }),
|
||||
).toMatchObject({
|
||||
label: "部分整理",
|
||||
tone: "warning",
|
||||
to: `/console/organize?open=${RID}`,
|
||||
});
|
||||
expect(organizeState({ status: "imported", organize: rec("running") })?.label).toBe("整理中");
|
||||
expect(organizeState({ status: "imported", organize: rec("failed") })?.tone).toBe("destructive");
|
||||
expect(organizeState({ status: "imported", organize: rec("succeeded") })?.tone).toBe("success");
|
||||
});
|
||||
@@ -50,12 +50,12 @@ export function organizeState(
|
||||
if (t.managed === false && !rec && t.status !== "importing" && t.status !== "imported") return null;
|
||||
// 有问题的整理在整理记录详情里就地处理(待处理中心已取消)
|
||||
const to = rec ? recordPath("organize", rec.recordId) : null;
|
||||
if (rec?.status === "succeeded" || (t.status === "imported" && rec?.status !== "failed"))
|
||||
return { label: "已整理", tone: "success", to, hint: null };
|
||||
if (rec?.status === "failed") return { label: "整理失败", tone: "destructive", to, hint: reasonHint(rec.reason) };
|
||||
if (rec?.status === "partial") return { label: "部分整理", tone: "warning", to, hint: reasonHint(rec.reason) };
|
||||
if (rec?.status === "succeeded") return { label: "已整理", tone: "success", to, hint: null };
|
||||
if (rec?.status === "queued" || rec?.status === "running" || t.status === "importing")
|
||||
return { label: "整理中", tone: "neutral", to, hint: null };
|
||||
if (t.status === "imported") return { label: "已整理", tone: "success", to, hint: null };
|
||||
return { label: "待整理", tone: "warning", to: rec?.decision ? to : null, hint: null };
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { PublicOrganizationRecord } from "@umt/contracts";
|
||||
import { createElement } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { MemoryRouter } from "react-router";
|
||||
import { expect, it } from "vitest";
|
||||
import { DownloadOrganizeIssue } from "./download-detail.tsx";
|
||||
|
||||
const render = (status: PublicOrganizationRecord["status"]) =>
|
||||
renderToStaticMarkup(
|
||||
createElement(
|
||||
MemoryRouter,
|
||||
null,
|
||||
createElement(DownloadOrganizeIssue, {
|
||||
record: {
|
||||
id: "record",
|
||||
status,
|
||||
reason: { code: "episode_unresolved", message: "集数待确认" },
|
||||
} as PublicOrganizationRecord,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
it("下载详情中的部分结果与失败都指向原整理记录,成功没有问题提示", () => {
|
||||
expect(render("partial")).toContain("部分整理完成,还有文件需要处理");
|
||||
expect(render("partial")).toContain("/console/organize?open=record");
|
||||
expect(render("partial")).toContain("集数待确认");
|
||||
expect(render("partial")).toContain("text-warning");
|
||||
expect(render("failed")).toContain("整理失败");
|
||||
expect(render("failed")).toContain("text-destructive");
|
||||
expect(render("succeeded")).toBe("");
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { DownloadLive, DownloadTask } from "@umt/contracts";
|
||||
import type { DownloadLive, DownloadTask, PublicOrganizationRecord } from "@umt/contracts";
|
||||
import { Bot, FolderInput, Pause, Play, ShieldAlert } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
@@ -24,7 +24,7 @@ import { Button } from "~/components/ui/button.tsx";
|
||||
import { relTime } from "~/components/workbench/release-fields.ts";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { humanizeReason, reasonCode, reasonLine } from "~/lib/reasons.ts";
|
||||
import { formatBytes, formatDateTime } from "~/lib/utils.ts";
|
||||
import { cn, formatBytes, formatDateTime } from "~/lib/utils.ts";
|
||||
import { liveItems } from "./board-format.ts";
|
||||
import { BusinessTimeline } from "./business-timeline.tsx";
|
||||
import { WhySection } from "./plan-row.tsx";
|
||||
@@ -89,6 +89,29 @@ export function PauseResume({ t, size = "sm" }: { t: DownloadTask; size?: "sm" |
|
||||
);
|
||||
}
|
||||
|
||||
/** 最近一次整理的部分完成和失败都留在下载详情里,并指向原记录。 */
|
||||
export function DownloadOrganizeIssue({ record }: { record: PublicOrganizationRecord | null | undefined }) {
|
||||
if (!record || (record.status !== "failed" && record.status !== "partial")) return null;
|
||||
const partial = record.status === "partial";
|
||||
const reason = reasonLine(record.reason);
|
||||
return (
|
||||
<p
|
||||
className={cn(
|
||||
"rounded-lg border px-3 py-2 text-caption",
|
||||
partial
|
||||
? "border-warning/40 bg-warning/10 text-warning"
|
||||
: "border-destructive/40 bg-destructive/10 text-destructive",
|
||||
)}
|
||||
>
|
||||
{partial ? "部分整理完成,还有文件需要处理" : "整理失败"}
|
||||
{reason ? `:${reason}` : ""}。
|
||||
<RecordLink page="organize" id={record.id} className="ml-1 underline underline-offset-4">
|
||||
去整理记录处理
|
||||
</RecordLink>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
/** 下载任务详情:状态、阈值说明、业务时间线、为什么下这个(DL-3)、文件 */
|
||||
export function DownloadDetail({ task, live }: { task: DownloadTask; live?: DownloadLive | null }) {
|
||||
const plan = usePlan(task.acquisitionPlanId);
|
||||
@@ -96,11 +119,12 @@ export function DownloadDetail({ task, live }: { task: DownloadTask; live?: Down
|
||||
const trace = useDownloadTrace(task, plan.data);
|
||||
const [organizing, setOrganizing] = useState(false);
|
||||
const canOrganize = ["completed", "importing", "imported"].includes(task.status);
|
||||
// 下载完成但最近一次整理失败:状态行只写"下载完成"会误导,单独说明整理失败
|
||||
// 下载任务状态不能覆盖最近一次整理的部分完成 / 失败结果
|
||||
const lastRecord = [...(trace.records.data?.items ?? [])].sort((a, b) =>
|
||||
(b.startedAt ?? b.createdAt).localeCompare(a.startedAt ?? a.createdAt),
|
||||
)[0];
|
||||
const importFailed = task.status === "completed" && lastRecord?.status === "failed" ? lastRecord : null;
|
||||
const importIssue =
|
||||
canOrganize && (lastRecord?.status === "failed" || lastRecord?.status === "partial") ? lastRecord : null;
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
@@ -141,14 +165,7 @@ export function DownloadDetail({ task, live }: { task: DownloadTask; live?: Down
|
||||
下载盘超过暂停线,任务已暂停;降到恢复线以下后自动恢复。
|
||||
</p>
|
||||
) : null}
|
||||
{importFailed ? (
|
||||
<p className="rounded-lg border border-destructive/40 bg-destructive/10 px-3 py-2 text-caption text-destructive">
|
||||
整理失败:{reasonLine(importFailed.reason)}。
|
||||
<RecordLink page="organize" id={importFailed.id} className="ml-1 underline underline-offset-4">
|
||||
去整理记录处理
|
||||
</RecordLink>
|
||||
</p>
|
||||
) : null}
|
||||
<DownloadOrganizeIssue record={importIssue} />
|
||||
{task.noProgressSince ? (
|
||||
<p className="rounded-lg border border-warning/40 bg-warning/10 px-3 py-2 text-caption text-warning">
|
||||
{formatDateTime(task.noProgressSince)}{" "}
|
||||
@@ -195,8 +212,8 @@ export function DownloadDetail({ task, live }: { task: DownloadTask; live?: Down
|
||||
{ label: "torrent hash", value: task.torrentHash, wide: true },
|
||||
{ label: "任务 ID", value: task.id, wide: true },
|
||||
{ label: "下载器 ID", value: task.instanceId, wide: true },
|
||||
importFailed ? { label: "整理失败原因码", value: reasonCode(importFailed.reason) } : null,
|
||||
importFailed ? { label: "整理记录 ID", value: importFailed.id, wide: true } : null,
|
||||
importIssue ? { label: "整理结果原因码", value: reasonCode(importIssue.reason) } : null,
|
||||
importIssue ? { label: "整理记录 ID", value: importIssue.id, wide: true } : null,
|
||||
]}
|
||||
>
|
||||
{task.lastError ? <JsonView label="下载器错误原文" value={task.lastError} tone="destructive" /> : null}
|
||||
|
||||
Reference in new issue
Block a user