Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

## [Unreleased]

### 新增
- 定时任务支持查看执行记录:在操作列或卡片更多菜单打开弹窗,按服务端分页展示完成时间、状态和失败原因。

## [0.9.22] - 2026-08-11

### 新增
Expand Down
6 changes: 6 additions & 0 deletions dashboard/src/api/modules/cronjob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
OctopCronRow,
OctopCronCreateBody,
OctopCronPatchBody,
OctopCronRunPage,
} from "../types";

export interface OctopCronSettings {
Expand Down Expand Up @@ -36,4 +37,9 @@ export const octopCronApi = {
request<void>(`/agents/${agentId}/cron/${cronId}/run-now`, {
method: "POST",
}),

listRuns: (agentId: string, cronId: string, page: number, pageSize: number) =>
request<OctopCronRunPage>(
`/agents/${agentId}/cron/${cronId}/runs?page=${page}&page_size=${pageSize}`,
),
};
14 changes: 14 additions & 0 deletions dashboard/src/api/types/cronjob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,17 @@ export interface OctopCronPatchBody {
task_type?: "text" | "agent";
mcp_servers?: string[];
}

export interface OctopCronRun {
id: number;
completed_at: number;
status: "ok" | "error";
error: string | null;
}

export interface OctopCronRunPage {
items: OctopCronRun[];
page: number;
page_size: number;
total: number;
}
12 changes: 12 additions & 0 deletions dashboard/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1152,6 +1152,18 @@
"operationFailed": "Operation failed",
"triggeredSuccess": "Task triggered, result will appear shortly",
"executeFailed": "Failed to trigger execution",
"runHistory": {
"action": "Execution Records",
"title": "Execution Records",
"titleWithName": "Execution Records · {{name}}",
"completedAt": "Completed At",
"status": "Status",
"error": "Failure Reason",
"succeeded": "Succeeded",
"failed": "Failed",
"empty": "No execution records",
"loadFailed": "Failed to load execution records"
},
"totalItems": "Total {{count}} items",
"loadingJobs": "Loading this expert's tasks…",
"syncing": "Syncing…",
Expand Down
12 changes: 12 additions & 0 deletions dashboard/src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -1149,6 +1149,18 @@
"operationFailed": "操作失败",
"triggeredSuccess": "任务已触发,结果稍后显示",
"executeFailed": "触发执行失败",
"runHistory": {
"action": "执行记录",
"title": "执行记录",
"titleWithName": "执行记录 · {{name}}",
"completedAt": "完成时间",
"status": "状态",
"error": "失败原因",
"succeeded": "成功",
"failed": "失败",
"empty": "暂无执行记录",
"loadFailed": "加载执行记录失败"
},
"totalItems": "共 {{count}} 项",
"loadingJobs": "正在加载当前专家的任务…",
"syncing": "正在同步…",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ interface CronJobCardProps {
timeZone: string;
onToggleEnabled: (job: CronJob) => void;
onExecuteNow: (job: CronJob) => void;
onViewRuns: (job: CronJob) => void;
onEdit: (job: CronJob) => void;
onDelete: (jobId: string) => void;
}
Expand All @@ -42,6 +43,7 @@ export function CronJobCard({
timeZone,
onToggleEnabled,
onExecuteNow,
onViewRuns,
onEdit,
onDelete,
}: CronJobCardProps) {
Expand Down Expand Up @@ -73,6 +75,11 @@ export function CronJobCard({
const taskType = job.task_type === "text" ? "text" : "agent";

const moreMenuItems: MenuProps["items"] = [
{
key: "runs",
label: t("cronJobs.runHistory.action"),
onClick: () => onViewRuns(job),
},
{
key: "edit",
label: t("common.edit"),
Expand Down
139 changes: 139 additions & 0 deletions dashboard/src/pages/Control/CronJobs/components/RunHistoryModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { useEffect, useState } from "react";
import { Empty, Modal, Table, Tag, Tooltip, message } from "antd";
import type { TableColumnsType } from "antd";
import { useTranslation } from "react-i18next";
import { octopCronApi } from "../../../../api/modules/cronjob";
import type { OctopCronRun } from "../../../../api/types";
import { formatCronTimestamp } from "../cronDisplay";

const DEFAULT_PAGE_SIZE = 10;

interface RunHistoryModalProps {
open: boolean;
agentId: string;
cronId: string | null;
jobName?: string;
timeZone: string;
onClose: () => void;
}

export function RunHistoryModal({
open,
agentId,
cronId,
jobName,
timeZone,
onClose,
}: RunHistoryModalProps) {
const { t } = useTranslation();
const [items, setItems] = useState<OctopCronRun[]>([]);
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
const [total, setTotal] = useState(0);

useEffect(() => {
if (!open || !cronId) return;
let cancelled = false;
setLoading(true);
void octopCronApi
.listRuns(agentId, cronId, page, pageSize)
.then((result) => {
if (cancelled) return;
setItems(result.items);
setTotal(result.total);
})
.catch((error) => {
if (cancelled) return;
console.error("Failed to load cron run history", error);
message.error(t("cronJobs.runHistory.loadFailed"));
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, [agentId, cronId, open, page, pageSize, t]);

useEffect(() => {
if (!open) {
setItems([]);
setPage(1);
setPageSize(DEFAULT_PAGE_SIZE);
setTotal(0);
}
}, [open]);

const columns: TableColumnsType<OctopCronRun> = [
{
title: t("cronJobs.runHistory.completedAt"),
dataIndex: "completed_at",
width: 190,
render: (value: number) => formatCronTimestamp(value, timeZone),
},
{
title: t("cronJobs.runHistory.status"),
dataIndex: "status",
width: 100,
render: (status: OctopCronRun["status"]) => (
<Tag color={status === "ok" ? "success" : "error"}>
{status === "ok"
? t("cronJobs.runHistory.succeeded")
: t("cronJobs.runHistory.failed")}
</Tag>
),
},
{
title: t("cronJobs.runHistory.error"),
dataIndex: "error",
ellipsis: true,
render: (error: string | null) =>
error ? (
<Tooltip title={error} placement="topLeft">
<span>{error}</span>
</Tooltip>
) : (
"—"
),
},
];

return (
<Modal
open={open}
title={
jobName
? t("cronJobs.runHistory.titleWithName", { name: jobName })
: t("cronJobs.runHistory.title")
}
onCancel={onClose}
footer={null}
width={760}
destroyOnHidden
>
<Table<OctopCronRun>
rowKey="id"
columns={columns}
dataSource={items}
loading={loading}
locale={{
emptyText: <Empty description={t("cronJobs.runHistory.empty")} />,
}}
scroll={{ x: 620, y: 420 }}
pagination={{
current: page,
pageSize,
total,
showSizeChanger: true,
pageSizeOptions: [10, 20, 50],
showTotal: (count) => t("cronJobs.totalItems", { count }),
onChange: (nextPage, nextPageSize) => {
setPage(nextPageSize === pageSize ? nextPage : 1);
setPageSize(nextPageSize);
},
}}
/>
</Modal>
);
}
10 changes: 9 additions & 1 deletion dashboard/src/pages/Control/CronJobs/components/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ interface ColumnHandlers {
onDetail: (job: CronJob) => void;
onToggleEnabled: (job: CronJob) => void;
onExecuteNow: (job: CronJob) => void;
onViewRuns: (job: CronJob) => void;
onEdit: (job: CronJob) => void;
onDelete: (jobId: string) => void;
t: TFunction;
Expand Down Expand Up @@ -248,7 +249,7 @@ export const createColumns = (
{
title: handlers.t("cronJobs.action"),
key: "action",
width: 200,
width: 280,
fixed: "right",
render: (_: unknown, record: CronJob) => {
const menuItems: MenuProps["items"] = [
Expand Down Expand Up @@ -285,6 +286,13 @@ export const createColumns = (
>
{handlers.t("cronJobs.executeNow")}
</Button>
<Button
type="link"
size="small"
onClick={() => handlers.onViewRuns(record)}
>
{handlers.t("cronJobs.runHistory.action")}
</Button>
<Dropdown menu={{ items: menuItems }} placement="bottomRight">
<Button
type="text"
Expand Down
1 change: 1 addition & 0 deletions dashboard/src/pages/Control/CronJobs/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ export { CronJobCard } from "./CronJobCard";
export { ExecuteNowModal } from "./ExecuteNowModal";
export { JobDrawer } from "./JobDrawer";
export { JobDetailDrawer } from "./JobDetailDrawer";
export { RunHistoryModal } from "./RunHistoryModal";
export { useCronJobs } from "../useCronJobs";
export { TIMEZONE_OPTIONS, DEFAULT_FORM_VALUES } from "./constants";
18 changes: 18 additions & 0 deletions dashboard/src/pages/Control/CronJobs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
ExecuteNowModal,
JobDrawer,
JobDetailDrawer,
RunHistoryModal,
useCronJobs,
} from "./components";
import type { CronJobFormValues } from "./useCronJobs";
Expand Down Expand Up @@ -111,6 +112,7 @@ function CronJobsPage() {
// Execute-now confirmation
const [executingJob, setExecutingJob] = useState<CronJob | null>(null);
const [executing, setExecuting] = useState(false);
const [historyJob, setHistoryJob] = useState<CronJob | null>(null);

// Close transient UI when switching experts — avoid dangling drawers
// tied to the previous agent's jobs.
Expand All @@ -120,6 +122,7 @@ function CronJobsPage() {
setDetailDrawerOpen(false);
setDetailJob(null);
setExecutingJob(null);
setHistoryJob(null);
}, [activeAgentId]);

const handleDetail = (job: CronJob) => {
Expand Down Expand Up @@ -179,6 +182,10 @@ function CronJobsPage() {
setExecutingJob(job);
};

const handleViewRuns = (job: CronJob) => {
setHistoryJob(job);
};

const handleExecuteNowConfirm = async () => {
if (!executingJob) return;
setExecuting(true);
Expand Down Expand Up @@ -226,6 +233,7 @@ function CronJobsPage() {
onDetail: handleDetail,
onToggleEnabled: handleToggleEnabled,
onExecuteNow: handleExecuteNow,
onViewRuns: handleViewRuns,
onEdit: handleEdit,
onDelete: handleDelete,
t,
Expand Down Expand Up @@ -342,6 +350,7 @@ function CronJobsPage() {
timeZone={cronTimezone}
onToggleEnabled={handleToggleEnabled}
onExecuteNow={handleExecuteNow}
onViewRuns={handleViewRuns}
onEdit={handleEdit}
onDelete={handleDelete}
/>
Expand Down Expand Up @@ -390,6 +399,15 @@ function CronJobsPage() {
onCancel={handleExecuteNowCancel}
onConfirm={handleExecuteNowConfirm}
/>

<RunHistoryModal
open={historyJob !== null}
agentId={activeAgentId}
cronId={historyJob?.id ?? null}
jobName={historyJob?.name}
timeZone={cronTimezone}
onClose={() => setHistoryJob(null)}
/>
</PageShell>
);
}
Expand Down
Loading