/** * 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"; } } export function isGithubReauthError(error: unknown): boolean { if (!(error instanceof ApiError)) return false; if (error.status === 401) return true; return /github token|re-login required/i.test(error.message); } 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; slack_oauth_enabled?: boolean; } export interface ModelOption { id: string; label: string; efforts: Array; default_effort: string; supports_images: boolean; } export interface OptionsPayload { models: Array; default_agent_model: string; default_agent_reasoning_effort: string; default_agent_subagent_model: string; default_agent_subagent_reasoning_effort: string; } export interface Profile { login?: string; email?: string; default_model?: string; reasoning_effort?: string; default_subagent_model?: string | null; subagent_reasoning_effort?: string | null; default_repo?: string | null; base_branch?: string | null; branch_prefix?: string | null; auto_fix_ci?: boolean; create_prs?: boolean; review_draft_prs?: boolean | null; updated_at?: string; } export interface ProfileUpdate { default_model: string; reasoning_effort: string; default_subagent_model?: string | null; subagent_reasoning_effort?: string | null; default_repo?: string | null; base_branch?: string | null; branch_prefix?: string | null; auto_fix_ci?: boolean; create_prs?: boolean; review_draft_prs?: boolean | null; } export type TriggerMode = "every_push" | "once_per_pr" | "manual"; export type AutofixMode = "off" | "low" | "medium" | "high"; export interface TeamSettings { trigger_mode: TriggerMode; review_draft_prs: boolean; pr_summaries: boolean; review_trace_links: boolean; autofix_mode: AutofixMode; autofix_severity_threshold: AutofixMode; org_guidelines?: string | null; default_agent_model?: string | null; default_agent_reasoning_effort?: string | null; default_agent_subagent_model?: string | null; default_agent_subagent_reasoning_effort?: string | null; default_repo?: string | null; default_reviewer_model?: string | null; default_reviewer_reasoning_effort?: string | null; default_reviewer_subagent_model?: string | null; default_reviewer_subagent_reasoning_effort?: string | null; updated_at?: string | null; } export interface UserMapping { github_login: string; work_email: string; slack_user_id?: string | null; source?: string; status?: string; created_at?: string; updated_at?: string; } export interface UserMappingsPage { items: Array; total: number; page: number; page_size: number; } export type UsageLeaderboardPeriod = "7d" | "30d" | "all"; export interface UsageLeaderboardRow { rank: number; user: { name: string; github_login: string | null; email: string | null; }; favorite_model: string; agent_runs: number; prs_opened: number; merged_prs: number; agent_loc: number; additions: number; deletions: number; } export interface ReviewerStatsCounterRow { name: string; count: number; } export interface ReviewerStatsPayload { period: UsageLeaderboardPeriod; reviewed_prs: number; prs_with_findings: number; findings_recorded: number; surfaced_findings: number; addressed_findings: number; resolved_after_update: number; dismissed_findings: number; unresolved_surfaced_findings: number; resolution_rate: number; human_replies: number; severity_counts: Record; top_categories: Array; generated_at_ms: number | null; } export interface UsageLeaderboardPayload { period: UsageLeaderboardPeriod; rows: Array; total_members: number; current_user_rank: number | null; generated_at_ms: number | null; reviewer_stats: ReviewerStatsPayload; } 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 interface AgentInstructions { full_name: string; owner?: string; name?: string; instructions: string; created_by?: string; created_at?: string; updated_at?: string; } export const api = { me: () => request("/me"), options: () => request("/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", }), cancelReviewStyle: (full_name: string) => request(`/review-styles/${encodeURIComponent(full_name)}/cancel`, { method: "POST", }), deleteReviewStyle: (full_name: string) => request(`/review-styles/${encodeURIComponent(full_name)}`, { method: "DELETE", }), listAgentInstructions: () => request>("/agent-instructions"), createAgentInstructions: (full_name: string) => request("/agent-instructions", { method: "POST", body: JSON.stringify({ full_name }), }), getAgentInstructions: (full_name: string) => request(`/agent-instructions/${encodeURIComponent(full_name)}`), saveAgentInstructions: (full_name: string, instructions: string) => request(`/agent-instructions/${encodeURIComponent(full_name)}`, { method: "PUT", body: JSON.stringify({ instructions }), }), deleteAgentInstructions: (full_name: string) => request(`/agent-instructions/${encodeURIComponent(full_name)}`, { method: "DELETE", }), getTeamSettings: () => request("/team-settings"), saveTeamSettings: (body: TeamSettings) => request("/team-settings", { method: "PUT", body: JSON.stringify(body) }), listEnabledReviewRepos: () => request<{ repos: Array }>("/enabled-review-repos"), setEnabledReviewRepo: (full_name: string, enabled: boolean) => request<{ repos: Array }>("/enabled-review-repos", { method: "PUT", body: JSON.stringify({ full_name, enabled }), }), usageLeaderboard: (period: UsageLeaderboardPeriod = "30d", limit = 10) => request( `/agent-usage-leaderboard?period=${encodeURIComponent(period)}&limit=${limit}`, ), myMapping: () => request>("/my-mapping"), adminListUserMappings: (page = 1, pageSize = 20) => request( `/admin/user-mappings?page=${page}&page_size=${pageSize}`, ), adminDeleteUserMapping: (github_login: string) => request<{ deleted: boolean }>( `/admin/user-mappings/${encodeURIComponent(github_login)}`, { method: "DELETE" }, ), 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}`; } export function slackConnectUrl(): string { return `${API_BASE}/dashboard/api/slack/login`; }