feat: implement agent scheduled execution with cron and time picker
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:
2026-04-26 10:45:48 +02:00
parent dc18dc3de4
commit 73de1cd26d
10 changed files with 439 additions and 1 deletions

View File

@@ -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

View File

@@ -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)}