Compare commits

...

1 Commits

Author SHA1 Message Date
jonny
b6f882a0ed 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
2026-04-13 12:01:12 +02:00
2 changed files with 19 additions and 12 deletions

View File

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

View File

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