// Backend always listens on localhost:3900 — both in dev (Vite @ 3901 talking // to a separate uvicorn) and in the built .app (Tauri webview @ tauri://localhost // talking to the venv-bootstrapped sidecar). Relative fetches against // tauri://localhost don't reach the sidecar, so we hardcode the absolute host. // Port 3900 chosen to avoid common 8000 conflicts (Django/Rails/Jupyter). export const API = 'http://localhost:3900'; export class ApiError extends Error { status?: number; detail?: unknown; constructor(message: string, init: { status?: number; detail?: unknown } = {}) { super(message); this.name = 'ApiError'; this.status = init.status; this.detail = init.detail; } } export function apiUrl(path?: string): string { if (!path) return API; return path.startsWith('http') ? path : `${API}${path.startsWith('/') ? '' : '/'}${path}`; } async function readError(res: Response): Promise { const text = await res.text().catch(() => ''); try { const j = JSON.parse(text); return j.detail || j.error || text || res.statusText; } catch { return text || res.statusText; } } export async function apiFetch(path: string, opts: RequestInit = {}): Promise { const res = await fetch(apiUrl(path), opts); if (!res.ok) { const detail = await readError(res); throw new ApiError(`${res.status} ${res.statusText}: ${detail}`, { status: res.status, detail }); } return res; } export async function apiJson(path: string, opts: RequestInit = {}): Promise { const res = await apiFetch(path, opts); return res.json() as Promise; } export async function apiPost( path: string, body?: unknown, opts: RequestInit = {}, ): Promise { const init: RequestInit = { method: 'POST', ...opts }; if (body instanceof FormData) { init.body = body; } else if (body !== undefined) { init.headers = { 'Content-Type': 'application/json', ...(opts.headers as Record || {}) }; init.body = JSON.stringify(body); } return apiJson(path, init); } export async function apiDelete(path: string, opts: RequestInit = {}): Promise { return apiFetch(path, { method: 'DELETE', ...opts }); }