/** * Typed client for the open-swe dashboard backend. * * All requests are sent with credentials so the httpOnly `osw_session` * cookie set by the OAuth callback rides along on cross-origin calls. */ const API_BASE = (import.meta.env.VITE_DASHBOARD_API_BASE_URL ?? "").replace(/\/$/, ""); if (!API_BASE && typeof window !== "undefined") { console.warn("VITE_DASHBOARD_API_BASE_URL is not set"); } export class ApiError extends Error { constructor(public readonly status: number, message: string) { super(message); this.name = "ApiError"; } } async function request(path: string, init: RequestInit = {}): Promise { const res = await fetch(`${API_BASE}/dashboard/api${path}`, { ...init, credentials: "include", headers: { "Content-Type": "application/json", ...(init.headers ?? {}), }, }); if (!res.ok) { let message = res.statusText; try { const body = await res.json(); if (body?.detail) message = typeof body.detail === "string" ? body.detail : JSON.stringify(body.detail); } catch { /* ignore */ } throw new ApiError(res.status, message); } if (res.status === 204) return undefined as T; return (await res.json()) as T; } export interface SessionUser { login: string; email: string | null; avatar_url: string | null; is_admin: boolean; } export interface ModelOption { id: string; label: string; efforts: Array; default_effort: string; } export interface Profile { login?: string; email?: string; default_model?: string; reasoning_effort?: string; default_repo?: string | null; first_name?: string | null; last_name?: string | null; base_branch?: string | null; branch_prefix?: string | null; auto_fix_ci?: boolean; create_prs?: boolean; allow_artifacts?: boolean; slack_notifications?: boolean; preferred_pr_destination?: string | null; updated_at?: string; } export interface ProfileUpdate { default_model: string; reasoning_effort: string; default_repo?: string | null; first_name?: string | null; last_name?: string | null; base_branch?: string | null; branch_prefix?: string | null; auto_fix_ci?: boolean; create_prs?: boolean; allow_artifacts?: boolean; slack_notifications?: boolean; preferred_pr_destination?: string | null; } export type TriggerMode = "every_push" | "ready_for_review" | "manual"; export type AutofixMode = "off" | "low" | "medium" | "high"; export interface TeamSettings { trigger_mode: TriggerMode; review_draft_prs: boolean; pr_summaries: boolean; autofix_mode: AutofixMode; autofix_severity_threshold: AutofixMode; updated_at?: string | null; } export interface Repository { full_name: string; private: boolean; } export interface Installation { id: number; account: string | null; account_type: string | null; } export interface ReposPayload { installations: Array; repositories: Array; } export type ReviewStyleStatus = "idle" | "running" | "completed" | "failed"; export interface ReviewStyle { full_name: string; owner?: string; name?: string; status: ReviewStyleStatus; custom_prompt: string | null; analysis_summary: string | null; top_reviewers: Array; prs_sampled: number; reviews_sampled: number; analysis_thread_id: string | null; analysis_run_id: string | null; error: string | null; created_by?: string; created_at?: string; updated_at?: string; } export const api = { me: () => request("/me"), options: () => request<{ models: Array }>("/options"), profile: () => request("/profile"), saveProfile: (body: ProfileUpdate) => request("/profile", { method: "PUT", body: JSON.stringify(body) }), repos: () => request("/repos"), listReviewStyles: () => request>("/review-styles"), createReviewStyle: (full_name: string) => request("/review-styles", { method: "POST", body: JSON.stringify({ full_name }), }), getReviewStyle: (full_name: string) => request(`/review-styles/${encodeURIComponent(full_name)}`), saveReviewStylePrompt: (full_name: string, custom_prompt: string) => request(`/review-styles/${encodeURIComponent(full_name)}`, { method: "PUT", body: JSON.stringify({ custom_prompt }), }), analyzeReviewStyle: (full_name: string) => request(`/review-styles/${encodeURIComponent(full_name)}/analyze`, { method: "POST", }), getTeamSettings: () => request("/team-settings"), saveTeamSettings: (body: TeamSettings) => request("/team-settings", { method: "PUT", body: JSON.stringify(body) }), adminListProfiles: () => request>("/admin/profiles"), adminSaveProfile: (login: string, body: ProfileUpdate & { email?: string }) => request(`/admin/profiles/${encodeURIComponent(login)}`, { method: "PUT", body: JSON.stringify(body), }), logout: () => request("/auth/logout", { method: "POST" }), }; export function loginUrl(redirectTo?: string): string { const target = redirectTo ?? (typeof window !== "undefined" ? window.location.origin : ""); const qs = target ? `?redirect_to=${encodeURIComponent(target)}` : ""; return `${API_BASE}/dashboard/api/auth/login${qs}`; }