Skip to content

Commit ee7fbdf

Browse files
committed
fix(control-ui): exclude disabled cron jobs from failed count
Overview surfaces counted any job whose lastRunStatus was "error" as a current failure, so an intentionally disabled job that previously failed kept inflating the top-level "failed cron" badge and attention callout. Add a shared isCronJobActiveFailure predicate that gates the error status on enabled, matching the adjacent overdue filter, and use it in both the overview card and the attention items list. Historical status stays visible in detail views via resolveCronJobLastRunStatus.
1 parent cb301cd commit ee7fbdf

4 files changed

Lines changed: 52 additions & 6 deletions

File tree

ui/src/ui/app-settings.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ import {
6464
stopWorkboardLifecycleRefresh,
6565
stopWorkboardPolling,
6666
} from "./controllers/workboard.ts";
67-
import { resolveCronJobLastRunStatus } from "./cron-status.ts";
67+
import { isCronJobActiveFailure } from "./cron-status.ts";
6868
import { syncCustomThemeStyleTag } from "./custom-theme.ts";
6969
import { isMonitoredAuthProvider } from "./model-auth-helpers.ts";
7070
import {
@@ -941,7 +941,7 @@ function buildAttentionItems(host: SettingsAppHost) {
941941
}
942942

943943
const cronJobs = host.cronJobs ?? [];
944-
const failedCron = cronJobs.filter((j) => resolveCronJobLastRunStatus(j) === "error");
944+
const failedCron = cronJobs.filter(isCronJobActiveFailure);
945945
if (failedCron.length > 0) {
946946
items.push({
947947
severity: "error",

ui/src/ui/cron-status.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Control UI tests cover cron status derivation behavior.
2+
import { describe, expect, it } from "vitest";
3+
import { isCronJobActiveFailure, resolveCronJobLastRunStatus } from "./cron-status.ts";
4+
import type { CronJob } from "./types.ts";
5+
6+
function job(overrides: Partial<CronJob> = {}): CronJob {
7+
return {
8+
id: "job",
9+
name: "Job",
10+
enabled: true,
11+
createdAtMs: 0,
12+
updatedAtMs: 0,
13+
schedule: { kind: "every", everyMs: 60_000 },
14+
sessionTarget: "main",
15+
wakeMode: "next-heartbeat",
16+
payload: { kind: "systemEvent", text: "test" },
17+
...overrides,
18+
};
19+
}
20+
21+
describe("isCronJobActiveFailure", () => {
22+
it("counts an enabled job whose last run errored", () => {
23+
expect(isCronJobActiveFailure(job({ state: { lastRunStatus: "error" } }))).toBe(true);
24+
});
25+
26+
it("ignores a disabled job that retains historical error state", () => {
27+
const disabled = job({
28+
enabled: false,
29+
state: { lastRunStatus: "error", consecutiveErrors: 6, nextRunAtMs: undefined },
30+
});
31+
// Historical status is still preserved for detail views.
32+
expect(resolveCronJobLastRunStatus(disabled)).toBe("error");
33+
expect(isCronJobActiveFailure(disabled)).toBe(false);
34+
});
35+
36+
it("does not count enabled jobs whose last run succeeded or is unknown", () => {
37+
expect(isCronJobActiveFailure(job({ state: { lastRunStatus: "ok" } }))).toBe(false);
38+
expect(isCronJobActiveFailure(job())).toBe(false);
39+
});
40+
});

ui/src/ui/cron-status.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,11 @@ export type CronJobLastRunStatus = CronRunStatus | "unknown";
66
export function resolveCronJobLastRunStatus(job: CronJob): CronJobLastRunStatus {
77
return job.state?.lastRunStatus ?? job.state?.lastStatus ?? "unknown";
88
}
9+
10+
// Overview "failed cron" surfaces track current actionability, so a failure only
11+
// counts while the job is still enabled. Disabled jobs keep their historical
12+
// `lastRunStatus: "error"` for detail views, but a retired job must not be
13+
// reported as an active operational problem.
14+
export function isCronJobActiveFailure(job: CronJob): boolean {
15+
return job.enabled && resolveCronJobLastRunStatus(job) === "error";
16+
}

ui/src/ui/views/overview-cards.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { asDateTimestampMs } from "@openclaw/normalization-core/number-coercion"
33
import { html, nothing, type TemplateResult } from "lit";
44
import { unsafeHTML } from "lit/directives/unsafe-html.js";
55
import { t } from "../../i18n/index.ts";
6-
import { resolveCronJobLastRunStatus } from "../cron-status.ts";
6+
import { isCronJobActiveFailure } from "../cron-status.ts";
77
import { formatCost, formatTokens, formatRelativeTimestamp } from "../format.ts";
88
import { isMonitoredAuthProvider } from "../model-auth-helpers.ts";
99
import { formatNextRun } from "../presenter.ts";
@@ -134,9 +134,7 @@ export function renderOverviewCards(props: OverviewCardsProps) {
134134
const cronEnabled = props.cronStatus?.enabled ?? null;
135135
const cronNext = props.cronStatus?.nextWakeAtMs ?? null;
136136
const cronJobCount = props.cronJobs.length;
137-
const failedCronCount = props.cronJobs.filter(
138-
(j) => resolveCronJobLastRunStatus(j) === "error",
139-
).length;
137+
const failedCronCount = props.cronJobs.filter(isCronJobActiveFailure).length;
140138
const authLoading = props.modelAuthStatus === null;
141139
const authProviders = props.modelAuthStatus?.providers ?? [];
142140
const monitoredProviders = authProviders.filter(isMonitoredAuthProvider);

0 commit comments

Comments
 (0)