feat: add task history diagnostics
This commit is contained in:
@@ -671,6 +671,7 @@ h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.08em; }
|
||||
|
||||
.table-wrap { overflow: auto; max-height: calc(100vh - 174px); }
|
||||
table { width: 100%; border-collapse: collapse; min-width: 760px; }
|
||||
.task-history-table { min-width: 1080px; }
|
||||
th, td {
|
||||
padding: 7px 9px;
|
||||
border-bottom: 1px solid var(--line-soft);
|
||||
@@ -689,6 +690,44 @@ th {
|
||||
}
|
||||
td { color: var(--text); }
|
||||
|
||||
.task-duration {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
min-width: 118px;
|
||||
}
|
||||
.task-duration strong {
|
||||
color: var(--accent-2);
|
||||
font-family: "Cascadia Mono", "IBM Plex Mono", "Liberation Mono", monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
.task-duration span {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
.task-diagnostic {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
min-width: 190px;
|
||||
max-width: 360px;
|
||||
}
|
||||
.task-diagnostic b {
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.task-diagnostic.ok b { color: var(--ok); }
|
||||
.task-diagnostic.warn b { color: var(--warn); }
|
||||
.task-diagnostic.failed b { color: var(--danger); }
|
||||
.diagnostic-reason {
|
||||
color: #ffd7cf;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.diagnostic-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.compact-row, .heartbeat-row, .log-row, .endpoint-list article, .policy-grid article {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(180px, 1fr) auto auto;
|
||||
|
||||
@@ -108,6 +108,34 @@ function fmtRelativeAge(value: any): string {
|
||||
return fmtDuration(Math.max(0, Math.floor((Date.now() - date.getTime()) / 1000)));
|
||||
}
|
||||
|
||||
function timeMs(value: any): number | null {
|
||||
if (!value) return null;
|
||||
const date = new Date(value);
|
||||
return Number.isNaN(date.getTime()) ? null : date.getTime();
|
||||
}
|
||||
|
||||
function taskElapsedSeconds(task: any): number | null {
|
||||
const started = timeMs(task?.createdAt);
|
||||
if (started === null) return null;
|
||||
const terminal = ["succeeded", "failed"].includes(String(task?.status || "").toLowerCase());
|
||||
const ended = terminal ? timeMs(task?.updatedAt) : Date.now();
|
||||
if (ended === null) return null;
|
||||
return Math.max(0, Math.floor((ended - started) / 1000));
|
||||
}
|
||||
|
||||
function taskFailureReason(task: any): string {
|
||||
if (String(task?.status || "").toLowerCase() !== "failed") return "";
|
||||
const result = task?.result;
|
||||
if (typeof result === "string") return result;
|
||||
if (result && typeof result === "object" && !Array.isArray(result)) {
|
||||
const record = result as AnyRecord;
|
||||
for (const key of ["error", "reason", "message", "stderr", "detail"]) {
|
||||
if (typeof record[key] === "string" && record[key].length > 0) return record[key];
|
||||
}
|
||||
}
|
||||
return "任务失败但 provider 未返回明确原因";
|
||||
}
|
||||
|
||||
function summarizeValue(value: any): string {
|
||||
if (value === null || value === undefined) return "--";
|
||||
if (typeof value === "boolean") return value ? "是" : "否";
|
||||
@@ -841,11 +869,48 @@ function TaskCompactRow({ task, onRaw }: AnyRecord) {
|
||||
return h("article", { className: "compact-row" },
|
||||
h(StatusBadge, { status: task.status }),
|
||||
h("div", null, h("strong", null, task.command), h("code", null, task.id)),
|
||||
h("span", null, isPendingTask(task) ? `已等待 ${fmtRelativeAge(task.updatedAt)}` : fmtDate(task.updatedAt)),
|
||||
h("span", null, isPendingTask(task) ? `已等待 ${fmtRelativeAge(task.updatedAt)}` : `耗时 ${fmtDuration(taskElapsedSeconds(task) ?? 0)}`),
|
||||
h(RawButton, { title: `Task ${task.id}`, data: task, onOpen: onRaw }),
|
||||
);
|
||||
}
|
||||
|
||||
function TaskDurationCell({ task }: AnyRecord) {
|
||||
const elapsed = taskElapsedSeconds(task);
|
||||
const pending = isPendingTask(task);
|
||||
return h("div", { className: "task-duration" },
|
||||
h("strong", null, elapsed === null ? "--" : fmtDuration(elapsed)),
|
||||
h("span", null, pending ? `已运行 / 创建 ${fmtDate(task.createdAt)}` : `创建 ${fmtDate(task.createdAt)}`),
|
||||
);
|
||||
}
|
||||
|
||||
function TaskDiagnosticCell({ task }: AnyRecord) {
|
||||
const status = String(task?.status || "").toLowerCase();
|
||||
const result = task?.result;
|
||||
const resultRecord = result && typeof result === "object" && !Array.isArray(result) ? result as AnyRecord : {};
|
||||
const metaKeys = ["exitCode", "code", "signal", "timeoutMs", "previousStatus", "mode"];
|
||||
const metas = metaKeys.filter((key) => resultRecord[key] !== undefined && resultRecord[key] !== null);
|
||||
if (status === "failed") {
|
||||
const reason = taskFailureReason(task);
|
||||
return h("div", { className: "task-diagnostic failed" },
|
||||
h("b", null, "失败原因"),
|
||||
h("span", { className: "diagnostic-reason" }, summarizeValue(reason)),
|
||||
metas.length > 0 ? h("div", { className: "diagnostic-meta" }, metas.map((key) =>
|
||||
h("span", { key, className: "data-chip" }, h("b", null, key), h("span", null, summarizeValue(resultRecord[key]))),
|
||||
)) : null,
|
||||
);
|
||||
}
|
||||
if (isPendingTask(task)) {
|
||||
return h("div", { className: "task-diagnostic warn" },
|
||||
h("b", null, "等待终态"),
|
||||
h("span", null, `最后更新 ${fmtRelativeAge(task.updatedAt)} 前`),
|
||||
);
|
||||
}
|
||||
return h("div", { className: "task-diagnostic ok" },
|
||||
h("b", null, "完成摘要"),
|
||||
h(DataSummary, { data: result, empty: "无执行输出" }),
|
||||
);
|
||||
}
|
||||
|
||||
function TaskPendingPage({ tasks, onRaw }: AnyRecord) {
|
||||
const pending = tasks.filter(isPendingTask);
|
||||
return h("div", { "data-testid": "pending-task-page" },
|
||||
@@ -867,19 +932,23 @@ function TaskPendingPage({ tasks, onRaw }: AnyRecord) {
|
||||
}
|
||||
|
||||
function TaskHistoryPage({ tasks, onRaw }: AnyRecord) {
|
||||
return h(Panel, { title: "任务历史", eyebrow: `${tasks.length} Tasks` },
|
||||
return h("div", { "data-testid": "task-history-page" },
|
||||
h(Panel, { title: "任务历史", eyebrow: `${tasks.length} Tasks` },
|
||||
tasks.length === 0 ? h(EmptyState, { title: "暂无任务", text: "下发任务后会在这里看到生命周期" }) :
|
||||
h("div", { className: "table-wrap" }, h("table", null,
|
||||
h("thead", null, h("tr", null, h("th", null, "状态"), h("th", null, "任务"), h("th", null, "Provider"), h("th", null, "载荷摘要"), h("th", null, "更新时间"), h("th", null, "操作"))),
|
||||
h("tbody", null, tasks.map((task: any) => h("tr", { key: task.id },
|
||||
h("div", { className: "table-wrap" }, h("table", { className: "task-history-table" },
|
||||
h("thead", null, h("tr", null, h("th", null, "状态"), h("th", null, "任务"), h("th", null, "Provider"), h("th", null, "任务耗时"), h("th", null, "载荷摘要"), h("th", null, "诊断信息"), h("th", null, "更新时间"), h("th", null, "操作"))),
|
||||
h("tbody", null, tasks.map((task: any) => h("tr", { key: task.id, "data-testid": `task-row-${safeId(task.id)}` },
|
||||
h("td", null, h(StatusBadge, { status: task.status })),
|
||||
h("td", null, h("strong", null, task.command), h("code", null, task.id)),
|
||||
h("td", null, h("code", null, task.providerId)),
|
||||
h("td", null, h(TaskDurationCell, { task })),
|
||||
h("td", null, h(DataSummary, { data: task.payload })),
|
||||
h("td", null, h(TaskDiagnosticCell, { task })),
|
||||
h("td", null, fmtDate(task.updatedAt)),
|
||||
h("td", null, h(RawButton, { title: `Task ${task.id}`, data: task, onOpen: onRaw })),
|
||||
))),
|
||||
)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user