Files
imap-copier/web/src/api.ts
T
vasyansk f024f329fc feat: per-account cancel + folder message-count progress in log
- orchestrator: per-account cancellable context registry + CancelAccount;
  on cancel, close IMAP connections to unblock in-flight FETCH; account ends
  in 'cancelled' status with a cancelled event
- imapx: CopyDeps.OnFolder callback fires after EXAMINE with the folder's
  message count (before the long fetch) for visibility
- httpapi: POST /tasks/{id}/accounts/{accountId}/cancel
- web: per-row cancel button while running, folder event shows N messages,
  cancelled/done_with_errors status badges

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMHQTtnQtQqL8muAXHr9kd
2026-07-02 12:47:07 +07:00

112 lines
3.3 KiB
TypeScript

// REST client for the imap-copier control API.
// All requests carry the session cookie; a 401 anywhere bounces to #/login.
export type TLSMode = 'ssl' | 'starttls' | 'plain'
export interface Endpoint {
id: number
role_label: string
host: string
port: number
tls_mode: TLSMode
}
export interface Task {
id: number
name: string
src_endpoint_id: number
dst_endpoint_id: number
status: string
folder_mapping?: Record<string, string>
}
export type TestStatus = 'pending' | 'ok' | 'fail' | string
export interface Account {
id: number
src_login: string
dst_login: string
test_src_status: TestStatus
test_dst_status: TestStatus
status: string
copied: number
skipped: number
errors: number
}
export interface TaskDetail {
task: Task
accounts: Account[]
}
export class ApiError extends Error {}
export async function api<T = unknown>(path: string, opts: RequestInit = {}): Promise<T> {
const res = await fetch(path, { credentials: 'include', ...opts })
if (res.status === 401) {
location.hash = '#/login'
throw new ApiError('unauthorized')
}
if (!res.ok) {
const body = await res.text()
throw new ApiError(body || res.statusText)
}
const ct = res.headers.get('content-type') || ''
if (ct.includes('application/json')) return res.json() as Promise<T>
return res.text() as unknown as T
}
const jsonBody = (body: unknown): RequestInit => ({
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
})
export const login = (user: string, pass: string) => api('/api/login', jsonBody({ user, pass }))
export const logout = () => api('/api/logout', { method: 'POST' })
export const listEndpoints = () => api<Endpoint[]>('/api/endpoints')
export const createEndpoint = (body: { role_label: string; host: string; port: number; tls_mode: TLSMode }) =>
api<{ id: number }>('/api/endpoints', jsonBody(body))
export const updateEndpoint = (
id: number,
body: { role_label: string; host: string; port: number; tls_mode: TLSMode },
) => api(`/api/endpoints/${id}`, { ...jsonBody(body), method: 'PUT' })
export const deleteTask = (id: number) => api(`/api/tasks/${id}`, { method: 'DELETE' })
export const deleteAccount = (taskId: number, accountId: number) =>
api(`/api/tasks/${taskId}/accounts/${accountId}`, { method: 'DELETE' })
export const cancelAccount = (taskId: number, accountId: number) =>
api(`/api/tasks/${taskId}/accounts/${accountId}/cancel`, { method: 'POST' })
export const listTasks = () => api<Task[]>('/api/tasks')
export const getTask = (id: number) => api<TaskDetail>(`/api/tasks/${id}`)
export const createTask = (body: {
name: string
src_endpoint_id: number
dst_endpoint_id: number
folder_mapping?: Record<string, string>
}) => api<{ id: number }>('/api/tasks', jsonBody(body))
export const createAccount = (
id: number,
body: { src_login: string; src_pass: string; dst_login: string; dst_pass: string },
) => api<{ id: number }>(`/api/tasks/${id}/accounts`, jsonBody(body))
export const testAccounts = (id: number) => api(`/api/tasks/${id}/test`, { method: 'POST' })
export const runTask = (id: number) => api(`/api/tasks/${id}/run`, { method: 'POST' })
export const importCSV = (id: number, file: File) => {
const fd = new FormData()
fd.append('file', file)
return api<{ imported: number }>(`/api/tasks/${id}/import`, { method: 'POST', body: fd })
}