'use client'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { format } from 'date-fns'; import { Activity, ChevronLeft, ChevronRight, RefreshCw, RotateCcw, } from 'lucide-react'; import { useTranslations } from 'next-intl'; import { useMemo, useState } from 'react'; import { toast } from 'sonner'; import { EmptyStateWithBorder } from '@/components/layout/empty'; import { ErrorInline } from '@/components/layout/error'; import { LoadingStateWithBorder } from '@/components/layout/loading'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, } from '@/components/ui/sheet'; import { Spinner } from '@/components/ui/spinner'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { AdminTaskService } from '@/lib/services/admin'; import type { ListTaskExecutionsResponse, TaskExecution, TaskExecutionStatus, } from '@/lib/services/admin'; import { cloudflareQueryKey } from '@/lib/services/openflare'; /** * Only domain-member and group sync jobs. * Uses exact asynq task_type filters (compatible with existing admin API). * Excludes cloudflare:sync_by_node and any unrelated scheduled system tasks. */ export const CLOUDFLARE_SYNC_TASK_TYPES = [ 'cloudflare:sync_member', 'cloudflare:sync_group', ] as const; const ALLOWED_TASK_TYPES = new Set(CLOUDFLARE_SYNC_TASK_TYPES); /** Per-type fetch window; merged and paginated on the client. */ const FETCH_PAGE_SIZE = 50; const PAGE_SIZE = 10; async function listCloudflareSyncExecutions(options: { page: number; status?: TaskExecutionStatus; }): Promise { const status = options.status; const responses = await Promise.all( CLOUDFLARE_SYNC_TASK_TYPES.map((taskType) => AdminTaskService.listTaskExecutions({ task_type: taskType, page: 1, page_size: FETCH_PAGE_SIZE, status, }), ), ); const merged = responses .flatMap((response) => response.items) .filter((item) => ALLOWED_TASK_TYPES.has(item.task_type)) .sort((a, b) => { const timeA = Date.parse(a.created_at) || 0; const timeB = Date.parse(b.created_at) || 0; if (timeA !== timeB) return timeB - timeA; return String(b.id).localeCompare(String(a.id), undefined, { numeric: true, }); }); const start = (options.page - 1) * PAGE_SIZE; return { items: merged.slice(start, start + PAGE_SIZE), total: merged.length, page: options.page, page_size: PAGE_SIZE, }; } function formatDateTime(value?: string | null) { if (!value) return '-'; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return format(date, 'yyyy-MM-dd HH:mm:ss'); } function formatDuration(duration: number) { if (!duration) return '-'; if (duration < 1000) return `${duration}ms`; return `${(duration / 1000).toFixed(2)}s`; } function statusVariant(status: TaskExecutionStatus) { if (status === 'failed') return 'destructive' as const; if (status === 'succeeded') return 'secondary' as const; return 'outline' as const; } export function SyncTasksPanel() { const t = useTranslations('cloudflare.tasks'); const queryClient = useQueryClient(); const [page, setPage] = useState(1); const [status, setStatus] = useState('all'); const [selectedId, setSelectedId] = useState(null); const [preview, setPreview] = useState(null); const [detailOpen, setDetailOpen] = useState(false); const executionsQuery = useQuery({ queryKey: [...cloudflareQueryKey, 'sync-executions', page, status], queryFn: () => listCloudflareSyncExecutions({ page, status: status === 'all' ? undefined : status, }), refetchInterval: (query) => { const items = query.state.data?.items ?? []; const active = items.some( (item) => item.status === 'pending' || item.status === 'running', ); return active ? 3000 : 15000; }, }); const detailQuery = useQuery({ queryKey: ['admin', 'task-execution', selectedId], queryFn: () => AdminTaskService.getTaskExecution(selectedId!), enabled: detailOpen && !!selectedId, }); const retryMutation = useMutation({ mutationFn: (id: string) => AdminTaskService.retryTaskExecution(id), onSuccess: (taskID) => { toast.success(t('retried'), { description: t('newTaskId', { id: taskID }), }); void queryClient.invalidateQueries({ queryKey: [...cloudflareQueryKey, 'sync-executions'], }); }, onError: (err: Error) => { toast.error(t('retryFailed'), { description: err.message || t('unknownError'), }); }, }); const executions = executionsQuery.data?.items ?? []; const total = executionsQuery.data?.total ?? 0; const loading = executionsQuery.isPending || executionsQuery.isFetching; const selected = detailQuery.data ?? preview; const totalPages = useMemo( () => Math.max(1, Math.ceil(total / PAGE_SIZE)), [total], ); const statusLabels: Record = { pending: t('pending'), running: t('running'), succeeded: t('succeeded'), failed: t('failed'), }; const triggerLabels: Record = { system: t('system'), manual: t('manual'), retry: t('retry'), schedule: t('schedule'), }; const openDetail = (execution: TaskExecution) => { setPreview(execution); setSelectedId(execution.id); setDetailOpen(true); }; return (
{t('title')} {t('description')}
{executionsQuery.isError ? ( void executionsQuery.refetch()} /> ) : loading && executions.length === 0 ? ( ) : executions.length === 0 ? ( ) : (
{t('columns.task')} {t('columns.status')} {t('columns.trigger')} {t('columns.duration')} {t('columns.result')} {t('columns.createdAt')} {executions.map((execution) => ( openDetail(execution)} >
{execution.task_name || execution.task_type} {execution.task_type}
{statusLabels[execution.status] || execution.status} {triggerLabels[execution.triggered_by] || execution.triggered_by} {formatDuration(execution.duration)} {execution.error_message || execution.result || '-'} {formatDateTime(execution.created_at)}
))}
)}
{t('pageSummary', { total, page, pages: totalPages })}
{t('detailTitle')} {selected?.task_name || selected?.task_type || t('defaultName')}
{detailQuery.isFetching && !selected ? ( ) : selected ? ( <>
{t('status')}
{statusLabels[selected.status] || selected.status}
{t('trigger')}
{triggerLabels[selected.triggered_by] || selected.triggered_by}
{t('retries')}
{selected.retry_count}/{selected.max_retry}
{t('duration')}
{formatDuration(selected.duration)}
{t('taskId')}
{selected.task_id}
{t('createdAt')}
{formatDateTime(selected.created_at)}
{t('finishedAt')}
{formatDateTime(selected.finished_at)}
{t('result')}
{selected.result || '-'}
{selected.error_message ? (
{t('error')}
{selected.error_message}
) : null}
Payload
                    {selected.payload || '{}'}
                  
{selected.log ? (
{t('logs')}
                      {selected.log}
                    
) : null} ) : null}
{selected?.retryable && selected.status === 'failed' ? ( ) : null}
); }