// Typed backend client (backend is the only source of truth; web never calls Sectors directly). const BASE = ""; function headers(): HeadersInit { return { "Content-Type": "application/json", "X-User-Key": localStorage.getItem("fs-key") || "demo" }; } async function req(path: string, init?: RequestInit): Promise { const r = await fetch(BASE + path, { ...init, headers: { ...headers(), ...(init?.headers || {}) } }); if (!r.ok) { let msg = r.statusText; try { const e = await r.json(); msg = e?.error?.message || msg; } catch { /* keep status */ } throw new Error(msg); } const ct = r.headers.get("content-type") || ""; if (ct.includes("application/pdf")) return (await r.blob()) as unknown as T; if (ct.includes("text/")) return (await r.text()) as unknown as T; return (await r.json()) as T; } export interface Citation { endpoint: string; snapshot_at: string; ticker?: string; stale?: boolean } export interface Health { last_cycle_at: string; credits_today: number; scheduler_ok: boolean; stale_flags: string[] } export interface FlowSummary { date: string; foreign_net_total: number; top_accumulation: { ticker: string; net_sum: number; brokers: number }[]; citations: Citation[] } export interface ScreenRow { symbol: string; name: string; composite: number; breakdown: Record; citations: Citation[] } export interface Routine { id: number; user_key: string; type: string; schedule_cron: string; channels_json?: string; channels?: string[]; enabled: boolean; last_run?: unknown } export interface AlertItem { id: number; user_key: string; name: string; rule_json: string; channels_json: string; last_fired: string } export interface ReportPayload { ticker: string; generated_at: string; sections: { name: string; body: string; citations: Citation[] }[]; synthesis: { recommendation: string; conviction: number; thesis: string; position_pct: number; conflict: boolean; conflict_note?: string }; agent_scores: unknown[]; citations: Citation[] } export interface AuthUser { id: number; email: string; name: string; avatar_url: string; user_key: string; google_configured: boolean } export const api = { health: (force = false) => req(`/api/health${force ? "?force=1" : ""}`), flowSummary: () => req("/api/flow/summary"), flowBroker: (ticker: string) => req<{ buyers: unknown[]; sellers: unknown[]; citations: Citation[] }>(`/api/flow/broker?ticker=${ticker}`), flowForeign: (ticker: string) => req<{ dates: string[]; nets: number[]; reversal: boolean; citations: Citation[] }>(`/api/flow/foreign?ticker=${ticker}`), screen: (body: Record) => req<{ rows: ScreenRow[]; count: number }>("/api/screen", { method: "POST", body: JSON.stringify(body) }), routines: () => req<{ routines: Routine[] }>("/api/routines"), createRoutine: (body: Record) => req<{ id: number }>("/api/routines", { method: "POST", body: JSON.stringify(body) }), updateRoutine: (id: number, body: Record) => req(`/api/routines/${id}`, { method: "PATCH", body: JSON.stringify(body) }), runs: (routine_id?: number) => req<{ runs: Record[] }>(`/api/routine-runs${routine_id ? `?routine_id=${routine_id}` : ""}`), briefing: () => req<{ date: string; payload: string; citations: string }>("/api/briefing/today"), destinations: () => req<{ destinations: { id: number; kind: string; label: string; enabled: boolean; configured: boolean }[] }>("/api/destinations"), createDestination: (body: Record) => req<{ id: number }>("/api/destinations", { method: "POST", body: JSON.stringify(body) }), updateDestination: (id: number, body: Record) => req(`/api/destinations/${id}`, { method: "PATCH", body: JSON.stringify(body) }), deleteDestination: (id: number) => req(`/api/destinations/${id}`, { method: "DELETE" }), alerts: () => req<{ alerts: AlertItem[] }>("/api/alerts"), createAlert: (body: Record) => req<{ id: number }>("/api/alerts", { method: "POST", body: JSON.stringify(body) }), deleteAlert: (id: number) => req(`/api/alerts/${id}`, { method: "DELETE" }), alertEvents: (since = "2000-01-01", ticker = "") => req<{ events: Record[] }>(`/api/alert-events?since=${since}${ticker ? `&ticker=${ticker}` : ""}`), report: (ticker: string, profile = "moderate") => req(`/api/report/${ticker}?profile=${profile}`, { method: "POST" }), reportMd: (ticker: string) => req(`/api/report/${ticker}?format=md`, { method: "POST" }), interrogate: (ticker: string, question: string, report_id?: number) => req<{ answer: string; report_id: number; citations: unknown }>(`/api/report/${ticker}/ask`, { method: "POST", body: JSON.stringify({ question, report_id }) }), watchlist: () => req<{ watchlist: string[] }>("/api/watchlist"), addWatch: (ticker: string) => req("/api/watchlist", { method: "POST", body: JSON.stringify({ ticker }) }), removeWatch: (ticker: string) => req(`/api/watchlist/${ticker}`, { method: "DELETE" }), risk: () => req<{ concentration: { ticker: string; sector: string; weight: number }[]; correlation: Record>; beta: number; warnings: string[] }>("/api/portfolio/risk"), accuracy: () => req<{ agents: { agent: string; calls: number; resolved: number; hits: number; hit_rate: number }[] }>("/api/accuracy"), chat: (message: string) => req<{ answer: string }>(`/api/chat`, { method: "POST", body: JSON.stringify({ message }) }), me: () => req<{ user: AuthUser }>(`/api/auth/me`), logout: () => req<{ ok: boolean }>(`/api/auth/logout`, { method: "POST" }), }; // SSE hook helper: subscribe to channels agents|alerts|activity. export function subscribeSSE(onEvent: (channel: string, data: string) => void): () => void { const es = new EventSource("/api/stream"); es.addEventListener("agents", (e) => onEvent("agents", (e as MessageEvent).data)); es.addEventListener("alerts", (e) => onEvent("alerts", (e as MessageEvent).data)); es.addEventListener("activity", (e) => onEvent("activity", (e as MessageEvent).data)); es.onmessage = (e) => onEvent("message", e.data); return () => es.close(); } export function fmtIDR(v: number): string { const neg = v < 0; const a = Math.abs(v); let s = `${Math.round(a)}`; if (a >= 1e12) s = `Rp${(a / 1e12).toFixed(2)}T`; else if (a >= 1e9) s = `Rp${Math.round(a / 1e9)}B`; else if (a >= 1e6) s = `Rp${Math.round(a / 1e6)}M`; return neg ? "-" + s : s; }