feat: implement agent scheduled execution with cron and time picker
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m9s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m9s
- Add scheduledTime, scheduledDay, timezone fields to Agent schema - Create calculateNextRun() helper with timezone-aware scheduling - Add POST /api/cron/agents endpoint for external scheduler - Calculate nextRun on agent create, update, and after execution - Add time/day picker in agent form (daily/weekly/monthly) - Show "Next run" countdown in agent card - Add i18n keys for schedule UI (FR + EN) External scheduler (N8N, Vercel Cron) should call /api/cron/agents every 5-15 min. Requires `prisma db push` to apply schema changes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,7 @@ interface AgentCardProps {
|
||||
isEnabled: boolean
|
||||
frequency: string
|
||||
lastRun: string | Date | null
|
||||
nextRun?: string | Date | null
|
||||
createdAt: string | Date
|
||||
updatedAt: string | Date
|
||||
_count: { actions: number }
|
||||
@@ -196,6 +197,16 @@ export function AgentCard({ agent, onEdit, onRefresh, onToggle }: AgentCardProps
|
||||
<span>{t('agents.metadata.executions', { count: agent._count.actions })}</span>
|
||||
</div>
|
||||
|
||||
{agent.frequency !== 'manual' && agent.nextRun && (
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground mb-3">
|
||||
<Clock className="w-3 h-3" />
|
||||
{t('agents.schedule.nextRun')}{' '}
|
||||
{mounted
|
||||
? formatDistanceToNow(new Date(agent.nextRun), { addSuffix: true, locale: dateLocale })
|
||||
: new Date(agent.nextRun).toISOString().split('T')[0]}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{lastAction && (
|
||||
<div className={`
|
||||
flex items-center gap-1.5 text-xs px-2 py-1 rounded-md mb-3
|
||||
|
||||
@@ -47,6 +47,9 @@ interface AgentFormProps {
|
||||
maxSteps?: number
|
||||
notifyEmail?: boolean
|
||||
includeImages?: boolean
|
||||
scheduledTime?: string | null
|
||||
scheduledDay?: number | null
|
||||
timezone?: string | null
|
||||
} | null
|
||||
notebooks: { id: string; name: string; icon?: string | null }[]
|
||||
onSave: (data: FormData) => Promise<void>
|
||||
@@ -88,6 +91,11 @@ export function AgentForm({ agent, notebooks, onSave, onCancel }: AgentFormProps
|
||||
const [sourceNotebookId, setSourceNotebookId] = useState(agent?.sourceNotebookId || '')
|
||||
const [targetNotebookId, setTargetNotebookId] = useState(agent?.targetNotebookId || '')
|
||||
const [frequency, setFrequency] = useState(agent?.frequency || 'manual')
|
||||
const [scheduledTime, setScheduledTime] = useState(agent?.scheduledTime || '08:00')
|
||||
const [scheduledDay, setScheduledDay] = useState<number>(agent?.scheduledDay ?? 1)
|
||||
const [timezone] = useState(() => {
|
||||
try { return Intl.DateTimeFormat().resolvedOptions().timeZone } catch { return 'UTC' }
|
||||
})
|
||||
const [selectedTools, setSelectedTools] = useState<string[]>(() => {
|
||||
if (agent?.tools) {
|
||||
try {
|
||||
@@ -177,6 +185,9 @@ export function AgentForm({ agent, notebooks, onSave, onCancel }: AgentFormProps
|
||||
formData.set('maxSteps', String(maxSteps))
|
||||
formData.set('notifyEmail', String(notifyEmail))
|
||||
formData.set('includeImages', String(includeImages))
|
||||
formData.set('scheduledTime', scheduledTime)
|
||||
formData.set('scheduledDay', String(scheduledDay))
|
||||
formData.set('timezone', timezone)
|
||||
|
||||
await onSave(formData)
|
||||
} catch {
|
||||
@@ -356,6 +367,59 @@ export function AgentForm({ agent, notebooks, onSave, onCancel }: AgentFormProps
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Schedule config: time + day pickers (hidden for manual/hourly) */}
|
||||
{frequency !== 'manual' && frequency !== 'hourly' && (
|
||||
<div className="flex gap-3">
|
||||
{/* Day selector (weekly/monthly only) */}
|
||||
{frequency === 'weekly' && (
|
||||
<div className="flex-1">
|
||||
<label className={labelCls}>{t('agents.schedule.dayOfWeek')}</label>
|
||||
<select
|
||||
value={scheduledDay}
|
||||
onChange={e => setScheduledDay(Number(e.target.value))}
|
||||
className={selectCls}
|
||||
>
|
||||
{[
|
||||
{ value: 0, label: t('agents.schedule.days.mon') },
|
||||
{ value: 1, label: t('agents.schedule.days.tue') },
|
||||
{ value: 2, label: t('agents.schedule.days.wed') },
|
||||
{ value: 3, label: t('agents.schedule.days.thu') },
|
||||
{ value: 4, label: t('agents.schedule.days.fri') },
|
||||
{ value: 5, label: t('agents.schedule.days.sat') },
|
||||
{ value: 6, label: t('agents.schedule.days.sun') },
|
||||
].map(d => (
|
||||
<option key={d.value} value={d.value}>{d.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
{frequency === 'monthly' && (
|
||||
<div className="flex-1">
|
||||
<label className={labelCls}>{t('agents.schedule.dayOfMonth')}</label>
|
||||
<select
|
||||
value={scheduledDay}
|
||||
onChange={e => setScheduledDay(Number(e.target.value))}
|
||||
className={selectCls}
|
||||
>
|
||||
{Array.from({ length: 31 }, (_, i) => i + 1).map(d => (
|
||||
<option key={d} value={d}>{d}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
{/* Time picker */}
|
||||
<div className="flex-1">
|
||||
<label className={labelCls}>{t('agents.schedule.time')}</label>
|
||||
<input
|
||||
type="time"
|
||||
value={scheduledTime}
|
||||
onChange={e => setScheduledTime(e.target.value)}
|
||||
className={inputCls}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Email Notification */}
|
||||
<div
|
||||
onClick={() => setNotifyEmail(!notifyEmail)}
|
||||
|
||||
Reference in New Issue
Block a user