/** * useInfoCenter Hook (Orchestrator) * * Main orchestrator hook for InfoCenter feature. * Composes focused hooks into a unified interface for backward compatibility. * * Refactored from 843 lines to ~100 lines by splitting into: * - useCrawledPages: TanStack Query for pages * - usePageAnalyses: TanStack Query for analyses * - useStatistics: Computed statistics * - useInfoCenterActions: Mutations for CRUD operations * - useScanProgress: TanStack Query for scan progress (uses useScanStatus internally) * * @layer Presentation */ import { useCallback } from 'react'; import { useCrawledPages } from './useCrawledPages'; import { usePageAnalyses } from './usePageAnalyses'; import { useStatistics } from './useStatistics'; import { useInfoCenterActions } from './useInfoCenterActions'; import { useScanProgress } from './useScanProgress'; import type { DiscoveredPage } from '@/domain/entities/DiscoveredPage'; import type { PageAnalysis, PageElement } from '@/domain/entities/PageAnalysis'; import type { InfoCenterStats } from './useStatistics'; import type { ScanProgress } from './useScanProgress'; /** * Pagination Metadata */ export interface PaginationInfo { /** * Current page number */ page: number; /** * Items per page */ limit: number; /** * Total number of items */ total: number; /** * Total number of pages */ totalPages: number; /** * Has next page */ hasNext: boolean; /** * Has previous page */ hasPrevious: boolean; } /** * InfoCenter Hook State */ interface UseInfoCenterState { discoveredPages: DiscoveredPage[]; pageAnalyses: PageAnalysis[]; stats: InfoCenterStats; isLoading: boolean; error: Error | null; scanProgress: ScanProgress; pagination: PaginationInfo; } /** * InfoCenter Hook Actions */ interface UseInfoCenterActions { loadData: (tenantId: string) => Promise; rescanPage: (pageId: string) => Promise; rescanAll: (tenantId: string) => Promise; addUrlToScan: (tenantId: string, url: string) => Promise; updateElement: (analysisId: string, elementId: string, updates: Partial) => Promise; refresh: () => Promise; } /** * InfoCenter Hook Return Type */ export type UseInfoCenterReturn = UseInfoCenterState & UseInfoCenterActions; /** * useInfoCenter Hook Options */ export interface UseInfoCenterOptions { /** * Tenant ID to fetch data for */ tenantId?: string; /** * Page number for pages list (1-indexed) * @default 1 */ page?: number; /** * Number of items per page for pages list * @default 50 */ limit?: number; /** * Enable scan progress polling (only when relevant, e.g., on pages tab) * @default true */ enableScanPolling?: boolean; } /** * useInfoCenter Hook (Orchestrator) * * Main hook for InfoCenter feature. Provides data and actions for managing * discovered pages and analyses. * * This orchestrator composes focused hooks: * - Data fetching via TanStack Query (automatic caching, deduplication) * - Scan progress via custom polling hook (UI state, not server state) * - Statistics via computed hook (useMemo-based) * - Mutations via TanStack Query mutations (automatic cache invalidation) * * @param options - Hook options including tenantId and pagination * @returns InfoCenter state and actions * * @example * ```tsx * function InfoCenterPage() { * const [page, setPage] = useState(1); * const { * discoveredPages, * pageAnalyses, * stats, * isLoading, * scanProgress, * pagination, * rescanPage, * rescanAll, * } = useInfoCenter({ tenantId: selectedTenantId, page, limit: 50 }); * * return ( *
* * * *
* ); * } * ``` */ export function useInfoCenter(options: UseInfoCenterOptions = {}): UseInfoCenterReturn { const { tenantId, page = 1, limit = 50, enableScanPolling = true } = options; // Data hooks (TanStack Query - automatic caching and request deduplication) const { pages, isLoading: pagesLoading, error: pagesError, refetch: refetchPages, total, totalPages, page: currentPage, limit: pageSize, hasNext, hasPrevious, } = useCrawledPages({ tenantId, page, limit }); const { analyses, isLoading: analysesLoading, error: analysesError, refetch: refetchAnalyses } = usePageAnalyses({ tenantId, limit: 100, // Performance: limit to 100 analyses }); // Computed statistics (useMemo-based) const { stats, isLoading: statsLoading, error: statsError } = useStatistics({ tenantId, limit: 100 }); // Mutation hooks (TanStack Query - automatic cache invalidation) const { retriggerAnalysis, updateElement: updateElementMutation } = useInfoCenterActions(); // Scan progress hook (uses TanStack Query for polling) const { scanProgress, startScan, checkActiveScans } = useScanProgress({ tenantId, enabled: enableScanPolling, onScanComplete: () => { // Refresh data after scan completes refetchPages(); refetchAnalyses(); }, }); /** * Load InfoCenter data for a tenant * * @deprecated This function is no longer needed. TanStack Query automatically * fetches data when tenantId changes via query keys. Manual refetch bypasses * TanStack Query's intelligent caching system and causes duplicate API requests. * * Migration: Remove calls to loadData(). Pass tenantId directly to useInfoCenter hook. * * @param tid - Tenant ID */ const loadData = useCallback(async (tid: string) => { // NO-OP: TanStack Query handles data fetching automatically via query keys // When tenantId changes, useCrawledPages and usePageAnalyses automatically refetch // due to queryKey changes: queryKeys.infoCenter.pages(tenantId) and queryKeys.infoCenter.analyses(tenantId) // Only check for active scans (not a data fetch) await checkActiveScans(tid); // Log deprecation warning in development const isDev = typeof (import.meta as any).env !== 'undefined' && (import.meta as any).env.DEV; if (isDev) { console.warn( '[DEPRECATED] useInfoCenter.loadData() is deprecated and will be removed. ' + 'TanStack Query automatically refetches when tenantId changes. ' + 'Remove manual loadData() calls to use cached data.' ); } }, [checkActiveScans]); /** * Rescan a single page * * Updates the page's analysis status to trigger re-analysis. * * @param pageId - Page ID to rescan */ const rescanPage = useCallback(async (pageId: string) => { if (!tenantId) { console.warn('No tenant selected'); return; } try { await retriggerAnalysis.mutateAsync({ pageId, tenantId, priority: 'NORMAL', createNewVersion: true, }); } catch (error) { console.error('Error rescanning page:', error); throw error; } }, [tenantId, retriggerAnalysis]); /** * Rescan all pages for a tenant * * Triggers a FULL SITE SCAN via the Scout crawler API. * Scout will crawl the entire website, discover pages, and analyze them. * Progress is polled from the API until the job completes. * * @param tid - Tenant ID */ const rescanAll = useCallback(async (tid: string) => { await startScan(tid); }, [startScan]); /** * Add URL to scan * * Creates a new discovered page for manual analysis. * Note: API endpoint not implemented yet. * * @param _tenantId - Tenant ID * @param _url - URL to scan */ const addUrlToScan = useCallback(async (_tenantId: string, _url: string) => { // TODO: API endpoint for creating pages doesn't exist yet // Need to implement POST /api/v1/info-center/pages endpoint in backend console.warn('addUrlToScan: API endpoint not implemented yet'); throw new Error('Feature not available: API endpoint not implemented'); }, []); /** * Update element in analysis * * Updates an element's properties in a page analysis. * * @param analysisId - Analysis ID * @param elementId - Element ID * @param updates - Element properties to update */ const updateElement = useCallback(async ( analysisId: string, elementId: string, updates: Partial ) => { if (!tenantId) { console.warn('No tenant selected'); return; } try { await updateElementMutation.mutateAsync({ elementId, pageId: analysisId, // Analysis ID is the same as page ID tenantId, updates, }); } catch (error) { console.error('Error updating element:', error); throw error; } }, [tenantId, updateElementMutation]); /** * Refresh current data * * Reloads data for the current tenant. */ const refresh = useCallback(async () => { if (tenantId) { await loadData(tenantId); } }, [tenantId, loadData]); // Combine loading and error states const isLoading = pagesLoading || analysesLoading || statsLoading; const error = pagesError || analysesError || statsError; return { // State discoveredPages: pages, pageAnalyses: analyses, stats, isLoading, error, scanProgress, pagination: { page: currentPage, limit: pageSize, total, totalPages, hasNext, hasPrevious, }, // Actions loadData, rescanPage, rescanAll, addUrlToScan, updateElement, refresh, }; }