fix(web): CronPage crash when rendering schedule object

The cron API returns schedule as {kind, expr, display} object but
CronPage.tsx rendered it directly as a React child, crashing with
'Objects are not valid as a React child'.

- Update CronJob interface in api.ts to match actual API response
- Use schedule_display (string) instead of schedule (object)
- Use state instead of status for job state
- Use last_error instead of error for error display
This commit is contained in:
jonny 2026-04-13 12:01:12 +02:00 committed by Teknium
parent 8dfee98d06
commit 70f490a12a
2 changed files with 19 additions and 12 deletions

View file

@ -222,12 +222,14 @@ export interface CronJob {
id: string;
name?: string;
prompt: string;
schedule: string;
status: "enabled" | "paused" | "error";
schedule: { kind: string; expr: string; display: string };
schedule_display: string;
enabled: boolean;
state: string;
deliver?: string;
last_run_at?: string | null;
next_run_at?: string | null;
error?: string | null;
last_error?: string | null;
}
export interface SkillInfo {

View file

@ -19,10 +19,14 @@ function formatTime(iso?: string | null): string {
const STATUS_VARIANT: Record<string, "success" | "warning" | "destructive"> = {
enabled: "success",
scheduled: "success",
paused: "warning",
error: "destructive",
exhausted: "destructive",
};
export default function CronPage() {
const [jobs, setJobs] = useState<CronJob[]>([]);
const [loading, setLoading] = useState(true);
@ -75,7 +79,8 @@ export default function CronPage() {
const handlePauseResume = async (job: CronJob) => {
try {
if (job.status === "paused") {
const isPaused = job.state === "paused";
if (isPaused) {
await api.resumeCronJob(job.id);
showToast(`Resumed "${job.name || job.prompt.slice(0, 30)}"`, "success");
} else {
@ -212,8 +217,8 @@ export default function CronPage() {
<span className="font-medium text-sm truncate">
{job.name || job.prompt.slice(0, 60) + (job.prompt.length > 60 ? "..." : "")}
</span>
<Badge variant={STATUS_VARIANT[job.status] ?? "secondary"}>
{job.status}
<Badge variant={STATUS_VARIANT[job.state] ?? "secondary"}>
{job.state}
</Badge>
{job.deliver && job.deliver !== "local" && (
<Badge variant="outline">{job.deliver}</Badge>
@ -225,12 +230,12 @@ export default function CronPage() {
</p>
)}
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span className="font-mono">{job.schedule}</span>
<span className="font-mono">{job.schedule_display}</span>
<span>Last: {formatTime(job.last_run_at)}</span>
<span>Next: {formatTime(job.next_run_at)}</span>
</div>
{job.error && (
<p className="text-xs text-destructive mt-1">{job.error}</p>
{job.last_error && (
<p className="text-xs text-destructive mt-1">{job.last_error}</p>
)}
</div>
@ -239,11 +244,11 @@ export default function CronPage() {
<Button
variant="ghost"
size="icon"
title={job.status === "paused" ? "Resume" : "Pause"}
aria-label={job.status === "paused" ? "Resume job" : "Pause job"}
title={job.state === "paused" ? "Resume" : "Pause"}
aria-label={job.state === "paused" ? "Resume job" : "Pause job"}
onClick={() => handlePauseResume(job)}
>
{job.status === "paused" ? (
{job.state === "paused" ? (
<Play className="h-4 w-4 text-success" />
) : (
<Pause className="h-4 w-4 text-warning" />