fix(webui): localize automation runtime labels
This commit is contained in:
@@ -872,11 +872,27 @@ async def test_webui_automations_route_lists_all_jobs_and_allows_user_actions(
|
||||
by_id = {job["id"]: job for job in disabled.json()["jobs"]}
|
||||
assert by_id[user_job.id]["enabled"] is False
|
||||
|
||||
disabled_run = await _http_get(
|
||||
f"http://127.0.0.1:29932/api/webui/automations/run?id={user_job.id}",
|
||||
headers=auth,
|
||||
)
|
||||
assert disabled_run.status_code == 409
|
||||
|
||||
protected_delete = await _http_get(
|
||||
"http://127.0.0.1:29932/api/webui/automations/delete?id=heartbeat",
|
||||
headers=auth,
|
||||
)
|
||||
assert protected_delete.status_code == 403
|
||||
protected_disable = await _http_get(
|
||||
"http://127.0.0.1:29932/api/webui/automations/disable?id=heartbeat",
|
||||
headers=auth,
|
||||
)
|
||||
assert protected_disable.status_code == 403
|
||||
protected_run = await _http_get(
|
||||
"http://127.0.0.1:29932/api/webui/automations/run?id=heartbeat",
|
||||
headers=auth,
|
||||
)
|
||||
assert protected_run.status_code == 403
|
||||
|
||||
enabled = await _http_get(
|
||||
f"http://127.0.0.1:29932/api/webui/automations/enable?id={user_job.id}",
|
||||
|
||||
@@ -3590,7 +3590,7 @@ function AutomationRow({
|
||||
)}
|
||||
title={record.error || fmtDateTime(record.run_at_ms, locale)}
|
||||
>
|
||||
{record.status} · {formatAutomationRunDuration(record.duration_ms)}
|
||||
{automationRunStatusLabel(record.status, tx)} · {formatAutomationRunDuration(record.duration_ms, locale, tx)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
@@ -3767,7 +3767,7 @@ function formatAutomationSchedule(
|
||||
}
|
||||
if (job.schedule.kind === "every" && job.schedule.every_ms) {
|
||||
return tx("settings.automations.schedule.every", "Every {{duration}}", {
|
||||
duration: formatAutomationInterval(job.schedule.every_ms),
|
||||
duration: formatAutomationInterval(job.schedule.every_ms, locale),
|
||||
});
|
||||
}
|
||||
if (job.schedule.kind === "cron" && job.schedule.expr) {
|
||||
@@ -3797,27 +3797,59 @@ function formatAutomationLast(
|
||||
tx: (key: string, fallback: string, values?: Record<string, unknown>) => string,
|
||||
): string {
|
||||
if (!job.state.last_run_at_ms) return tx("settings.automations.last.never", "Never");
|
||||
const status = job.state.last_status || tx("settings.automations.last.unknown", "unknown");
|
||||
const status = automationRunStatusLabel(job.state.last_status, tx);
|
||||
return `${fmtDateTime(job.state.last_run_at_ms, locale)} · ${status}`;
|
||||
}
|
||||
|
||||
function formatAutomationInterval(ms: number): string {
|
||||
const units: Array<[string, number]> = [
|
||||
["d", 86_400_000],
|
||||
["h", 3_600_000],
|
||||
["m", 60_000],
|
||||
["s", 1000],
|
||||
];
|
||||
for (const [suffix, size] of units) {
|
||||
if (ms >= size && ms % size === 0) return `${ms / size}${suffix}`;
|
||||
}
|
||||
return `${Math.round(ms / 1000)}s`;
|
||||
function automationRunStatusLabel(
|
||||
status: string | null | undefined,
|
||||
tx: (key: string, fallback: string, values?: Record<string, unknown>) => string,
|
||||
): string {
|
||||
if (status === "ok") return tx("settings.automations.history.ok", "Completed");
|
||||
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");
|
||||
}
|
||||
|
||||
function formatAutomationRunDuration(ms: number | undefined): string {
|
||||
if (!ms || ms < 1000) return "<1s";
|
||||
if (ms < 60_000) return `${Math.round(ms / 1000)}s`;
|
||||
return `${Math.round(ms / 60_000)}m`;
|
||||
function formatAutomationUnit(
|
||||
value: number,
|
||||
unit: Intl.NumberFormatOptions["unit"],
|
||||
locale: string,
|
||||
maximumFractionDigits = 0,
|
||||
): string {
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: "unit",
|
||||
unit,
|
||||
unitDisplay: "long",
|
||||
maximumFractionDigits,
|
||||
}).format(value);
|
||||
}
|
||||
|
||||
function formatAutomationInterval(ms: number, locale: string): string {
|
||||
const units: Array<[Intl.NumberFormatOptions["unit"], number]> = [
|
||||
["day", 86_400_000],
|
||||
["hour", 3_600_000],
|
||||
["minute", 60_000],
|
||||
["second", 1000],
|
||||
];
|
||||
for (const [unit, size] of units) {
|
||||
if (ms >= size && ms % size === 0) return formatAutomationUnit(ms / size, unit, locale);
|
||||
}
|
||||
const fallbackUnit = ms < 60_000 ? "second" : "minute";
|
||||
const fallbackSize = fallbackUnit === "second" ? 1000 : 60_000;
|
||||
return formatAutomationUnit(ms / fallbackSize, fallbackUnit, locale, 1);
|
||||
}
|
||||
|
||||
function formatAutomationRunDuration(
|
||||
ms: number | undefined,
|
||||
locale: string,
|
||||
tx: (key: string, fallback: string, values?: Record<string, unknown>) => string,
|
||||
): string {
|
||||
if (!ms || ms < 1000) {
|
||||
return tx("settings.automations.duration.lessThanSecond", "< 1 second");
|
||||
}
|
||||
if (ms < 60_000) return formatAutomationUnit(ms / 1000, "second", locale, 1);
|
||||
return formatAutomationUnit(ms / 60_000, "minute", locale, 1);
|
||||
}
|
||||
|
||||
function AppsCatalogSettings({
|
||||
|
||||
@@ -529,6 +529,14 @@
|
||||
"last": {
|
||||
"never": "Never",
|
||||
"unknown": "unknown"
|
||||
},
|
||||
"history": {
|
||||
"ok": "Completed",
|
||||
"error": "Error",
|
||||
"skipped": "Skipped"
|
||||
},
|
||||
"duration": {
|
||||
"lessThanSecond": "< 1 second"
|
||||
}
|
||||
},
|
||||
"oauth": {
|
||||
|
||||
@@ -529,6 +529,14 @@
|
||||
"last": {
|
||||
"never": "Nunca",
|
||||
"unknown": "desconocido"
|
||||
},
|
||||
"history": {
|
||||
"ok": "Completada",
|
||||
"error": "Error",
|
||||
"skipped": "Omitida"
|
||||
},
|
||||
"duration": {
|
||||
"lessThanSecond": "menos de 1 segundo"
|
||||
}
|
||||
},
|
||||
"oauth": {
|
||||
|
||||
@@ -529,6 +529,14 @@
|
||||
"last": {
|
||||
"never": "Jamais",
|
||||
"unknown": "inconnu"
|
||||
},
|
||||
"history": {
|
||||
"ok": "Terminée",
|
||||
"error": "Erreur",
|
||||
"skipped": "Ignorée"
|
||||
},
|
||||
"duration": {
|
||||
"lessThanSecond": "moins de 1 seconde"
|
||||
}
|
||||
},
|
||||
"oauth": {
|
||||
|
||||
@@ -529,6 +529,14 @@
|
||||
"last": {
|
||||
"never": "Belum pernah",
|
||||
"unknown": "tidak dikenal"
|
||||
},
|
||||
"history": {
|
||||
"ok": "Selesai",
|
||||
"error": "Error",
|
||||
"skipped": "Dilewati"
|
||||
},
|
||||
"duration": {
|
||||
"lessThanSecond": "kurang dari 1 detik"
|
||||
}
|
||||
},
|
||||
"oauth": {
|
||||
|
||||
@@ -529,6 +529,14 @@
|
||||
"last": {
|
||||
"never": "未実行",
|
||||
"unknown": "不明"
|
||||
},
|
||||
"history": {
|
||||
"ok": "完了",
|
||||
"error": "エラー",
|
||||
"skipped": "スキップ"
|
||||
},
|
||||
"duration": {
|
||||
"lessThanSecond": "1 秒未満"
|
||||
}
|
||||
},
|
||||
"oauth": {
|
||||
|
||||
@@ -529,6 +529,14 @@
|
||||
"last": {
|
||||
"never": "실행된 적 없음",
|
||||
"unknown": "알 수 없음"
|
||||
},
|
||||
"history": {
|
||||
"ok": "완료",
|
||||
"error": "오류",
|
||||
"skipped": "건너뜀"
|
||||
},
|
||||
"duration": {
|
||||
"lessThanSecond": "1초 미만"
|
||||
}
|
||||
},
|
||||
"oauth": {
|
||||
|
||||
@@ -529,6 +529,14 @@
|
||||
"last": {
|
||||
"never": "Chưa từng chạy",
|
||||
"unknown": "không xác định"
|
||||
},
|
||||
"history": {
|
||||
"ok": "Hoàn tất",
|
||||
"error": "Lỗi",
|
||||
"skipped": "Đã bỏ qua"
|
||||
},
|
||||
"duration": {
|
||||
"lessThanSecond": "dưới 1 giây"
|
||||
}
|
||||
},
|
||||
"oauth": {
|
||||
|
||||
@@ -529,6 +529,14 @@
|
||||
"last": {
|
||||
"never": "从未运行",
|
||||
"unknown": "未知"
|
||||
},
|
||||
"history": {
|
||||
"ok": "完成",
|
||||
"error": "错误",
|
||||
"skipped": "已跳过"
|
||||
},
|
||||
"duration": {
|
||||
"lessThanSecond": "不到 1 秒"
|
||||
}
|
||||
},
|
||||
"oauth": {
|
||||
|
||||
@@ -529,6 +529,14 @@
|
||||
"last": {
|
||||
"never": "從未執行",
|
||||
"unknown": "未知"
|
||||
},
|
||||
"history": {
|
||||
"ok": "完成",
|
||||
"error": "錯誤",
|
||||
"skipped": "已略過"
|
||||
},
|
||||
"duration": {
|
||||
"lessThanSecond": "不到 1 秒"
|
||||
}
|
||||
},
|
||||
"oauth": {
|
||||
|
||||
@@ -405,7 +405,43 @@ describe("App layout", () => {
|
||||
await i18n.changeLanguage("zh-CN");
|
||||
mockFetchRoutes({
|
||||
"/api/settings": baseSettingsPayload(),
|
||||
"/api/webui/automations": { jobs: [] },
|
||||
"/api/webui/automations": {
|
||||
jobs: [
|
||||
{
|
||||
id: "job-zh",
|
||||
name: "每日检查",
|
||||
enabled: true,
|
||||
protected: false,
|
||||
delete_after_run: false,
|
||||
schedule: { kind: "every", every_ms: 86_400_000 },
|
||||
payload: {
|
||||
message: "检查仓库状态",
|
||||
kind: "agent_turn",
|
||||
session_key: "websocket:chat-a",
|
||||
},
|
||||
state: {
|
||||
next_run_at_ms: Date.UTC(2026, 3, 17, 10, 0, 0),
|
||||
last_run_at_ms: Date.UTC(2026, 3, 16, 10, 0, 0),
|
||||
last_status: "ok",
|
||||
pending: false,
|
||||
run_history: [
|
||||
{
|
||||
run_at_ms: Date.UTC(2026, 3, 16, 10, 0, 0),
|
||||
status: "ok",
|
||||
duration_ms: 500,
|
||||
},
|
||||
],
|
||||
},
|
||||
origin: {
|
||||
session_key: "websocket:chat-a",
|
||||
channel: "websocket",
|
||||
chat_id: "chat-a",
|
||||
title: "发布准备",
|
||||
preview: "检查发布阻塞项",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
render(<App />);
|
||||
@@ -419,7 +455,10 @@ describe("App layout", () => {
|
||||
expect(screen.getByText("统一查看 cron 提醒、周期性 agent 任务、一次性自动任务和受保护的系统自动任务。")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "刷新" })).toBeInTheDocument();
|
||||
expect(screen.getByText("任务队列")).toBeInTheDocument();
|
||||
expect(screen.getByText("暂无自动任务。")).toBeInTheDocument();
|
||||
expect(screen.getByText("每日检查")).toBeInTheDocument();
|
||||
expect(screen.getByText("检查仓库状态")).toBeInTheDocument();
|
||||
expect(screen.getByText("每 1天")).toBeInTheDocument();
|
||||
expect(screen.getByText("完成 · 不到 1 秒")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Workspace automations")).not.toBeInTheDocument();
|
||||
expect(document.title).toBe("自动任务 · nanobot");
|
||||
});
|
||||
|
||||
@@ -58,6 +58,8 @@ const LOCALIZED_SETTINGS_COPY_KEYS = [
|
||||
"settings.automations.systemTask",
|
||||
"settings.automations.labels.schedule",
|
||||
"settings.automations.status.active",
|
||||
"settings.automations.history.ok",
|
||||
"settings.automations.duration.lessThanSecond",
|
||||
"settings.automations.deleteTitle",
|
||||
"settings.sections.interface",
|
||||
"settings.sections.localPreferences",
|
||||
|
||||
Reference in New Issue
Block a user