From f9d9b9da2b9cb5c0ac562ad030ea342a73770091 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 14 Aug 2026 11:07:08 -0700 Subject: [PATCH] fix(ui): stop flagging running automations as overdue and show their state (#123745) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(ui): stop flagging running automations as overdue and show their state The gateway intentionally leaves nextRunAtMs past-due while a run executes (it advances only on the outcome), and agentTurn runs may take up to an hour — 12x the sidebar's 5-minute overdue grace. The overdue chip and the cron table therefore reported healthy in-flight jobs as 'overdue' / 'next run: X minutes ago'. runningAtMs is the recorded fact for an in-flight run (already public and used by the CLI's status), but no UI surface read it. Add the shared isCronJobRunning helper, exclude running jobs from the overdue filter, and render 'Running' in the next-run cell. * test(cron): move running-state row test beside the capped view suite --- ui/src/components/sidebar-attention-items.ts | 3 ++- ui/src/components/sidebar-attention.test.ts | 16 ++++++++++++++++ ui/src/i18n/locales/en.ts | 1 + ui/src/lib/cron-status.ts | 8 ++++++++ ui/src/pages/cron/view-running-state.test.ts | 17 +++++++++++++++++ ui/src/pages/cron/view.ts | 12 ++++++++++-- ui/src/styles/cron.css | 4 ++++ 7 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 ui/src/pages/cron/view-running-state.test.ts diff --git a/ui/src/components/sidebar-attention-items.ts b/ui/src/components/sidebar-attention-items.ts index 20617925446a..0fdd3d49a35b 100644 --- a/ui/src/components/sidebar-attention-items.ts +++ b/ui/src/components/sidebar-attention-items.ts @@ -5,7 +5,7 @@ import type { CronJob, ModelAuthStatusResult } from "../api/types.ts"; import type { NavigationRouteId } from "../app-navigation.ts"; import type { ExecApprovalRequest } from "../app/exec-approval.ts"; import { t } from "../i18n/index.ts"; -import { isCronJobActiveFailure } from "../lib/cron-status.ts"; +import { isCronJobActiveFailure, isCronJobRunning } from "../lib/cron-status.ts"; import { clampText } from "../lib/format.ts"; import { isMonitoredAuthProvider } from "../lib/model-auth.ts"; import type { IconName } from "./icons.ts"; @@ -89,6 +89,7 @@ export function buildSidebarAttentionItems(params: { const overdueCron = params.cronJobs.filter( (job) => job.enabled && + !isCronJobRunning(job) && job.state?.nextRunAtMs != null && params.now - job.state.nextRunAtMs > CRON_OVERDUE_GRACE_MS, ); diff --git a/ui/src/components/sidebar-attention.test.ts b/ui/src/components/sidebar-attention.test.ts index d257994e8974..e1cfc8834e86 100644 --- a/ui/src/components/sidebar-attention.test.ts +++ b/ui/src/components/sidebar-attention.test.ts @@ -153,6 +153,22 @@ describe("cron attention details", () => { expect(overdue?.detail).toBe("Nightly backup\nunnamed-id"); }); + + it("does not flag an actively running job as overdue", () => { + // The gateway leaves nextRunAtMs past-due during execution; runningAtMs is + // the recorded fact that a run is in flight (agentTurn runs may take up to + // an hour, far beyond the 5-minute overdue grace). + const running = cronJob("running-id"); + running.state = { lastRunStatus: "ok", nextRunAtMs: 1, runningAtMs: 2 }; + const stalled = cronJob("stalled-id"); + stalled.state = { lastRunStatus: "ok", nextRunAtMs: 2 }; + + const overdue = cronItems([running, stalled], 300_003).find( + (item) => item.kind === "cronOverdue", + ); + + expect(overdue?.detail).toBe("stalled-id"); + }); }); describe("pending approval attention", () => { diff --git a/ui/src/i18n/locales/en.ts b/ui/src/i18n/locales/en.ts index 8ebc6b0fb63f..9524c4855802 100644 --- a/ui/src/i18n/locales/en.ts +++ b/ui/src/i18n/locales/en.ts @@ -5839,6 +5839,7 @@ export const en: TranslationMap = { runStatusError: "Error", runStatusSkipped: "Skipped", runStatusUnknown: "Unknown", + runStatusRunning: "Running", deliveryDelivered: "Delivered", deliveryNotDelivered: "Not delivered", deliveryUnknown: "Unknown", diff --git a/ui/src/lib/cron-status.ts b/ui/src/lib/cron-status.ts index c5b857af74e9..9d7db298aaf3 100644 --- a/ui/src/lib/cron-status.ts +++ b/ui/src/lib/cron-status.ts @@ -7,6 +7,14 @@ export function resolveCronJobLastRunStatus(job: CronJob): CronJobLastRunStatus return job.state?.lastRunStatus ?? job.state?.lastStatus ?? "unknown"; } +// The gateway intentionally leaves nextRunAtMs past-due while a run executes +// (it only advances on the outcome), so "overdue" surfaces must not flag a +// job that is actively running. runningAtMs is the recorded fact for that. +export function isCronJobRunning(job: CronJob): boolean { + const runningAtMs = job.state?.runningAtMs; + return typeof runningAtMs === "number" && Number.isFinite(runningAtMs); +} + // "Failed cron" surfaces (cron page, sidebar attention chips) track current // actionability, so a failure only counts while the job is still enabled. // Disabled jobs keep their historical `lastRunStatus: "error"` for detail diff --git a/ui/src/pages/cron/view-running-state.test.ts b/ui/src/pages/cron/view-running-state.test.ts new file mode 100644 index 000000000000..467fea2a735d --- /dev/null +++ b/ui/src/pages/cron/view-running-state.test.ts @@ -0,0 +1,17 @@ +// Running-state rendering for the Automations (cron) table. +// Lives beside view.test.ts, which sits at the max-lines cap. +import { expect, it } from "vitest"; +import { + createCronViewJob as createJob, + renderCronView as renderView, +} from "./view.test-support.ts"; + +it("shows Running instead of a past-due next-run time while a run executes", () => { + const running = createJob("job-running", { + state: { nextRunAtMs: Date.now() - 600_000, runningAtMs: Date.now() - 60_000 }, + }); + const container = renderView({ jobs: [running] }); + const row = container.querySelector(".cron-table__row"); + expect(row?.querySelector(".cron-table__running")?.textContent).toBe("Running"); + expect(row?.textContent).not.toContain("ago"); +}); diff --git a/ui/src/pages/cron/view.ts b/ui/src/pages/cron/view.ts index 3d0a8862c14b..61738ede3530 100644 --- a/ui/src/pages/cron/view.ts +++ b/ui/src/pages/cron/view.ts @@ -36,7 +36,11 @@ import { renderSettingsToggleRow, } from "../../components/settings-ui.ts"; import { t } from "../../i18n/index.ts"; -import { isCronJobActiveFailure, resolveCronJobLastRunStatus } from "../../lib/cron-status.ts"; +import { + isCronJobActiveFailure, + isCronJobRunning, + resolveCronJobLastRunStatus, +} from "../../lib/cron-status.ts"; import { parseCronEveryMs } from "../../lib/cron/decimal.ts"; import type { CronFieldErrors, @@ -755,7 +759,11 @@ function renderJobRow(job: CronJob, props: CronProps) { ${formatCronSchedule(job)} - ${hasNextRun ? formatRelativeTimestamp(nextRunAtMs) : t("common.na")} + ${isCronJobRunning(job) + ? html`${t("cron.runs.runStatusRunning")}` + : hasNextRun + ? formatRelativeTimestamp(nextRunAtMs) + : t("common.na")} ${renderLastRunCell(job)}