spb/company-atlas
Public
Python 66.3%
TypeScript 22.7%
JavaScript 8.6%
HTML 1.4%
CSS 0.7%
1'use client';2/**3 * Browser-side fetches: same origin `/api/v1/*` (Next rewrite → FastAPI). Never import the server `api.ts` in client code.4 * Owner and admin calls attach their token headers (`lib/owner.ts`, `lib/admin.ts`).5 */6import type {7 Alert,8 AlertCondition,9 AlertDelivery,10 AdminCompany,11 AdminConnector,12 AdminCosts,13 AdminFailure,14 AdminLlmJob,15 AdminOverview,16 AdminQuality,17 AdminQueueItem,18 AdminReview,19 AdminSensor,20 AskPayload,21 CompanyCard,22 Event,23 EventDetail,24 Page,25 SearchPayload,26 Suggestion,27 WatchlistPayload,28} from './types';2930export class ClientApiError extends Error {31 readonly status: number;32 readonly detail: string | null;33 constructor(status: number, path: string, detail: string | null = null) {34 super(detail ?? `API ${status} on ${path}`);35 this.name = 'ClientApiError';36 this.status = status;37 this.detail = detail;38 }39}4041type Init = { method?: string; body?: unknown; headers?: Record<string, string>; signal?: AbortSignal };4243async function call<T>(path: string, init: Init = {}): Promise<T> {44 const headers: Record<string, string> = { accept: 'application/json', ...(init.headers ?? {}) };45 if (init.body !== undefined) headers['content-type'] = 'application/json';46 const res = await fetch(`/api/v1${path}`, { method: init.method ?? 'GET', headers, body: init.body === undefined ? undefined : JSON.stringify(init.body), signal: init.signal, cache: 'no-store' });47 if (!res.ok) {48 let detail: string | null = null;49 try {50 const b = (await res.json()) as { detail?: unknown };51 detail = typeof b.detail === 'string' ? b.detail : null;52 } catch {53 /* ignore */54 }55 throw new ClientApiError(res.status, path, detail);56 }57 if (res.status === 204) return undefined as T;58 return (await res.json()) as T;59}6061const enc = encodeURIComponent;6263export function buildQs(q: Record<string, string | number | boolean | null | undefined>): string {64 const p = new URLSearchParams();65 for (const [k, v] of Object.entries(q)) if (v !== undefined && v !== null && v !== '') p.set(k, typeof v === 'boolean' ? (v ? '1' : '0') : String(v));66 const s = p.toString();67 return s ? `?${s}` : '';68}6970export const clientApi = {71 suggest: (q: string, signal?: AbortSignal) => call<{ items: Suggestion[] }>(`/search/suggest?q=${enc(q)}`, { signal }),72 search: (q: string, limit = 8, signal?: AbortSignal) => call<SearchPayload>(`/search?q=${enc(q)}&limit=${limit}`, { signal }),73 ask: (q: string, signal?: AbortSignal) => call<AskPayload>(`/ask?q=${enc(q)}`, { signal }),74 live: (q: Record<string, string | number | undefined>, signal?: AbortSignal) => call<{ items: Event[] } | Event[]>(`/live${buildQs(q)}`, { signal }),75 event: (id: string, signal?: AbortSignal) => call<EventDetail>(`/events/${enc(id)}`, { signal }),76 companies: (q: Record<string, string | number | undefined>, signal?: AbortSignal) => call<Page<CompanyCard>>(`/companies${buildQs(q)}`, { signal }),77};7879/** Watchlist + alerts with the owner token. */80export function ownerApi(token: string) {81 const h = { 'X-CA-Owner-Token': token };82 return {83 watchlist: (signal?: AbortSignal) => call<WatchlistPayload>('/watchlist', { headers: h, signal }),84 watch: (slug: string) => call<unknown>('/watchlist', { method: 'POST', body: { company: slug }, headers: h }),85 unwatch: (slug: string) => call<unknown>(`/watchlist/${enc(slug)}`, { method: 'DELETE', headers: h }),86 alerts: (signal?: AbortSignal) => call<{ items: Alert[] }>('/alerts', { headers: h, signal }),87 createAlert: (body: { name: string; company?: string; condition: AlertCondition; channel: 'web' | 'webhook'; target?: string }) => call<Alert>('/alerts', { method: 'POST', body, headers: h }),88 deleteAlert: (id: string) => call<unknown>(`/alerts/${enc(id)}`, { method: 'DELETE', headers: h }),89 deliveries: (limit = 50, signal?: AbortSignal) => call<{ items: AlertDelivery[] }>(`/alerts/deliveries?limit=${limit}`, { headers: h, signal }),90 };91}9293/** Admin console with the admin token. */94export function adminApi(token: string) {95 const h = { 'X-CA-Admin-Token': token };96 const g = <T>(path: string, signal?: AbortSignal) => call<T>(path, { headers: h, signal });97 const post = <T>(path: string, body?: unknown) => call<T>(path, { method: 'POST', body: body ?? {}, headers: h });98 return {99 overview: (signal?: AbortSignal) => g<AdminOverview>('/admin/overview', signal),100 connectors: (signal?: AbortSignal) => g<{ items: AdminConnector[] }>('/admin/connectors', signal),101 sensors: (q: Record<string, string | number | undefined>, signal?: AbortSignal) => g<Page<AdminSensor>>(`/admin/sensors${buildQs(q)}`, signal),102 sensorAction: (id: string, action: string, body?: unknown) => post<unknown>(`/admin/sensors/${enc(id)}/${action}`, body),103 companies: (q: Record<string, string | number | undefined>, signal?: AbortSignal) => g<Page<AdminCompany>>(`/admin/companies${buildQs(q)}`, signal),104 createCompany: (body: { website: string; display_name?: string; country?: string; industries?: string[] }) => post<unknown>('/admin/companies', body),105 rediscover: (slug: string) => post<unknown>(`/admin/companies/${enc(slug)}/rediscover`),106 failures: (q: Record<string, string | number | undefined>, signal?: AbortSignal) => g<Page<AdminFailure>>(`/admin/failures${buildQs(q)}`, signal),107 queue: (q: Record<string, string | number | undefined>, signal?: AbortSignal) => g<{ items: AdminQueueItem[]; counts?: Record<string, number> } | Page<AdminQueueItem>>(`/admin/queue${buildQs(q)}`, signal),108 requeueDead: () => post<unknown>('/admin/queue/requeue-dead'),109 llm: (q: Record<string, string | number | undefined>, signal?: AbortSignal) => g<Page<AdminLlmJob>>(`/admin/llm${buildQs(q)}`, signal),110 reviews: (q: Record<string, string | number | undefined>, signal?: AbortSignal) => g<{ items: AdminReview[] } | Page<AdminReview>>(`/admin/reviews${buildQs(q)}`, signal),111 resolveReview: (id: string, resolution: 'accepted' | 'rejected', note?: string) => post<unknown>(`/admin/reviews/${enc(id)}`, { resolution, note }),112 retractEvent: (id: string, reason: string) => post<unknown>(`/admin/events/${enc(id)}/retract`, { reason }),113 restoreEvent: (id: string) => post<unknown>(`/admin/events/${enc(id)}/restore`),114 quality: (signal?: AbortSignal) => g<AdminQuality>('/admin/quality', signal),115 costs: (days = 30, signal?: AbortSignal) => g<AdminCosts>(`/admin/costs?days=${days}`, signal),116 clearCache: () => post<unknown>('/admin/cache/clear'),117 };118}119