'use client' import { useState, useEffect, useCallback } from 'react' import { useTranslations } from 'next-intl' import { Button } from '@/components/ui/button' interface GitHubLabel { name: string color?: string } interface GitHubIssue { number: number title: string body: string | null state: 'open' | 'closed' labels: GitHubLabel[] assignee: { login: string } | null html_url: string created_at: string updated_at: string } interface SyncRecord { id: number repo: string last_synced_at: number issue_count: number sync_direction: string status: string error: string | null created_at: number } interface LinkedTask { id: number title: string status: string priority: string metadata: { github_repo?: string github_issue_number?: number github_issue_url?: string github_synced_at?: string github_state?: string } } export function GitHubSyncPanel() { const t = useTranslations('githubSync') // Connection status const [tokenStatus, setTokenStatus] = useState<{ connected: boolean; user?: string } | null>(null) // Import form const [repo, setRepo] = useState('') const [labelFilter, setLabelFilter] = useState('') const [stateFilter, setStateFilter] = useState<'open' | 'closed' | 'all'>('open') const [assignAgent, setAssignAgent] = useState('') const [agents, setAgents] = useState<{ name: string }[]>([]) // Preview const [previewIssues, setPreviewIssues] = useState([]) const [previewing, setPreviewing] = useState(false) // Sync const [syncing, setSyncing] = useState(false) const [syncResult, setSyncResult] = useState<{ imported: number; skipped: number; errors: number } | null>(null) // Sync history const [syncHistory, setSyncHistory] = useState([]) // Linked tasks const [linkedTasks, setLinkedTasks] = useState([]) // Two-way sync const [projects, setProjects] = useState>([]) const [syncingProjectId, setSyncingProjectId] = useState(null) // Feedback const [feedback, setFeedback] = useState<{ ok: boolean; text: string } | null>(null) const [loading, setLoading] = useState(true) const showFeedback = (ok: boolean, text: string) => { setFeedback({ ok, text }) setTimeout(() => setFeedback(null), 4000) } // Check GitHub token status const checkToken = useCallback(async () => { try { const res = await fetch('/api/integrations', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'test', integrationId: 'github' }), signal: AbortSignal.timeout(8000), }) const data = await res.json() setTokenStatus({ connected: data.ok === true, user: data.detail?.replace('User: ', ''), }) } catch { setTokenStatus({ connected: false }) } }, []) // Fetch sync history const fetchSyncHistory = useCallback(async () => { try { const res = await fetch('/api/github', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'status' }), signal: AbortSignal.timeout(8000), }) if (res.ok) { const data = await res.json() setSyncHistory(data.syncs || []) } } catch { /* ignore */ } }, []) // Fetch linked tasks const fetchLinkedTasks = useCallback(async () => { try { const res = await fetch('/api/tasks?limit=200', { signal: AbortSignal.timeout(8000) }) if (res.ok) { const data = await res.json() const linked = (data.tasks || []).filter( (t: LinkedTask) => t.metadata?.github_repo ) setLinkedTasks(linked) } } catch { /* ignore */ } }, []) // Fetch projects for two-way sync const fetchProjects = useCallback(async () => { try { const res = await fetch('/api/projects', { signal: AbortSignal.timeout(8000) }) if (res.ok) { const data = await res.json() setProjects(data.projects || []) } } catch { /* ignore */ } }, []) // Fetch agents for assign dropdown const fetchAgents = useCallback(async () => { try { const res = await fetch('/api/agents', { signal: AbortSignal.timeout(8000) }) if (res.ok) { const data = await res.json() setAgents((data.agents || []).map((a: any) => ({ name: a.name }))) } } catch { /* ignore */ } }, []) useEffect(() => { Promise.allSettled([checkToken(), fetchSyncHistory(), fetchLinkedTasks(), fetchAgents(), fetchProjects()]) .finally(() => setLoading(false)) }, [checkToken, fetchSyncHistory, fetchLinkedTasks, fetchAgents, fetchProjects]) // Preview issues from GitHub const handlePreview = async () => { if (!repo) { showFeedback(false, t('enterRepo')) return } setPreviewing(true) setPreviewIssues([]) setSyncResult(null) try { const params = new URLSearchParams({ action: 'issues', repo, state: stateFilter }) if (labelFilter) params.set('labels', labelFilter) const res = await fetch(`/api/github?${params}`) const data = await res.json() if (res.ok) { setPreviewIssues(data.issues || []) if (data.issues?.length === 0) showFeedback(true, t('noIssuesFound')) } else { showFeedback(false, data.error || t('failedFetchIssues')) } } catch { showFeedback(false, t('networkError')) } finally { setPreviewing(false) } } // Import issues as tasks const handleImport = async () => { if (!repo) return setSyncing(true) setSyncResult(null) try { const res = await fetch('/api/github', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'sync', repo, labels: labelFilter || undefined, state: stateFilter, assignAgent: assignAgent || undefined, }), }) const data = await res.json() if (res.ok) { setSyncResult({ imported: data.imported, skipped: data.skipped, errors: data.errors }) showFeedback(true, t('importedFeedback', { imported: data.imported, skipped: data.skipped })) setPreviewIssues([]) fetchSyncHistory() fetchLinkedTasks() } else { showFeedback(false, data.error || t('syncFailed')) } } catch { showFeedback(false, t('networkError')) } finally { setSyncing(false) } } // Two-way sync handlers const handleToggleSync = async (project: typeof projects[number]) => { try { const res = await fetch(`/api/projects/${project.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ github_sync_enabled: !project.github_sync_enabled }), }) if (res.ok) { await fetchProjects() showFeedback(true, `Sync ${project.github_sync_enabled ? 'disabled' : 'enabled'} for ${project.name}`) } else { const data = await res.json() showFeedback(false, data.error || t('failedToggleSync')) } } catch { showFeedback(false, t('networkError')) } } const handleSyncProject = async (projectId: number) => { setSyncingProjectId(projectId) try { const res = await fetch('/api/github/sync', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'trigger', project_id: projectId }), }) const data = await res.json() if (res.ok) { showFeedback(true, data.message || 'Sync triggered') fetchSyncHistory() } else { showFeedback(false, data.error || t('syncFailed')) } } catch { showFeedback(false, t('networkError')) } finally { setSyncingProjectId(null) } } const handleSyncAll = async () => { setSyncingProjectId(-1) try { const res = await fetch('/api/github/sync', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'trigger-all' }), }) const data = await res.json() if (res.ok) { showFeedback(true, data.message || 'Sync triggered for all projects') fetchSyncHistory() } else { showFeedback(false, data.error || t('syncFailed')) } } catch { showFeedback(false, t('networkError')) } finally { setSyncingProjectId(null) } } if (loading) { return (
{t('loading')}
) } return (
{/* Header */}

{t('title')}

{t('subtitle')}

{/* Connection status badge */}
{tokenStatus?.connected ? t('connectedAs', { user: tokenStatus.user || 'connected' }) : t('notConfigured')}
{/* Not configured notice */} {tokenStatus && !tokenStatus.connected && (
!

{t('tokenNotConfigured')}

{t.rich('tokenNotConfiguredDesc', { code: (chunks) => {chunks} })}

)} {/* Feedback */} {feedback && (
{feedback.text}
)} {/* Sync result banner */} {syncResult && (
{t('syncResultImported', { count: syncResult.imported })} {t('syncResultSkipped', { count: syncResult.skipped })} {syncResult.errors > 0 && {t('syncResultErrors', { count: syncResult.errors })}}
)} {/* Import Issues Form */}

{t('importIssues')}

{/* Repo input */}
setRepo(e.target.value)} placeholder={t('placeholderRepo')} className="w-full px-3 py-1.5 text-sm rounded-md border border-border bg-background text-foreground placeholder:text-muted-foreground/50 focus:outline-none focus:ring-1 focus:ring-primary" />
{/* Label filter */}
setLabelFilter(e.target.value)} placeholder={t('placeholderLabels')} className="w-full px-3 py-1.5 text-sm rounded-md border border-border bg-background text-foreground placeholder:text-muted-foreground/50 focus:outline-none focus:ring-1 focus:ring-primary" />
{/* State filter */}
{/* Assign to agent */}
{/* Actions */}
{/* Two-Way Sync */}

