90 lines
No EOL
3.3 KiB
JavaScript
90 lines
No EOL
3.3 KiB
JavaScript
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' };
|
|
} |