spb/satelliteindex
Public
TypeScript 66.5%
Python 30.9%
JavaScript 1.4%
CSS 0.7%
1import 'server-only';2import type { AdminOverview, ConnectorRunsPayload, CostsPayload, DataQualityPayload, RawRecord, RawRecordDetail, ReviewDecision, ReviewItem, AdminPaginated } from '@/components/admin/types';3import type { Envelope } from './types';4import { API_URL } from './api';56/**7 * Server-only client for the FastAPI admin endpoints. Adds `x-si-admin-token` from the server environment (the browser never8 * sees it) and never caches. Used by admin server components and by the Next route handlers under `app/api/admin/*`.9 */10const BASE = `${API_URL}/api/v1/admin`;1112export class AdminApiError extends Error {13 readonly status: number;14 readonly path: string;15 constructor(status: number, path: string, message: string) {16 super(message);17 this.name = 'AdminApiError';18 this.status = status;19 this.path = path;20 }21 get notFound(): boolean {22 return this.status === 404;23 }24}2526type Query = Record<string, string | number | boolean | null | undefined>;2728function qs(query?: Query): string {29 if (!query) return '';30 const p = new URLSearchParams();31 for (const [k, v] of Object.entries(query)) {32 if (v === undefined || v === null || v === '') continue;33 p.set(k, String(v));34 }35 const s = p.toString();36 return s ? `?${s}` : '';37}3839async function adminFetch<T>(path: string, opts: { method?: 'GET' | 'POST'; body?: unknown; query?: Query } = {}): Promise<T> {40 const token = process.env.SI_ADMIN_TOKEN ?? '';41 if (!token) throw new AdminApiError(500, path, 'SI_ADMIN_TOKEN is not configured on the server');42 const url = `${BASE}${path}${qs(opts.query)}`;43 let res: Response;44 try {45 res = await fetch(url, {46 method: opts.method ?? 'GET',47 cache: 'no-store',48 headers: { accept: 'application/json', 'x-si-admin-token': token, ...(opts.body !== undefined ? { 'content-type': 'application/json' } : {}) },49 body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,50 });51 } catch (e) {52 throw new AdminApiError(0, path, `Admin API unreachable: ${(e as Error).message}`);53 }54 if (!res.ok) {55 let detail = `Admin API ${res.status} on ${path}`;56 try {57 const body = (await res.json()) as { error?: { title?: string; detail?: string | null } };58 detail = body.error?.detail || body.error?.title || detail;59 } catch {60 /* non-JSON body */61 }62 throw new AdminApiError(res.status, path, detail);63 }64 return (await res.json()) as T;65}6667export const adminApi = {68 overview: () => adminFetch<Envelope<AdminOverview>>('/overview'),69 connectorRuns: (name: string, page = 1, pageSize = 50) => adminFetch<ConnectorRunsPayload>(`/connectors/${encodeURIComponent(name)}/runs`, { query: { page, page_size: pageSize } }),70 triggerRun: (name: string) => adminFetch<Envelope<{ queued: boolean; connector: string; note: string | null }>>(`/connectors/${encodeURIComponent(name)}/run`, { method: 'POST' }),71 setEnabled: (name: string, enabled: boolean) => adminFetch<Envelope<{ connector: string; enabled: boolean }>>(`/connectors/${encodeURIComponent(name)}/enabled`, { method: 'POST', body: { enabled } }),72 raw: (page = 1, connector?: string | null, pageSize = 50) => adminFetch<AdminPaginated<RawRecord>>('/raw', { query: { page, page_size: pageSize, connector } }),73 rawRecord: (id: string, maxBytes = 200_000) => adminFetch<Envelope<RawRecordDetail>>(`/raw/${encodeURIComponent(id)}`, { query: { max_bytes: maxBytes } }),74 dataQuality: (page = 1, flag?: string | null, pageSize = 50) => adminFetch<DataQualityPayload>('/data-quality', { query: { page, page_size: pageSize, flag } }),75 review: (page = 1, status = 'open', pageSize = 50) => adminFetch<AdminPaginated<ReviewItem>>('/review', { query: { page, page_size: pageSize, status } }),76 decide: (id: number, decision: ReviewDecision, by = 'admin') => adminFetch<Envelope<{ id: number; decision: string }>>(`/review/${id}`, { method: 'POST', body: { decision, by } }),77 costs: () => adminFetch<Envelope<CostsPayload>>('/costs'),78};7980export async function safeAdmin<T>(p: Promise<T>): Promise<{ data: T; error: null } | { data: null; error: string }> {81 try {82 return { data: await p, error: null };83 } catch (e) {84 return { data: null, error: e instanceof Error ? e.message : String(e) };85 }86}87