// Tiny fetch wrapper. Reads token + investisseurId from localStorage. const BASE = import.meta.env.VITE_API_URL || '/api'; function authHeaders() { const token = localStorage.getItem('cl_token'); const investisseurId = localStorage.getItem('cl_investisseur_id'); const deviceToken = localStorage.getItem('cl_device_token'); // Même clé que WorkspaceContext.jsx (KEY_ACTIVE) — miroir de X-Investisseur-Id, // sert à flêcher côté serveur les nouvelles lignes vers le workspace actif // (Étape 2a du retrofit workspace_id, cf. workspaceScope.js). const workspaceId = localStorage.getItem('cl_active_workspace_id'); const h = {}; if (token) h['Authorization'] = `Bearer ${token}`; if (investisseurId) h['X-Investisseur-Id'] = investisseurId; if (deviceToken) h['X-Device-Token'] = deviceToken; if (workspaceId) h['X-Workspace-Id'] = workspaceId; return h; } async function handle(res) { if (res.status === 204) return null; const text = await res.text(); let body; try { body = text ? JSON.parse(text) : null; } catch { body = text; } if (!res.ok) { const msg = (body && body.error) || res.statusText || 'Request failed'; const err = new Error(msg); err.status = res.status; err.code = body && body.code; err.details = body && body.details; throw err; } return body; } export const api = { get: (path, params) => { const qs = params ? '?' + new URLSearchParams( Object.entries(params).filter(([, v]) => v !== undefined && v !== null && v !== '') ).toString() : ''; return fetch(BASE + path + qs, { headers: authHeaders(), cache: 'no-store' }).then(handle); }, post: (path, body) => fetch(BASE + path, { method: 'POST', headers: { 'Content-Type': 'application/json', ...authHeaders() }, body: JSON.stringify(body), }).then(handle), put: (path, body) => fetch(BASE + path, { method: 'PUT', headers: { 'Content-Type': 'application/json', ...authHeaders() }, body: JSON.stringify(body), }).then(handle), patch: (path, body) => fetch(BASE + path, { method: 'PATCH', headers: { 'Content-Type': 'application/json', ...authHeaders() }, body: JSON.stringify(body), }).then(handle), del: (path, body) => fetch(BASE + path, { method: 'DELETE', headers: body ? { 'Content-Type': 'application/json', ...authHeaders() } : authHeaders(), ...(body ? { body: JSON.stringify(body) } : {}), }).then(handle), upload: (path, formData) => fetch(BASE + path, { method: 'POST', body: formData, headers: authHeaders() }).then(handle), postForm: (path, formData) => fetch(BASE + path, { method: 'POST', body: formData, headers: authHeaders() }).then(handle), /** Flux SSE (Server-Sent Events) en POST — utilisé par l'assistant IA * (POST /api/ai/chat). EventSource ne permet pas d'en-têtes personnalisés * (Authorization, X-Investisseur-Id) : on lit donc le flux manuellement * via fetch + ReadableStream, en parsant les trames "event:"/"data:" * séparées par une ligne vide, au format écrit par le backend. Générateur * asynchrone : `for await (const {event, data} of api.stream(...))`. */ stream: async function* (path, body) { const res = await fetch(BASE + path, { method: 'POST', headers: { 'Content-Type': 'application/json', ...authHeaders() }, body: JSON.stringify(body), }); if (!res.ok || !res.body) { let msg = res.statusText || 'Requête échouée'; try { const text = await res.text(); const parsed = text ? JSON.parse(text) : null; if (parsed && parsed.error) msg = parsed.error; } catch { /* corps non-JSON, on garde statusText */ } const err = new Error(msg); err.status = res.status; throw err; } const reader = res.body.getReader(); const decoder = new TextDecoder(); let buf = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buf += decoder.decode(value, { stream: true }); let sepIdx; while ((sepIdx = buf.indexOf('\n\n')) !== -1) { const frame = buf.slice(0, sepIdx); buf = buf.slice(sepIdx + 2); let event = 'message'; const dataLines = []; for (const line of frame.split('\n')) { if (line.startsWith('event:')) event = line.slice(6).trim(); else if (line.startsWith('data:')) dataLines.push(line.slice(5).trim()); } if (dataLines.length === 0) continue; let data; try { data = JSON.parse(dataLines.join('\n')); } catch { data = dataLines.join('\n'); } yield { event, data }; } } }, blob: (path) => fetch(BASE + path, { headers: authHeaders(), cache: 'no-store' }).then(async res => { if (!res.ok) { const t = await res.text(); throw new Error(t || res.statusText); } return res.blob(); }), exportUrl: (path, params) => { const qs = params ? '?' + new URLSearchParams(params).toString() : ''; const token = localStorage.getItem('cl_token'); return BASE + path + qs + (token ? `${qs ? '&' : '?'}token=${token}` : ''); }, };