diff --git a/electron/main/ipc-handlers.ts b/electron/main/ipc-handlers.ts index 0a90b9352..cb5f23f0f 100644 --- a/electron/main/ipc-handlers.ts +++ b/electron/main/ipc-handlers.ts @@ -150,11 +150,11 @@ function transformCronJob(job: GatewayCronJob) { // Build lastRun from state const lastRun = job.state?.lastRunAtMs ? { - time: new Date(job.state.lastRunAtMs).toISOString(), - success: job.state.lastStatus === 'ok', - error: job.state.lastError, - duration: job.state.lastDurationMs, - } + time: new Date(job.state.lastRunAtMs).toISOString(), + success: job.state.lastStatus === 'ok', + error: job.state.lastError, + duration: job.state.lastDurationMs, + } : undefined; // Build nextRun from state @@ -208,6 +208,12 @@ function registerCronHandlers(gatewayManager: GatewayManager): void { }) => { try { // Transform frontend input to Gateway cron.add format + // For Discord, the recipient must be prefixed with "channel:" or "user:" + const recipientId = input.target.channelId; + const deliveryTo = input.target.channelType === 'discord' && recipientId + ? `channel:${recipientId}` + : recipientId; + const gatewayInput = { name: input.name, schedule: { kind: 'cron', expr: input.schedule }, @@ -218,6 +224,7 @@ function registerCronHandlers(gatewayManager: GatewayManager): void { delivery: { mode: 'announce', channel: input.target.channelType, + to: deliveryTo, }, }; const result = await gatewayManager.rpc('cron.add', gatewayInput); diff --git a/src/pages/Cron/index.tsx b/src/pages/Cron/index.tsx index 44f6f4607..359b082c8 100644 --- a/src/pages/Cron/index.tsx +++ b/src/pages/Cron/index.tsx @@ -3,13 +3,13 @@ * Manage scheduled tasks */ import { useEffect, useState, useCallback } from 'react'; -import { - Plus, - Clock, - Play, - Pause, - Trash2, - Edit, +import { + Plus, + Clock, + Play, + Pause, + Trash2, + Edit, RefreshCw, X, Calendar, @@ -90,12 +90,12 @@ function parseCronSchedule(schedule: unknown): string { function parseCronExpr(cron: string): string { const preset = schedulePresets.find((p) => p.value === cron); if (preset) return preset.label; - + const parts = cron.split(' '); if (parts.length !== 5) return cron; - + const [minute, hour, dayOfMonth, , dayOfWeek] = parts; - + if (minute === '*' && hour === '*') return 'Every minute'; if (minute.startsWith('*/')) return `Every ${minute.slice(2)} minutes`; if (hour === '*' && minute === '0') return 'Every hour'; @@ -109,7 +109,7 @@ function parseCronExpr(cron: string): string { if (hour !== '*') { return `Daily at ${hour}:${minute.padStart(2, '0')}`; } - + return cron; } @@ -123,7 +123,7 @@ interface TaskDialogProps { function TaskDialog({ job, onClose, onSave }: TaskDialogProps) { const { channels } = useChannelsStore(); const [saving, setSaving] = useState(false); - + const [name, setName] = useState(job?.name || ''); const [message, setMessage] = useState(job?.message || ''); // Extract cron expression string from CronSchedule object or use as-is if string @@ -140,10 +140,12 @@ function TaskDialog({ job, onClose, onSave }: TaskDialogProps) { const [customSchedule, setCustomSchedule] = useState(''); const [useCustom, setUseCustom] = useState(false); const [channelId, setChannelId] = useState(job?.target.channelId || ''); + const [discordChannelId, setDiscordChannelId] = useState(''); const [enabled, setEnabled] = useState(job?.enabled ?? true); - + const selectedChannel = channels.find((c) => c.id === channelId); - + const isDiscord = selectedChannel?.type === 'discord'; + const handleSubmit = async () => { if (!name.trim()) { toast.error('Please enter a task name'); @@ -157,22 +159,32 @@ function TaskDialog({ job, onClose, onSave }: TaskDialogProps) { toast.error('Please select a channel'); return; } - + // Validate Discord channel ID when Discord is selected + if (selectedChannel?.type === 'discord' && !discordChannelId.trim()) { + toast.error('Please enter a Discord Channel ID'); + return; + } + const finalSchedule = useCustom ? customSchedule : schedule; if (!finalSchedule.trim()) { toast.error('Please select or enter a schedule'); return; } - + setSaving(true); try { + // For Discord, use the manually entered channel ID; for others, use empty + const actualChannelId = selectedChannel!.type === 'discord' + ? discordChannelId.trim() + : ''; + await onSave({ name: name.trim(), message: message.trim(), schedule: finalSchedule, target: { channelType: selectedChannel!.type, - channelId: selectedChannel!.id, + channelId: actualChannelId, channelName: selectedChannel!.name, }, enabled, @@ -185,7 +197,7 @@ function TaskDialog({ job, onClose, onSave }: TaskDialogProps) { setSaving(false); } }; - + return (
+ Right-click the Discord channel → Copy Channel ID +
+