import { useEffect, useRef, useState, type ChangeEvent, type FormEvent } from 'react' import { cancelAccount, createAccount, deleteAccount, getTask, importCSV, runTask, testAccounts, type TaskDetail as TaskDetailData } from '../api' import { connectTaskWS, type TaskEvent } from '../ws' import { StatusBadge } from '../components/StatusBadge' import { useConfirm } from '../components/ConfirmProvider' const emptyAccount = { src_login: '', src_pass: '', dst_login: '', dst_pass: '' } // Human-readable one-line description of a task event for the log panel. function describeEvent(ev: TaskEvent): string { const d = (ev.data ?? {}) as Record const at = d.host ? `${d.login ?? ''}@${d.host}:${d.port}` : '' switch (ev.type) { case 'account_test': { const where = d.side === 'src' ? 'SOURCE' : 'DEST' const base = `${where} test ${String(d.status).toUpperCase()} — ${at}` return d.error ? `${base} — ${d.error}` : base } case 'account_started': return `START #${d.account_id}: ${d.src_login}@${d.src_host}:${d.src_port} → ${d.dst_login}@${d.dst_host}:${d.dst_port}` case 'account_done': return `DONE #${d.account_id} (${d.src_login} → ${d.dst_login}): copied ${d.copied}, skipped ${d.skipped}, errors ${d.errors}` case 'progress': return `progress #${d.account_id}: copied ${d.copied}, skipped ${d.skipped}` case 'folder': { const route = d.dst_folder && d.dst_folder !== d.folder ? ` → "${d.dst_folder}"` : '' return `folder "${d.folder}"${route}: ${d.messages ?? 0} messages — fetching (#${d.account_id})` } case 'cancelled': return `CANCELLED #${d.account_id} (${d.src_login}): copied ${d.copied ?? 0}, skipped ${d.skipped ?? 0}` case 'error': { const where = d.folder ? ` folder "${d.folder}"` : d.side ? ` (${d.side} ${at})` : '' return `ERROR #${d.account_id}${where}: ${d.error}` } case 'run_started': return `RUN started (run #${d.run_id})` case 'run_done': return `RUN finished: copied ${d.copied}, skipped ${d.skipped}, errors ${d.errors}` default: return JSON.stringify(ev.data) } } export function TaskDetail({ id }: { id: number }) { const [data, setData] = useState(null) const [notFound, setNotFound] = useState(false) const [log, setLog] = useState<{ type: string; text: string }[]>([]) const [form, setForm] = useState(emptyAccount) const [busy, setBusy] = useState<'test' | 'run' | 'add' | 'import' | 'delete' | null>(null) const confirm = useConfirm() const [error, setError] = useState(null) const fileInputRef = useRef(null) function reload() { getTask(id) .then((d) => { setData(d) setNotFound(false) }) .catch(() => setNotFound(true)) } useEffect(reload, [id]) useEffect( () => connectTaskWS(id, (ev: TaskEvent) => { setLog((l) => [{ type: ev.type, text: describeEvent(ev) }, ...l].slice(0, 300)) if (['account_started', 'account_test', 'account_done', 'progress', 'run_started', 'run_done', 'error', 'folder', 'cancelled'].includes(ev.type)) { reload() } }), [id], ) async function submitAccount(e: FormEvent) { e.preventDefault() setBusy('add') setError(null) try { await createAccount(id, form) setForm(emptyAccount) reload() } catch (err) { setError(err instanceof Error ? err.message : 'Failed to add account') } finally { setBusy(null) } } function downloadExampleCSV() { const sample = [ 'alice@source.example,SrcPass1,alice@dest.example,DstPass1', 'bob@source.example,SrcPass2,bob@dest.example,DstPass2', 'carol@source.example,SrcPass3,carol@dest.example,DstPass3', ].join('\n') + '\n' const url = URL.createObjectURL(new Blob([sample], { type: 'text/csv' })) const a = document.createElement('a') a.href = url a.download = 'imap-copier-accounts-example.csv' a.click() URL.revokeObjectURL(url) } async function onFileChosen(e: ChangeEvent) { const file = e.target.files?.[0] if (!file) return setBusy('import') setError(null) try { await importCSV(id, file) reload() } catch (err) { setError(err instanceof Error ? err.message : 'CSV import failed') } finally { setBusy(null) if (fileInputRef.current) fileInputRef.current.value = '' } } async function onDeleteAccount(accId: number, login: string) { const ok = await confirm({ title: 'Remove account', message: `Remove account "${login}" from this task?`, confirmLabel: 'Remove', danger: true, }) if (!ok) return setBusy('delete') setError(null) try { await deleteAccount(id, accId) reload() } catch (err) { setError(err instanceof Error ? err.message : 'Failed to remove account') } finally { setBusy(null) } } async function onCancelAccount(accId: number) { setError(null) try { await cancelAccount(id, accId) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to cancel account') } } async function onTest() { setBusy('test') setError(null) try { await testAccounts(id) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to start connection tests') } finally { setBusy(null) } } async function onRun() { setBusy('run') setError(null) try { await runTask(id) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to start run') } finally { setBusy(null) } } if (notFound) { return (

Task #{id} not found.

← back to tasks
) } if (!data) { return
loading task #{id}…
} const { task, accounts } = data const allTested = accounts.length > 0 && accounts.every((a) => a.test_src_status === 'ok' && a.test_dst_status === 'ok') const totals = accounts.reduce( (acc, a) => ({ copied: acc.copied + a.copied, skipped: acc.skipped + a.skipped, errors: acc.errors + a.errors }), { copied: 0, skipped: 0, errors: 0 }, ) return ( <>
← all tasks

{task.name} /// task #{task.id}

Run control
{totals.copied} copied
{totals.skipped} skipped
{totals.errors} errors
{accounts.length} accounts
{error &&
{error}
}
{!allTested && accounts.length > 0 && run unlocks once every account tests OK on both sides}
Add account
setForm({ ...form, src_login: e.target.value })} required />
setForm({ ...form, src_pass: e.target.value })} required />
setForm({ ...form, dst_login: e.target.value })} required />
setForm({ ...form, dst_pass: e.target.value })} required />
or bulk import
Event log {log.length > 0 && ( )}
{log.length === 0 ? (
awaiting events over websocket…
) : ( log.map((l, i) => (
{l.type} {l.text}
)) )}
Accounts ({accounts.length})
{accounts.length === 0 ? ( ) : ( accounts.map((a) => ( )) )}
Source Destination Src test Dst test Status Copied Skipped Errors
no accounts yet — add one or import a CSV above
{a.src_login} {a.dst_login} {a.copied} {a.skipped} {a.errors} {a.status === 'running' ? ( ) : ( )}
) }