/** * API client for Team Board Tracker. */ const API_BASE = import.meta.env.VITE_API_URL!; // Required — set in .env function getToken(): string | null { if (typeof window === "undefined") return null; return localStorage.getItem("tb_token"); } export function getCurrentSlug(): string { const token = getToken(); if (!token) return "unknown"; try { const payload = JSON.parse(atob(token.split(".")[1])); return payload.slug || payload.sub || "unknown"; } catch { return "unknown"; } } async function request(path: string, options: RequestInit = {}): Promise { const token = getToken(); const headers: Record = { "Content-Type": "application/json", ...(options.headers as Record), }; if (token) headers["Authorization"] = `Bearer ${token}`; const res = await fetch(`${API_BASE}${path}`, { ...options, headers }); if (!res.ok) { if (res.status === 401 && typeof window !== "undefined") { localStorage.removeItem("tb_token"); window.location.href = "/login"; throw new Error("Unauthorized"); } const err = await res.json().catch(() => ({ error: res.statusText })); throw new Error(err.error || `HTTP ${res.status}`); } if (res.status === 204) return {} as T; return res.json(); } // --- Types --- export interface AgentConfig { capabilities: string[]; chat_listen: string; task_listen: string; prompt: string | null; model: string | null; } export interface Member { id: string; name: string; slug: string; type: "human" | "agent"; role: string; status: string; avatar_url: string | null; agent_config: AgentConfig | null; token?: string | null; } export interface MemberCreateResponse extends Member { token?: string; } export interface Project { id: string; name: string; slug: string; description: string | null; repo_urls: string[]; status: string; task_counter: number; chat_id: string | null; } export interface Step { id: string; title: string; done: boolean; position: number; } export interface Task { id: string; project_id: string; parent_id: string | null; number: number; key: string; title: string; description: string | null; type: string; status: string; priority: string; labels: string[]; assignee_slug: string | null; reviewer_slug: string | null; watchers: string[]; depends_on: string[]; position: number; time_spent: number; steps: Step[]; } export interface Attachment { id: string; filename: string; mime_type: string | null; size: number; } export interface Message { id: string; chat_id: string | null; task_id: string | null; parent_id: string | null; author_type: string; author_slug: string; content: string; mentions: string[]; voice_url: string | null; attachments: Attachment[]; created_at: string; } // --- Auth --- export async function login(login: string, password: string) { return request("/api/v1/auth/login", { method: "POST", body: JSON.stringify({ login, password }), }); } // --- Projects --- export async function getProjects(): Promise { return request("/api/v1/projects"); } export async function getProject(slug: string): Promise { return request(`/api/v1/projects/${slug}`); } export async function createProject(data: { name: string; slug: string; description?: string }): Promise { return request("/api/v1/projects", { method: "POST", body: JSON.stringify(data) }); } export async function updateProject(slug: string, data: Partial>): Promise { return request(`/api/v1/projects/${slug}`, { method: "PATCH", body: JSON.stringify(data) }); } export async function deleteProject(slug: string): Promise { await request(`/api/v1/projects/${slug}`, { method: "DELETE" }); } // --- Tasks --- export async function getTasks(projectId: string): Promise { return request(`/api/v1/tasks?project_id=${projectId}`); } export async function getTask(taskId: string): Promise { return request(`/api/v1/tasks/${taskId}`); } export async function createTask(projectSlug: string, data: Partial): Promise { return request(`/api/v1/tasks?project_slug=${projectSlug}`, { method: "POST", body: JSON.stringify(data), }); } export async function updateTask(taskId: string, data: Partial, actor?: string): Promise { const qs = actor ? `?actor=${encodeURIComponent(actor)}` : ""; return request(`/api/v1/tasks/${taskId}${qs}`, { method: "PATCH", body: JSON.stringify(data) }); } export async function deleteTask(taskId: string): Promise { await request(`/api/v1/tasks/${taskId}`, { method: "DELETE" }); } export async function takeTask(taskId: string, slug: string): Promise { return request(`/api/v1/tasks/${taskId}/take?slug=${slug}`, { method: "POST" }); } export async function rejectTask(taskId: string, reason: string): Promise<{ok: boolean; reason: string; old_assignee: string}> { return request(`/api/v1/tasks/${taskId}/reject`, { method: "POST", body: JSON.stringify({ reason }) }); } export async function assignTask(taskId: string, assigneeSlug: string): Promise { return request(`/api/v1/tasks/${taskId}/assign`, { method: "POST", body: JSON.stringify({ assignee_slug: assigneeSlug }) }); } export async function watchTask(taskId: string, slug: string): Promise<{ok: boolean; watchers: string[]}> { return request(`/api/v1/tasks/${taskId}/watch?slug=${slug}`, { method: "POST" }); } export async function unwatchTask(taskId: string, slug: string): Promise<{ok: boolean; watchers: string[]}> { return request(`/api/v1/tasks/${taskId}/watch?slug=${slug}`, { method: "DELETE" }); } // --- Steps --- export async function getSteps(taskId: string): Promise { return request(`/api/v1/tasks/${taskId}/steps`); } export async function createStep(taskId: string, title: string): Promise { return request(`/api/v1/tasks/${taskId}/steps`, { method: "POST", body: JSON.stringify({ title }), }); } export async function updateStep(taskId: string, stepId: string, data: Partial): Promise { return request(`/api/v1/tasks/${taskId}/steps/${stepId}`, { method: "PATCH", body: JSON.stringify(data), }); } export async function deleteStep(taskId: string, stepId: string): Promise { await request(`/api/v1/tasks/${taskId}/steps/${stepId}`, { method: "DELETE" }); } // --- Messages (unified: chat + task comments) --- export async function getMessages(params: { chat_id?: string; task_id?: string; limit?: number; offset?: number }): Promise { const qs = new URLSearchParams(); if (params.chat_id) qs.set("chat_id", params.chat_id); if (params.task_id) qs.set("task_id", params.task_id); if (params.limit) qs.set("limit", String(params.limit)); if (params.offset) qs.set("offset", String(params.offset)); return request(`/api/v1/messages?${qs}`); } export async function sendMessage(data: { chat_id?: string; task_id?: string; content: string; mentions?: string[]; }): Promise { return request("/api/v1/messages", { method: "POST", body: JSON.stringify(data) }); } // --- Members --- export async function getMembers(): Promise { return request("/api/v1/members"); } export async function getMember(slug: string): Promise { return request(`/api/v1/members/${slug}`); } export async function createMember(data: { name: string; slug: string; type?: string; agent_config?: Partial; }): Promise { return request("/api/v1/members", { method: "POST", body: JSON.stringify(data) }); } export async function updateMember(slug: string, data: { name?: string; role?: string; status?: string; agent_config?: Partial; }): Promise { return request(`/api/v1/members/${slug}`, { method: "PATCH", body: JSON.stringify(data) }); } export async function regenerateToken(slug: string): Promise<{ token: string }> { return request(`/api/v1/members/${slug}/regenerate-token`, { method: "POST" }); } export async function revokeToken(slug: string): Promise { await request(`/api/v1/members/${slug}/revoke-token`, { method: "POST" }); }