{visible.map((run) => {
const status = automationRunStatusLabel(run.status, tx);
+ const needsAttention = automationRunNeedsAttention(run.status);
const duration = run.duration_ms === undefined
? null
: formatAutomationRunDuration(run.duration_ms, locale, tx);
+ const runTime = fmtDateTime(run.run_at_ms, locale);
+ const runMeta = duration
+ ? tx("settings.automations.history.runMetaWithDuration", "{{time}} · {{duration}}", {
+ time: runTime,
+ duration,
+ })
+ : runTime;
+ const primary = needsAttention
+ ? run.error || status
+ : tx("settings.automations.history.noError", "No error recorded");
+ const secondary = needsAttention ? `${status} · ${runMeta}` : runMeta;
return (
- {status}
- {run.error ? ` · ${run.error}` : ""}
+ {primary}
- {fmtDateTime(run.run_at_ms, locale)}
+ {secondary}
- {duration ? (
-
- {duration}
-
- ) : null}
+
+ {status}
+
);
})}
@@ -4028,6 +4046,10 @@ function AutomationRunHistory({
);
}
+function automationRunNeedsAttention(status: string | null | undefined): boolean {
+ return Boolean(status) && status !== "ok";
+}
+
function AutomationDetail({
label,
title,
@@ -4678,9 +4700,9 @@ function formatAutomationNextTitle(
return fmtDateTime(job.state.next_run_at_ms, locale);
}
-function automationLastResult(
+function automationRunSummary(
job: SessionAutomationJob,
- latestRun: NonNullable[number] | undefined,
+ history: NonNullable,
locale: string,
tx: (key: string, fallback: string, values?: Record) => string,
): {
@@ -4700,37 +4722,64 @@ function automationLastResult(
tone: "warning",
};
}
- if (latestRun) {
- const status = automationRunStatusLabel(latestRun.status, tx);
- const duration = latestRun.duration_ms === undefined
- ? null
- : formatAutomationRunDuration(latestRun.duration_ms, locale, tx);
- const primary = duration
- ? tx("settings.automations.history.statusWithDuration", "{{status}} · {{duration}}", {
- status,
- duration,
- })
- : status;
- const secondary = fmtDateTime(latestRun.run_at_ms, locale);
+ const latestRun = history[history.length - 1];
+ const issueRuns = history.filter((run) => automationRunNeedsAttention(run.status));
+ const latestIssue = issueRuns[issueRuns.length - 1];
+ if (latestIssue) {
+ const status = automationRunStatusLabel(latestIssue.status, tx);
+ const runTime = fmtDateTime(latestIssue.run_at_ms, locale);
+ const primary = tx("settings.automations.history.issueCount", "Issues: {{count}}", {
+ count: issueRuns.length,
+ });
+ const secondary = latestIssue.error
+ ? `${latestIssue.error} · ${runTime}`
+ : `${status} · ${runTime}`;
return {
primary,
secondary,
- title: latestRun.error ? `${secondary} · ${latestRun.error}` : secondary,
- tone: latestRun.status === "error"
- ? "danger"
- : latestRun.status === "skipped"
- ? "warning"
- : "success",
+ title: secondary,
+ tone: latestIssue.status === "error" ? "danger" : "warning",
+ };
+ }
+ if (latestRun) {
+ const duration = latestRun.duration_ms === undefined
+ ? null
+ : formatAutomationRunDuration(latestRun.duration_ms, locale, tx);
+ const runTime = fmtDateTime(latestRun.run_at_ms, locale);
+ const secondary = duration
+ ? tx("settings.automations.history.lastRanWithDuration", "Last ran {{time}} · {{duration}}", {
+ time: runTime,
+ duration,
+ })
+ : tx("settings.automations.history.lastRan", "Last ran {{time}}", { time: runTime });
+ return {
+ primary: tx("settings.automations.history.noRecentIssues", "No recent issues"),
+ secondary,
+ title: secondary,
+ tone: "success",
};
}
if (job.state.last_run_at_ms) {
const status = automationRunStatusLabel(job.state.last_status, tx);
- const secondary = fmtDateTime(job.state.last_run_at_ms, locale);
+ const runTime = fmtDateTime(job.state.last_run_at_ms, locale);
+ if (!automationRunNeedsAttention(job.state.last_status)) {
+ return {
+ primary: tx("settings.automations.history.noRecentIssues", "No recent issues"),
+ secondary: tx("settings.automations.history.lastRan", "Last ran {{time}}", {
+ time: runTime,
+ }),
+ title: runTime,
+ tone: "success",
+ };
+ }
+ const secondary = job.state.last_error ? `${job.state.last_error} · ${runTime}` : `${status} · ${runTime}`;
return {
- primary: status,
+ primary: tx("settings.automations.history.issueCount", "Issues: {{count}}", {
+ count: 1,
+ }),
secondary,
title: secondary,
- tone: job.state.last_status === "error" ? "danger" : "success",
+ tone: job.state.last_status === "error" ? "danger" : "warning",
};
}
const never = tx("settings.automations.last.never", "Never");
@@ -4763,7 +4812,7 @@ function automationRunStatusLabel(
status: string | null | undefined,
tx: (key: string, fallback: string, values?: Record) => string,
): string {
- if (status === "ok") return tx("settings.automations.history.ok", "Completed");
+ if (status === "ok") return tx("settings.automations.history.ok", "Healthy");
if (status === "error") return tx("settings.automations.history.error", "Error");
if (status === "skipped") return tx("settings.automations.history.skipped", "Skipped");
return status || tx("settings.automations.last.unknown", "unknown");
diff --git a/webui/src/i18n/locales/en/common.json b/webui/src/i18n/locales/en/common.json
index 085bb19c..d547a396 100644
--- a/webui/src/i18n/locales/en/common.json
+++ b/webui/src/i18n/locales/en/common.json
@@ -577,12 +577,18 @@
"unknown": "unknown"
},
"history": {
- "ok": "Completed",
+ "ok": "Healthy",
"error": "Error",
"skipped": "Skipped",
- "recent": "Last result",
+ "health": "Recent health",
"timeline": "Run history",
- "statusWithDuration": "{{status}} · {{duration}}"
+ "summary": "Runs: {{total}} · Issues: {{issues}}",
+ "issueCount": "Issues: {{count}}",
+ "noRecentIssues": "No recent issues",
+ "lastRan": "Last ran {{time}}",
+ "lastRanWithDuration": "Last ran {{time}} · {{duration}}",
+ "runMetaWithDuration": "{{time}} · {{duration}}",
+ "noError": "No error recorded"
},
"message": {
"showMore": "Show full message",
diff --git a/webui/src/i18n/locales/es/common.json b/webui/src/i18n/locales/es/common.json
index ae36bc52..e48ceffe 100644
--- a/webui/src/i18n/locales/es/common.json
+++ b/webui/src/i18n/locales/es/common.json
@@ -577,12 +577,18 @@
"unknown": "desconocido"
},
"history": {
- "ok": "Completada",
+ "ok": "Sin errores",
"error": "Error",
"skipped": "Omitida",
- "recent": "Último resultado",
+ "health": "Estado reciente",
"timeline": "Historial de ejecuciones",
- "statusWithDuration": "{{status}} · {{duration}}"
+ "summary": "Ejecuciones: {{total}} · Problemas: {{issues}}",
+ "issueCount": "Problemas: {{count}}",
+ "noRecentIssues": "Sin problemas recientes",
+ "lastRan": "Última ejecución: {{time}}",
+ "lastRanWithDuration": "Última ejecución: {{time}} · {{duration}}",
+ "runMetaWithDuration": "{{time}} · {{duration}}",
+ "noError": "No se registraron errores"
},
"message": {
"showMore": "Mostrar mensaje completo",
diff --git a/webui/src/i18n/locales/fr/common.json b/webui/src/i18n/locales/fr/common.json
index b7c886c5..3cd2e61a 100644
--- a/webui/src/i18n/locales/fr/common.json
+++ b/webui/src/i18n/locales/fr/common.json
@@ -577,12 +577,18 @@
"unknown": "inconnu"
},
"history": {
- "ok": "Terminée",
+ "ok": "Sans erreur",
"error": "Erreur",
"skipped": "Ignorée",
- "recent": "Dernier résultat",
+ "health": "État récent",
"timeline": "Historique d’exécution",
- "statusWithDuration": "{{status}} · {{duration}}"
+ "summary": "Exécutions : {{total}} · Problèmes : {{issues}}",
+ "issueCount": "Problèmes : {{count}}",
+ "noRecentIssues": "Aucun problème récent",
+ "lastRan": "Dernière exécution : {{time}}",
+ "lastRanWithDuration": "Dernière exécution : {{time}} · {{duration}}",
+ "runMetaWithDuration": "{{time}} · {{duration}}",
+ "noError": "Aucune erreur enregistrée"
},
"message": {
"showMore": "Afficher le message complet",
diff --git a/webui/src/i18n/locales/id/common.json b/webui/src/i18n/locales/id/common.json
index 588bf3a1..0adce308 100644
--- a/webui/src/i18n/locales/id/common.json
+++ b/webui/src/i18n/locales/id/common.json
@@ -577,12 +577,18 @@
"unknown": "tidak dikenal"
},
"history": {
- "ok": "Selesai",
+ "ok": "Tanpa error",
"error": "Error",
"skipped": "Dilewati",
- "recent": "Hasil terakhir",
+ "health": "Status terbaru",
"timeline": "Riwayat eksekusi",
- "statusWithDuration": "{{status}} · {{duration}}"
+ "summary": "Eksekusi: {{total}} · Masalah: {{issues}}",
+ "issueCount": "Masalah: {{count}}",
+ "noRecentIssues": "Tidak ada masalah terbaru",
+ "lastRan": "Terakhir berjalan {{time}}",
+ "lastRanWithDuration": "Terakhir berjalan {{time}} · {{duration}}",
+ "runMetaWithDuration": "{{time}} · {{duration}}",
+ "noError": "Tidak ada error tercatat"
},
"message": {
"showMore": "Tampilkan pesan lengkap",
diff --git a/webui/src/i18n/locales/ja/common.json b/webui/src/i18n/locales/ja/common.json
index 0c0bb222..dc9637e3 100644
--- a/webui/src/i18n/locales/ja/common.json
+++ b/webui/src/i18n/locales/ja/common.json
@@ -577,12 +577,18 @@
"unknown": "不明"
},
"history": {
- "ok": "完了",
+ "ok": "正常",
"error": "エラー",
"skipped": "スキップ",
- "recent": "最新結果",
+ "health": "最近の状態",
"timeline": "実行履歴",
- "statusWithDuration": "{{status}} · {{duration}}"
+ "summary": "実行: {{total}} · 問題: {{issues}}",
+ "issueCount": "問題: {{count}}",
+ "noRecentIssues": "最近の問題なし",
+ "lastRan": "最終実行: {{time}}",
+ "lastRanWithDuration": "最終実行: {{time}} · {{duration}}",
+ "runMetaWithDuration": "{{time}} · {{duration}}",
+ "noError": "エラー記録なし"
},
"message": {
"showMore": "メッセージ全文を表示",
diff --git a/webui/src/i18n/locales/ko/common.json b/webui/src/i18n/locales/ko/common.json
index a7ac679a..2e3b2a80 100644
--- a/webui/src/i18n/locales/ko/common.json
+++ b/webui/src/i18n/locales/ko/common.json
@@ -577,12 +577,18 @@
"unknown": "알 수 없음"
},
"history": {
- "ok": "완료",
+ "ok": "정상",
"error": "오류",
"skipped": "건너뜀",
- "recent": "최근 결과",
+ "health": "최근 상태",
"timeline": "실행 기록",
- "statusWithDuration": "{{status}} · {{duration}}"
+ "summary": "실행: {{total}} · 문제: {{issues}}",
+ "issueCount": "문제: {{count}}",
+ "noRecentIssues": "최근 문제 없음",
+ "lastRan": "마지막 실행: {{time}}",
+ "lastRanWithDuration": "마지막 실행: {{time}} · {{duration}}",
+ "runMetaWithDuration": "{{time}} · {{duration}}",
+ "noError": "기록된 오류 없음"
},
"message": {
"showMore": "전체 메시지 보기",
diff --git a/webui/src/i18n/locales/vi/common.json b/webui/src/i18n/locales/vi/common.json
index be264693..4561507f 100644
--- a/webui/src/i18n/locales/vi/common.json
+++ b/webui/src/i18n/locales/vi/common.json
@@ -577,12 +577,18 @@
"unknown": "không xác định"
},
"history": {
- "ok": "Hoàn tất",
+ "ok": "Ổn",
"error": "Lỗi",
"skipped": "Đã bỏ qua",
- "recent": "Kết quả gần nhất",
+ "health": "Tình trạng gần đây",
"timeline": "Lịch sử chạy",
- "statusWithDuration": "{{status}} · {{duration}}"
+ "summary": "Lượt chạy: {{total}} · Vấn đề: {{issues}}",
+ "issueCount": "Vấn đề: {{count}}",
+ "noRecentIssues": "Không có vấn đề gần đây",
+ "lastRan": "Chạy lần cuối {{time}}",
+ "lastRanWithDuration": "Chạy lần cuối {{time}} · {{duration}}",
+ "runMetaWithDuration": "{{time}} · {{duration}}",
+ "noError": "Không ghi nhận lỗi"
},
"message": {
"showMore": "Hiển thị toàn bộ tin nhắn",
diff --git a/webui/src/i18n/locales/zh-CN/common.json b/webui/src/i18n/locales/zh-CN/common.json
index 36e80dba..5a9efd42 100644
--- a/webui/src/i18n/locales/zh-CN/common.json
+++ b/webui/src/i18n/locales/zh-CN/common.json
@@ -577,12 +577,18 @@
"unknown": "未知"
},
"history": {
- "ok": "完成",
+ "ok": "正常",
"error": "错误",
"skipped": "已跳过",
- "recent": "最近结果",
+ "health": "最近健康状态",
"timeline": "运行记录",
- "statusWithDuration": "{{status}} · {{duration}}"
+ "summary": "运行:{{total}} · 问题:{{issues}}",
+ "issueCount": "问题:{{count}}",
+ "noRecentIssues": "近期无问题",
+ "lastRan": "最后运行于 {{time}}",
+ "lastRanWithDuration": "最后运行于 {{time}} · {{duration}}",
+ "runMetaWithDuration": "{{time}} · {{duration}}",
+ "noError": "未记录错误"
},
"message": {
"showMore": "查看完整消息",
diff --git a/webui/src/i18n/locales/zh-TW/common.json b/webui/src/i18n/locales/zh-TW/common.json
index 6e5e7713..7d1f9a00 100644
--- a/webui/src/i18n/locales/zh-TW/common.json
+++ b/webui/src/i18n/locales/zh-TW/common.json
@@ -577,12 +577,18 @@
"unknown": "未知"
},
"history": {
- "ok": "完成",
+ "ok": "正常",
"error": "錯誤",
"skipped": "已略過",
- "recent": "最近結果",
+ "health": "最近健康狀態",
"timeline": "執行記錄",
- "statusWithDuration": "{{status}} · {{duration}}"
+ "summary": "執行:{{total}} · 問題:{{issues}}",
+ "issueCount": "問題:{{count}}",
+ "noRecentIssues": "近期無問題",
+ "lastRan": "最後執行於 {{time}}",
+ "lastRanWithDuration": "最後執行於 {{time}} · {{duration}}",
+ "runMetaWithDuration": "{{time}} · {{duration}}",
+ "noError": "未記錄錯誤"
},
"message": {
"showMore": "查看完整訊息",
diff --git a/webui/src/tests/app-layout.test.tsx b/webui/src/tests/app-layout.test.tsx
index 965d89ce..7f4cbc04 100644
--- a/webui/src/tests/app-layout.test.tsx
+++ b/webui/src/tests/app-layout.test.tsx
@@ -628,15 +628,21 @@ describe("App layout", () => {
expect(within(detailPanel).getByRole("button", { name: "Show less" })).toBeInTheDocument();
expect(message!).not.toHaveClass("line-clamp-6");
- expect(screen.queryByText(/oldest failure/)).not.toBeInTheDocument();
+ expect(within(detailPanel).getByText("Recent health")).toBeInTheDocument();
+ expect(within(detailPanel).getByText("Issues: 2")).toBeInTheDocument();
const historyToggle = within(detailPanel).getByRole("button", { name: /Run history/ });
+ const historySection = historyToggle.closest("section") as HTMLElement;
+ expect(historySection).not.toBeNull();
+ expect(within(historySection).queryByText(/oldest failure/)).not.toBeInTheDocument();
expect(historyToggle).toHaveAttribute("aria-expanded", "false");
+ expect(historyToggle).toHaveTextContent("Runs: 6 · Issues: 2");
fireEvent.click(historyToggle);
expect(historyToggle).toHaveAttribute("aria-expanded", "true");
- expect(screen.getAllByText(/oldest failure/)).toHaveLength(2);
+ expect(within(historySection).getAllByText(/oldest failure/)).toHaveLength(2);
+ expect(within(detailPanel).getAllByText("No error recorded").length).toBeGreaterThanOrEqual(1);
fireEvent.click(historyToggle);
expect(historyToggle).toHaveAttribute("aria-expanded", "false");
- expect(screen.queryByText(/oldest failure/)).not.toBeInTheDocument();
+ expect(within(historySection).queryByText(/oldest failure/)).not.toBeInTheDocument();
});
it("localizes the Automations surface", async () => {
@@ -697,8 +703,9 @@ describe("App layout", () => {
expect(screen.getAllByText("每日检查").length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText("检查仓库状态").length).toBeGreaterThanOrEqual(1);
expect(screen.getByText("每 1天")).toBeInTheDocument();
- expect(screen.getByText("最近结果")).toBeInTheDocument();
- expect(screen.getByText("完成 · 不到 1 秒")).toBeInTheDocument();
+ expect(screen.getByText("最近健康状态")).toBeInTheDocument();
+ expect(screen.getByText("近期无问题")).toBeInTheDocument();
+ expect(screen.getByText(/最后运行于/)).toBeInTheDocument();
expect(screen.queryByText("Workspace automations")).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "刷新" })).not.toBeInTheDocument();
expect(document.title).toBe("自动任务 · nanobot");
diff --git a/webui/src/tests/i18n.test.tsx b/webui/src/tests/i18n.test.tsx
index b41bd0ad..95fe29fa 100644
--- a/webui/src/tests/i18n.test.tsx
+++ b/webui/src/tests/i18n.test.tsx
@@ -56,7 +56,10 @@ const LOCALIZED_SETTINGS_COPY_KEYS = [
"settings.automations.labels.schedule",
"settings.automations.status.active",
"settings.automations.history.ok",
- "settings.automations.history.recent",
+ "settings.automations.history.health",
+ "settings.automations.history.summary",
+ "settings.automations.history.noRecentIssues",
+ "settings.automations.history.noError",
"settings.automations.duration.lessThanSecond",
"settings.automations.deleteTitle",
"settings.sections.interface",