Web UI: add full cron edit parity, all-jobs run history, and compact filters (openclaw#24155) thanks @Takhoffman

Verified:
- pnpm install --frozen-lockfile
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: Takhoffman <781889+Takhoffman@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
Tak Hoffman
2026-02-22 23:05:42 -06:00
committed by GitHub
parent 610863e733
commit 77c3b142a9
23 changed files with 3769 additions and 344 deletions

View File

@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import {
validateCronAddParams,
validateCronListParams,
validateCronRemoveParams,
validateCronRunParams,
validateCronRunsParams,
@@ -40,6 +41,21 @@ describe("cron protocol validators", () => {
expect(validateCronRunParams({ jobId: "job-2", mode: "due" })).toBe(true);
});
it("accepts list paging/filter/sort params", () => {
expect(
validateCronListParams({
includeDisabled: true,
limit: 50,
offset: 0,
query: "daily",
enabled: "all",
sortBy: "nextRunAtMs",
sortDir: "asc",
}),
).toBe(true);
expect(validateCronListParams({ offset: -1 })).toBe(false);
});
it("enforces runs limit minimum for id and jobId selectors", () => {
expect(validateCronRunsParams({ id: "job-1", limit: 1 })).toBe(true);
expect(validateCronRunsParams({ jobId: "job-2", limit: 1 })).toBe(true);
@@ -53,4 +69,37 @@ describe("cron protocol validators", () => {
expect(validateCronRunsParams({ jobId: "..\\job-2" })).toBe(false);
expect(validateCronRunsParams({ jobId: "nested\\job-2" })).toBe(false);
});
it("accepts runs paging/filter/sort params", () => {
expect(
validateCronRunsParams({
id: "job-1",
limit: 50,
offset: 0,
status: "error",
query: "timeout",
sortDir: "desc",
}),
).toBe(true);
expect(validateCronRunsParams({ id: "job-1", offset: -1 })).toBe(false);
});
it("accepts all-scope runs with multi-select filters", () => {
expect(
validateCronRunsParams({
scope: "all",
limit: 25,
statuses: ["ok", "error"],
deliveryStatuses: ["delivered", "not-requested"],
query: "fail",
sortDir: "desc",
}),
).toBe(true);
expect(
validateCronRunsParams({
scope: "job",
statuses: [],
}),
).toBe(false);
});
});

View File

@@ -26,6 +26,28 @@ const CronRunStatusSchema = Type.Union([
Type.Literal("error"),
Type.Literal("skipped"),
]);
const CronSortDirSchema = Type.Union([Type.Literal("asc"), Type.Literal("desc")]);
const CronJobsEnabledFilterSchema = Type.Union([
Type.Literal("all"),
Type.Literal("enabled"),
Type.Literal("disabled"),
]);
const CronJobsSortBySchema = Type.Union([
Type.Literal("nextRunAtMs"),
Type.Literal("updatedAtMs"),
Type.Literal("name"),
]);
const CronRunsStatusFilterSchema = Type.Union([
Type.Literal("all"),
Type.Literal("ok"),
Type.Literal("error"),
Type.Literal("skipped"),
]);
const CronRunsStatusValueSchema = Type.Union([
Type.Literal("ok"),
Type.Literal("error"),
Type.Literal("skipped"),
]);
const CronDeliveryStatusSchema = Type.Union([
Type.Literal("delivered"),
Type.Literal("not-delivered"),
@@ -65,25 +87,6 @@ const CronRunLogJobIdSchema = Type.String({
pattern: "^[^/\\\\]+$",
});
function cronRunsIdOrJobIdParams(extraFields: Record<string, TSchema>) {
return Type.Union([
Type.Object(
{
id: CronRunLogJobIdSchema,
...extraFields,
},
{ additionalProperties: false },
),
Type.Object(
{
jobId: CronRunLogJobIdSchema,
...extraFields,
},
{ additionalProperties: false },
),
]);
}
export const CronScheduleSchema = Type.Union([
Type.Object(
{
@@ -223,6 +226,12 @@ export const CronJobSchema = Type.Object(
export const CronListParamsSchema = Type.Object(
{
includeDisabled: Type.Optional(Type.Boolean()),
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 200 })),
offset: Type.Optional(Type.Integer({ minimum: 0 })),
query: Type.Optional(Type.String()),
enabled: Type.Optional(CronJobsEnabledFilterSchema),
sortBy: Type.Optional(CronJobsSortBySchema),
sortDir: Type.Optional(CronSortDirSchema),
},
{ additionalProperties: false },
);
@@ -266,9 +275,24 @@ export const CronRunParamsSchema = cronIdOrJobIdParams({
mode: Type.Optional(Type.Union([Type.Literal("due"), Type.Literal("force")])),
});
export const CronRunsParamsSchema = cronRunsIdOrJobIdParams({
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 5000 })),
});
export const CronRunsParamsSchema = Type.Object(
{
scope: Type.Optional(Type.Union([Type.Literal("job"), Type.Literal("all")])),
id: Type.Optional(CronRunLogJobIdSchema),
jobId: Type.Optional(CronRunLogJobIdSchema),
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 200 })),
offset: Type.Optional(Type.Integer({ minimum: 0 })),
statuses: Type.Optional(Type.Array(CronRunsStatusValueSchema, { minItems: 1, maxItems: 3 })),
status: Type.Optional(CronRunsStatusFilterSchema),
deliveryStatuses: Type.Optional(
Type.Array(CronDeliveryStatusSchema, { minItems: 1, maxItems: 4 }),
),
deliveryStatus: Type.Optional(CronDeliveryStatusSchema),
query: Type.Optional(Type.String()),
sortDir: Type.Optional(CronSortDirSchema),
},
{ additionalProperties: false },
);
export const CronRunLogEntrySchema = Type.Object(
{
@@ -286,6 +310,21 @@ export const CronRunLogEntrySchema = Type.Object(
runAtMs: Type.Optional(Type.Integer({ minimum: 0 })),
durationMs: Type.Optional(Type.Integer({ minimum: 0 })),
nextRunAtMs: Type.Optional(Type.Integer({ minimum: 0 })),
model: Type.Optional(Type.String()),
provider: Type.Optional(Type.String()),
usage: Type.Optional(
Type.Object(
{
input_tokens: Type.Optional(Type.Number()),
output_tokens: Type.Optional(Type.Number()),
total_tokens: Type.Optional(Type.Number()),
cache_read_tokens: Type.Optional(Type.Number()),
cache_write_tokens: Type.Optional(Type.Number()),
},
{ additionalProperties: false },
),
),
jobName: Type.Optional(Type.String()),
},
{ additionalProperties: false },
);