import type { Board, Card, CardFile, CardHistoryResponse, CardMessage, Column, KanbanModule, Metrics, MetricsFilter, ModuleLog, ModuleTestResult, Notification, 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 getVersion(): Promise<{ version: string }> { return fetchJSON("/version"); } 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 interface DailySummary { date: string; summary: string; prompt?: string; model?: string; generated_at?: string; generated_by?: string | null; exists: boolean; } export function getDailySummary(date: string): Promise { return fetchJSON(`/reports/daily/summary?date=${encodeURIComponent(date)}`); } export function generateDailySummary(date: string, tz?: string): Promise { const params = new URLSearchParams({ date }); if (tz) params.set("tz", tz); return fetchJSON(`/reports/daily/summary?${params.toString()}`, { method: "POST" }); } export function getSetting(key: string): Promise<{ key: string; value: string }> { return fetchJSON(`/settings/${encodeURIComponent(key)}`); } export function setSetting(key: string, value: string): Promise { return fetchJSON(`/settings/${encodeURIComponent(key)}`, { method: "PUT", body: JSON.stringify({ value }), }); } 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`; } export function cardChatWSURL(cardId: string): string { const proto = window.location.protocol === "https:" ? "wss:" : "ws:"; return `${proto}//${window.location.host}/api/cards/${cardId}/chat/ws`; } export function listNotifications(unreadOnly = false): Promise { return fetchJSON(`/notifications${unreadOnly ? "?unread=1" : ""}`); } export function unreadNotificationCount(): Promise<{ count: number }> { return fetchJSON("/notifications/unread-count"); } export function markNotificationRead(id: string): Promise { return fetchJSON(`/notifications/${id}/read`, { method: "POST" }); } export function markAllNotificationsRead(): Promise<{ count: number }> { return fetchJSON("/notifications/read-all", { method: "POST" }); } export function listModules(): Promise { return fetchJSON("/modules"); } export interface ModuleInput { name: string; kind: string; enabled: boolean; event_filter: string[]; config: Record; } export function createModule(body: ModuleInput): Promise { return fetchJSON("/modules", { method: "POST", body: JSON.stringify(body) }); } export function updateModule(id: string, patch: Partial): Promise { return fetchJSON(`/modules/${id}`, { method: "PATCH", body: JSON.stringify(patch) }); } export function deleteModule(id: string): Promise { return fetchJSON(`/modules/${id}`, { method: "DELETE" }); } export function listModuleLogs(id: string, limit = 100): Promise { return fetchJSON(`/modules/${id}/logs?limit=${limit}`); } export function testModule(idOrDraft: string, body?: ModuleInput): Promise { const init: RequestInit = { method: "POST" }; if (body) init.body = JSON.stringify(body); return fetchJSON(`/modules/${idOrDraft}/test`, init); } // 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"); } // --- Files (issue 0128) ----------------------------------------------------- export function listCardFiles(cardId: string): Promise { return fetchJSON(`/cards/${cardId}/files`); } export async function uploadCardFile( cardId: string, file: File, source: "upload" | "description" | "chat" = "upload" ): Promise { const fd = new FormData(); fd.append("file", file); fd.append("source", source); const res = await fetch(`${BASE}/cards/${cardId}/files`, { method: "POST", credentials: "same-origin", body: fd, }); if (!res.ok) { let msg = `upload failed: ${res.status}`; try { const body = (await res.json()) as { Message?: string; message?: string }; if (body.Message || body.message) msg = body.Message || body.message || msg; } catch { /* ignore */ } throw new HTTPError(res.status, msg); } return (await res.json()) as CardFile; } export function deleteCardFile(fileId: string): Promise { return fetchJSON(`/files/${fileId}`, { method: "DELETE" }); } // --- MCP per-user tokens ---------------------------------------------------- export interface MCPToken { id: string; name: string; created_at: string; last_used_at?: string; } export interface MCPTokenCreated extends MCPToken { token: string; } export function createMCPToken(name: string): Promise { return fetchJSON("/mcp-tokens", { method: "POST", body: JSON.stringify({ name }) }); } export function listMCPTokens(): Promise { return fetchJSON("/mcp-tokens"); } export function revokeMCPToken(id: string): Promise { return fetchJSON(`/mcp-tokens/${id}`, { method: "DELETE" }); } 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}` : ""}`); }