import { useState, useEffect, useRef } from 'react' import { apiFetch, ApiError } from '@/lib/http' import { useUIStore } from '@/stores/uiStore' import { cn } from '@/lib/utils' type JobStatus = 'pending' | 'running' | 'completed' | 'failed' interface Props { source: 'jira' | 'confluence' sourceKey: string label: string lastSynced?: string } const STATUS_LABEL: Record = { pending: 'Queued…', running: 'Syncing…', completed: 'Sync complete', failed: 'Sync failed', } const STATUS_COLOR: Record = { pending: 'text-stone-400', running: 'text-blue-500', completed: 'text-green-600', failed: 'text-red-500', } export function SyncTrigger({ source, sourceKey, label, lastSynced }: Props) { const [taskId, setTaskId] = useState(null) const [jobStatus, setJobStatus] = useState(null) const [loading, setLoading] = useState(false) const addToast = useUIStore((s) => s.addToast) const pollRef = useRef | null>(null) // Poll job status until terminal state useEffect(() => { if (!taskId) return if (pollRef.current) clearInterval(pollRef.current) const poll = async () => { try { const res = await apiFetch(`/ingest/jobs/${taskId}`) const data = await res.json() as { status: JobStatus } setJobStatus(data.status) if (data.status === 'completed' || data.status === 'failed') { clearInterval(pollRef.current!) pollRef.current = null addToast({ type: data.status === 'completed' ? 'success' : 'error', message: data.status === 'completed' ? `${label} sync finished` : `${label} sync failed`, }) } } catch { /* ignore poll errors */ } } poll() // immediate first check pollRef.current = setInterval(poll, 4000) return () => { if (pollRef.current) clearInterval(pollRef.current) } }, [taskId]) // eslint-disable-line react-hooks/exhaustive-deps const trigger = async () => { setLoading(true) setTaskId(null) setJobStatus(null) try { const path = source === 'jira' ? `/jira/sync/${sourceKey}` : `/confluence/sync/${sourceKey}` const res = await apiFetch(path, { method: 'POST' }) const data = await res.json() as { task_id: string } setTaskId(data.task_id) setJobStatus('pending') addToast({ type: 'success', message: `${label} sync queued` }) } catch (err) { if (!(err instanceof ApiError)) { addToast({ type: 'error', message: `Failed to trigger ${label} sync` }) } } finally { setLoading(false) } } const isBusy = loading || jobStatus === 'pending' || jobStatus === 'running' return (

{label}

{lastSynced && !jobStatus && (

Last synced {lastSynced}

)} {jobStatus && (
{(jobStatus === 'pending' || jobStatus === 'running') && ( )}

{STATUS_LABEL[jobStatus]}

)} {taskId && (

Task {taskId.slice(0, 8)}…

)}
) }