const RAW_BASE = import.meta.env.VITE_API_BASE ?? ''; const DEFAULT_TIMEOUT = 15000; /** API error carrying the HTTP status and the server's structured error code. */ export class ApiError extends Error { constructor(message, status, code) { super(message); this.name = 'ApiError'; this.status = status; this.code = code; } } async function request(path, { method = 'GET', body, headers = {}, signal } = {}) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT); if (signal) signal.addEventListener('abort', () => controller.abort(), { once: true }); try { const response = await fetch(`${RAW_BASE}${path}`, { method, headers: { Accept: 'application/json', ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}), ...headers, }, body: body === undefined ? undefined : JSON.stringify(body), signal: controller.signal, }); if (response.status === 204) return null; const text = await response.text(); let payload = null; if (text) { try { payload = JSON.parse(text); } catch { payload = null; } } if (!response.ok) { const code = payload?.error?.code; const message = payload?.error?.message ?? `${response.status} ${response.statusText}`; throw new ApiError(message, response.status, code); } return payload; } catch (error) { if (error instanceof ApiError) throw error; if (error.name === 'AbortError') { throw new ApiError('Request timed out or was cancelled', 0, 'timeout'); } throw new ApiError(error.message || 'Network unreachable', 0, 'network'); } finally { clearTimeout(timer); } } export const api = { health: () => request('/api/health'), roles: () => request('/api/roles'), listDocs: (query = '') => request(`/api/docs${query ? `?q=${encodeURIComponent(query)}` : ''}`), listDocsDetailed: () => request('/api/docs?counts=1'), getDoc: (slug) => request(`/api/docs/${encodeURIComponent(slug)}?format=json`), saveDoc: (slug, content, title) => request(`/api/docs/${encodeURIComponent(slug)}`, { method: 'POST', body: { content, title }, }), deleteDoc: (slug) => request(`/api/docs/${encodeURIComponent(slug)}`, { method: 'DELETE' }), listComments: (slug) => request(`/api/docs/${encodeURIComponent(slug)}/comments`), createComment: (slug, comment) => request(`/api/docs/${encodeURIComponent(slug)}/comments`, { method: 'POST', body: comment }), deleteComment: (slug, id) => request(`/api/docs/${encodeURIComponent(slug)}/comments/${encodeURIComponent(id)}`, { method: 'DELETE', }), }; /** Audience segments mirrored from the Go engine; badges stay authoritative server-side. */ export const ROLES = [ { role: 'user', badge: '🟢', label: 'User', hex: '#22c55e', description: 'Day-to-day workflow guidance' }, { role: 'operator', badge: '🔵', label: 'Operator', hex: '#3b82f6', description: 'Deployment, health and rollback' }, { role: 'developer', badge: '🔴', label: 'Developer', hex: '#ef4444', description: 'Implementation and API contracts' }, ]; export function roleMeta(role) { return ROLES.find((r) => r.role === role) ?? { role, badge: '⚪', label: role, hex: '#94a3b8' }; }