diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index 6568e979bc0..2b571b62771 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -553,13 +553,14 @@ export interface ModelsAnalyticsResponse { export interface CronJob { id: string; - name?: string; - prompt: string; - schedule: { kind: string; expr: string; display: string }; - schedule_display: string; + name?: string | null; + prompt?: string | null; + script?: string | null; + schedule?: { kind?: string; expr?: string; display?: string }; + schedule_display?: string | null; enabled: boolean; - state: string; - deliver?: string; + state?: string | null; + deliver?: string | null; last_run_at?: string | null; next_run_at?: string | null; last_error?: string | null; diff --git a/web/src/pages/CronPage.tsx b/web/src/pages/CronPage.tsx index 90cc25abe0b..7234007b10a 100644 --- a/web/src/pages/CronPage.tsx +++ b/web/src/pages/CronPage.tsx @@ -23,6 +23,50 @@ function formatTime(iso?: string | null): string { return d.toLocaleString(); } +function asText(value: unknown): string { + return typeof value === "string" ? value : ""; +} + +function truncateText(value: string, maxLength: number): string { + return value.length > maxLength + ? value.slice(0, maxLength) + "..." + : value; +} + +function getJobPrompt(job: CronJob): string { + return asText(job.prompt); +} + +function getJobName(job: CronJob): string { + return asText(job.name).trim(); +} + +function getJobTitle(job: CronJob): string { + const name = getJobName(job); + if (name) return name; + + const prompt = getJobPrompt(job); + if (prompt) return truncateText(prompt, 60); + + const script = asText(job.script); + if (script) return truncateText(script, 60); + + return job.id || "Cron job"; +} + +function getJobScheduleDisplay(job: CronJob): string { + return ( + asText(job.schedule_display) || + asText(job.schedule?.display) || + asText(job.schedule?.expr) || + "—" + ); +} + +function getJobState(job: CronJob): string { + return asText(job.state) || (job.enabled === false ? "disabled" : "scheduled"); +} + const STATUS_TONE: Record = { enabled: "success", scheduled: "success", @@ -88,13 +132,13 @@ export default function CronPage() { if (isPaused) { await api.resumeCronJob(job.id); showToast( - `${t.cron.resume}: "${job.name || job.prompt.slice(0, 30)}"`, + `${t.cron.resume}: "${truncateText(getJobTitle(job), 30)}"`, "success", ); } else { await api.pauseCronJob(job.id); showToast( - `${t.cron.pause}: "${job.name || job.prompt.slice(0, 30)}"`, + `${t.cron.pause}: "${truncateText(getJobTitle(job), 30)}"`, "success", ); } @@ -108,7 +152,7 @@ export default function CronPage() { try { await api.triggerCronJob(job.id); showToast( - `${t.cron.triggerNow}: "${job.name || job.prompt.slice(0, 30)}"`, + `${t.cron.triggerNow}: "${truncateText(getJobTitle(job), 30)}"`, "success", ); loadJobs(); @@ -124,7 +168,7 @@ export default function CronPage() { try { await api.deleteCronJob(id); showToast( - `${t.common.delete}: "${job?.name || (job?.prompt ?? "").slice(0, 30) || id}"`, + `${t.common.delete}: "${job ? truncateText(getJobTitle(job), 30) : id}"`, "success", ); loadJobs(); @@ -161,7 +205,9 @@ export default function CronPage() { title={t.cron.confirmDeleteTitle} description={ pendingJob - ? `"${pendingJob.name || pendingJob.prompt.slice(0, 40)}" — ${t.cron.confirmDeleteMessage}` + ? `"${truncateText(getJobTitle(pendingJob), 40)}" — ${ + t.cron.confirmDeleteMessage + }` : t.cron.confirmDeleteMessage } loading={jobDelete.isDeleting} @@ -265,85 +311,90 @@ export default function CronPage() { )} - {jobs.map((job) => ( - - -
-
- - {job.name || - job.prompt.slice(0, 60) + - (job.prompt.length > 60 ? "..." : "")} - - - {job.state} - - {job.deliver && job.deliver !== "local" && ( - {job.deliver} + {jobs.map((job) => { + const state = getJobState(job); + const promptText = getJobPrompt(job); + const title = getJobTitle(job); + const hasName = Boolean(getJobName(job)); + const deliver = asText(job.deliver); + + return ( + + +
+
+ + {title} + + + {state} + + {deliver && deliver !== "local" && ( + {deliver} + )} +
+ {hasName && promptText && ( +

+ {truncateText(promptText, 100)} +

+ )} +
+ {getJobScheduleDisplay(job)} + + {t.cron.last}: {formatTime(job.last_run_at)} + + + {t.cron.next}: {formatTime(job.next_run_at)} + +
+ {job.last_error && ( +

+ {job.last_error} +

)}
- {job.name && ( -

- {job.prompt.slice(0, 100)} - {job.prompt.length > 100 ? "..." : ""} -

- )} -
- {job.schedule_display} - - {t.cron.last}: {formatTime(job.last_run_at)} - - - {t.cron.next}: {formatTime(job.next_run_at)} - + +
+ + + + +
- {job.last_error && ( -

- {job.last_error} -

- )} -
- -
- - - - - -
-
-
- ))} + + + ); + })}