import type { Profile, Instance, InstanceTab, InstanceMetrics, Agent, CreateProfileRequest, CreateProfileResponse, LaunchInstanceRequest, } from "../generated/types"; import type { BackendConfig, BackendConfigState, DashboardServerInfo, MonitoringServerMetrics, MonitoringSnapshot, } from "../types"; import { normalizeBackendConfigState, normalizeDashboardServerInfo, normalizeMonitoringSnapshot, } from "../types"; import { addTokenToUrl, dispatchAuthRequired, getStoredAuthToken, } from "./auth"; const BASE = ""; // Uses proxy in dev type RequestMeta = { authToken?: string; suppressAuthRedirect?: boolean; }; async function request( url: string, options?: RequestInit, meta?: RequestMeta, ): Promise { const headers = new Headers(options?.headers ?? {}); const token = meta?.authToken?.trim() || getStoredAuthToken(); if (token) { headers.set("Authorization", `Bearer ${token}`); } const res = await fetch(BASE + url, { ...options, headers, }); if (!res.ok) { const err = await res.json().catch(() => ({ error: res.statusText })); if ( res.status === 401 && !meta?.suppressAuthRedirect && typeof window !== "undefined" ) { window.localStorage.removeItem("pinchtab.auth.token"); dispatchAuthRequired(err.code || "unauthorized"); } throw new Error(err.error || "Request failed"); } return res.json(); } async function requestText( url: string, options?: RequestInit, meta?: RequestMeta, ): Promise { const headers = new Headers(options?.headers ?? {}); const token = meta?.authToken?.trim() || getStoredAuthToken(); if (token) { headers.set("Authorization", `Bearer ${token}`); } const res = await fetch(BASE + url, { ...options, headers, }); if (!res.ok) { const err = await res.json().catch(() => ({ error: res.statusText })); if ( res.status === 401 && !meta?.suppressAuthRedirect && typeof window !== "undefined" ) { window.localStorage.removeItem("pinchtab.auth.token"); dispatchAuthRequired(err.code || "unauthorized"); } throw new Error(err.error || "Request failed"); } return res.text(); } async function requestBlob( url: string, options?: RequestInit, meta?: RequestMeta, ): Promise { const headers = new Headers(options?.headers ?? {}); const token = meta?.authToken?.trim() || getStoredAuthToken(); if (token) { headers.set("Authorization", `Bearer ${token}`); } const res = await fetch(BASE + url, { ...options, headers, }); if (!res.ok) { const err = await res.json().catch(() => ({ error: res.statusText })); if ( res.status === 401 && !meta?.suppressAuthRedirect && typeof window !== "undefined" ) { window.localStorage.removeItem("pinchtab.auth.token"); dispatchAuthRequired(err.code || "unauthorized"); } throw new Error(err.error || "Request failed"); } return res.blob(); } // Profiles — endpoint is /profiles (no /api prefix) export async function fetchProfiles(): Promise { return request("/profiles"); } export async function createProfile( data: CreateProfileRequest, ): Promise { return request("/profiles", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); } export async function deleteProfile(id: string): Promise { await request(`/profiles/${encodeURIComponent(id)}`, { method: "DELETE", }); } export interface UpdateProfileRequest { name?: string; useWhen?: string; description?: string; } export interface UpdateProfileResponse { status: string; id: string; name: string; } export async function updateProfile( id: string, data: UpdateProfileRequest, ): Promise { return request(`/profiles/${encodeURIComponent(id)}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); } // Instances — endpoint is /instances (no /api prefix) export async function fetchInstances(): Promise { return request("/instances"); } export async function launchInstance( data: LaunchInstanceRequest, ): Promise { return request("/instances/launch", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); } export async function stopInstance(id: string): Promise { await request(`/instances/${encodeURIComponent(id)}/stop`, { method: "POST", }); } export async function fetchInstanceTabs(id: string): Promise { return request(`/instances/${encodeURIComponent(id)}/tabs`); } export async function fetchInstanceLogs(id: string): Promise { return requestText(`/instances/${encodeURIComponent(id)}/logs`); } export async function fetchTabScreenshot( tabId: string, format: "jpeg" | "png" = "jpeg", ): Promise { return requestBlob( `/tabs/${encodeURIComponent(tabId)}/screenshot?raw=true&format=${format}`, ); } export async function fetchTabPdf(tabId: string): Promise { return requestBlob(`/tabs/${encodeURIComponent(tabId)}/pdf?raw=true`); } export function subscribeToInstanceLogs( id: string, handlers: { onLogs?: (logs: string) => void }, ): () => void { const url = addTokenToUrl(`/instances/${encodeURIComponent(id)}/logs/stream`); const es = new EventSource(url); es.addEventListener("log", (e) => { try { const payload = JSON.parse(e.data) as { logs?: string }; handlers.onLogs?.(payload.logs || ""); } catch { // ignore malformed events } }); es.onerror = () => { if (!getStoredAuthToken()) { dispatchAuthRequired("missing_token"); } }; const cleanup = () => es.close(); window.addEventListener("beforeunload", cleanup); return () => { window.removeEventListener("beforeunload", cleanup); es.close(); }; } export async function fetchAllTabs(): Promise { return request("/instances/tabs"); } export async function fetchAllMetrics(): Promise { return request("/instances/metrics"); } export async function fetchServerMetrics(): Promise { const res = await request<{ metrics: MonitoringServerMetrics }>("/metrics"); return res.metrics; } // Health export async function fetchHealth(): Promise { return normalizeDashboardServerInfo( await request("/health"), ); } export async function probeBackendAuth(): Promise<{ requiresAuth: boolean; health?: DashboardServerInfo; }> { const res = await fetch(BASE + "/health"); if (res.ok) { return { requiresAuth: false, health: normalizeDashboardServerInfo( (await res.json()) as DashboardServerInfo, ), }; } const err = (await res .json() .catch(() => ({ code: "", error: res.statusText }))) as { code?: string; error?: string; }; if ( res.status === 401 && (err.code === "missing_token" || err.code === "bad_token" || err.error === "unauthorized") ) { return { requiresAuth: true }; } throw new Error(err.error || "Request failed"); } export async function verifyBackendToken( token: string, ): Promise { return normalizeDashboardServerInfo( await request("/health", undefined, { authToken: token, suppressAuthRedirect: true, }), ); } export async function fetchBackendConfig(): Promise { return normalizeBackendConfigState( await request("/api/config"), ); } export async function saveBackendConfig( config: BackendConfig, ): Promise { return normalizeBackendConfigState( await request("/api/config", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(config), }), ); } export async function generateBackendToken(): Promise { const res = await request<{ token: string }>("/api/config/generate-token", { method: "POST", }); return res.token; } // SSE Events — endpoint is /api/events export interface SystemEvent { type: "instance.started" | "instance.stopped" | "instance.error"; instance?: Instance; } export interface AgentEvent { agentId: string; action: string; url?: string; timestamp: string; } export type EventHandler = { onSystem?: (event: SystemEvent) => void; onAgent?: (event: AgentEvent) => void; onInit?: (agents: Agent[]) => void; onMonitoring?: (snapshot: MonitoringSnapshot) => void; }; export function subscribeToEvents( handlers: EventHandler, options?: { includeMemory?: boolean }, ): () => void { const url = addTokenToUrl( options?.includeMemory ? "/api/events?memory=1" : "/api/events", ); const es = new EventSource(url); es.addEventListener("init", (e) => { try { const agents = JSON.parse(e.data) as Agent[]; handlers.onInit?.(agents); } catch { // ignore } }); es.addEventListener("system", (e) => { try { const event = JSON.parse(e.data) as SystemEvent; handlers.onSystem?.(event); } catch { // ignore } }); es.addEventListener("action", (e) => { try { const event = JSON.parse(e.data) as AgentEvent; handlers.onAgent?.(event); } catch { // ignore } }); es.addEventListener("monitoring", (e) => { try { const snapshot = normalizeMonitoringSnapshot( JSON.parse(e.data) as Partial, ); handlers.onMonitoring?.(snapshot); } catch { // ignore } }); // Suppress connection errors (expected on page reload/navigation) es.onerror = () => { if (!getStoredAuthToken()) { dispatchAuthRequired("missing_token"); } }; // Clean up on page unload to prevent ERR_INCOMPLETE_CHUNKED_ENCODING const cleanup = () => es.close(); window.addEventListener("beforeunload", cleanup); return () => { window.removeEventListener("beforeunload", cleanup); es.close(); }; }