fix(ui): stop flagging running automations as overdue and show their state (#123745)

* 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
This commit is contained in:
Peter Steinberger
2026-08-14 11:07:08 -07:00
committed by GitHub
parent 7fb47729c3
commit f9d9b9da2b
7 changed files with 58 additions and 3 deletions
+2 -1
View File
@@ -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,
);
@@ -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", () => {
+1
View File
@@ -5839,6 +5839,7 @@ export const en: TranslationMap = {
runStatusError: "Error",
runStatusSkipped: "Skipped",
runStatusUnknown: "Unknown",
runStatusRunning: "Running",
deliveryDelivered: "Delivered",
deliveryNotDelivered: "Not delivered",
deliveryUnknown: "Unknown",
+8
View File
@@ -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
@@ -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");
});
+10 -2
View File
@@ -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) {
</span>
<span class="cron-table__cell">${formatCronSchedule(job)}</span>
<span class="cron-table__cell">
${hasNextRun ? formatRelativeTimestamp(nextRunAtMs) : t("common.na")}
${isCronJobRunning(job)
? html`<span class="cron-table__running">${t("cron.runs.runStatusRunning")}</span>`
: hasNextRun
? formatRelativeTimestamp(nextRunAtMs)
: t("common.na")}
</span>
<span class="cron-table__cell cron-table__last">${renderLastRunCell(job)}</span>
<span
+4
View File
@@ -358,6 +358,10 @@
font-size: var(--control-ui-text-xs);
}
.cron-table__running {
color: var(--accent);
}
.cron-table__cell {
color: var(--text);
font-size: 12.5px;