{t('twoWaySync')}

{projects.filter(p => p.github_repo).map(project => (
{project.name}
{project.github_repo}
{project.github_sync_enabled && ( )}
))} {projects.filter(p => p.github_repo).length === 0 && (
{t('noProjectsLinked')}
)}
{/* Issue Preview Table */} {previewIssues.length > 0 && (

{t('previewTitle', { count: previewIssues.length })}

{previewIssues.map(issue => ( ))}
{t('colNumber')} {t('colTitle')} {t('colLabels')} {t('colState')} {t('colCreated')}
{issue.number} {issue.title}
{issue.labels.map(l => ( {l.name} ))}
{issue.state} {new Date(issue.created_at).toLocaleDateString()}
)} {/* Sync History */}

{t('syncHistory')}

{syncHistory.length > 0 ? (
{syncHistory.map(sync => ( ))}
{t('colRepo')} {t('colIssues')} {t('colStatus')} {t('colSyncedAt')}
{sync.repo} {sync.issue_count} {sync.status} {new Date(sync.created_at * 1000).toLocaleString()}
) : (
{t('noSyncHistory')}
)}
{/* Linked Tasks */}

{linkedTasks.length > 0 ? t('linkedTasksWithCount', { count: linkedTasks.length }) : t('linkedTasks')}

{linkedTasks.length > 0 ? (
{linkedTasks.map(task => ( ))}
{t('colTask')} {t('colStatus')} {t('colPriority')} {t('colGitHub')} {t('colSynced')}
{task.title} {task.status} {task.priority} {task.metadata.github_issue_url ? ( {task.metadata.github_repo}#{task.metadata.github_issue_number} ) : ( )} {task.metadata.github_synced_at ? new Date(task.metadata.github_synced_at).toLocaleDateString() : '—'}
) : (
{t('noLinkedTasks')}
)}
) }