feat: add task history diagnostics

This commit is contained in:
Codex
2026-05-04 17:41:10 +00:00
parent 3adce947cf
commit 308e7c858e
8 changed files with 158 additions and 8 deletions
+39
View File
@@ -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;
+74 -5
View File
@@ -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 })),
))),
)),
),
);
}