const API = '/api'; async function apiFetch(path: string, init?: RequestInit): Promise { const res = await fetch(`${API}${path}`, { credentials: 'include', ...init, headers: { 'Content-Type': 'application/json', ...init?.headers }, }); if (!res.ok) { const err = await res.json().catch(() => ({ error: res.statusText })); throw Object.assign(new Error(err.error ?? 'API error'), { status: res.status }); } return res.json() as T; } // ── Types ────────────────────────────────────────────────────────────────── export interface AdminStats { dau: number; wau: number; tipsServedLast7d: number; reactionsLast7d: Record; totalUsers: number; activatedUsers: number; } export interface AdminUser { id: string; email: string; name: string | null; image: string | null; role: string; consentGiven: boolean; consentAt: string | null; createdAt: string; deletedAt: string | null; } export interface ProfileFeatureView { name: string; value: number | string | null; updatedAt: string | null; ageSec: number | null; fresh: boolean; ttlSec: number; dtype: 'numeric' | 'categorical'; description: string; } export interface AdminUserDetail { user: AdminUser; integrations: { provider: string; connectedAt: string }[]; tipsServed: number; lastTipAt: string | null; recentFeedback: { id: string; action: string; createdAt: string; tipId: string }[]; profile: ProfileFeatureView[]; } export interface AuditAction { id: string; adminId: string; action: string; targetType: string | null; targetId: string | null; detail: string | null; createdAt: string; } export interface StoredEvent { id: number; subject: string; payload: unknown; ts: string; } export interface TipScore { id: string; userId: string; tipId: string; policy: string; mlScore: number | null; featuresJson: string | null; candidateCount: number | null; latencyMs: number | null; servedAt: string; } export interface HealthStatus { ok: boolean; checkedAt: string; services: { name: string; status: string; latencyMs: number }[]; } export interface PolicyInfo { name: string; active: boolean; } export interface SavedQuery { id: string; name: string; sql: string; createdAt: string; } export interface BanditStats { user_id: string; pulls: number; reward_count: number; cumulative_reward: number; estimated_mean_reward: number; theta: number[]; last_updated: string | null; } export interface FeatureHistory { user_id: string; history: { ts: string; features: Record; score: number; tip_id: string }[]; } // ── Fetchers ─────────────────────────────────────────────────────────────── export function getStats() { return apiFetch('/admin/stats'); } export function getUsers(limit = 50, offset = 0) { return apiFetch<{ users: AdminUser[]; total: number }>( `/admin/users?limit=${limit}&offset=${offset}`, ); } export function getUserDetail(id: string) { return apiFetch(`/admin/users/${id}`); } export function revokeIntegration(userId: string, provider: string) { return apiFetch<{ ok: boolean }>(`/admin/users/${userId}/revoke-integration`, { method: 'POST', body: JSON.stringify({ provider }), }); } export function resetBandit(userId: string) { return apiFetch<{ ok: boolean }>(`/admin/users/${userId}/reset-bandit`, { method: 'POST', }); } export function rebuildUserProfile(userId: string) { return apiFetch<{ ok: boolean; profile: ProfileFeatureView[] }>( `/admin/users/${userId}/profile/rebuild`, { method: 'POST' }, ); } export function getAuditLog(limit = 50, offset = 0) { return apiFetch<{ actions: AuditAction[]; total: number }>( `/admin/audit?limit=${limit}&offset=${offset}`, ); } export function getEvents(params: { subject?: string; userId?: string; limit?: number; since?: number } = {}) { const q = new URLSearchParams(); if (params.subject) q.set('subject', params.subject); if (params.userId) q.set('userId', params.userId); if (params.limit) q.set('limit', String(params.limit)); if (params.since) q.set('since', String(params.since)); return apiFetch<{ events: StoredEvent[]; nextSince: number }>(`/admin/events?${q}`); } export function getTips(params: { limit?: number; offset?: number; userId?: string } = {}) { const q = new URLSearchParams(); if (params.limit) q.set('limit', String(params.limit)); if (params.offset) q.set('offset', String(params.offset)); if (params.userId) q.set('userId', params.userId); return apiFetch<{ tips: TipScore[]; total: number }>(`/admin/tips?${q}`); } export type QualityBreakdownRow = { key: string | null; served: number; done: number; snooze: number; dismiss: number; helpful: number; not_helpful: number; avgRewardMilli: number | null; }; export function getRewardAnalytics(days = 30) { return apiFetch<{ daily: { date: string; action: string; count: number }[]; byPolicy: { policy: string; action: string; count: number }[]; byHour: { action: string; count: number; avgHour: number }[]; byModel: QualityBreakdownRow[]; byPromptVersion: QualityBreakdownRow[]; byKind: QualityBreakdownRow[]; }>(`/admin/reward-analytics?days=${days}`); } export interface FeatureFreshnessRow { feature: string; ttlSec: number; totalEligible: number; missing: number; stale: number; } export function getDataQuality() { return apiFetch<{ scoringCallsLast30d: number; missingFeatureRate: number; staleTokenRate: number; totalTokens: number; staleTokens: number; dailyQuality: { date: string; total: number; withFeatures: number; avgCandidates: number }[]; profileFreshness: FeatureFreshnessRow[]; }>('/admin/data-quality'); } export function getHealth() { return apiFetch('/admin/health'); } export function getPolicies() { return apiFetch<{ policies: PolicyInfo[] }>('/admin/policies'); } export function togglePolicy(name: string, active: boolean) { return apiFetch<{ ok: boolean }>(`/admin/policies/${name}/toggle`, { method: 'POST', body: JSON.stringify({ active }), }); } export function replaySignal(subject: string, payload: Record) { return apiFetch<{ ok: boolean }>('/admin/replay-signal', { method: 'POST', body: JSON.stringify({ subject, payload }), }); } export function runSql(query: string) { return apiFetch<{ rows: unknown[]; rowCount: number }>('/admin/sql', { method: 'POST', body: JSON.stringify({ query }), }); } export function getSavedQueries() { return apiFetch<{ queries: SavedQuery[] }>('/admin/saved-queries'); } export function saveQuery(name: string, querySql: string) { return apiFetch<{ id: string }>('/admin/saved-queries', { method: 'POST', body: JSON.stringify({ name, querySql }), }); } export function deleteSavedQuery(id: string) { return apiFetch<{ ok: boolean }>(`/admin/saved-queries/${id}`, { method: 'DELETE' }); } // ── Simulations ──────────────────────────────────────────────────────────── export interface SimRun { id: string; policyA: string; policyB: string; nUsers: number; nRounds: number; tasksPerRound: number; judgeMode: string; nPolicies: number; status: 'pending' | 'running' | 'done' | 'failed'; summaryJson: string | null; winner: string | null; personaBreakdownJson: string | null; mlflowRunId: string | null; createdAt: string; finishedAt: string | null; } export interface SimStartRequest { nUsers?: number; nRounds?: number; tasksPerRound?: number; judgeMode?: 'rule' | 'llm'; policies?: string[]; } export function startSimulation(req: SimStartRequest) { return apiFetch<{ id: string; status: string }>( '/admin/simulate/start', { method: 'POST', body: JSON.stringify(req) }, ); } export function getSimulationRuns() { return apiFetch<{ runs: SimRun[] }>('/admin/simulate/runs'); } export function getSimulationRun(id: string) { return apiFetch<{ run: SimRun & { isRunning: boolean }; events: unknown[] }>( `/admin/simulate/${id}`, ); }