initial commit
This commit is contained in:
commit
23816d00f7
71 changed files with 14497 additions and 0 deletions
90
frontend/src/lib/api.js
Normal file
90
frontend/src/lib/api.js
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
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' };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue