/** * Info Center API Methods * * HTTP client methods for Info Center operations (pages, analysis, elements). * All methods accept/return domain objects - schemas are handled internally by mappers. * * Data Flow: * - Pages/Analysis are READ-ONLY (created by crawler service) * - Dashboard can TRIGGER/RETRIGGER scans (POST endpoints queue jobs) * - Dashboard can UPDATE/CORRECT metadata (PUT endpoints for user corrections) * * @layer Infrastructure - API Client */ import { PAGES_LIST, PAGES_GET, PAGES_UPDATE, PAGES_DELETE, PAGE_STATS, ANALYSIS_GET, ANALYSIS_TRIGGER, ANALYSIS_RETRIGGER, ANALYSIS_DELETE, ELEMENT_STATS, ELEMENT_UPDATE, TRIGGER_SITE_SCAN, GET_ACTIVE_SCANS, GET_SCAN_STATUS, } from "@archer/api-interface/endpoints/customer-api"; import type { ListPagesRequest, UpdateElementRequest, PageElementResponse, } from '@archer/api-interface'; import { ReliabilityStatus } from '@archer/domain'; import { ApiClient } from '../../ApiClient'; import { DiscoveredPage } from '@/domain/entities/DiscoveredPage'; import { PageAnalysis } from '@/domain/entities/PageAnalysis'; import type { PaginationMeta, PaginatedResponse } from '../shared/types'; import { PageRequestMapper } from './mappers/PageRequestMapper'; import { PageResponseMapper } from './mappers/PageResponseMapper'; import { PageAnalysisResponseMapper } from './mappers/PageAnalysisResponseMapper'; /** * Get Page Analyses Request Parameters * * Query parameters for listing page analyses with pagination and filtering. */ export interface GetPageAnalysesParams { /** * Tenant ID to filter analyses */ tenantId: string; /** * Number of items to skip (for pagination) * Default: 0 */ skip?: number; /** * Maximum number of items to return * Default: 20, Max: 100 */ limit?: number; /** * Filter by reliability status */ reliabilityStatus?: ReliabilityStatus; } /** * Info Center API * * Provides Info Center operations with domain-driven interface. * UI layer works with DiscoveredPage and PageAnalysis domain objects, * API client handles schema mapping internally. */ export class InfoCenterApi { constructor(private client: ApiClient) {} // ============================================ // PAGE ENDPOINTS // ============================================ /** * Lists discovered pages for a tenant with pagination and filters * * Pages are created by crawler service, not by dashboard. * * @param tenantId - Tenant ID to list pages for * @param params - Optional pagination and filter params * @returns Object with pages array and pagination metadata * @throws ApiError on failure */ async listPages( tenantId: string, params?: Partial ): Promise<{ pages: DiscoveredPage[]; pagination: PaginationMeta }> { // Replace :tenantId in path const path = PAGES_LIST.path.replace(':tenantId', tenantId); // HTTP call with query params const responseData = await this.client.get(path, { params }); // Map response schema → domain (internal) return PageResponseMapper.toPageList(responseData); } /** * Gets a single discovered page by ID * * @param id - Page ID * @param tenantId - Tenant ID (required for multi-tenancy) * @returns DiscoveredPage domain entity * @throws ApiError on failure (404 if not found) */ async getPage(id: string, tenantId: string): Promise { // Replace :id in path const path = PAGES_GET.path.replace(':id', id); // HTTP call with tenantId query param (required for multi-tenancy) const responseData = await this.client.get(path, { params: { tenantId } }); // Map response schema → domain (internal) return PageResponseMapper.toPage(responseData); } /** * Updates discovered page metadata * * Used for correcting page status, type, semantic labels, or other metadata * after initial crawler discovery. Dashboard allows users to correct crawler mistakes. * * @param id - Page ID * @param tenantId - Tenant ID (required for multi-tenancy) * @param page - Partial DiscoveredPage domain object with updates * @returns Updated DiscoveredPage domain entity * @throws ApiError on failure (404 if not found) */ async updatePage(id: string, tenantId: string, page: Partial): Promise { // Map domain → request schema (internal) const requestDto = PageRequestMapper.toUpdateRequest(page); // Replace :id in path const path = PAGES_UPDATE.path.replace(':id', id); // HTTP call with tenantId query param (required for multi-tenancy) const responseData = await this.client.put(path, requestDto, { params: { tenantId } }); // Map response schema → domain (internal) return PageResponseMapper.toPage(responseData); } /** * Deletes a discovered page * * @param id - Page ID * @param tenantId - Tenant ID (required for multi-tenancy) * @returns void * @throws ApiError on failure (404 if not found) */ async deletePage(id: string, tenantId: string): Promise { // Replace :id in path const path = PAGES_DELETE.path.replace(':id', id); // HTTP call with tenantId query param (required for multi-tenancy) await this.client.delete(path, { params: { tenantId } }); } /** * Gets aggregate statistics for all discovered pages in a tenant * * Returns summary data for dashboard widgets: total pages, status breakdown, * analysis progress, crawl depth, and last discovery timestamp. * * @param tenantId - Tenant ID * @returns Page statistics object * @throws ApiError on failure */ async getPageStats(tenantId: string): Promise<{ totalPages: number; activePages: number; brokenPages: number; analyzedPages: number; pendingAnalysis: number; averageDepth: number; lastCrawledAt?: Date; }> { // Replace :tenantId in path const path = PAGE_STATS.path.replace(':tenantId', tenantId); // HTTP call const responseData = await this.client.get(path); // Map response schema → domain (internal) return PageResponseMapper.toPageStats(responseData); } // ============================================ // ANALYSIS ENDPOINTS // ============================================ /** * Gets latest page analysis with discovered elements * * Returns complete analysis data including element details and selector strategies. * * @param pageId - Page ID * @param tenantId - Tenant ID (required for multi-tenancy) * @returns PageAnalysis domain entity * @throws ApiError on failure (404 if not found or not analyzed) */ async getAnalysis(pageId: string, tenantId: string): Promise { // Replace :pageId in path const path = ANALYSIS_GET.path.replace(':pageId', pageId); // HTTP call with tenantId query param (required for multi-tenancy) const responseData = await this.client.get(path, { params: { tenantId } }); // Map response schema → domain (internal) return PageAnalysisResponseMapper.toAnalysis(responseData); } /** * Gets page analyses with pagination and filtering * * Returns paginated list of page analyses for a tenant with optional * reliability status filtering. * * @param params - Query parameters (tenantId, skip, limit, reliabilityStatus) * @returns Paginated response with analyses and metadata * @throws ApiError on failure */ async getPageAnalyses(params: GetPageAnalysesParams): Promise> { // Build query parameters const queryParams: Record = { skip: params.skip ?? 0, limit: params.limit ?? 20, }; if (params.reliabilityStatus) { queryParams.reliabilityStatus = params.reliabilityStatus; } // Use listPages endpoint path with tenantId const path = PAGES_LIST.path.replace(':tenantId', params.tenantId); // HTTP call with pagination and filter params const responseData = await this.client.get(path, { params: queryParams }); // Map response to paginated analyses // Note: This assumes the API returns pages with their analyses // If the API returns a separate analyses endpoint, update the path accordingly const analyses = responseData.items?.map((item: any) => PageAnalysisResponseMapper.toAnalysis(item) ) || []; return { items: analyses, meta: { page: Math.floor((params.skip ?? 0) / (params.limit ?? 20)) + 1, limit: params.limit ?? 20, total: responseData.total || responseData.meta?.total || 0, totalPages: Math.ceil((responseData.total || responseData.meta?.total || 0) / (params.limit ?? 20)), hasNext: responseData.meta?.hasMore ?? false, hasPrevious: (params.skip ?? 0) > 0, }, }; } /** * Triggers initial page analysis (first-time scan) * * Queues page for element discovery and selector generation by crawler. * Used when user clicks "Analyze this page" button in dashboard. * * Data flow: Dashboard POSTs → API queues scan → Crawler analyzes → Results written to database * * @param pageId - Page ID to analyze * @param tenantId - Tenant ID (required for multi-tenancy) * @param priority - Job priority (LOW, NORMAL, HIGH) - default NORMAL * @param forceRescan - Override cache/existing scan - default false * @returns void (analysis is queued, results are retrieved later via getAnalysis) * @throws ApiError on failure */ async triggerAnalysis( pageId: string, tenantId: string, priority: 'LOW' | 'NORMAL' | 'HIGH' = 'NORMAL', forceRescan: boolean = false ): Promise { // Map params → request schema (internal) const requestDto = PageRequestMapper.toTriggerAnalysisRequest(priority, forceRescan); // Replace :pageId in path const path = ANALYSIS_TRIGGER.path.replace(':pageId', pageId); // HTTP call with tenantId query param (required for multi-tenancy) await this.client.post(path, requestDto, { params: { tenantId } }); } /** * Triggers page re-analysis (rescan of already analyzed page) * * Queues page for fresh element discovery, creating new analysis version. * Used when user clicks "Re-analyze" button to get updated element data. * * Data flow: Dashboard POSTs → API queues rescan → Crawler re-analyzes → New version created * * @param pageId - Page ID to re-analyze * @param tenantId - Tenant ID (required for multi-tenancy) * @param priority - Job priority (LOW, NORMAL, HIGH) - default NORMAL * @param createNewVersion - Create new analysis version - default true * @returns void (re-analysis is queued, results are retrieved later via getAnalysis) * @throws ApiError on failure */ async retriggerAnalysis( pageId: string, tenantId: string, priority: 'LOW' | 'NORMAL' | 'HIGH' = 'NORMAL', createNewVersion: boolean = true ): Promise { // Map params → request schema (internal) const requestDto = PageRequestMapper.toRetriggerAnalysisRequest(priority, createNewVersion); // Replace :pageId in path const path = ANALYSIS_RETRIGGER.path.replace(':pageId', pageId); // HTTP call with tenantId query param (required for multi-tenancy) await this.client.post(path, requestDto, { params: { tenantId } }); } /** * Deletes page analysis and all associated elements * * Removes analysis data while keeping discovered page record intact. * * @param pageId - Page ID * @param tenantId - Tenant ID (required for multi-tenancy) * @returns void * @throws ApiError on failure (404 if not found) */ async deleteAnalysis(pageId: string, tenantId: string): Promise { // Replace :pageId in path const path = ANALYSIS_DELETE.path.replace(':pageId', pageId); // HTTP call with tenantId query param (required for multi-tenancy) await this.client.delete(path, { params: { tenantId } }); } /** * Gets element statistics for a specific page analysis * * Returns element type breakdown, visibility counts, and interactive element summary. * Used for dashboard charts and element distribution visualizations. * * @param pageId - Page ID * @param tenantId - Tenant ID (required for multi-tenancy) * @returns Element statistics object * @throws ApiError on failure */ async getElementStats(pageId: string, tenantId: string): Promise<{ totalElements: number; byType: Record; visibleElements: number; interactiveElements: number; }> { // Replace :pageId in path const path = ELEMENT_STATS.path.replace(':pageId', pageId); // HTTP call with tenantId query param (required for multi-tenancy) const responseData = await this.client.get(path, { params: { tenantId } }); // Map response schema → domain (internal) return PageAnalysisResponseMapper.toElementStats(responseData); } // ============================================ // ELEMENT ENDPOINTS // ============================================ /** * Updates page element properties * * Used for correcting crawler mistakes in element detection. * Allows users to fix element type, selectors, semantic description, and other properties. * * Use cases: * - Correct wrong element type (e.g., crawler detected BUTTON, actually is LINK) * - Add/fix semantic description for better understanding * - Adjust selector strategies for better reliability * - Update visibility or position data * * @param elementId - Element ID * @param pageId - Page ID (required for element lookup) * @param tenantId - Tenant ID (required for multi-tenancy) * @param updates - Element updates (type, selectors, semanticDescription, text, isVisible, position) * @returns Updated element as PageElement (via mapper) * @throws ApiError on failure (404 if not found) */ async updateElement(elementId: string, pageId: string, tenantId: string, updates: UpdateElementRequest): Promise<{ id: string; type: string; selectors: Array<{ type: string; value: string; score: number }>; semanticDescription: string; text?: string; isVisible: boolean; position?: { x: number; y: number; width: number; height: number }; }> { // Replace :id in path const path = ELEMENT_UPDATE.path.replace(':id', elementId); // HTTP call with tenantId and pageId query params (required for multi-tenancy) const responseData = await this.client.put(path, updates, { params: { tenantId, pageId } }); // Map response schema → domain object (internal) return PageAnalysisResponseMapper.mapElement(responseData); } // ============================================ // SITE SCAN ENDPOINTS // ============================================ /** * Triggers a full site scan for a tenant * * Creates a scan job that instructs the Scout crawler to: * - Crawl the entire website * - Discover all pages * - Analyze all pages and extract training data * * @param tenantId - Tenant ID to scan * @param options - Scan options (priority, forceRescan, clearExisting) * @returns ScanJobResponse with job ID and status * @throws ApiError on failure */ async triggerSiteScan( tenantId: string, options: { priority?: 'LOW' | 'NORMAL' | 'HIGH'; forceRescan?: boolean; maxDepth?: number; clearExisting?: boolean; } = {} ): Promise<{ jobId: string; tenantId: string; status: 'QUEUED' | 'DISCOVERING' | 'RUNNING' | 'COMPLETED' | 'FAILED' | 'CANCELLED'; pagesDiscovered: number; pagesAnalyzed: number; pagesFailed: number; priority: 'LOW' | 'NORMAL' | 'HIGH'; progress: number; createdAt: string; startedAt: string | null; completedAt: string | null; errorMessage: string | null; message: string; }> { // Replace :tenantId in path const path = TRIGGER_SITE_SCAN.path.replace(':tenantId', tenantId); // Build request body with defaults const requestBody = { priority: options.priority || 'HIGH', forceRescan: options.forceRescan ?? true, maxDepth: options.maxDepth, clearExisting: options.clearExisting ?? false, }; // HTTP call const responseData = await this.client.post(path, requestBody); return responseData; } /** * Gets active (non-completed) scan jobs for a tenant * * Returns jobs with status QUEUED, DISCOVERING, or RUNNING. * Used by dashboard to restore scan progress after page refresh. * * @param tenantId - Tenant ID * @returns Array of active scan jobs (usually 0 or 1) * @throws ApiError on failure */ async getActiveScans( tenantId: string ): Promise> { // Replace :tenantId in path const path = GET_ACTIVE_SCANS.path.replace(':tenantId', tenantId); // HTTP call const responseData = await this.client.get(path); return responseData; } /** * Gets the status of a scan job * * Used to poll for scan progress and completion. * * @param tenantId - Tenant ID * @param jobId - Scan job ID * @returns ScanJobResponse with current status * @throws ApiError on failure (404 if job not found) */ async getScanStatus( tenantId: string, jobId: string ): Promise<{ jobId: string; tenantId: string; status: 'QUEUED' | 'DISCOVERING' | 'RUNNING' | 'COMPLETED' | 'FAILED' | 'CANCELLED'; pagesDiscovered: number; pagesAnalyzed: number; pagesFailed: number; priority: 'LOW' | 'NORMAL' | 'HIGH'; progress: number; createdAt: string; startedAt: string | null; completedAt: string | null; errorMessage: string | null; message: string; }> { // Replace path params const path = GET_SCAN_STATUS.path .replace(':tenantId', tenantId) .replace(':jobId', jobId); // HTTP call const responseData = await this.client.get(path); return responseData; } } /** * Factory function to create InfoCenterApi instance * * @param client - ApiClient instance * @returns InfoCenterApi instance */ export function createInfoCenterApi(client: ApiClient): InfoCenterApi { return new InfoCenterApi(client); }