mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 02:06:43 +00:00
fix(control-ui): exclude disabled cron jobs from failed count (#95723)
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.
This commit is contained in:
@@ -64,7 +64,7 @@ import {
|
||||
stopWorkboardLifecycleRefresh,
|
||||
stopWorkboardPolling,
|
||||
} from "./controllers/workboard.ts";
|
||||
import { resolveCronJobLastRunStatus } from "./cron-status.ts";
|
||||
import { isCronJobActiveFailure } from "./cron-status.ts";
|
||||
import { syncCustomThemeStyleTag } from "./custom-theme.ts";
|
||||
import { isMonitoredAuthProvider } from "./model-auth-helpers.ts";
|
||||
import {
|
||||
@@ -941,7 +941,7 @@ function buildAttentionItems(host: SettingsAppHost) {
|
||||
}
|
||||
|
||||
const cronJobs = host.cronJobs ?? [];
|
||||
const failedCron = cronJobs.filter((j) => resolveCronJobLastRunStatus(j) === "error");
|
||||
const failedCron = cronJobs.filter(isCronJobActiveFailure);
|
||||
if (failedCron.length > 0) {
|
||||
items.push({
|
||||
severity: "error",
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// Control UI tests cover cron status derivation behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isCronJobActiveFailure, resolveCronJobLastRunStatus } from "./cron-status.ts";
|
||||
import type { CronJob } from "./types.ts";
|
||||
|
||||
function job(overrides: Partial<CronJob> = {}): CronJob {
|
||||
return {
|
||||
id: "job",
|
||||
name: "Job",
|
||||
enabled: true,
|
||||
createdAtMs: 0,
|
||||
updatedAtMs: 0,
|
||||
schedule: { kind: "every", everyMs: 60_000 },
|
||||
sessionTarget: "main",
|
||||
wakeMode: "next-heartbeat",
|
||||
payload: { kind: "systemEvent", text: "test" },
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("isCronJobActiveFailure", () => {
|
||||
it("counts an enabled job whose last run errored", () => {
|
||||
expect(isCronJobActiveFailure(job({ state: { lastRunStatus: "error" } }))).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores a disabled job that retains historical error state", () => {
|
||||
const disabled = job({
|
||||
enabled: false,
|
||||
state: { lastRunStatus: "error", consecutiveErrors: 6, nextRunAtMs: undefined },
|
||||
});
|
||||
// Historical status is still preserved for detail views.
|
||||
expect(resolveCronJobLastRunStatus(disabled)).toBe("error");
|
||||
expect(isCronJobActiveFailure(disabled)).toBe(false);
|
||||
});
|
||||
|
||||
it("does not count enabled jobs whose last run succeeded or is unknown", () => {
|
||||
expect(isCronJobActiveFailure(job({ state: { lastRunStatus: "ok" } }))).toBe(false);
|
||||
expect(isCronJobActiveFailure(job())).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -6,3 +6,11 @@ export type CronJobLastRunStatus = CronRunStatus | "unknown";
|
||||
export function resolveCronJobLastRunStatus(job: CronJob): CronJobLastRunStatus {
|
||||
return job.state?.lastRunStatus ?? job.state?.lastStatus ?? "unknown";
|
||||
}
|
||||
|
||||
// Overview "failed cron" surfaces track current actionability, so a failure only
|
||||
// counts while the job is still enabled. Disabled jobs keep their historical
|
||||
// `lastRunStatus: "error"` for detail views, but a retired job must not be
|
||||
// reported as an active operational problem.
|
||||
export function isCronJobActiveFailure(job: CronJob): boolean {
|
||||
return job.enabled && resolveCronJobLastRunStatus(job) === "error";
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { asDateTimestampMs } from "@openclaw/normalization-core/number-coercion"
|
||||
import { html, nothing, type TemplateResult } from "lit";
|
||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||
import { t } from "../../i18n/index.ts";
|
||||
import { resolveCronJobLastRunStatus } from "../cron-status.ts";
|
||||
import { isCronJobActiveFailure } from "../cron-status.ts";
|
||||
import { formatCost, formatTokens, formatRelativeTimestamp } from "../format.ts";
|
||||
import { isMonitoredAuthProvider } from "../model-auth-helpers.ts";
|
||||
import { formatNextRun } from "../presenter.ts";
|
||||
@@ -134,9 +134,7 @@ export function renderOverviewCards(props: OverviewCardsProps) {
|
||||
const cronEnabled = props.cronStatus?.enabled ?? null;
|
||||
const cronNext = props.cronStatus?.nextWakeAtMs ?? null;
|
||||
const cronJobCount = props.cronJobs.length;
|
||||
const failedCronCount = props.cronJobs.filter(
|
||||
(j) => resolveCronJobLastRunStatus(j) === "error",
|
||||
).length;
|
||||
const failedCronCount = props.cronJobs.filter(isCronJobActiveFailure).length;
|
||||
const authLoading = props.modelAuthStatus === null;
|
||||
const authProviders = props.modelAuthStatus?.providers ?? [];
|
||||
const monitoredProviders = authProviders.filter(isMonitoredAuthProvider);
|
||||
|
||||
Reference in New Issue
Block a user