web-client-vite/src/lib/api.ts

292 lines
8.2 KiB
TypeScript

/**
* 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<T = any>(path: string, options: RequestInit = {}): Promise<T> {
const token = getToken();
const headers: Record<string, string> = {
"Content-Type": "application/json",
...(options.headers as Record<string, string>),
};
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<Project[]> {
return request("/api/v1/projects");
}
export async function getProject(slug: string): Promise<Project> {
return request(`/api/v1/projects/${slug}`);
}
export async function createProject(data: { name: string; slug: string; description?: string }): Promise<Project> {
return request("/api/v1/projects", { method: "POST", body: JSON.stringify(data) });
}
export async function updateProject(slug: string, data: Partial<Pick<Project, "name" | "description" | "repo_urls" | "status">>): Promise<Project> {
return request(`/api/v1/projects/${slug}`, { method: "PATCH", body: JSON.stringify(data) });
}
export async function deleteProject(slug: string): Promise<void> {
await request(`/api/v1/projects/${slug}`, { method: "DELETE" });
}
// --- Tasks ---
export async function getTasks(projectId: string): Promise<Task[]> {
return request(`/api/v1/tasks?project_id=${projectId}`);
}
export async function getTask(taskId: string): Promise<Task> {
return request(`/api/v1/tasks/${taskId}`);
}
export async function createTask(projectSlug: string, data: Partial<Task>): Promise<Task> {
return request(`/api/v1/tasks?project_slug=${projectSlug}`, {
method: "POST",
body: JSON.stringify(data),
});
}
export async function updateTask(taskId: string, data: Partial<Task>, actor?: string): Promise<Task> {
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<void> {
await request(`/api/v1/tasks/${taskId}`, { method: "DELETE" });
}
export async function takeTask(taskId: string, slug: string): Promise<Task> {
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<Task> {
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<Step[]> {
return request(`/api/v1/tasks/${taskId}/steps`);
}
export async function createStep(taskId: string, title: string): Promise<Step> {
return request(`/api/v1/tasks/${taskId}/steps`, {
method: "POST",
body: JSON.stringify({ title }),
});
}
export async function updateStep(taskId: string, stepId: string, data: Partial<Step>): Promise<Step> {
return request(`/api/v1/tasks/${taskId}/steps/${stepId}`, {
method: "PATCH",
body: JSON.stringify(data),
});
}
export async function deleteStep(taskId: string, stepId: string): Promise<void> {
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<Message[]> {
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<Message> {
return request("/api/v1/messages", { method: "POST", body: JSON.stringify(data) });
}
// --- Members ---
export async function getMembers(): Promise<Member[]> {
return request("/api/v1/members");
}
export async function getMember(slug: string): Promise<Member> {
return request(`/api/v1/members/${slug}`);
}
export async function createMember(data: {
name: string;
slug: string;
type?: string;
agent_config?: Partial<AgentConfig>;
}): Promise<MemberCreateResponse> {
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<AgentConfig>;
}): Promise<Member> {
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<void> {
await request(`/api/v1/members/${slug}/revoke-token`, { method: "POST" });
}