fix crontab task (#19)
This commit is contained in:
committed by
GitHub
Unverified
parent
19ae5b0f75
commit
905b828e9b
@@ -150,11 +150,11 @@ function transformCronJob(job: GatewayCronJob) {
|
|||||||
// Build lastRun from state
|
// Build lastRun from state
|
||||||
const lastRun = job.state?.lastRunAtMs
|
const lastRun = job.state?.lastRunAtMs
|
||||||
? {
|
? {
|
||||||
time: new Date(job.state.lastRunAtMs).toISOString(),
|
time: new Date(job.state.lastRunAtMs).toISOString(),
|
||||||
success: job.state.lastStatus === 'ok',
|
success: job.state.lastStatus === 'ok',
|
||||||
error: job.state.lastError,
|
error: job.state.lastError,
|
||||||
duration: job.state.lastDurationMs,
|
duration: job.state.lastDurationMs,
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
// Build nextRun from state
|
// Build nextRun from state
|
||||||
@@ -208,6 +208,12 @@ function registerCronHandlers(gatewayManager: GatewayManager): void {
|
|||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
// Transform frontend input to Gateway cron.add format
|
// 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 = {
|
const gatewayInput = {
|
||||||
name: input.name,
|
name: input.name,
|
||||||
schedule: { kind: 'cron', expr: input.schedule },
|
schedule: { kind: 'cron', expr: input.schedule },
|
||||||
@@ -218,6 +224,7 @@ function registerCronHandlers(gatewayManager: GatewayManager): void {
|
|||||||
delivery: {
|
delivery: {
|
||||||
mode: 'announce',
|
mode: 'announce',
|
||||||
channel: input.target.channelType,
|
channel: input.target.channelType,
|
||||||
|
to: deliveryTo,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const result = await gatewayManager.rpc('cron.add', gatewayInput);
|
const result = await gatewayManager.rpc('cron.add', gatewayInput);
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
* Manage scheduled tasks
|
* Manage scheduled tasks
|
||||||
*/
|
*/
|
||||||
import { useEffect, useState, useCallback } from 'react';
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Plus,
|
Plus,
|
||||||
Clock,
|
Clock,
|
||||||
Play,
|
Play,
|
||||||
Pause,
|
Pause,
|
||||||
Trash2,
|
Trash2,
|
||||||
Edit,
|
Edit,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
X,
|
X,
|
||||||
Calendar,
|
Calendar,
|
||||||
@@ -90,12 +90,12 @@ function parseCronSchedule(schedule: unknown): string {
|
|||||||
function parseCronExpr(cron: string): string {
|
function parseCronExpr(cron: string): string {
|
||||||
const preset = schedulePresets.find((p) => p.value === cron);
|
const preset = schedulePresets.find((p) => p.value === cron);
|
||||||
if (preset) return preset.label;
|
if (preset) return preset.label;
|
||||||
|
|
||||||
const parts = cron.split(' ');
|
const parts = cron.split(' ');
|
||||||
if (parts.length !== 5) return cron;
|
if (parts.length !== 5) return cron;
|
||||||
|
|
||||||
const [minute, hour, dayOfMonth, , dayOfWeek] = parts;
|
const [minute, hour, dayOfMonth, , dayOfWeek] = parts;
|
||||||
|
|
||||||
if (minute === '*' && hour === '*') return 'Every minute';
|
if (minute === '*' && hour === '*') return 'Every minute';
|
||||||
if (minute.startsWith('*/')) return `Every ${minute.slice(2)} minutes`;
|
if (minute.startsWith('*/')) return `Every ${minute.slice(2)} minutes`;
|
||||||
if (hour === '*' && minute === '0') return 'Every hour';
|
if (hour === '*' && minute === '0') return 'Every hour';
|
||||||
@@ -109,7 +109,7 @@ function parseCronExpr(cron: string): string {
|
|||||||
if (hour !== '*') {
|
if (hour !== '*') {
|
||||||
return `Daily at ${hour}:${minute.padStart(2, '0')}`;
|
return `Daily at ${hour}:${minute.padStart(2, '0')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return cron;
|
return cron;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ interface TaskDialogProps {
|
|||||||
function TaskDialog({ job, onClose, onSave }: TaskDialogProps) {
|
function TaskDialog({ job, onClose, onSave }: TaskDialogProps) {
|
||||||
const { channels } = useChannelsStore();
|
const { channels } = useChannelsStore();
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
const [name, setName] = useState(job?.name || '');
|
const [name, setName] = useState(job?.name || '');
|
||||||
const [message, setMessage] = useState(job?.message || '');
|
const [message, setMessage] = useState(job?.message || '');
|
||||||
// Extract cron expression string from CronSchedule object or use as-is if string
|
// 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 [customSchedule, setCustomSchedule] = useState('');
|
||||||
const [useCustom, setUseCustom] = useState(false);
|
const [useCustom, setUseCustom] = useState(false);
|
||||||
const [channelId, setChannelId] = useState(job?.target.channelId || '');
|
const [channelId, setChannelId] = useState(job?.target.channelId || '');
|
||||||
|
const [discordChannelId, setDiscordChannelId] = useState('');
|
||||||
const [enabled, setEnabled] = useState(job?.enabled ?? true);
|
const [enabled, setEnabled] = useState(job?.enabled ?? true);
|
||||||
|
|
||||||
const selectedChannel = channels.find((c) => c.id === channelId);
|
const selectedChannel = channels.find((c) => c.id === channelId);
|
||||||
|
const isDiscord = selectedChannel?.type === 'discord';
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!name.trim()) {
|
if (!name.trim()) {
|
||||||
toast.error('Please enter a task name');
|
toast.error('Please enter a task name');
|
||||||
@@ -157,22 +159,32 @@ function TaskDialog({ job, onClose, onSave }: TaskDialogProps) {
|
|||||||
toast.error('Please select a channel');
|
toast.error('Please select a channel');
|
||||||
return;
|
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;
|
const finalSchedule = useCustom ? customSchedule : schedule;
|
||||||
if (!finalSchedule.trim()) {
|
if (!finalSchedule.trim()) {
|
||||||
toast.error('Please select or enter a schedule');
|
toast.error('Please select or enter a schedule');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
|
// For Discord, use the manually entered channel ID; for others, use empty
|
||||||
|
const actualChannelId = selectedChannel!.type === 'discord'
|
||||||
|
? discordChannelId.trim()
|
||||||
|
: '';
|
||||||
|
|
||||||
await onSave({
|
await onSave({
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
message: message.trim(),
|
message: message.trim(),
|
||||||
schedule: finalSchedule,
|
schedule: finalSchedule,
|
||||||
target: {
|
target: {
|
||||||
channelType: selectedChannel!.type,
|
channelType: selectedChannel!.type,
|
||||||
channelId: selectedChannel!.id,
|
channelId: actualChannelId,
|
||||||
channelName: selectedChannel!.name,
|
channelName: selectedChannel!.name,
|
||||||
},
|
},
|
||||||
enabled,
|
enabled,
|
||||||
@@ -185,7 +197,7 @@ function TaskDialog({ job, onClose, onSave }: TaskDialogProps) {
|
|||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 bg-black/50 flex items-center justify-center p-4" onClick={onClose}>
|
<div className="fixed inset-0 z-50 bg-black/50 flex items-center justify-center p-4" onClick={onClose}>
|
||||||
<Card className="w-full max-w-lg max-h-[90vh] overflow-y-auto" onClick={(e) => e.stopPropagation()}>
|
<Card className="w-full max-w-lg max-h-[90vh] overflow-y-auto" onClick={(e) => e.stopPropagation()}>
|
||||||
@@ -209,7 +221,7 @@ function TaskDialog({ job, onClose, onSave }: TaskDialogProps) {
|
|||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Message */}
|
{/* Message */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="message">Message / Prompt</Label>
|
<Label htmlFor="message">Message / Prompt</Label>
|
||||||
@@ -221,7 +233,7 @@ function TaskDialog({ job, onClose, onSave }: TaskDialogProps) {
|
|||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Schedule */}
|
{/* Schedule */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Schedule</Label>
|
<Label>Schedule</Label>
|
||||||
@@ -258,7 +270,7 @@ function TaskDialog({ job, onClose, onSave }: TaskDialogProps) {
|
|||||||
{useCustom ? 'Use presets' : 'Use custom cron'}
|
{useCustom ? 'Use presets' : 'Use custom cron'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Target Channel */}
|
{/* Target Channel */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Target Channel</Label>
|
<Label>Target Channel</Label>
|
||||||
@@ -284,7 +296,21 @@ function TaskDialog({ job, onClose, onSave }: TaskDialogProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Discord Channel ID - only shown when Discord is selected */}
|
||||||
|
{isDiscord && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Discord Channel ID</Label>
|
||||||
|
<Input
|
||||||
|
value={discordChannelId}
|
||||||
|
onChange={(e) => setDiscordChannelId(e.target.value)}
|
||||||
|
placeholder="e.g., 1438452657525100686"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Right-click the Discord channel → Copy Channel ID
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* Enabled */}
|
{/* Enabled */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
@@ -295,7 +321,7 @@ function TaskDialog({ job, onClose, onSave }: TaskDialogProps) {
|
|||||||
</div>
|
</div>
|
||||||
<Switch checked={enabled} onCheckedChange={setEnabled} />
|
<Switch checked={enabled} onCheckedChange={setEnabled} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex justify-end gap-2 pt-4 border-t">
|
<div className="flex justify-end gap-2 pt-4 border-t">
|
||||||
<Button variant="outline" onClick={onClose}>
|
<Button variant="outline" onClick={onClose}>
|
||||||
@@ -332,7 +358,7 @@ interface CronJobCardProps {
|
|||||||
|
|
||||||
function CronJobCard({ job, onToggle, onEdit, onDelete, onTrigger }: CronJobCardProps) {
|
function CronJobCard({ job, onToggle, onEdit, onDelete, onTrigger }: CronJobCardProps) {
|
||||||
const [triggering, setTriggering] = useState(false);
|
const [triggering, setTriggering] = useState(false);
|
||||||
|
|
||||||
const handleTrigger = async () => {
|
const handleTrigger = async () => {
|
||||||
setTriggering(true);
|
setTriggering(true);
|
||||||
try {
|
try {
|
||||||
@@ -345,13 +371,13 @@ function CronJobCard({ job, onToggle, onEdit, onDelete, onTrigger }: CronJobCard
|
|||||||
setTriggering(false);
|
setTriggering(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
if (confirm('Are you sure you want to delete this task?')) {
|
if (confirm('Are you sure you want to delete this task?')) {
|
||||||
onDelete();
|
onDelete();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={cn(
|
<Card className={cn(
|
||||||
'transition-colors',
|
'transition-colors',
|
||||||
@@ -362,8 +388,8 @@ function CronJobCard({ job, onToggle, onEdit, onDelete, onTrigger }: CronJobCard
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
'rounded-full p-2',
|
'rounded-full p-2',
|
||||||
job.enabled
|
job.enabled
|
||||||
? 'bg-green-100 dark:bg-green-900/30'
|
? 'bg-green-100 dark:bg-green-900/30'
|
||||||
: 'bg-muted'
|
: 'bg-muted'
|
||||||
)}>
|
)}>
|
||||||
<Clock className={cn(
|
<Clock className={cn(
|
||||||
@@ -398,14 +424,14 @@ function CronJobCard({ job, onToggle, onEdit, onDelete, onTrigger }: CronJobCard
|
|||||||
{job.message}
|
{job.message}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Metadata */}
|
{/* Metadata */}
|
||||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-muted-foreground">
|
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-muted-foreground">
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
{CHANNEL_ICONS[job.target.channelType]}
|
{CHANNEL_ICONS[job.target.channelType]}
|
||||||
{job.target.channelName}
|
{job.target.channelName}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{job.lastRun && (
|
{job.lastRun && (
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<History className="h-4 w-4" />
|
<History className="h-4 w-4" />
|
||||||
@@ -417,7 +443,7 @@ function CronJobCard({ job, onToggle, onEdit, onDelete, onTrigger }: CronJobCard
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{job.nextRun && job.enabled && (
|
{job.nextRun && job.enabled && (
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<Calendar className="h-4 w-4" />
|
<Calendar className="h-4 w-4" />
|
||||||
@@ -425,7 +451,7 @@ function CronJobCard({ job, onToggle, onEdit, onDelete, onTrigger }: CronJobCard
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Last Run Error */}
|
{/* Last Run Error */}
|
||||||
{job.lastRun && !job.lastRun.success && job.lastRun.error && (
|
{job.lastRun && !job.lastRun.success && job.lastRun.error && (
|
||||||
<div className="flex items-start gap-2 p-2 rounded-lg bg-red-50 dark:bg-red-900/20 text-sm text-red-600 dark:text-red-400">
|
<div className="flex items-start gap-2 p-2 rounded-lg bg-red-50 dark:bg-red-900/20 text-sm text-red-600 dark:text-red-400">
|
||||||
@@ -433,12 +459,12 @@ function CronJobCard({ job, onToggle, onEdit, onDelete, onTrigger }: CronJobCard
|
|||||||
<span>{job.lastRun.error}</span>
|
<span>{job.lastRun.error}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex justify-end gap-1 pt-2 border-t">
|
<div className="flex justify-end gap-1 pt-2 border-t">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleTrigger}
|
onClick={handleTrigger}
|
||||||
disabled={triggering}
|
disabled={triggering}
|
||||||
>
|
>
|
||||||
@@ -469,9 +495,9 @@ export function Cron() {
|
|||||||
const gatewayStatus = useGatewayStore((state) => state.status);
|
const gatewayStatus = useGatewayStore((state) => state.status);
|
||||||
const [showDialog, setShowDialog] = useState(false);
|
const [showDialog, setShowDialog] = useState(false);
|
||||||
const [editingJob, setEditingJob] = useState<CronJob | undefined>();
|
const [editingJob, setEditingJob] = useState<CronJob | undefined>();
|
||||||
|
|
||||||
const isGatewayRunning = gatewayStatus.state === 'running';
|
const isGatewayRunning = gatewayStatus.state === 'running';
|
||||||
|
|
||||||
// Fetch jobs and channels on mount
|
// Fetch jobs and channels on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isGatewayRunning) {
|
if (isGatewayRunning) {
|
||||||
@@ -479,12 +505,12 @@ export function Cron() {
|
|||||||
fetchChannels();
|
fetchChannels();
|
||||||
}
|
}
|
||||||
}, [fetchJobs, fetchChannels, isGatewayRunning]);
|
}, [fetchJobs, fetchChannels, isGatewayRunning]);
|
||||||
|
|
||||||
// Statistics
|
// Statistics
|
||||||
const activeJobs = jobs.filter((j) => j.enabled);
|
const activeJobs = jobs.filter((j) => j.enabled);
|
||||||
const pausedJobs = jobs.filter((j) => !j.enabled);
|
const pausedJobs = jobs.filter((j) => !j.enabled);
|
||||||
const failedJobs = jobs.filter((j) => j.lastRun && !j.lastRun.success);
|
const failedJobs = jobs.filter((j) => j.lastRun && !j.lastRun.success);
|
||||||
|
|
||||||
const handleSave = useCallback(async (input: CronJobCreateInput) => {
|
const handleSave = useCallback(async (input: CronJobCreateInput) => {
|
||||||
if (editingJob) {
|
if (editingJob) {
|
||||||
await updateJob(editingJob.id, input);
|
await updateJob(editingJob.id, input);
|
||||||
@@ -492,7 +518,7 @@ export function Cron() {
|
|||||||
await createJob(input);
|
await createJob(input);
|
||||||
}
|
}
|
||||||
}, [editingJob, createJob, updateJob]);
|
}, [editingJob, createJob, updateJob]);
|
||||||
|
|
||||||
const handleToggle = useCallback(async (id: string, enabled: boolean) => {
|
const handleToggle = useCallback(async (id: string, enabled: boolean) => {
|
||||||
try {
|
try {
|
||||||
await toggleJob(id, enabled);
|
await toggleJob(id, enabled);
|
||||||
@@ -501,7 +527,7 @@ export function Cron() {
|
|||||||
toast.error('Failed to update task');
|
toast.error('Failed to update task');
|
||||||
}
|
}
|
||||||
}, [toggleJob]);
|
}, [toggleJob]);
|
||||||
|
|
||||||
const handleDelete = useCallback(async (id: string) => {
|
const handleDelete = useCallback(async (id: string) => {
|
||||||
try {
|
try {
|
||||||
await deleteJob(id);
|
await deleteJob(id);
|
||||||
@@ -510,7 +536,7 @@ export function Cron() {
|
|||||||
toast.error('Failed to delete task');
|
toast.error('Failed to delete task');
|
||||||
}
|
}
|
||||||
}, [deleteJob]);
|
}, [deleteJob]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-96 items-center justify-center">
|
<div className="flex h-96 items-center justify-center">
|
||||||
@@ -518,7 +544,7 @@ export function Cron() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -534,7 +560,7 @@ export function Cron() {
|
|||||||
<RefreshCw className="h-4 w-4 mr-2" />
|
<RefreshCw className="h-4 w-4 mr-2" />
|
||||||
Refresh
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditingJob(undefined);
|
setEditingJob(undefined);
|
||||||
setShowDialog(true);
|
setShowDialog(true);
|
||||||
@@ -546,7 +572,7 @@ export function Cron() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Gateway Warning */}
|
{/* Gateway Warning */}
|
||||||
{!isGatewayRunning && (
|
{!isGatewayRunning && (
|
||||||
<Card className="border-yellow-500 bg-yellow-50 dark:bg-yellow-900/10">
|
<Card className="border-yellow-500 bg-yellow-50 dark:bg-yellow-900/10">
|
||||||
@@ -558,7 +584,7 @@ export function Cron() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Statistics */}
|
{/* Statistics */}
|
||||||
<div className="grid grid-cols-4 gap-4">
|
<div className="grid grid-cols-4 gap-4">
|
||||||
<Card>
|
<Card>
|
||||||
@@ -614,7 +640,7 @@ export function Cron() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Error Display */}
|
{/* Error Display */}
|
||||||
{error && (
|
{error && (
|
||||||
<Card className="border-destructive">
|
<Card className="border-destructive">
|
||||||
@@ -624,7 +650,7 @@ export function Cron() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Jobs List */}
|
{/* Jobs List */}
|
||||||
{jobs.length === 0 ? (
|
{jobs.length === 0 ? (
|
||||||
<Card>
|
<Card>
|
||||||
@@ -632,10 +658,10 @@ export function Cron() {
|
|||||||
<Clock className="h-12 w-12 text-muted-foreground mb-4" />
|
<Clock className="h-12 w-12 text-muted-foreground mb-4" />
|
||||||
<h3 className="text-lg font-medium mb-2">No scheduled tasks</h3>
|
<h3 className="text-lg font-medium mb-2">No scheduled tasks</h3>
|
||||||
<p className="text-muted-foreground text-center mb-4 max-w-md">
|
<p className="text-muted-foreground text-center mb-4 max-w-md">
|
||||||
Create scheduled tasks to automate AI workflows.
|
Create scheduled tasks to automate AI workflows.
|
||||||
Tasks can send messages, run queries, or perform actions at specified times.
|
Tasks can send messages, run queries, or perform actions at specified times.
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditingJob(undefined);
|
setEditingJob(undefined);
|
||||||
setShowDialog(true);
|
setShowDialog(true);
|
||||||
@@ -664,7 +690,7 @@ export function Cron() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Create/Edit Dialog */}
|
{/* Create/Edit Dialog */}
|
||||||
{showDialog && (
|
{showDialog && (
|
||||||
<TaskDialog
|
<TaskDialog
|
||||||
|
|||||||
Reference in New Issue
Block a user