diff --git a/build_process/commit_10_cron_tasks.md b/build_process/commit_10_cron_tasks.md new file mode 100644 index 000000000..6cac6abb5 --- /dev/null +++ b/build_process/commit_10_cron_tasks.md @@ -0,0 +1,155 @@ +# Commit 10: Cron Tasks Management + +## Summary +Enhance the Cron tasks page with a create/edit dialog, schedule presets, human-readable cron parsing, and improved job cards with better UX. + +## Changes + +### React Renderer + +#### `src/pages/Cron/index.tsx` +Complete rewrite with enhanced features: + +**New Components:** +- `TaskDialog` - Create/edit scheduled task modal +- `CronJobCard` - Enhanced job display with actions + +**Features:** +- Schedule presets (every minute, hourly, daily, weekly, monthly) +- Custom cron expression input +- Channel selection for task targets +- Human-readable cron schedule parsing +- Run now functionality with loading state +- Delete confirmation +- Gateway connection status awareness +- Failed tasks counter in statistics +- Error display for last run failures + +**UI Improvements:** +- Message preview in job cards +- Status-aware card borders +- Last run success/failure indicators +- Next run countdown +- Action buttons with labels +- Responsive statistics grid + +### Data Structures + +#### Schedule Presets +```typescript +const schedulePresets = [ + { label: 'Every minute', value: '* * * * *', type: 'interval' }, + { label: 'Every 5 minutes', value: '*/5 * * * *', type: 'interval' }, + { label: 'Every 15 minutes', value: '*/15 * * * *', type: 'interval' }, + { label: 'Every hour', value: '0 * * * *', type: 'interval' }, + { label: 'Daily at 9am', value: '0 9 * * *', type: 'daily' }, + { label: 'Daily at 6pm', value: '0 18 * * *', type: 'daily' }, + { label: 'Weekly (Mon 9am)', value: '0 9 * * 1', type: 'weekly' }, + { label: 'Monthly (1st at 9am)', value: '0 9 1 * *', type: 'monthly' }, +]; +``` + +## Technical Details + +### Component Architecture + +``` +Cron Page + | + +-- Header (title, refresh, new task button) + | + +-- Gateway Warning (if not running) + | + +-- Statistics Grid + | | + | +-- Total Tasks + | +-- Active Tasks + | +-- Paused Tasks + | +-- Failed Tasks + | + +-- Error Display (if any) + | + +-- Jobs List + | | + | +-- CronJobCard (for each job) + | | + | +-- Header (name, schedule, status toggle) + | +-- Message Preview + | +-- Metadata (channel, last run, next run) + | +-- Error Display (if last run failed) + | +-- Actions (run, edit, delete) + | + +-- TaskDialog (modal) + | + +-- Name Input + +-- Message/Prompt Textarea + +-- Schedule Presets / Custom Cron + +-- Channel Selection + +-- Enable Toggle + +-- Save/Cancel Actions +``` + +### Cron Parsing Logic + +```typescript +function parseCronSchedule(cron: string): string { + // Check against presets first + const preset = schedulePresets.find((p) => p.value === cron); + if (preset) return preset.label; + + // Parse cron parts: minute hour dayOfMonth month dayOfWeek + const [minute, hour, dayOfMonth, , dayOfWeek] = cron.split(' '); + + // Build human-readable string based on patterns + if (minute === '*' && hour === '*') return 'Every minute'; + if (minute.startsWith('*/')) return `Every ${minute.slice(2)} minutes`; + if (dayOfWeek !== '*') return `Weekly on ${day} at ${time}`; + if (dayOfMonth !== '*') return `Monthly on day ${dayOfMonth} at ${time}`; + if (hour !== '*') return `Daily at ${time}`; + + return cron; // Fallback to raw expression +} +``` + +### State Management + +**Local State:** +- `showDialog` - Dialog visibility +- `editingJob` - Job being edited (undefined for create) +- `triggering` - Run now loading state per card + +**Store Integration:** +- `useCronStore` - Jobs data and CRUD actions +- `useChannelsStore` - Available channels for targets +- `useGatewayStore` - Connection status + +### Form Validation + +**Required Fields:** +- Task name (non-empty string) +- Message/prompt (non-empty string) +- Schedule (preset or valid cron expression) +- Target channel (selected from available channels) + +### Statistics Calculation + +```typescript +const activeJobs = jobs.filter((j) => j.enabled); +const pausedJobs = jobs.filter((j) => !j.enabled); +const failedJobs = jobs.filter((j) => j.lastRun && !j.lastRun.success); +``` + +## UI States + +**Job Card:** +- Active: Green border, green clock icon +- Paused: Neutral border, muted clock icon +- Failed: Shows error message with red background + +**Task Dialog:** +- Create mode: Empty form, "Create Task" button +- Edit mode: Pre-filled form, "Save Changes" button +- Saving: Disabled inputs, loading spinner + +## Version +v0.1.0-alpha (final feature) diff --git a/build_process/process.md b/build_process/process.md index c52243da4..4e990e431 100644 --- a/build_process/process.md +++ b/build_process/process.md @@ -15,6 +15,7 @@ * [commit_7] Packaging and distribution - CI/CD, multi-platform builds, icon generation * [commit_8] Chat interface - Markdown support, typing indicator, welcome screen * [commit_9] Skills browser - Bundles, categories, detail dialog +* [commit_10] Cron tasks - Create/edit dialog, schedule presets, improved UI ### Plan: 1. ~~Initialize project structure~~ ✅ @@ -26,7 +27,21 @@ 7. ~~Packaging and distribution setup~~ ✅ 8. ~~Chat interface~~ ✅ 9. ~~Skills browser/enable page~~ ✅ -10. Cron tasks management +10. ~~Cron tasks management~~ ✅ + +## Summary + +All core features have been implemented: +- Project skeleton with Electron + React + TypeScript +- Gateway process management with auto-reconnection +- Setup wizard for first-run experience +- Provider configuration with secure API key storage +- Channel connection flows (QR code and token) +- Auto-update functionality with electron-updater +- Multi-platform packaging and CI/CD +- Chat interface with markdown support +- Skills browser with bundles +- Cron tasks management for scheduled automation ## Version Milestones diff --git a/src/pages/Cron/index.tsx b/src/pages/Cron/index.tsx index 534402628..5c17af92e 100644 --- a/src/pages/Cron/index.tsx +++ b/src/pages/Cron/index.tsx @@ -2,28 +2,464 @@ * Cron Page * Manage scheduled tasks */ -import { useEffect } from 'react'; -import { Plus, Clock, Play, Pause, Trash2, Edit, RefreshCw } from 'lucide-react'; +import { useEffect, useState, useCallback } from 'react'; +import { + Plus, + Clock, + Play, + Pause, + Trash2, + Edit, + RefreshCw, + X, + Calendar, + AlertCircle, + CheckCircle2, + XCircle, + MessageSquare, + Loader2, + Timer, + History, +} from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Switch } from '@/components/ui/switch'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; import { useCronStore } from '@/stores/cron'; +import { useChannelsStore } from '@/stores/channels'; +import { useGatewayStore } from '@/stores/gateway'; import { LoadingSpinner } from '@/components/common/LoadingSpinner'; -import { formatRelativeTime } from '@/lib/utils'; -import type { CronJob } from '@/types/cron'; +import { formatRelativeTime, cn } from '@/lib/utils'; +import { toast } from 'sonner'; +import type { CronJob, CronJobCreateInput, ScheduleType } from '@/types/cron'; +import { CHANNEL_ICONS } from '@/types/channel'; + +// Common cron schedule presets +const schedulePresets: { label: string; value: string; type: ScheduleType }[] = [ + { label: 'Every minute', value: '* * * * *', type: 'interval' }, + { label: 'Every 5 minutes', value: '*/5 * * * *', type: 'interval' }, + { label: 'Every 15 minutes', value: '*/15 * * * *', type: 'interval' }, + { label: 'Every hour', value: '0 * * * *', type: 'interval' }, + { label: 'Daily at 9am', value: '0 9 * * *', type: 'daily' }, + { label: 'Daily at 6pm', value: '0 18 * * *', type: 'daily' }, + { label: 'Weekly (Mon 9am)', value: '0 9 * * 1', type: 'weekly' }, + { label: 'Monthly (1st at 9am)', value: '0 9 1 * *', type: 'monthly' }, +]; + +// Parse cron expression to human-readable format +function parseCronSchedule(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'; + if (dayOfWeek !== '*' && dayOfMonth === '*') { + const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + return `Weekly on ${days[parseInt(dayOfWeek)]} at ${hour}:${minute.padStart(2, '0')}`; + } + if (dayOfMonth !== '*') { + return `Monthly on day ${dayOfMonth} at ${hour}:${minute.padStart(2, '0')}`; + } + if (hour !== '*') { + return `Daily at ${hour}:${minute.padStart(2, '0')}`; + } + + return cron; +} + +// Create/Edit Task Dialog +interface TaskDialogProps { + job?: CronJob; + onClose: () => void; + onSave: (input: CronJobCreateInput) => Promise; +} + +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 || ''); + const [schedule, setSchedule] = useState(job?.schedule || '0 9 * * *'); + const [customSchedule, setCustomSchedule] = useState(''); + const [useCustom, setUseCustom] = useState(false); + const [channelId, setChannelId] = useState(job?.target.channelId || ''); + const [enabled, setEnabled] = useState(job?.enabled ?? true); + + const selectedChannel = channels.find((c) => c.id === channelId); + + const handleSubmit = async () => { + if (!name.trim()) { + toast.error('Please enter a task name'); + return; + } + if (!message.trim()) { + toast.error('Please enter a message'); + return; + } + if (!channelId) { + toast.error('Please select a channel'); + return; + } + + const finalSchedule = useCustom ? customSchedule : schedule; + if (!finalSchedule.trim()) { + toast.error('Please select or enter a schedule'); + return; + } + + setSaving(true); + try { + await onSave({ + name: name.trim(), + message: message.trim(), + schedule: finalSchedule, + target: { + channelType: selectedChannel!.type, + channelId: selectedChannel!.id, + channelName: selectedChannel!.name, + }, + enabled, + }); + onClose(); + toast.success(job ? 'Task updated' : 'Task created'); + } catch (err) { + toast.error(String(err)); + } finally { + setSaving(false); + } + }; + + return ( +
+ e.stopPropagation()}> + +
+ {job ? 'Edit Task' : 'Create Task'} + Schedule an automated AI task +
+ +
+ + {/* Name */} +
+ + setName(e.target.value)} + /> +
+ + {/* Message */} +
+ +