import 'server-only'; import type { AdminOverview, ConnectorRunsPayload, CostsPayload, DataQualityPayload, RawRecord, RawRecordDetail, ReviewDecision, ReviewItem, AdminPaginated } from '@/components/admin/types'; import type { Envelope } from './types'; import { API_URL } from './api'; /** * Server-only client for the FastAPI admin endpoints. Adds `x-si-admin-token` from the server environment (the browser never * sees it) and never caches. Used by admin server components and by the Next route handlers under `app/api/admin/*`. */ const BASE = `${API_URL}/api/v1/admin`; export class AdminApiError extends Error { readonly status: number; readonly path: string; constructor(status: number, path: string, message: string) { super(message); this.name = 'AdminApiError'; this.status = status; this.path = path; } get notFound(): boolean { return this.status === 404; } } type Query = Record; function qs(query?: Query): string { if (!query) return ''; const p = new URLSearchParams(); for (const [k, v] of Object.entries(query)) { if (v === undefined || v === null || v === '') continue; p.set(k, String(v)); } const s = p.toString(); return s ? `?${s}` : ''; } async function adminFetch(path: string, opts: { method?: 'GET' | 'POST'; body?: unknown; query?: Query } = {}): Promise { const token = process.env.SI_ADMIN_TOKEN ?? ''; if (!token) throw new AdminApiError(500, path, 'SI_ADMIN_TOKEN is not configured on the server'); const url = `${BASE}${path}${qs(opts.query)}`; let res: Response; try { res = await fetch(url, { method: opts.method ?? 'GET', cache: 'no-store', headers: { accept: 'application/json', 'x-si-admin-token': token, ...(opts.body !== undefined ? { 'content-type': 'application/json' } : {}) }, body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined, }); } catch (e) { throw new AdminApiError(0, path, `Admin API unreachable: ${(e as Error).message}`); } if (!res.ok) { let detail = `Admin API ${res.status} on ${path}`; try { const body = (await res.json()) as { error?: { title?: string; detail?: string | null } }; detail = body.error?.detail || body.error?.title || detail; } catch { /* non-JSON body */ } throw new AdminApiError(res.status, path, detail); } return (await res.json()) as T; } export const adminApi = { overview: () => adminFetch>('/overview'), connectorRuns: (name: string, page = 1, pageSize = 50) => adminFetch(`/connectors/${encodeURIComponent(name)}/runs`, { query: { page, page_size: pageSize } }), triggerRun: (name: string) => adminFetch>(`/connectors/${encodeURIComponent(name)}/run`, { method: 'POST' }), setEnabled: (name: string, enabled: boolean) => adminFetch>(`/connectors/${encodeURIComponent(name)}/enabled`, { method: 'POST', body: { enabled } }), raw: (page = 1, connector?: string | null, pageSize = 50) => adminFetch>('/raw', { query: { page, page_size: pageSize, connector } }), rawRecord: (id: string, maxBytes = 200_000) => adminFetch>(`/raw/${encodeURIComponent(id)}`, { query: { max_bytes: maxBytes } }), dataQuality: (page = 1, flag?: string | null, pageSize = 50) => adminFetch('/data-quality', { query: { page, page_size: pageSize, flag } }), review: (page = 1, status = 'open', pageSize = 50) => adminFetch>('/review', { query: { page, page_size: pageSize, status } }), decide: (id: number, decision: ReviewDecision, by = 'admin') => adminFetch>(`/review/${id}`, { method: 'POST', body: { decision, by } }), costs: () => adminFetch>('/costs'), }; export async function safeAdmin(p: Promise): Promise<{ data: T; error: null } | { data: null; error: string }> { try { return { data: await p, error: null }; } catch (e) { return { data: null, error: e instanceof Error ? e.message : String(e) }; } }