claire/src/clare/web/app/src/lib/api.js
Natalie 23712cdf4e feat(@projects): add core routing and page scaffolding
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-18 17:12:11 -07:00

128 lines
4.9 KiB
JavaScript

/**
* Typed fetch wrapper for Clare's JSON API.
*
* Every page calls these — never raw `fetch`. Centralizes:
* - URL construction (relative paths; Vite proxies in dev, same-origin in prod)
* - error translation: non-2xx → `ApiError(status, detail)`
* - JSON parsing with typed return
* - AbortSignal threading so callers can cancel
*/
export class ApiError extends Error {
status;
detail;
constructor(status, detail) {
super(`${status}: ${detail}`);
this.status = status;
this.detail = detail;
this.name = "ApiError";
}
}
async function request(method, path, options = {}) {
const url = buildUrl(path, options.params);
const init = {
method,
signal: options.signal,
headers: options.body !== undefined ? { "content-type": "application/json" } : undefined,
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
};
const resp = await fetch(url, init);
if (!resp.ok) {
let detail = resp.statusText;
try {
const j = await resp.json();
if (j && typeof j === "object" && "detail" in j) {
detail = String(j.detail);
}
}
catch {
// body wasn't json — keep statusText
}
throw new ApiError(resp.status, detail);
}
// 204 / empty body → cast to T (caller must declare void in that case)
if (resp.status === 204)
return undefined;
return (await resp.json());
}
function buildUrl(path, params) {
if (!params)
return path;
const usp = new URLSearchParams();
for (const [k, v] of Object.entries(params)) {
if (v !== undefined && v !== null)
usp.append(k, String(v));
}
const q = usp.toString();
return q ? `${path}?${q}` : path;
}
// ---------------------------------------------------------------------------
// Projects
// ---------------------------------------------------------------------------
export async function listProjects(signal) {
const r = await request("GET", "/api/v1/projects", { signal });
return r.projects;
}
export async function createProject(body, signal) {
return request("POST", "/api/v1/projects", { body, signal });
}
export async function getProject(nameOrId, signal) {
return request("GET", `/api/v1/projects/${encodeURIComponent(nameOrId)}`, { signal });
}
// ---------------------------------------------------------------------------
// Tasks
// ---------------------------------------------------------------------------
export async function listTasks(params = {}, signal) {
const r = await request("GET", "/api/v1/tasks", { params, signal });
return r.tasks;
}
export async function createTask(body, signal) {
return request("POST", "/api/v1/tasks", { body, signal });
}
export async function updateTask(taskId, body, signal) {
return request("POST", `/api/v1/tasks/${encodeURIComponent(taskId)}`, { body, signal });
}
// ---------------------------------------------------------------------------
// Assignments
// ---------------------------------------------------------------------------
export async function listAssignments(active = true, signal) {
const r = await request("GET", "/api/v1/assignments", {
params: { active: active ? 1 : 0 }, signal,
});
return r.assignments;
}
export async function createAssignment(body, signal) {
return request("POST", "/api/v1/assignments", { body, signal });
}
// ---------------------------------------------------------------------------
// Sessions + pull
// ---------------------------------------------------------------------------
export async function listSessions(signal) {
const r = await request("GET", "/api/v1/sessions", { signal });
return r.sessions;
}
export async function pull(signal) {
return request("POST", "/api/v1/pull", { signal });
}
// ---------------------------------------------------------------------------
// Broadcast
// ---------------------------------------------------------------------------
export async function broadcast(body, signal) {
return request("POST", "/api/v1/broadcast", { body, signal });
}
// ---------------------------------------------------------------------------
// Chat
// ---------------------------------------------------------------------------
export async function listChatMessages(params, signal) {
const r = await request("GET", "/api/v1/chat", { params, signal });
return r.messages;
}
export async function postChatMessage(body, signal) {
return request("POST", "/api/v1/chat", { body, signal });
}
// ---------------------------------------------------------------------------
// Autocomplete
// ---------------------------------------------------------------------------
export async function autocomplete(params, signal) {
const r = await request("GET", "/api/v1/autocomplete", { params, signal });
return r.hits;
}