type FetchOptions = RequestInit & { timeoutMs?: number }; async function opsFetch(url: string, options?: FetchOptions): Promise { const res = await fetch(url, { cache: "no-store", ...options }); if (!res.ok) { const detail = (await res.text().catch(() => "")).slice(0, 300); throw new Error(`${res.status} ${res.statusText}: ${detail}`); } return res.json() as Promise; } export const opsApi = { health() { return opsFetch<{ status: string }>("/api/healthz"); }, systemStatus() { return opsFetch>("/api/system/status"); }, paymentRuntime() { return opsFetch>("/api/payments/runtime"); }, async funnel(days = 30) { const raw = await opsFetch<{ events?: Record; rates?: Record; window_days?: number; }>(`/api/ops/analytics/funnel?days=${days}`); const stepOrder = ["signup_completed", "dashboard_active", "paywall_feature_clicked", "paywall_viewed", "checkout_started", "checkout_succeeded"]; const stepLabels: Record = { signup_completed: "注册", dashboard_active: "活跃", paywall_feature_clicked: "点击付费", paywall_viewed: "看到入口", checkout_started: "发起支付", checkout_succeeded: "支付成功", }; const steps = stepOrder.map((key, i) => { const evt = raw?.events?.[key]; const count = evt?.total ?? 0; let pct_of_prev: number | undefined; if (i > 0) { const prevCount = raw?.events?.[stepOrder[i - 1]]?.total ?? 0; pct_of_prev = prevCount > 0 ? Math.round((count / prevCount) * 100) : 0; } else { pct_of_prev = 100; } return { label: stepLabels[key] ?? key, count, pct_of_prev }; }); return { steps, rates: raw?.rates, window_days: raw?.window_days }; }, users(q: string, limit = 20) { return opsFetch>(`/api/ops/users?q=${encodeURIComponent(q)}&limit=${limit}`); }, grantPoints(email: string, points: number) { return opsFetch>("/api/ops/users/grant-points", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, points }), }); }, leaderboard(limit = 10) { return opsFetch>(`/api/ops/leaderboard/weekly?limit=${limit}`); }, memberships() { return opsFetch>("/api/ops/memberships?limit=200"); }, incidents(limit = 20, reason?: string) { const params = new URLSearchParams({ limit: String(limit) }); if (reason) params.set("reason", reason); return opsFetch>(`/api/ops/payments/incidents?${params}`); }, resolveIncident(eventId: string | number) { return opsFetch>(`/api/ops/payments/incidents/${eventId}/resolve`, { method: "POST", }); }, truthHistory(params: Record) { const qs = new URLSearchParams(params).toString(); return opsFetch>(`/api/ops/truth-history?${qs}`); }, };