// Client de l'API CHLOVA. Same-origin en prod (le backend sert le SPA) ; // en dev, Vite proxifie /api vers le backend. export interface Asset { id: string; type: string; version: string; riskTier: string; status: string; createdAt: number; expiresAt: number | null; execCount: number; commitLink: string | null; docLink: string | null; } export interface ConversationMeta { id: string; title: string; createdAt: number; updatedAt: number; } export interface ChatMessage { role: "user" | "assistant"; content: string; ts: number; } export class ApiError extends Error { constructor( public status: number, message: string, ) { super(message); } } async function req(path: string, token: string | null, init?: RequestInit): Promise { const headers: Record = {}; // content-type seulement s'il y a un corps : sinon Fastify rejette un body // vide annoncé en application/json (DELETE, etc.). if (init?.body != null) headers["content-type"] = "application/json"; if (token) headers.authorization = `Bearer ${token}`; const res = await fetch(`/api${path}`, { ...init, headers: { ...headers, ...(init?.headers as Record) } }); if (!res.ok) { const body = (await res.json().catch(() => ({}))) as { error?: string }; throw new ApiError(res.status, body.error ?? `HTTP ${res.status}`); } return (await res.json()) as T; } export const api = { login: (user: string, password: string, totp: string) => req<{ token: string }>("/auth/login", null, { method: "POST", body: JSON.stringify({ user, password, totp }), }), chat: (token: string, message: string, conversationId?: string | null) => req<{ reply: string; conversationId: string | null }>("/chat", token, { method: "POST", body: JSON.stringify({ message, conversationId: conversationId ?? undefined }), }), conversations: (token: string) => req<{ conversations: ConversationMeta[] }>("/conversations", token), conversation: (token: string, id: string) => req<{ messages: ChatMessage[] }>(`/conversations/${encodeURIComponent(id)}`, token), deleteConversation: (token: string, id: string) => req<{ ok: boolean }>(`/conversations/${encodeURIComponent(id)}`, token, { method: "DELETE" }), review: (token: string) => req<{ assets: Asset[] }>("/review", token), approve: (token: string, id: string) => req<{ asset: Asset }>(`/review/${encodeURIComponent(id)}/approve`, token, { method: "POST" }), refuse: (token: string, id: string) => req<{ asset: Asset }>(`/review/${encodeURIComponent(id)}/refuse`, token, { method: "POST" }), state: (token: string) => req<{ phase: string; tools: number }>("/state", token), };