import React, { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Check, RefreshCw } from 'lucide-react'; import { useAppContext } from '../context/AppContext'; import { formatLocalDateTime } from '../utils/time'; type TaskAuditItem = { task_id?: string; time?: string; channel?: string; session?: string; chat_id?: string; sender_id?: string; status?: string; source?: string; idle_run?: boolean; block_reason?: string; last_pause_reason?: string; last_pause_at?: string; duration_ms?: number; retry_count?: number; attempts?: number; error?: string; provider?: string; model?: string; input_preview?: string; logs?: string[]; media_items?: Array<{ source?: string; type?: string; ref?: string; path?: string; channel?: string }>; [key: string]: any; }; type NodeDispatchItem = { time?: string; node?: string; action?: string; ok?: boolean; used_transport?: string; fallback_from?: string; duration_ms?: number; error?: string; artifact_count?: number; artifact_kinds?: string[]; artifacts?: any[]; [key: string]: any; }; function dataUrlForArtifact(artifact: any) { const mime = String(artifact?.mime_type || '').trim() || 'application/octet-stream'; const content = String(artifact?.content_base64 || '').trim(); if (!content) return ''; return `data:${mime};base64,${content}`; } function formatBytes(value: unknown) { const size = Number(value || 0); if (!Number.isFinite(size) || size <= 0) return '-'; if (size < 1024) return `${size} B`; if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`; return `${(size / (1024 * 1024)).toFixed(1)} MB`; } const TaskAudit: React.FC = () => { const { t } = useTranslation(); const { q } = useAppContext(); const [items, setItems] = useState([]); const [selected, setSelected] = useState(null); const [nodeItems, setNodeItems] = useState([]); const [selectedNode, setSelectedNode] = useState(null); const [loading, setLoading] = useState(false); const [sourceFilter, setSourceFilter] = useState('all'); const [statusFilter, setStatusFilter] = useState('all'); const fetchData = async () => { setLoading(true); try { const taskURL = `/webui/api/task_queue${q ? `${q}&limit=300` : '?limit=300'}`; const nodeURL = `/webui/api/node_dispatches${q ? `${q}&limit=150` : '?limit=150'}`; const [taskResp, nodeResp] = await Promise.all([fetch(taskURL), fetch(nodeURL)]); if (!taskResp.ok) throw new Error(await taskResp.text()); if (!nodeResp.ok) throw new Error(await nodeResp.text()); const taskJSON = await taskResp.json(); const nodeJSON = await nodeResp.json(); const arr = Array.isArray(taskJSON.items) ? taskJSON.items : []; const sorted = arr.sort((a: any, b: any) => String(b.time || '').localeCompare(String(a.time || ''))); setItems(sorted); if (sorted.length > 0) setSelected(sorted[0]); const nodeArr = Array.isArray(nodeJSON.items) ? nodeJSON.items : []; setNodeItems(nodeArr); if (nodeArr.length > 0) setSelectedNode(nodeArr[0]); } catch (e) { console.error(e); setItems([]); setSelected(null); setNodeItems([]); setSelectedNode(null); } finally { setLoading(false); } }; useEffect(() => { fetchData(); }, [q]); const filteredItems = useMemo(() => items.filter((it) => { if (sourceFilter !== 'all' && String(it.source || '-') !== sourceFilter) return false; if (statusFilter !== 'all' && String(it.status || '-') !== statusFilter) return false; return true; }), [items, sourceFilter, statusFilter]); const selectedPretty = useMemo(() => selected ? JSON.stringify(selected, null, 2) : '', [selected]); return (

{t('taskAudit')}

{t('taskQueue')}
{filteredItems.length === 0 ? (
{t('noTaskAudit')}
) : filteredItems.map((it, idx) => { const active = selected?.task_id === it.task_id && selected?.time === it.time; return ( ); })}
{t('taskDetail')}
{!selected ? (
{t('selectTask')}
) : ( <>
{t('taskId')}
{selected.task_id}
{t('status')}
{selected.status}
{t('source')}
{selected.source || '-'}
{t('duration')}
{selected.duration_ms || 0}ms
{t('channel')}
{selected.channel}
{t('session')}
{selected.session}
{t('provider')}
{selected.provider || '-'}
{t('model')}
{selected.model || '-'}
{t('time')}
{formatLocalDateTime(selected.time)}
{t('inputPreview')}
{selected.input_preview || '-'}
{t('error')}
{selected.error || '-'}
{t('blockReason')}
{selected.block_reason || '-'}
{t('lastPauseReason')}
{selected.last_pause_reason || '-'}
{t('lastPauseAt')}
{formatLocalDateTime(selected.last_pause_at)}
{t('taskLogs')}
{Array.isArray(selected.logs) && selected.logs.length ? selected.logs.join('\n') : '-'}
{t('mediaSources')}
{Array.isArray(selected.media_items) && selected.media_items.length > 0 ? (
{selected.media_items.map((m, i) => (
[{m.channel || '-'}] {m.source || '-'} / {m.type || '-'} · {m.path || m.ref || '-'}
))}
) : '-'}
{t('rawJson')}
{selectedPretty}
)}
{t('dashboardNodeDispatches')}
{nodeItems.length === 0 ? (
{t('dashboardNodeDispatchesEmpty')}
) : nodeItems.map((it, idx) => { const active = selectedNode?.time === it.time && selectedNode?.node === it.node && selectedNode?.action === it.action; return ( ); })}
{!selectedNode ? (
{t('selectTask')}
) : ( <>
{t('nodeP2P')}
{selectedNode.node || '-'}
{t('action')}
{selectedNode.action || '-'}
{t('dashboardNodeDispatchTransport')}
{selectedNode.used_transport || '-'}
{t('dashboardNodeDispatchFallback')}
{selectedNode.fallback_from || '-'}
{t('duration')}
{selectedNode.duration_ms || 0}ms
{t('status')}
{selectedNode.ok ? 'ok' : 'error'}
{t('error')}
{selectedNode.error || '-'}
{t('dashboardNodeDispatchArtifactPreview')}
{Array.isArray(selectedNode.artifacts) && selectedNode.artifacts.length > 0 ? selectedNode.artifacts.map((artifact, artifactIndex) => { const kind = String(artifact?.kind || '').trim().toLowerCase(); const mime = String(artifact?.mime_type || '').trim().toLowerCase(); const isImage = kind === 'image' || mime.startsWith('image/'); const isVideo = kind === 'video' || mime.startsWith('video/'); const dataUrl = dataUrlForArtifact(artifact); return (
{String(artifact?.name || artifact?.source_path || `artifact-${artifactIndex + 1}`)}
{[artifact?.kind, artifact?.mime_type, formatBytes(artifact?.size_bytes)].filter(Boolean).join(' · ')}
{isImage && dataUrl && {String(artifact?.name} {isVideo && dataUrl &&
); }) : (
-
)}
{t('rawJson')}
{JSON.stringify(selectedNode, null, 2)}
)}
); }; export default TaskAudit;