import type { LinkKind } from '../shared/ids.ts' import type { PaperStatus } from '../shared/paperCard.ts' // The board service's REST API (T08) from the browser: JSON in and out, the member ID as the // bearer (decision D5). Routes and shapes are in the root README. export class ApiError extends Error { status: number code: string constructor(status: number, code: string, message: string) { super(message) this.status = status this.code = code } } export interface Member { memberId: string displayName: string lastSeenSeq: number joinedAt: string } /** `GET /boards/:id`, the fields the browser reads. */ export interface Board { id: string title: string question: string context: string /** A member id, or "demo:" on a seeded demo board. */ createdBy?: string | null seedCount: number paperCount: number /** The newest event's seq: what `seen` marks as read. */ lastSeq: number agentActive: boolean /** For the caller: events since they last looked ("Catch me up"). */ newCount?: number } export interface VoteResult { status: PaperStatus keepBy: string[] discardBy: string[] } async function call(method: string, path: string, memberId: string, body?: unknown): Promise { const res = await fetch(`/api/v1${path}`, { method, headers: { authorization: `Bearer ${memberId}`, ...(body === undefined ? {} : { 'content-type': 'application/json' }) }, body: body === undefined ? undefined : JSON.stringify(body), }) if (res.status === 204) return undefined as T const data = await res.json().catch(() => null) if (!res.ok) throw new ApiError(res.status, data?.error?.code ?? 'error', data?.error?.message ?? `The board service answered ${res.status}`) return data as T } /** One board's routes, as one member. */ export function boardApi(boardId: string, memberId: string) { const board = `/boards/${encodeURIComponent(boardId)}` return { get: () => call('GET', board, memberId), /** Everything up to `seq` counts as seen: Home's "Catch me up" count starts over. */ seen: (seq: number) => call('POST', `${board}/seen`, memberId, { seq }), /** Join the board; with a name, also set the name the others see. */ join: (displayName?: string) => call('PUT', `${board}/members/me`, memberId, { displayName }), /** Keep, Discard, or (null) take the vote back; the card updates for everyone through sync. */ vote: (paperId: string, decision: 'keep' | 'discard' | null) => call('PUT', `${board}/papers/${encodeURIComponent(paperId)}/vote`, memberId, { decision }), /** A link a person deleted: get_board lists it in removedLinks and link_papers never draws it again (T14). */ removeLink: (from: string, to: string, kind: LinkKind) => call('PUT', `${board}/removed-links`, memberId, { from, to, kind }), } }