From 63a92cf7d75bd4fbc236816196d5c506f5c620c3 Mon Sep 17 00:00:00 2001 From: Wirasm <152263317+Wirasm@users.noreply.github.com> Date: Mon, 22 Sep 2025 14:59:33 +0300 Subject: [PATCH] refactor: reorganize features/shared directory for better maintainability (#730) * refactor: reorganize features/shared directory structure - Created organized subdirectories for better code organization: - api/ - API clients and HTTP utilities (renamed apiWithEtag.ts to apiClient.ts) - config/ - Configuration files (queryClient, queryPatterns) - types/ - Shared type definitions (errors) - utils/ - Pure utility functions (optimistic, clipboard) - hooks/ - Shared React hooks (already existed) - Updated all import paths across the codebase (~40+ files) - Updated all AI documentation in PRPs/ai_docs/ to reflect new structure - All tests passing, build successful, no functional changes This improves maintainability and follows vertical slice architecture patterns. Co-Authored-By: Claude * fix: address PR review comments and code improvements - Update imports to use @/features alias path for optimistic utils - Fix optimistic upload item replacement by matching on source_id instead of id - Clean up test suite naming and remove meta-terms from comments - Only set Content-Type header on requests with body - Add explicit TypeScript typing to useProjectFeatures hook - Complete Phase 4 improvements with proper query typing * fix: address additional PR review feedback - Clear feature queries when deleting project to prevent cache memory leaks - Update KnowledgeCard comments to follow documentation guidelines - Add explanatory comment for accessibility pattern in KnowledgeCard --------- Co-authored-by: Claude --- PRPs/ai_docs/API_NAMING_CONVENTIONS.md | 2 +- PRPs/ai_docs/ARCHITECTURE.md | 6 +- PRPs/ai_docs/DATA_FETCHING_ARCHITECTURE.md | 10 +- PRPs/ai_docs/ETAG_IMPLEMENTATION.md | 10 +- PRPs/ai_docs/QUERY_PATTERNS.md | 8 +- PRPs/ai_docs/optimistic_updates.md | 6 +- archon-ui-main/src/App.tsx | 2 +- .../layout/hooks/useBackendHealth.ts | 4 +- .../knowledge/components/KnowledgeCard.tsx | 7 +- .../knowledge/hooks/useKnowledgeQueries.ts | 17 +- .../components/KnowledgeInspector.tsx | 2 +- .../inspector/hooks/useInspectorPagination.ts | 2 +- .../knowledge/services/knowledgeService.ts | 4 +- .../utils/tests/providerErrorHandler.test.ts | 206 +++++++++--------- .../knowledge/views/KnowledgeView.tsx | 2 +- .../mcp/components/McpConfigSection.tsx | 5 +- .../src/features/mcp/hooks/useMcpQueries.ts | 2 +- .../src/features/mcp/services/mcpApi.ts | 2 +- .../hooks/tests/useProgressQueries.test.ts | 2 +- .../progress/hooks/useProgressQueries.ts | 4 +- .../progress/services/progressService.ts | 2 +- .../projects/components/ProjectCard.tsx | 2 +- .../documents/components/DocumentCard.tsx | 2 +- .../documents/hooks/useDocumentQueries.ts | 2 +- .../projects/hooks/useProjectQueries.ts | 14 +- .../projects/services/projectService.ts | 4 +- .../projects/tasks/components/TaskCard.tsx | 2 +- .../projects/tasks/hooks/useTaskQueries.ts | 8 +- .../projects/tasks/services/taskService.ts | 4 +- .../tasks/services/tests/taskService.test.ts | 4 +- .../components/MigrationStatusCard.tsx | 6 +- .../components/PendingMigrationsModal.tsx | 17 +- .../migrations/hooks/useMigrationQueries.ts | 2 +- .../migrations/services/migrationService.ts | 2 +- .../version/components/UpdateBanner.tsx | 3 +- .../version/components/VersionStatusCard.tsx | 3 +- .../version/hooks/useVersionQueries.ts | 2 +- .../version/services/versionService.ts | 2 +- .../{apiWithEtag.ts => api/apiClient.ts} | 23 +- .../tests/apiClient.test.ts} | 18 +- .../shared/{ => config}/queryClient.ts | 0 .../shared/{ => config}/queryPatterns.ts | 0 .../src/features/shared/hooks/index.ts | 2 +- .../src/features/shared/hooks/useToast.ts | 2 +- .../src/features/shared/{ => types}/errors.ts | 0 .../features/shared/{ => utils}/optimistic.ts | 0 .../{ => utils/tests}/optimistic.test.ts | 14 +- .../src/features/testing/test-utils.tsx | 2 +- 48 files changed, 230 insertions(+), 215 deletions(-) rename archon-ui-main/src/features/shared/{apiWithEtag.ts => api/apiClient.ts} (83%) rename archon-ui-main/src/features/shared/{apiWithEtag.test.ts => api/tests/apiClient.test.ts} (96%) rename archon-ui-main/src/features/shared/{ => config}/queryClient.ts (100%) rename archon-ui-main/src/features/shared/{ => config}/queryPatterns.ts (100%) rename archon-ui-main/src/features/shared/{ => types}/errors.ts (100%) rename archon-ui-main/src/features/shared/{ => utils}/optimistic.ts (100%) rename archon-ui-main/src/features/shared/{ => utils/tests}/optimistic.test.ts (97%) diff --git a/PRPs/ai_docs/API_NAMING_CONVENTIONS.md b/PRPs/ai_docs/API_NAMING_CONVENTIONS.md index 5688912b..2135bc8d 100644 --- a/PRPs/ai_docs/API_NAMING_CONVENTIONS.md +++ b/PRPs/ai_docs/API_NAMING_CONVENTIONS.md @@ -198,7 +198,7 @@ Database values used directly - no mapping layers: - Operation statuses: `"pending"`, `"processing"`, `"completed"`, `"failed"` ### Time Constants -**Location**: `archon-ui-main/src/features/shared/queryPatterns.ts` +**Location**: `archon-ui-main/src/features/shared/config/queryPatterns.ts` - `STALE_TIMES.instant` - 0ms - `STALE_TIMES.realtime` - 3 seconds - `STALE_TIMES.frequent` - 5 seconds diff --git a/PRPs/ai_docs/ARCHITECTURE.md b/PRPs/ai_docs/ARCHITECTURE.md index a5c0ae7a..eb3a7f81 100644 --- a/PRPs/ai_docs/ARCHITECTURE.md +++ b/PRPs/ai_docs/ARCHITECTURE.md @@ -88,8 +88,8 @@ Pattern: `{METHOD} /api/{resource}/{id?}/{sub-resource?}` ### Data Fetching **Core**: TanStack Query v5 -**Configuration**: `archon-ui-main/src/features/shared/queryClient.ts` -**Patterns**: `archon-ui-main/src/features/shared/queryPatterns.ts` +**Configuration**: `archon-ui-main/src/features/shared/config/queryClient.ts` +**Patterns**: `archon-ui-main/src/features/shared/config/queryPatterns.ts` ### State Management - **Server State**: TanStack Query @@ -139,7 +139,7 @@ TanStack Query is the single source of truth. No separate state management neede No translation layers. Database values (e.g., `"todo"`, `"doing"`) used directly in UI. ### Browser-Native Caching -ETags handled by browser, not JavaScript. See `archon-ui-main/src/features/shared/apiWithEtag.ts`. +ETags handled by browser, not JavaScript. See `archon-ui-main/src/features/shared/api/apiClient.ts`. ## Deployment diff --git a/PRPs/ai_docs/DATA_FETCHING_ARCHITECTURE.md b/PRPs/ai_docs/DATA_FETCHING_ARCHITECTURE.md index d8a9822b..8d1bbb62 100644 --- a/PRPs/ai_docs/DATA_FETCHING_ARCHITECTURE.md +++ b/PRPs/ai_docs/DATA_FETCHING_ARCHITECTURE.md @@ -8,7 +8,7 @@ Archon uses **TanStack Query v5** for all data fetching, caching, and synchroniz ### 1. Query Client Configuration -**Location**: `archon-ui-main/src/features/shared/queryClient.ts` +**Location**: `archon-ui-main/src/features/shared/config/queryClient.ts` Centralized QueryClient with: @@ -30,7 +30,7 @@ Visibility-aware polling that: ### 3. Query Patterns -**Location**: `archon-ui-main/src/features/shared/queryPatterns.ts` +**Location**: `archon-ui-main/src/features/shared/config/queryPatterns.ts` Shared constants: @@ -64,7 +64,7 @@ Standard pattern across all features: ### ETag Support -**Location**: `archon-ui-main/src/features/shared/apiWithEtag.ts` +**Location**: `archon-ui-main/src/features/shared/api/apiClient.ts` ETag implementation: @@ -83,7 +83,7 @@ Backend endpoints follow RESTful patterns: ## Optimistic Updates -**Utilities**: `archon-ui-main/src/features/shared/optimistic.ts` +**Utilities**: `archon-ui-main/src/features/shared/utils/optimistic.ts` All mutations use nanoid-based optimistic updates: @@ -105,7 +105,7 @@ Polling intervals are defined in each feature's query hooks. See actual implemen - **Progress**: `archon-ui-main/src/features/progress/hooks/useProgressQueries.ts` - **MCP**: `archon-ui-main/src/features/mcp/hooks/useMcpQueries.ts` -Standard intervals from `archon-ui-main/src/features/shared/queryPatterns.ts`: +Standard intervals from `archon-ui-main/src/features/shared/config/queryPatterns.ts`: - `STALE_TIMES.instant`: 0ms (always fresh) - `STALE_TIMES.frequent`: 5 seconds (frequently changing data) - `STALE_TIMES.normal`: 30 seconds (standard cache) diff --git a/PRPs/ai_docs/ETAG_IMPLEMENTATION.md b/PRPs/ai_docs/ETAG_IMPLEMENTATION.md index 70e4ce63..8560dbb5 100644 --- a/PRPs/ai_docs/ETAG_IMPLEMENTATION.md +++ b/PRPs/ai_docs/ETAG_IMPLEMENTATION.md @@ -17,7 +17,7 @@ The backend generates ETags for API responses: - Returns `304 Not Modified` when ETags match ### Frontend Handling -**Location**: `archon-ui-main/src/features/shared/apiWithEtag.ts` +**Location**: `archon-ui-main/src/features/shared/api/apiClient.ts` The frontend relies on browser-native HTTP caching: - Browser automatically sends `If-None-Match` headers with cached ETags @@ -28,7 +28,7 @@ The frontend relies on browser-native HTTP caching: #### Browser vs Non-Browser Behavior - **Standard Browsers**: Per the Fetch spec, a 304 response freshens the HTTP cache and returns the cached body to JavaScript - **Non-Browser Runtimes** (React Native, custom fetch): May surface 304 with empty body to JavaScript -- **Client Fallback**: The `apiWithEtag.ts` implementation handles both scenarios, ensuring consistent behavior across environments +- **Client Fallback**: The `apiClient.ts` implementation handles both scenarios, ensuring consistent behavior across environments ## Implementation Details @@ -81,8 +81,8 @@ Unlike previous implementations, the current approach: ### Configuration Cache behavior is controlled through TanStack Query's `staleTime`: -- See `archon-ui-main/src/features/shared/queryPatterns.ts` for standard times -- See `archon-ui-main/src/features/shared/queryClient.ts` for global configuration +- See `archon-ui-main/src/features/shared/config/queryPatterns.ts` for standard times +- See `archon-ui-main/src/features/shared/config/queryClient.ts` for global configuration ## Performance Benefits @@ -100,7 +100,7 @@ Cache behavior is controlled through TanStack Query's `staleTime`: ### Core Implementation - **Backend Utilities**: `python/src/server/utils/etag_utils.py` -- **Frontend Client**: `archon-ui-main/src/features/shared/apiWithEtag.ts` +- **Frontend Client**: `archon-ui-main/src/features/shared/api/apiClient.ts` - **Tests**: `python/tests/server/utils/test_etag_utils.py` ### Usage Examples diff --git a/PRPs/ai_docs/QUERY_PATTERNS.md b/PRPs/ai_docs/QUERY_PATTERNS.md index 3c3204db..499daa36 100644 --- a/PRPs/ai_docs/QUERY_PATTERNS.md +++ b/PRPs/ai_docs/QUERY_PATTERNS.md @@ -5,7 +5,7 @@ This guide documents the standardized patterns for using TanStack Query v5 in th ## Core Principles 1. **Feature Ownership**: Each feature owns its query keys in `{feature}/hooks/use{Feature}Queries.ts` -2. **Consistent Patterns**: Always use shared patterns from `shared/queryPatterns.ts` +2. **Consistent Patterns**: Always use shared patterns from `shared/config/queryPatterns.ts` 3. **No Hardcoded Values**: Never hardcode stale times or disabled keys 4. **Mirror Backend API**: Query keys should exactly match backend API structure @@ -49,7 +49,7 @@ export const taskKeys = { ### Import Required Patterns ```typescript -import { DISABLED_QUERY_KEY, STALE_TIMES } from "@/features/shared/queryPatterns"; +import { DISABLED_QUERY_KEY, STALE_TIMES } from "@/features/shared/config/queryPatterns"; ``` ### Disabled Queries @@ -106,7 +106,7 @@ export function useFeatureDetail(id: string | undefined) { ## Mutations with Optimistic Updates ```typescript -import { createOptimisticEntity, replaceOptimisticEntity } from "@/features/shared/optimistic"; +import { createOptimisticEntity, replaceOptimisticEntity } from "@/features/shared/utils/optimistic"; export function useCreateFeature() { const queryClient = useQueryClient(); @@ -161,7 +161,7 @@ vi.mock("../../services", () => ({ })); // Mock shared patterns with ALL values -vi.mock("../../../shared/queryPatterns", () => ({ +vi.mock("../../../shared/config/queryPatterns", () => ({ DISABLED_QUERY_KEY: ["disabled"] as const, STALE_TIMES: { instant: 0, diff --git a/PRPs/ai_docs/optimistic_updates.md b/PRPs/ai_docs/optimistic_updates.md index 7be11ea6..219b7866 100644 --- a/PRPs/ai_docs/optimistic_updates.md +++ b/PRPs/ai_docs/optimistic_updates.md @@ -3,7 +3,7 @@ ## Core Architecture ### Shared Utilities Module -**Location**: `src/features/shared/optimistic.ts` +**Location**: `src/features/shared/utils/optimistic.ts` Provides type-safe utilities for managing optimistic state across all features: - `createOptimisticId()` - Generates stable UUIDs using nanoid @@ -73,13 +73,13 @@ Reusable component showing: - Uses `createOptimisticId()` directly for progress tracking ### Toasts -- **Location**: `src/features/ui/hooks/useToast.ts:43` +- **Location**: `src/features/shared/hooks/useToast.ts:43` - Uses `createOptimisticId()` for unique toast IDs ## Testing ### Unit Tests -**Location**: `src/features/shared/optimistic.test.ts` +**Location**: `src/features/shared/utils/tests/optimistic.test.ts` Covers all utility functions with 8 test cases: - ID uniqueness and format validation diff --git a/archon-ui-main/src/App.tsx b/archon-ui-main/src/App.tsx index 1d4e22d3..ea2539cc 100644 --- a/archon-ui-main/src/App.tsx +++ b/archon-ui-main/src/App.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; import { QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; -import { queryClient } from './features/shared/queryClient'; +import { queryClient } from './features/shared/config/queryClient'; import { KnowledgeBasePage } from './pages/KnowledgeBasePage'; import { SettingsPage } from './pages/SettingsPage'; import { MCPPage } from './pages/MCPPage'; diff --git a/archon-ui-main/src/components/layout/hooks/useBackendHealth.ts b/archon-ui-main/src/components/layout/hooks/useBackendHealth.ts index 626d23b6..59e9ccfa 100644 --- a/archon-ui-main/src/components/layout/hooks/useBackendHealth.ts +++ b/archon-ui-main/src/components/layout/hooks/useBackendHealth.ts @@ -1,6 +1,6 @@ import { useQuery } from "@tanstack/react-query"; -import { callAPIWithETag } from "../../../features/shared/apiWithEtag"; -import { createRetryLogic, STALE_TIMES } from "../../../features/shared/queryPatterns"; +import { callAPIWithETag } from "../../../features/shared/api/apiClient"; +import { createRetryLogic, STALE_TIMES } from "../../../features/shared/config/queryPatterns"; import type { HealthResponse } from "../types"; /** diff --git a/archon-ui-main/src/features/knowledge/components/KnowledgeCard.tsx b/archon-ui-main/src/features/knowledge/components/KnowledgeCard.tsx index bb49edd9..05c882de 100644 --- a/archon-ui-main/src/features/knowledge/components/KnowledgeCard.tsx +++ b/archon-ui-main/src/features/knowledge/components/KnowledgeCard.tsx @@ -1,6 +1,6 @@ /** - * Enhanced Knowledge Card Component - * Individual knowledge item card with excellent UX and inline progress + * Knowledge Card component + * Displays a knowledge item with inline progress and status UI * Following the pattern from ProjectCard */ @@ -10,7 +10,7 @@ import { Clock, Code, ExternalLink, File, FileText, Globe } from "lucide-react"; import { useState } from "react"; import { KnowledgeCardProgress } from "../../progress/components/KnowledgeCardProgress"; import type { ActiveOperation } from "../../progress/types"; -import { isOptimistic } from "../../shared/optimistic"; +import { isOptimistic } from "@/features/shared/utils/optimistic"; import { StatPill } from "../../ui/primitives"; import { OptimisticIndicator } from "../../ui/primitives/OptimisticIndicator"; import { cn } from "../../ui/primitives/styles"; @@ -144,6 +144,7 @@ export const KnowledgeCard: React.FC = ({ }; return ( + // biome-ignore lint/a11y/useSemanticElements: Card contains nested interactive elements (buttons, links) - using div to avoid invalid HTML nesting ); - const tempItemId = optimisticItem.id; // Update all summaries caches with optimistic data, respecting each cache's filter const entries = queryClient.getQueriesData({ @@ -229,7 +228,7 @@ export function useCrawlUrl() { }); // Return context for rollback and replacement - return { previousSummaries, previousOperations, tempProgressId, tempItemId }; + return { previousSummaries, previousOperations, tempProgressId }; }, onSuccess: (response, _variables, context) => { // Replace temporary IDs with real ones from the server @@ -313,7 +312,6 @@ export function useUploadDocument() { previousSummaries?: Array<[readonly unknown[], KnowledgeItemsResponse | undefined]>; previousOperations?: ActiveOperationsResponse; tempProgressId: string; - tempItemId: string; } >({ mutationFn: ({ file, metadata }: { file: File; metadata: UploadMetadata }) => @@ -352,7 +350,6 @@ export function useUploadDocument() { created_at: new Date().toISOString(), updated_at: new Date().toISOString(), } as Omit); - const tempItemId = optimisticItem.id; // Respect each cache's filter (knowledge_type, tags, etc.) const entries = queryClient.getQueriesData({ @@ -410,7 +407,7 @@ export function useUploadDocument() { }; }); - return { previousSummaries, previousOperations, tempProgressId, tempItemId }; + return { previousSummaries, previousOperations, tempProgressId }; }, onSuccess: (response, _variables, context) => { // Replace temporary IDs with real ones from the server @@ -421,7 +418,7 @@ export function useUploadDocument() { return { ...old, items: old.items.map((item) => { - if (item.id === context.tempItemId) { + if (item.source_id === context.tempProgressId) { return { ...item, source_id: response.progressId, diff --git a/archon-ui-main/src/features/knowledge/inspector/components/KnowledgeInspector.tsx b/archon-ui-main/src/features/knowledge/inspector/components/KnowledgeInspector.tsx index 69e8f050..334d4567 100644 --- a/archon-ui-main/src/features/knowledge/inspector/components/KnowledgeInspector.tsx +++ b/archon-ui-main/src/features/knowledge/inspector/components/KnowledgeInspector.tsx @@ -4,13 +4,13 @@ */ import { useCallback, useEffect, useState } from "react"; +import { copyToClipboard } from "../../../shared/utils/clipboard"; import { InspectorDialog, InspectorDialogContent, InspectorDialogTitle } from "../../../ui/primitives"; import type { CodeExample, DocumentChunk, InspectorSelectedItem, KnowledgeItem } from "../../types"; import { useInspectorPagination } from "../hooks/useInspectorPagination"; import { ContentViewer } from "./ContentViewer"; import { InspectorHeader } from "./InspectorHeader"; import { InspectorSidebar } from "./InspectorSidebar"; -import { copyToClipboard } from "../../../shared/utils/clipboard"; interface KnowledgeInspectorProps { item: KnowledgeItem; diff --git a/archon-ui-main/src/features/knowledge/inspector/hooks/useInspectorPagination.ts b/archon-ui-main/src/features/knowledge/inspector/hooks/useInspectorPagination.ts index 613aa19d..a1f286d5 100644 --- a/archon-ui-main/src/features/knowledge/inspector/hooks/useInspectorPagination.ts +++ b/archon-ui-main/src/features/knowledge/inspector/hooks/useInspectorPagination.ts @@ -5,7 +5,7 @@ import { useInfiniteQuery } from "@tanstack/react-query"; import { useMemo } from "react"; -import { STALE_TIMES } from "@/features/shared/queryPatterns"; +import { STALE_TIMES } from "@/features/shared/config/queryPatterns"; import { knowledgeKeys } from "../../hooks/useKnowledgeQueries"; import { knowledgeService } from "../../services"; import type { ChunksResponse, CodeExample, CodeExamplesResponse, DocumentChunk } from "../../types"; diff --git a/archon-ui-main/src/features/knowledge/services/knowledgeService.ts b/archon-ui-main/src/features/knowledge/services/knowledgeService.ts index b9d6af06..cfab3f7f 100644 --- a/archon-ui-main/src/features/knowledge/services/knowledgeService.ts +++ b/archon-ui-main/src/features/knowledge/services/knowledgeService.ts @@ -3,8 +3,8 @@ * Handles all knowledge-related API operations using TanStack Query patterns */ -import { callAPIWithETag } from "../../shared/apiWithEtag"; -import { APIServiceError } from "../../shared/errors"; +import { callAPIWithETag } from "../../shared/api/apiClient"; +import { APIServiceError } from "../../shared/types/errors"; import type { ChunksResponse, CodeExamplesResponse, diff --git a/archon-ui-main/src/features/knowledge/utils/tests/providerErrorHandler.test.ts b/archon-ui-main/src/features/knowledge/utils/tests/providerErrorHandler.test.ts index 193e2444..9ddf380a 100644 --- a/archon-ui-main/src/features/knowledge/utils/tests/providerErrorHandler.test.ts +++ b/archon-ui-main/src/features/knowledge/utils/tests/providerErrorHandler.test.ts @@ -1,70 +1,70 @@ -import { describe, it, expect } from 'vitest'; -import { parseProviderError, getProviderErrorMessage, type ProviderError } from '../providerErrorHandler'; +import { describe, expect, it } from "vitest"; +import { getProviderErrorMessage, type ProviderError, parseProviderError } from "../providerErrorHandler"; -describe('providerErrorHandler', () => { - describe('parseProviderError', () => { - it('should handle basic Error objects', () => { - const error = new Error('Basic error message'); +describe("providerErrorHandler", () => { + describe("parseProviderError", () => { + it("should handle basic Error objects", () => { + const error = new Error("Basic error message"); const result = parseProviderError(error); - expect(result.message).toBe('Basic error message'); + expect(result.message).toBe("Basic error message"); expect(result.isProviderError).toBeUndefined(); }); - it('should handle errors with statusCode property', () => { - const error = { statusCode: 401, message: 'Unauthorized' }; + it("should handle errors with statusCode property", () => { + const error = { statusCode: 401, message: "Unauthorized" }; const result = parseProviderError(error); expect(result.statusCode).toBe(401); - expect(result.message).toBe('Unauthorized'); + expect(result.message).toBe("Unauthorized"); }); - it('should handle errors with status property', () => { - const error = { status: 429, message: 'Rate limited' }; + it("should handle errors with status property", () => { + const error = { status: 429, message: "Rate limited" }; const result = parseProviderError(error); expect(result.statusCode).toBe(429); - expect(result.message).toBe('Rate limited'); + expect(result.message).toBe("Rate limited"); }); - it('should prioritize statusCode over status when both are present', () => { - const error = { statusCode: 401, status: 429, message: 'Auth error' }; + it("should prioritize statusCode over status when both are present", () => { + const error = { statusCode: 401, status: 429, message: "Auth error" }; const result = parseProviderError(error); expect(result.statusCode).toBe(401); }); - it('should parse structured provider errors from backend', () => { + it("should parse structured provider errors from backend", () => { const error = { message: JSON.stringify({ detail: { - error_type: 'authentication_failed', - provider: 'OpenAI', - message: 'Invalid API key' - } - }) + error_type: "authentication_failed", + provider: "OpenAI", + message: "Invalid API key", + }, + }), }; const result = parseProviderError(error); expect(result.isProviderError).toBe(true); - expect(result.provider).toBe('OpenAI'); - expect(result.errorType).toBe('authentication_failed'); - expect(result.message).toBe('Invalid API key'); + expect(result.provider).toBe("OpenAI"); + expect(result.errorType).toBe("authentication_failed"); + expect(result.message).toBe("Invalid API key"); }); - it('should handle malformed JSON in message gracefully', () => { + it("should handle malformed JSON in message gracefully", () => { const error = { - message: 'invalid json { detail' + message: "invalid json { detail", }; const result = parseProviderError(error); expect(result.isProviderError).toBeUndefined(); - expect(result.message).toBe('invalid json { detail'); + expect(result.message).toBe("invalid json { detail"); }); - it('should handle null and undefined inputs safely', () => { + it("should handle null and undefined inputs safely", () => { expect(() => parseProviderError(null)).not.toThrow(); expect(() => parseProviderError(undefined)).not.toThrow(); @@ -75,7 +75,7 @@ describe('providerErrorHandler', () => { expect(undefinedResult).toBeDefined(); }); - it('should handle empty objects', () => { + it("should handle empty objects", () => { const result = parseProviderError({}); expect(result).toBeDefined(); @@ -83,171 +83,171 @@ describe('providerErrorHandler', () => { expect(result.isProviderError).toBeUndefined(); }); - it('should handle primitive values', () => { - expect(() => parseProviderError('string error')).not.toThrow(); + it("should handle primitive values", () => { + expect(() => parseProviderError("string error")).not.toThrow(); expect(() => parseProviderError(42)).not.toThrow(); expect(() => parseProviderError(true)).not.toThrow(); }); - it('should handle structured errors without provider field', () => { + it("should handle structured errors without provider field", () => { const error = { message: JSON.stringify({ detail: { - error_type: 'quota_exhausted', - message: 'Usage limit exceeded' - } - }) + error_type: "quota_exhausted", + message: "Usage limit exceeded", + }, + }), }; const result = parseProviderError(error); expect(result.isProviderError).toBe(true); - expect(result.provider).toBe('LLM'); // Default fallback - expect(result.errorType).toBe('quota_exhausted'); - expect(result.message).toBe('Usage limit exceeded'); + expect(result.provider).toBe("LLM"); // Default fallback + expect(result.errorType).toBe("quota_exhausted"); + expect(result.message).toBe("Usage limit exceeded"); }); - it('should handle partial structured errors', () => { + it("should handle partial structured errors", () => { const error = { message: JSON.stringify({ detail: { - error_type: 'rate_limit' + error_type: "rate_limit", // Missing message field - } - }) + }, + }), }; const result = parseProviderError(error); expect(result.isProviderError).toBe(true); - expect(result.errorType).toBe('rate_limit'); + expect(result.errorType).toBe("rate_limit"); expect(result.message).toBe(error.message); // Falls back to original message }); }); - describe('getProviderErrorMessage', () => { - it('should return user-friendly message for authentication_failed', () => { + describe("getProviderErrorMessage", () => { + it("should return user-friendly message for authentication_failed", () => { const error: ProviderError = { - name: 'Error', - message: 'Auth failed', + name: "Error", + message: "Auth failed", isProviderError: true, - provider: 'OpenAI', - errorType: 'authentication_failed' + provider: "OpenAI", + errorType: "authentication_failed", }; const result = getProviderErrorMessage(error); - expect(result).toBe('Please verify your OpenAI API key in Settings.'); + expect(result).toBe("Please verify your OpenAI API key in Settings."); }); - it('should return user-friendly message for quota_exhausted', () => { + it("should return user-friendly message for quota_exhausted", () => { const error: ProviderError = { - name: 'Error', - message: 'Quota exceeded', + name: "Error", + message: "Quota exceeded", isProviderError: true, - provider: 'Google AI', - errorType: 'quota_exhausted' + provider: "Google AI", + errorType: "quota_exhausted", }; const result = getProviderErrorMessage(error); - expect(result).toBe('Google AI quota exhausted. Please check your billing settings.'); + expect(result).toBe("Google AI quota exhausted. Please check your billing settings."); }); - it('should return user-friendly message for rate_limit', () => { + it("should return user-friendly message for rate_limit", () => { const error: ProviderError = { - name: 'Error', - message: 'Rate limited', + name: "Error", + message: "Rate limited", isProviderError: true, - provider: 'Anthropic', - errorType: 'rate_limit' + provider: "Anthropic", + errorType: "rate_limit", }; const result = getProviderErrorMessage(error); - expect(result).toBe('Anthropic rate limit exceeded. Please wait and try again.'); + expect(result).toBe("Anthropic rate limit exceeded. Please wait and try again."); }); - it('should return generic provider message for unknown error types', () => { + it("should return generic provider message for unknown error types", () => { const error: ProviderError = { - name: 'Error', - message: 'Unknown error', + name: "Error", + message: "Unknown error", isProviderError: true, - provider: 'OpenAI', - errorType: 'unknown_error' + provider: "OpenAI", + errorType: "unknown_error", }; const result = getProviderErrorMessage(error); - expect(result).toBe('OpenAI API error. Please check your configuration.'); + expect(result).toBe("OpenAI API error. Please check your configuration."); }); - it('should use default provider when provider is missing', () => { + it("should use default provider when provider is missing", () => { const error: ProviderError = { - name: 'Error', - message: 'Auth failed', + name: "Error", + message: "Auth failed", isProviderError: true, - errorType: 'authentication_failed' + errorType: "authentication_failed", }; const result = getProviderErrorMessage(error); - expect(result).toBe('Please verify your LLM API key in Settings.'); + expect(result).toBe("Please verify your LLM API key in Settings."); }); - it('should handle 401 status code for non-provider errors', () => { - const error = { statusCode: 401, message: 'Unauthorized' }; + it("should handle 401 status code for non-provider errors", () => { + const error = { statusCode: 401, message: "Unauthorized" }; const result = getProviderErrorMessage(error); - expect(result).toBe('Please verify your API key in Settings.'); + expect(result).toBe("Please verify your API key in Settings."); }); - it('should return original message for non-provider errors', () => { - const error = new Error('Network connection failed'); + it("should return original message for non-provider errors", () => { + const error = new Error("Network connection failed"); const result = getProviderErrorMessage(error); - expect(result).toBe('Network connection failed'); + expect(result).toBe("Network connection failed"); }); - it('should return default message when no message is available', () => { + it("should return default message when no message is available", () => { const error = {}; const result = getProviderErrorMessage(error); - expect(result).toBe('An error occurred.'); + expect(result).toBe("An error occurred."); }); - it('should handle complex error objects with structured backend response', () => { + it("should handle complex error objects with structured backend response", () => { const backendError = { statusCode: 400, message: JSON.stringify({ detail: { - error_type: 'authentication_failed', - provider: 'OpenAI', - message: 'API key invalid or expired' - } - }) + error_type: "authentication_failed", + provider: "OpenAI", + message: "API key invalid or expired", + }, + }), }; const result = getProviderErrorMessage(backendError); - expect(result).toBe('Please verify your OpenAI API key in Settings.'); + expect(result).toBe("Please verify your OpenAI API key in Settings."); }); it('should handle edge case: message contains "detail" but is not JSON', () => { const error = { - message: 'Error detail: something went wrong' + message: "Error detail: something went wrong", }; const result = getProviderErrorMessage(error); - expect(result).toBe('Error detail: something went wrong'); + expect(result).toBe("Error detail: something went wrong"); }); - it('should handle null and undefined gracefully', () => { - expect(getProviderErrorMessage(null)).toBe('An error occurred.'); - expect(getProviderErrorMessage(undefined)).toBe('An error occurred.'); + it("should handle null and undefined gracefully", () => { + expect(getProviderErrorMessage(null)).toBe("An error occurred."); + expect(getProviderErrorMessage(undefined)).toBe("An error occurred."); }); }); - describe('TypeScript strict mode compliance', () => { - it('should handle type-safe property access', () => { + describe("TypeScript strict mode compliance", () => { + it("should handle type-safe property access", () => { // Test that our type guards work properly const errorWithStatus = { statusCode: 500 }; - const errorWithMessage = { message: 'test' }; - const errorWithBoth = { statusCode: 401, message: 'unauthorized' }; + const errorWithMessage = { message: "test" }; + const errorWithBoth = { statusCode: 401, message: "unauthorized" }; // These should not throw TypeScript errors and should work correctly expect(() => parseProviderError(errorWithStatus)).not.toThrow(); @@ -259,13 +259,13 @@ describe('providerErrorHandler', () => { const result3 = parseProviderError(errorWithBoth); expect(result1.statusCode).toBe(500); - expect(result2.message).toBe('test'); + expect(result2.message).toBe("test"); expect(result3.statusCode).toBe(401); - expect(result3.message).toBe('unauthorized'); + expect(result3.message).toBe("unauthorized"); }); - it('should handle objects without expected properties safely', () => { - const objectWithoutStatus = { someOtherProperty: 'value' }; + it("should handle objects without expected properties safely", () => { + const objectWithoutStatus = { someOtherProperty: "value" }; const objectWithoutMessage = { anotherProperty: 42 }; expect(() => parseProviderError(objectWithoutStatus)).not.toThrow(); @@ -278,4 +278,4 @@ describe('providerErrorHandler', () => { expect(result2.message).toBeUndefined(); }); }); -}); \ No newline at end of file +}); diff --git a/archon-ui-main/src/features/knowledge/views/KnowledgeView.tsx b/archon-ui-main/src/features/knowledge/views/KnowledgeView.tsx index 6f6a66df..0bedc7b2 100644 --- a/archon-ui-main/src/features/knowledge/views/KnowledgeView.tsx +++ b/archon-ui-main/src/features/knowledge/views/KnowledgeView.tsx @@ -4,9 +4,9 @@ */ import { useEffect, useMemo, useRef, useState } from "react"; +import { useToast } from "@/features/shared/hooks/useToast"; import { CrawlingProgress } from "../../progress/components/CrawlingProgress"; import type { ActiveOperation } from "../../progress/types"; -import { useToast } from "@/features/shared/hooks/useToast"; import { AddKnowledgeDialog } from "../components/AddKnowledgeDialog"; import { KnowledgeHeader } from "../components/KnowledgeHeader"; import { KnowledgeList } from "../components/KnowledgeList"; diff --git a/archon-ui-main/src/features/mcp/components/McpConfigSection.tsx b/archon-ui-main/src/features/mcp/components/McpConfigSection.tsx index c36b2f01..b5344bda 100644 --- a/archon-ui-main/src/features/mcp/components/McpConfigSection.tsx +++ b/archon-ui-main/src/features/mcp/components/McpConfigSection.tsx @@ -2,9 +2,9 @@ import { Copy, ExternalLink } from "lucide-react"; import type React from "react"; import { useState } from "react"; import { useToast } from "@/features/shared/hooks"; +import { copyToClipboard } from "../../shared/utils/clipboard"; import { Button, cn, glassmorphism, Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/primitives"; import type { McpServerConfig, McpServerStatus, SupportedIDE } from "../types"; -import { copyToClipboard } from "../../shared/utils/clipboard"; interface McpConfigSectionProps { config?: McpServerConfig; @@ -324,7 +324,8 @@ export const McpConfigSection: React.FC = ({ config, stat

Platform Note: The configuration below shows{" "} {navigator.platform.toLowerCase().includes("win") ? "Windows" : "Linux/macOS"} format. Adjust paths - according to your system. This setup is complex right now because Codex has some bugs with MCP currently. + according to your system. This setup is complex right now because Codex has some bugs with MCP + currently.

)} diff --git a/archon-ui-main/src/features/mcp/hooks/useMcpQueries.ts b/archon-ui-main/src/features/mcp/hooks/useMcpQueries.ts index aef5ec68..eaf8f404 100644 --- a/archon-ui-main/src/features/mcp/hooks/useMcpQueries.ts +++ b/archon-ui-main/src/features/mcp/hooks/useMcpQueries.ts @@ -1,6 +1,6 @@ import { useQuery } from "@tanstack/react-query"; -import { STALE_TIMES } from "../../shared/queryPatterns"; import { useSmartPolling } from "@/features/shared/hooks"; +import { STALE_TIMES } from "../../shared/config/queryPatterns"; import { mcpApi } from "../services"; // Query keys factory diff --git a/archon-ui-main/src/features/mcp/services/mcpApi.ts b/archon-ui-main/src/features/mcp/services/mcpApi.ts index 008c800c..d4b02ed4 100644 --- a/archon-ui-main/src/features/mcp/services/mcpApi.ts +++ b/archon-ui-main/src/features/mcp/services/mcpApi.ts @@ -1,4 +1,4 @@ -import { callAPIWithETag } from "../../shared/apiWithEtag"; +import { callAPIWithETag } from "../../shared/api/apiClient"; import type { McpClient, McpServerConfig, McpServerStatus, McpSessionInfo } from "../types"; export const mcpApi = { diff --git a/archon-ui-main/src/features/progress/hooks/tests/useProgressQueries.test.ts b/archon-ui-main/src/features/progress/hooks/tests/useProgressQueries.test.ts index 565919aa..d305a146 100644 --- a/archon-ui-main/src/features/progress/hooks/tests/useProgressQueries.test.ts +++ b/archon-ui-main/src/features/progress/hooks/tests/useProgressQueries.test.ts @@ -19,7 +19,7 @@ vi.mock("../../services", () => ({ })); // Mock shared query patterns -vi.mock("../../../shared/queryPatterns", () => ({ +vi.mock("../../../shared/config/queryPatterns", () => ({ DISABLED_QUERY_KEY: ["disabled"] as const, STALE_TIMES: { instant: 0, diff --git a/archon-ui-main/src/features/progress/hooks/useProgressQueries.ts b/archon-ui-main/src/features/progress/hooks/useProgressQueries.ts index ae82ba17..1ebec2a9 100644 --- a/archon-ui-main/src/features/progress/hooks/useProgressQueries.ts +++ b/archon-ui-main/src/features/progress/hooks/useProgressQueries.ts @@ -5,9 +5,9 @@ import { type UseQueryResult, useQueries, useQuery, useQueryClient } from "@tanstack/react-query"; import { useEffect, useMemo, useRef } from "react"; -import { APIServiceError } from "../../shared/errors"; -import { DISABLED_QUERY_KEY, STALE_TIMES } from "../../shared/queryPatterns"; +import { DISABLED_QUERY_KEY, STALE_TIMES } from "../../shared/config/queryPatterns"; import { useSmartPolling } from "../../shared/hooks"; +import { APIServiceError } from "../../shared/types/errors"; import { progressService } from "../services"; import type { ActiveOperationsResponse, ProgressResponse, ProgressStatus } from "../types"; diff --git a/archon-ui-main/src/features/progress/services/progressService.ts b/archon-ui-main/src/features/progress/services/progressService.ts index d3f6e61e..ba0e68ba 100644 --- a/archon-ui-main/src/features/progress/services/progressService.ts +++ b/archon-ui-main/src/features/progress/services/progressService.ts @@ -3,7 +3,7 @@ * Uses ETag support for efficient polling */ -import { callAPIWithETag } from "../../shared/apiWithEtag"; +import { callAPIWithETag } from "../../shared/api/apiClient"; import type { ActiveOperationsResponse, ProgressResponse } from "../types"; export const progressService = { diff --git a/archon-ui-main/src/features/projects/components/ProjectCard.tsx b/archon-ui-main/src/features/projects/components/ProjectCard.tsx index df990710..a6b62349 100644 --- a/archon-ui-main/src/features/projects/components/ProjectCard.tsx +++ b/archon-ui-main/src/features/projects/components/ProjectCard.tsx @@ -1,7 +1,7 @@ import { motion } from "framer-motion"; import { Activity, CheckCircle2, ListTodo } from "lucide-react"; import type React from "react"; -import { isOptimistic } from "../../shared/optimistic"; +import { isOptimistic } from "@/features/shared/utils/optimistic"; import { OptimisticIndicator } from "../../ui/primitives/OptimisticIndicator"; import { cn } from "../../ui/primitives/styles"; import type { Project } from "../types"; diff --git a/archon-ui-main/src/features/projects/documents/components/DocumentCard.tsx b/archon-ui-main/src/features/projects/documents/components/DocumentCard.tsx index 25b12365..06241a46 100644 --- a/archon-ui-main/src/features/projects/documents/components/DocumentCard.tsx +++ b/archon-ui-main/src/features/projects/documents/components/DocumentCard.tsx @@ -13,9 +13,9 @@ import { } from "lucide-react"; import type React from "react"; import { memo, useCallback, useState } from "react"; +import { copyToClipboard } from "../../../shared/utils/clipboard"; import { Button } from "../../../ui/primitives"; import type { DocumentCardProps, DocumentType } from "../types"; -import { copyToClipboard } from "../../../shared/utils/clipboard"; const getDocumentIcon = (type?: DocumentType) => { switch (type) { diff --git a/archon-ui-main/src/features/projects/documents/hooks/useDocumentQueries.ts b/archon-ui-main/src/features/projects/documents/hooks/useDocumentQueries.ts index 0a7d23ee..00c6eea6 100644 --- a/archon-ui-main/src/features/projects/documents/hooks/useDocumentQueries.ts +++ b/archon-ui-main/src/features/projects/documents/hooks/useDocumentQueries.ts @@ -1,5 +1,5 @@ import { useQuery } from "@tanstack/react-query"; -import { DISABLED_QUERY_KEY, STALE_TIMES } from "../../../shared/queryPatterns"; +import { DISABLED_QUERY_KEY, STALE_TIMES } from "../../../shared/config/queryPatterns"; import { projectService } from "../../services"; import type { ProjectDocument } from "../types"; diff --git a/archon-ui-main/src/features/projects/hooks/useProjectQueries.ts b/archon-ui-main/src/features/projects/hooks/useProjectQueries.ts index ae216e66..946647ab 100644 --- a/archon-ui-main/src/features/projects/hooks/useProjectQueries.ts +++ b/archon-ui-main/src/features/projects/hooks/useProjectQueries.ts @@ -1,13 +1,13 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useSmartPolling } from "@/features/shared/hooks"; +import { useToast } from "@/features/shared/hooks/useToast"; import { createOptimisticEntity, type OptimisticEntity, removeDuplicateEntities, replaceOptimisticEntity, -} from "@/features/shared/optimistic"; -import { DISABLED_QUERY_KEY, STALE_TIMES } from "../../shared/queryPatterns"; -import { useSmartPolling } from "@/features/shared/hooks"; -import { useToast } from "@/features/shared/hooks/useToast"; +} from "@/features/shared/utils/optimistic"; +import { DISABLED_QUERY_KEY, STALE_TIMES } from "../../shared/config/queryPatterns"; import { projectService } from "../services"; import type { CreateProjectRequest, Project, UpdateProjectRequest } from "../types"; @@ -36,9 +36,7 @@ export function useProjects() { // Fetch project features export function useProjectFeatures(projectId: string | undefined) { - // TODO: Phase 4 - Add explicit typing: useQuery>> - // See PRPs/local/frontend-state-management-refactor.md Phase 4: Configure Request Deduplication - return useQuery({ + return useQuery>>({ queryKey: projectId ? projectKeys.features(projectId) : DISABLED_QUERY_KEY, queryFn: () => (projectId ? projectService.getProjectFeatures(projectId) : Promise.reject("No project ID")), enabled: !!projectId, @@ -208,6 +206,8 @@ export function useDeleteProject() { // Don't refetch on success - trust optimistic update // Only remove the specific project's detail data (including nested keys) queryClient.removeQueries({ queryKey: projectKeys.detail(projectId), exact: false }); + // Also remove the project's feature queries + queryClient.removeQueries({ queryKey: projectKeys.features(projectId), exact: false }); showToast("Project deleted successfully", "success"); }, }); diff --git a/archon-ui-main/src/features/projects/services/projectService.ts b/archon-ui-main/src/features/projects/services/projectService.ts index f74675ca..58b1f3e6 100644 --- a/archon-ui-main/src/features/projects/services/projectService.ts +++ b/archon-ui-main/src/features/projects/services/projectService.ts @@ -3,8 +3,8 @@ * Focused service for project CRUD operations only */ -import { callAPIWithETag } from "../../shared/apiWithEtag"; -import { formatZodErrors, ValidationError } from "../../shared/errors"; +import { callAPIWithETag } from "../../shared/api/apiClient"; +import { formatZodErrors, ValidationError } from "../../shared/types/errors"; import { validateCreateProject, validateUpdateProject } from "../schemas"; import { formatRelativeTime } from "../shared/api"; import type { CreateProjectRequest, Project, ProjectFeatures, UpdateProjectRequest } from "../types"; diff --git a/archon-ui-main/src/features/projects/tasks/components/TaskCard.tsx b/archon-ui-main/src/features/projects/tasks/components/TaskCard.tsx index 913964c6..c8e09464 100644 --- a/archon-ui-main/src/features/projects/tasks/components/TaskCard.tsx +++ b/archon-ui-main/src/features/projects/tasks/components/TaskCard.tsx @@ -2,7 +2,7 @@ import { Tag } from "lucide-react"; import type React from "react"; import { useCallback } from "react"; import { useDrag, useDrop } from "react-dnd"; -import { isOptimistic } from "../../../shared/optimistic"; +import { isOptimistic } from "@/features/shared/utils/optimistic"; import { OptimisticIndicator } from "../../../ui/primitives/OptimisticIndicator"; import { useTaskActions } from "../hooks"; import type { Assignee, Task, TaskPriority } from "../types"; diff --git a/archon-ui-main/src/features/projects/tasks/hooks/useTaskQueries.ts b/archon-ui-main/src/features/projects/tasks/hooks/useTaskQueries.ts index 55b4bbd0..2020a96d 100644 --- a/archon-ui-main/src/features/projects/tasks/hooks/useTaskQueries.ts +++ b/archon-ui-main/src/features/projects/tasks/hooks/useTaskQueries.ts @@ -1,11 +1,11 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { createOptimisticEntity, - replaceOptimisticEntity, - removeDuplicateEntities, type OptimisticEntity, -} from "@/features/shared/optimistic"; -import { DISABLED_QUERY_KEY, STALE_TIMES } from "../../../shared/queryPatterns"; + removeDuplicateEntities, + replaceOptimisticEntity, +} from "@/features/shared/utils/optimistic"; +import { DISABLED_QUERY_KEY, STALE_TIMES } from "../../../shared/config/queryPatterns"; import { useSmartPolling } from "../../../shared/hooks"; import { useToast } from "../../../shared/hooks/useToast"; import { taskService } from "../services"; diff --git a/archon-ui-main/src/features/projects/tasks/services/taskService.ts b/archon-ui-main/src/features/projects/tasks/services/taskService.ts index 223bdb73..dc2db1ed 100644 --- a/archon-ui-main/src/features/projects/tasks/services/taskService.ts +++ b/archon-ui-main/src/features/projects/tasks/services/taskService.ts @@ -3,8 +3,8 @@ * Focused service for task CRUD operations only */ -import { callAPIWithETag } from "../../../shared/apiWithEtag"; -import { formatZodErrors, ValidationError } from "../../../shared/errors"; +import { callAPIWithETag } from "../../../shared/api/apiClient"; +import { formatZodErrors, ValidationError } from "../../../shared/types/errors"; import { validateCreateTask, validateUpdateTask, validateUpdateTaskStatus } from "../schemas"; import type { CreateTaskRequest, DatabaseTaskStatus, Task, TaskCounts, UpdateTaskRequest } from "../types"; diff --git a/archon-ui-main/src/features/projects/tasks/services/tests/taskService.test.ts b/archon-ui-main/src/features/projects/tasks/services/tests/taskService.test.ts index d86cc94d..d4215814 100644 --- a/archon-ui-main/src/features/projects/tasks/services/tests/taskService.test.ts +++ b/archon-ui-main/src/features/projects/tasks/services/tests/taskService.test.ts @@ -1,10 +1,10 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { callAPIWithETag } from "../../../../shared/apiWithEtag"; +import { callAPIWithETag } from "../../../../shared/api/apiClient"; import type { CreateTaskRequest, DatabaseTaskStatus, Task, UpdateTaskRequest } from "../../types"; import { taskService } from "../taskService"; // Mock the API call -vi.mock("../../../../shared/apiWithEtag", () => ({ +vi.mock("../../../../shared/api/apiClient", () => ({ callAPIWithETag: vi.fn(), })); diff --git a/archon-ui-main/src/features/settings/migrations/components/MigrationStatusCard.tsx b/archon-ui-main/src/features/settings/migrations/components/MigrationStatusCard.tsx index 2b29531c..be4317a5 100644 --- a/archon-ui-main/src/features/settings/migrations/components/MigrationStatusCard.tsx +++ b/archon-ui-main/src/features/settings/migrations/components/MigrationStatusCard.tsx @@ -29,7 +29,8 @@ export function MigrationStatusCard() {

Database Migrations

- - -