/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { useQuery } from '@tanstack/react-query' import { ListChecks, RefreshCw } from 'lucide-react' import { useTranslation } from 'react-i18next' import { ErrorState } from '@/components/error-state' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Progress } from '@/components/ui/progress' import { Skeleton } from '@/components/ui/skeleton' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { listSystemTasks } from '@/features/system-settings/api' import type { SystemTask, SystemTaskStatus, } from '@/features/system-settings/types' import { toIntlLocale } from '@/i18n/languages' import { formatTimestampRelative, formatTimestampToDate } from '@/lib/format' import { cn } from '@/lib/utils' const TASK_LIMIT = 20 const ACTIVE_POLL_INTERVAL_MS = 8000 const STATUS_VARIANT: Record = { pending: 'secondary', running: 'secondary', succeeded: 'secondary', failed: 'destructive', } const STATUS_CLASS_NAME: Record = { pending: 'bg-amber-50 text-amber-700 dark:bg-amber-500/15 dark:text-amber-300', running: 'bg-sky-50 text-sky-700 dark:bg-sky-500/15 dark:text-sky-300 [&_span]:bg-sky-500', succeeded: 'bg-emerald-50 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300', failed: '', } const STATUS_DOT_CLASS_NAME: Record = { pending: 'bg-amber-500', running: 'bg-sky-500', succeeded: 'bg-emerald-500', failed: 'bg-destructive', } const PROGRESS_BAR_CLASS_NAME: Record = { pending: '[&_[data-slot=progress-indicator]]:bg-amber-500', running: '[&_[data-slot=progress-indicator]]:bg-sky-500', succeeded: '[&_[data-slot=progress-indicator]]:bg-emerald-500', failed: '[&_[data-slot=progress-indicator]]:bg-destructive', } // Maps backend system task type constants to i18n source keys. Unknown/future // types fall back to their raw identifier so the panel never shows blank. const TYPE_LABEL: Record = { log_cleanup: 'Log cleanup', channel_test: 'Batch channel test', model_update: 'Batch upstream model update', midjourney_poll: 'Drawing task polling', async_task_poll: 'Async task polling', } const TYPE_DISPLAY_ID: Record = { midjourney_poll: 'drawing_task_poll', } function isActiveStatus(status: SystemTaskStatus) { return status === 'pending' || status === 'running' } function getProgress(task: SystemTask): number | null { const progress = (task.state as { progress?: unknown } | undefined)?.progress if (typeof progress !== 'number' || Number.isNaN(progress)) return null return Math.min(100, Math.max(0, progress)) } type SystemTasksTableProps = { tasks: SystemTask[] } function SystemTasksTable(props: SystemTasksTableProps) { const { t, i18n } = useTranslation() return (
{t('Type')} {t('Status')} {t('Progress')} {t('Executor')} {t('Updated')} {t('Detail')} {props.tasks.map((task) => { const progress = getProgress(task) return (
{t(TYPE_LABEL[task.type] ?? task.type)}
{TYPE_DISPLAY_ID[task.type] ?? task.type}
{progress === null ? '-' : `${progress}%`}
{task.locked_by || '-'} {formatTimestampRelative( task.updated_at, 'seconds', toIntlLocale(i18n.language) )} {task.error || '-'}
) })}
) } export function SystemTasksPanel() { const { t } = useTranslation() const tasksQuery = useQuery({ queryKey: ['system-info', 'system-tasks'], queryFn: async () => { const res = await listSystemTasks(TASK_LIMIT) if (!res.success || !Array.isArray(res.data)) { throw new Error(res.message || t('We could not load system tasks.')) } return res.data }, staleTime: 30 * 1000, retry: false, refetchInterval: (query) => query.state.data?.some((task) => isActiveStatus(task.status)) ? ACTIVE_POLL_INTERVAL_MS : false, }) const tasks = tasksQuery.data ?? [] const loading = tasksQuery.isLoading const refreshing = tasksQuery.isFetching && !tasksQuery.isLoading const hasActiveTasks = tasks.some((task) => isActiveStatus(task.status)) const activeTasks = tasks.filter((task) => isActiveStatus(task.status)) const historyTasks = tasks.filter((task) => !isActiveStatus(task.status)) return (

{t('System Tasks')}

{t( 'Recent maintenance tasks running across instances and their execution status.' )}

{loading ? (
{Array.from({ length: 4 }).map((_, i) => ( ))}
) : tasksQuery.isError ? ( { void tasksQuery.refetch() }} className='min-h-[260px]' /> ) : tasks.length === 0 ? (

{t('No system tasks yet.')}

) : (

{t('Active Tasks')}

{t('Tasks currently pending or running.')}

{activeTasks.length}
{activeTasks.length > 0 ? ( ) : (
{t('No active system tasks.')}
)}

{t('Task History')}

{t('Recently completed or failed system task runs.')}

{historyTasks.length}
{historyTasks.length > 0 ? ( ) : (
{t('No historical system tasks.')}
)}
)}
) }