Spaces:
Sleeping
Sleeping
| /** | |
| * CronManagerPanel — displays and manages scheduled cron jobs. | |
| * Uses real JSON API endpoint /api/cron/:sessionId | |
| */ | |
| import { useState, useEffect, useCallback } from "react"; | |
| import { Timer, RefreshCw, Clock } from "lucide-react"; | |
| interface CronJob { | |
| id: string; | |
| schedule: string; | |
| command: string; | |
| description: string; | |
| status: "active" | "paused" | "deleted"; | |
| createdAt: number; | |
| lastRun?: number; | |
| runCount: number; | |
| output: string; | |
| } | |
| interface CronManagerPanelProps { | |
| sessionId: number; | |
| onSlashCommand: (command: string, args?: string) => Promise<string>; | |
| } | |
| export function CronManagerPanel({ sessionId }: CronManagerPanelProps) { | |
| const [jobs, setJobs] = useState<CronJob[]>([]); | |
| const [loading, setLoading] = useState(false); | |
| const [expandedJob, setExpandedJob] = useState<string | null>(null); | |
| const refreshJobs = useCallback(async () => { | |
| setLoading(true); | |
| try { | |
| const res = await fetch(`/api/cron/${sessionId}`); | |
| if (res.ok) { | |
| const data = await res.json(); | |
| if (Array.isArray(data)) setJobs(data); | |
| } | |
| } catch { | |
| // ignore fetch errors | |
| } finally { | |
| setLoading(false); | |
| } | |
| }, [sessionId]); | |
| useEffect(() => { | |
| refreshJobs(); | |
| // Auto-refresh every 5 seconds | |
| const interval = setInterval(refreshJobs, 5000); | |
| return () => clearInterval(interval); | |
| }, [refreshJobs]); | |
| const statusBadge = (status: string) => { | |
| const colors: Record<string, string> = { | |
| active: "bg-green-500/10 text-green-400 border-green-500/20", | |
| paused: "bg-yellow-500/10 text-yellow-400 border-yellow-500/20", | |
| deleted: "bg-red-500/10 text-red-400 border-red-500/20", | |
| }; | |
| return ( | |
| <span className={`px-2 py-0.5 rounded-full text-xs border ${colors[status] || "bg-muted text-muted-foreground"}`}> | |
| {status} | |
| </span> | |
| ); | |
| }; | |
| return ( | |
| <div className="flex flex-col h-full"> | |
| <div className="flex items-center justify-between px-4 py-3 border-b border-border"> | |
| <h3 className="text-sm font-semibold flex items-center gap-2"> | |
| <Timer className="w-4 h-4" /> | |
| Cron Jobs ({jobs.length}) | |
| </h3> | |
| <button | |
| onClick={refreshJobs} | |
| disabled={loading} | |
| className="p-1.5 rounded hover:bg-accent transition-colors" | |
| > | |
| <RefreshCw className={`w-4 h-4 ${loading ? "animate-spin" : ""}`} /> | |
| </button> | |
| </div> | |
| <div className="flex-1 overflow-y-auto"> | |
| {jobs.length === 0 ? ( | |
| <div className="flex flex-col items-center justify-center h-full text-muted-foreground text-sm gap-2 p-4"> | |
| <Clock className="w-8 h-8 opacity-40" /> | |
| <p>No scheduled cron jobs</p> | |
| <p className="text-xs opacity-60">Jobs created via CronCreate tool will appear here</p> | |
| </div> | |
| ) : ( | |
| <div className="divide-y divide-border"> | |
| {jobs.map((job) => ( | |
| <div key={job.id} className="p-3"> | |
| <button | |
| onClick={() => setExpandedJob(expandedJob === job.id ? null : job.id)} | |
| className="w-full flex items-start justify-between gap-2 text-left" | |
| > | |
| <div className="flex-1 min-w-0"> | |
| <div className="flex items-center gap-2"> | |
| <p className="text-sm font-medium truncate">{job.description || job.command}</p> | |
| {statusBadge(job.status)} | |
| </div> | |
| <p className="text-xs text-muted-foreground mt-1 font-mono"> | |
| {job.schedule} | |
| </p> | |
| <p className="text-xs text-muted-foreground"> | |
| Runs: {job.runCount} · {job.lastRun ? `Last: ${new Date(job.lastRun).toLocaleTimeString()}` : "Not yet run"} | |
| </p> | |
| </div> | |
| </button> | |
| {expandedJob === job.id && ( | |
| <pre className="mt-2 p-2 rounded bg-muted text-xs font-mono whitespace-pre-wrap max-h-40 overflow-y-auto"> | |
| {job.output || "(no output yet)"} | |
| </pre> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |