export type Price = { amount: number; currency: string; model: string; }; export type ListingStats = { installs?: number; proposals_shown?: number; proposals_accepted?: number; }; export type ListingSummary = { listing_id: string; name: string; summary: string; category?: string; price: Price; seller: string; stats?: ListingStats; score?: number; }; export type ListingDetail = ListingSummary & { evidence_summary: string; permissions: string[]; install_ref?: string; install_command: string; solution_version?: string; version?: string; }; export type Transaction = { ts: string; from: string; to: string; amount: number; reason: string; ref?: string; }; export type Wallet = { operator_id: string; balance: number; as_of?: string; transactions: Transaction[]; }; export type Economy = { operators: Array<{ operator_id: string; balance: number }>; audit?: { date: string; url: string }; }; export type Governance = { open_prs: Array<{ number: number; title: string; age_days: number; labels: string[]; url: string }>; listings: Array<{ listing_id: string; status: string; url: string }>; }; export type PanelName = 'search' | 'listing' | 'wallet' | 'economy' | 'governance' | 'login'; export class ApiError extends Error { status: number; panel?: string; constructor(message: string, status: number, panel?: string) { super(message); this.name = 'ApiError'; this.status = status; this.panel = panel; } } async function request(path: string, init?: RequestInit): Promise { const response = await fetch(path, { credentials: 'include', headers: { 'Content-Type': 'application/json', ...(init?.headers ?? {}), }, ...init, }); if (response.status === 401) { if (window.location.hash !== '#/login') { window.location.hash = '#/login'; } throw new ApiError('Login required', 401); } if (response.status === 502) { const body = (await response.json().catch(() => null)) as { error?: string; panel?: string } | null; throw new ApiError(body?.error ?? 'Panel is temporarily unavailable', 502, body?.panel); } if (!response.ok) { const body = (await response.json().catch(() => null)) as { error?: string } | null; throw new ApiError(body?.error ?? `Request failed (${response.status})`, response.status); } if (response.status === 204) { return undefined as T; } return (await response.json()) as T; } export const api = { login(passcode: string) { return request('/api/login', { method: 'POST', body: JSON.stringify({ passcode }), }); }, search(q: string, limit = 10) { const params = new URLSearchParams({ q, limit: String(limit) }); return request(`/api/search?${params}`); }, listing(listingId: string) { return request(`/api/listing/${encodeURIComponent(listingId)}`); }, wallet(operatorId: string) { return request(`/api/wallet/${encodeURIComponent(operatorId)}`); }, economy() { return request('/api/economy'); }, governance() { return request('/api/governance'); }, };