/** * REST API client for Web Inspector backend. * Uses native fetch (Node 20+) with AbortController timeout. */ const DEFAULT_TIMEOUT = 120_000; // 120 seconds export class ApiClient { constructor(private baseUrl: string) { // Strip trailing slash this.baseUrl = baseUrl.replace(/\/+$/, ""); } // ── Single-page inspection ────────────────────────────── async startInspection( url: string, accessibilityStandard?: string, ): Promise<{ inspection_id: string; status: string }> { const body: Record = { url }; if (accessibilityStandard) body.accessibility_standard = accessibilityStandard; return this.post("/api/inspections", body); } async getInspection(inspectionId: string): Promise { return this.get(`/api/inspections/${inspectionId}`); } async getIssues( inspectionId: string, category?: string, severity?: string, ): Promise { const params = new URLSearchParams(); if (category) params.set("category", category); if (severity) params.set("severity", severity); const qs = params.toString(); return this.get(`/api/inspections/${inspectionId}/issues${qs ? `?${qs}` : ""}`); } async getInspections( url?: string, limit = 10, ): Promise { const params = new URLSearchParams({ limit: String(limit) }); if (url) params.set("url", url); return this.get(`/api/inspections?${params.toString()}`); } // ── Site-wide inspection ──────────────────────────────── async startSiteInspection( url: string, maxPages?: number, maxDepth?: number, accessibilityStandard?: string, ): Promise<{ site_inspection_id: string; status: string }> { const body: Record = { url }; if (maxPages !== undefined) body.max_pages = maxPages; if (maxDepth !== undefined) body.max_depth = maxDepth; if (accessibilityStandard) body.accessibility_standard = accessibilityStandard; return this.post("/api/site-inspections", body); } async getSiteInspection(siteInspectionId: string): Promise { return this.get(`/api/site-inspections/${siteInspectionId}`); } async getSiteInspections(limit = 10): Promise { return this.get(`/api/site-inspections?limit=${limit}`); } // ── HTTP helpers ──────────────────────────────────────── private async get(path: string): Promise { return this.request("GET", path); } private async post(path: string, body: unknown): Promise { return this.request("POST", path, body); } private async request(method: string, path: string, body?: unknown): Promise { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT); try { const res = await fetch(`${this.baseUrl}${path}`, { method, headers: body ? { "Content-Type": "application/json" } : undefined, body: body ? JSON.stringify(body) : undefined, signal: controller.signal, }); if (!res.ok) { const text = await res.text().catch(() => ""); throw new Error(`API ${method} ${path} failed: ${res.status} ${text}`); } return (await res.json()) as T; } finally { clearTimeout(timer); } } } // ── Response types ────────────────────────────────────── export interface Issue { code: string; category: string; severity: "critical" | "major" | "minor" | "info"; message: string; element?: string; line?: number; suggestion: string; wcag_criterion?: string; kwcag_criterion?: string; kwcag_name?: string; kwcag_principle?: string; } export interface CategoryResult { score: number; grade: string; total_issues: number; critical: number; major: number; minor: number; info: number; issues: Issue[]; wcag_level?: string; } export interface InspectionResult { inspection_id: string; url: string; status: "running" | "completed" | "error"; created_at: string; completed_at?: string; duration_seconds?: number; overall_score: number; grade: string; accessibility_standard?: string; categories: { html_css: CategoryResult; accessibility: CategoryResult; seo: CategoryResult; performance_security: CategoryResult; }; summary: { total_issues: number; critical: number; major: number; minor: number; info: number; }; } export interface IssuesResult { inspection_id: string; total: number; filters: Record; issues: Issue[]; } export interface InspectionsListResult { items: Array<{ inspection_id: string; url: string; created_at: string; overall_score: number; grade: string; total_issues: number; }>; total: number; page: number; limit: number; total_pages: number; } export interface SiteInspectionResult { site_inspection_id: string; root_url: string; domain: string; status: "crawling" | "inspecting" | "completed" | "error"; created_at: string; completed_at?: string; config: { max_pages: number; max_depth: number; concurrency: number; accessibility_standard: string; }; discovered_pages: Array<{ url: string; depth: number; parent_url?: string; inspection_id?: string; status: string; title?: string; overall_score?: number; grade?: string; }>; aggregate_scores?: { overall_score: number; grade: string; html_css: number; accessibility: number; seo: number; performance_security: number; total_issues: number; pages_inspected: number; pages_total: number; }; } export interface SiteInspectionsListResult { items: Array<{ site_inspection_id: string; root_url: string; domain: string; status: string; created_at: string; pages_total: number; pages_inspected: number; overall_score?: number; grade?: string; }>; total: number; page: number; limit: number; total_pages: number; }