import React, { useState } from 'react'; import { Plus, RefreshCw, CheckCircle2, Pause, Edit2, Trash2, X, Play, Clock } from 'lucide-react'; import { motion, AnimatePresence } from 'motion/react'; import { useTranslation } from 'react-i18next'; import { useAppContext } from '../context/AppContext'; import { useUI } from '../context/UIContext'; import { CronJob } from '../types'; import { formatLocalDateTime } from '../utils/time'; const initialCronForm = { name: '', expr: '*/10 * * * *', message: '', deliver: false, channel: 'telegram', to: '', enabled: true, }; const isNonGroupRecipient = (channel: string, id: string) => { const ch = String(channel || '').toLowerCase(); const v = String(id || '').trim(); if (!v) return false; if (ch === 'telegram') { if (v.startsWith('-')) return false; if (v.startsWith('telegram:')) { const raw = v.slice('telegram:'.length); if (raw.startsWith('-')) return false; } } if (ch === 'discord') { if (v.startsWith('#') || v.startsWith('discord:channel:')) return false; } return true; }; const formatSchedule = (job: CronJob, t: (key: string) => string) => { const kind = String(job.schedule?.kind || '').toLowerCase(); if (kind === 'at' && job.schedule?.atMs) { return { label: t('runAt'), value: formatLocalDateTime(job.schedule.atMs), }; } return { label: t('cronExpression'), value: job.expr || '-', }; }; const Cron: React.FC = () => { const { t } = useTranslation(); const ui = useUI(); const { cron, refreshCron, q, cfg } = useAppContext(); const [isCronModalOpen, setIsCronModalOpen] = useState(false); const [editingCron, setEditingCron] = useState(null); const [cronForm, setCronForm] = useState(initialCronForm); const enabledChannels = React.useMemo(() => { const channels = (cfg as any)?.channels || {}; return Object.keys(channels).filter((k) => { const v = channels[k]; return v && typeof v === 'object' && v.enabled === true; }); }, [cfg]); const channelRecipients = React.useMemo(() => { const channels = (cfg as any)?.channels || {}; const out: Record = {}; enabledChannels.forEach((ch) => { const arr = Array.isArray(channels?.[ch]?.allow_from) ? channels[ch].allow_from : []; out[ch] = arr.map((x: any) => String(x || '').trim()).filter((id: string) => isNonGroupRecipient(ch, id)); }); return out; }, [cfg, enabledChannels]); async function cronAction(action: 'delete' | 'enable' | 'disable', id: string) { if (action === 'delete') { const ok = await ui.confirmDialog({ title: t('cronDeleteConfirmTitle'), message: t('cronDeleteConfirmMessage'), danger: true, confirmText: t('delete'), }); if (!ok) return; } if (action === 'disable') { const ok = await ui.confirmDialog({ title: t('cronDisableConfirmTitle'), message: t('cronDisableConfirmMessage'), confirmText: t('pause'), }); if (!ok) return; } try { await ui.withLoading(async () => { const r = await fetch(`/webui/api/cron${q}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action, id }), }); if (!r.ok) { throw new Error(await r.text()); } }, t('loading')); await refreshCron(); } catch (e) { await ui.notify({ title: t('actionFailed'), message: String(e) }); } } async function openCronModal(job?: CronJob) { if (job) { try { const r = await fetch(`/webui/api/cron${q}&id=${job.id}`); if (r.ok) { const details = await r.json(); const isAtSchedule = String(details.job?.schedule?.kind || '').toLowerCase() === 'at'; setEditingCron(details.job); setCronForm({ name: details.job.name || '', expr: isAtSchedule ? '' : (details.job.expr || ''), message: details.job.message || '', deliver: details.job.deliver || false, channel: details.job.channel || 'telegram', to: details.job.to || '', enabled: details.job.enabled ?? true, }); } } catch (e) { console.error('L0068', e); } } else { setEditingCron(null); const defaultChannel = enabledChannels[0] || initialCronForm.channel; const defaultTo = (channelRecipients[defaultChannel] && channelRecipients[defaultChannel][0]) || ''; setCronForm({ ...initialCronForm, channel: defaultChannel, to: defaultTo }); } setIsCronModalOpen(true); } async function handleCronSubmit() { try { const action = editingCron ? 'update' : 'create'; const payload = { action, ...(editingCron && { id: editingCron.id }), ...cronForm, }; const r = await fetch(`/webui/api/cron${q}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (r.ok) { setIsCronModalOpen(false); await refreshCron(); await ui.notify({ title: t('saved'), message: t('cronSaved') }); } else { const err = await r.text(); await ui.notify({ title: t('actionFailed'), message: err }); } } catch (e) { await ui.notify({ title: t('actionFailed'), message: String(e) }); } } return (

{t('cronJobs')}

{cron.map((j) => { const schedule = formatSchedule(j, t); return (

{j.name || j.id}

{t('id')}: {j.id.slice(-6)}
{j.enabled ? ( {t('active')} ) : ( {t('paused')} )}
"{j.message}"
{schedule.label}
{schedule.value}
); })} {cron.length === 0 && (

{t('noCronJobs')}

)}
{isCronModalOpen && (
setIsCronModalOpen(false)} className="ui-overlay-strong absolute inset-0 backdrop-blur-sm" />

{editingCron ? t('editJob') : t('addJob')}