import type { Board, Card, CardHistoryResponse, CardMessage, Column, Metrics, MetricsFilter, Sticker, User, } from "./types"; import { fetchJSON as registryFetchJSON, HTTPError } from "@fn_library/infra/fetch_json"; export { HTTPError }; const BASE = "/api"; function fetchJSON(path: string, init?: RequestInit): Promise { return registryFetchJSON(path, init, BASE); } export function getBoard(): Promise { return fetchJSON("/board"); } export function getFlags(): Promise> { return fetchJSON("/flags"); } export function createColumn(name: string): Promise { return fetchJSON("/columns", { method: "POST", body: JSON.stringify({ name }) }); } export interface UpdateColumnInput { name?: string; position?: number; location?: "board" | "sidebar"; width?: number; wip_limit?: number; is_done?: boolean; max_time_minutes?: number; } export function updateColumn(id: string, patch: UpdateColumnInput): Promise { return fetchJSON(`/columns/${id}`, { method: "PATCH", body: JSON.stringify(patch), }); } export function deleteColumn(id: string): Promise { return fetchJSON(`/columns/${id}`, { method: "DELETE" }); } export function reorderColumns(ids: string[]): Promise { return fetchJSON("/columns/reorder", { method: "POST", body: JSON.stringify({ ids }) }); } export interface CreateCardInput { column_id: string; requester?: string; title: string; description?: string; assignee_id?: string | null; tags?: string[]; } export function createCard(input: CreateCardInput): Promise { return fetchJSON("/cards", { method: "POST", body: JSON.stringify(input) }); } export interface UpdateCardInput { requester?: string; title?: string; description?: string; color?: string; locked?: boolean; assignee_id?: string | null; tags?: string[]; deadline?: string | null; } export function updateCard(id: string, patch: UpdateCardInput): Promise { return fetchJSON(`/cards/${id}`, { method: "PATCH", body: JSON.stringify(patch) }); } export function deleteCard(id: string): Promise { return fetchJSON(`/cards/${id}`, { method: "DELETE" }); } export function updateCardStickers(id: string, stickers: Sticker[]): Promise { return fetchJSON(`/cards/${id}/stickers`, { method: "PUT", body: JSON.stringify({ stickers }), }); } export function listTrash(): Promise { return fetchJSON("/trash"); } export function restoreCard(id: string): Promise { return fetchJSON(`/cards/${id}/restore`, { method: "POST" }); } export function purgeCard(id: string): Promise { return fetchJSON(`/cards/${id}/purge`, { method: "DELETE" }); } export function listArchive(): Promise { return fetchJSON("/archive"); } export function archiveCard(id: string): Promise { return fetchJSON(`/cards/${id}/archive`, { method: "POST" }); } export function unarchiveCard(id: string): Promise { return fetchJSON(`/cards/${id}/unarchive`, { method: "POST" }); } export interface DailyReport { date: string; tz: string; start_ts: string; end_ts: string; kpis: { done: number; created: number; moves: number; blocked_ms: number; deadlines_met: number; deadlines_missed: number; reopened: number; archived_auto: number; archived_manual: number; }; top_assignees_done: { user_id: string; name: string; count: number }[]; top_assignees_created: { user_id: string; name: string; count: number }[]; top_requesters_added: { name: string; count: number }[]; top_requesters_done: { name: string; count: number }[]; done_cards: { id: string; seq_num: number; title: string; requester: string; assignee_id: string | null; assignee_name: string | null; tags: string[]; column_id: string; column_name: string; completed_at: string; created_at: string; lead_time_ms: number; color: string; }[]; reopened_cards: { card_id: string; title: string; seq_num: number; from_column: string; to_column: string; ts: string; actor_id: string | null; actor_name: string | null; }[]; stale_cards: { d7: StaleEntry[]; d14: StaleEntry[]; d30: StaleEntry[]; }; lead_time: { avg_ms: number; p50_ms: number; p95_ms: number; samples: number }; hourly_moves: number[]; deadlines: { met: number; missed: number; list: { card_id: string; title: string; seq_num: number; deadline: string; completed_at: string; late_ms: number; }[]; }; tags_done: { name: string; count: number }[]; archived_today: number; } export interface StaleEntry { card_id: string; title: string; seq_num: number; column_id: string; column_name: string; entered_at: string; days: number; } export function dailyReport(date: string, tz?: string): Promise { const params = new URLSearchParams({ date }); if (tz) params.set("tz", tz); return fetchJSON(`/reports/daily?${params.toString()}`); } export function moveCard(id: string, column_id: string, ordered_ids: string[]): Promise { return fetchJSON(`/cards/${id}/move`, { method: "POST", body: JSON.stringify({ column_id, ordered_ids }), }); } export function cardHistory(id: string): Promise { return fetchJSON(`/cards/${id}/history`); } export function listCardMessages(id: string): Promise { return fetchJSON(`/cards/${id}/messages`); } export function createCardMessage(id: string, body: string): Promise { return fetchJSON(`/cards/${id}/messages`, { method: "POST", body: JSON.stringify({ body }), }); } export function deleteCardMessage(cardId: string, messageId: string): Promise { return fetchJSON(`/cards/${cardId}/messages/${messageId}`, { method: "DELETE" }); } export function duplicateCard(id: string): Promise { return fetchJSON(`/cards/${id}/duplicate`, { method: "POST" }); } export interface ChatMessage { role: "user" | "assistant"; content: string; } export interface ChatToolCall { tool: string; ok: boolean; error?: string; input?: unknown; } // WebSocket streaming events emitted by /api/chat/ws. export type ChatStreamEvent = | { type: "delta"; text: string } | { type: "tool_use"; tool_id: string; tool: string; input?: unknown } | { type: "tool_result"; tool_id: string; result?: string; is_error?: boolean } | { type: "result"; text?: string; is_error?: boolean } | { type: "done"; board_changed?: boolean } | { type: "error"; error: string }; // chatWSURL builds the absolute ws:// or wss:// URL of the streaming endpoint. export function chatWSURL(): string { const proto = window.location.protocol === "https:" ? "wss:" : "ws:"; return `${proto}//${window.location.host}/api/chat/ws`; } // streamChat opens a WebSocket, sends the message history, and streams events // to onEvent. Returns a Promise that resolves when the server closes the // connection (after a "done" event) and rejects on transport errors. export function streamChat( messages: ChatMessage[], onEvent: (ev: ChatStreamEvent) => void, signal?: AbortSignal ): Promise { return new Promise((resolve, reject) => { const ws = new WebSocket(chatWSURL()); let settled = false; const finish = (err?: Error) => { if (settled) return; settled = true; try { ws.close(); } catch { /* ignore */ } if (err) reject(err); else resolve(); }; if (signal) { const abort = () => finish(new Error("aborted")); if (signal.aborted) { abort(); return; } signal.addEventListener("abort", abort, { once: true }); } ws.onopen = () => { ws.send(JSON.stringify({ messages })); }; ws.onmessage = (e) => { try { const ev = JSON.parse(typeof e.data === "string" ? e.data : "") as ChatStreamEvent; onEvent(ev); if (ev.type === "done" || ev.type === "error") { finish(ev.type === "error" ? new Error(ev.error) : undefined); } } catch (err) { finish(err as Error); } }; ws.onerror = () => finish(new Error("websocket error")); ws.onclose = () => finish(); }); } export function login(username: string, password: string): Promise { return fetchJSON("/auth/login", { method: "POST", body: JSON.stringify({ username, password }), }); } export function register(username: string, password: string, display_name?: string): Promise { return fetchJSON("/auth/register", { method: "POST", body: JSON.stringify({ username, password, display_name }), }); } export function logout(): Promise { return fetchJSON("/auth/logout", { method: "POST" }); } export function getMe(): Promise { return fetchJSON("/me"); } export function updateMe(patch: { color?: string }): Promise { return fetchJSON("/me", { method: "PATCH", body: JSON.stringify(patch) }); } export function listUsers(): Promise { return fetchJSON("/users"); } export function listTags(): Promise { return fetchJSON("/tags"); } export function listRequesters(): Promise { return fetchJSON("/requesters"); } export function getMetrics(f: MetricsFilter): Promise { const qs = new URLSearchParams(); if (f.from) qs.set("from", f.from); if (f.to) qs.set("to", f.to); if (f.assignee_id) qs.set("assignee_id", f.assignee_id); if (f.requester) qs.set("requester", f.requester); if (f.tags && f.tags.length > 0) qs.set("tags", f.tags.join(",")); const q = qs.toString(); return fetchJSON(`/metrics${q ? `?${q}` : ""}`); }