From b383c8cbecfd14393281661bdbcda2f80b0db66e Mon Sep 17 00:00:00 2001 From: Wirasm <152263317+Wirasm@users.noreply.github.com> Date: Wed, 17 Sep 2025 16:45:23 +0300 Subject: [PATCH] refactor: remove ETag Map cache layer for TanStack Query single source of truth (#676) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: remove ETag Map cache layer for TanStack Query single source of truth - Remove Map-based cache from apiWithEtag.ts to eliminate double-caching anti-pattern - Move apiWithEtag.ts to shared location since used across multiple features - Implement NotModifiedError for 304 responses to work with TanStack Query - Remove invalidateETagCache calls from all service files - Preserve browser ETag headers for bandwidth optimization (70-90% reduction) - Add comprehensive test coverage (10 test cases) - All existing functionality maintained with zero breaking changes This addresses Phase 1 of frontend state management refactor, making TanStack Query the sole authority for cache decisions while maintaining HTTP 304 performance benefits. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * fix: increase API timeout to 20s for large delete operations Temporary fix for database performance issue where DELETE operations on crawled_pages table with 7K+ rows take 13+ seconds due to sequential scan. Root cause analysis: - Source '9529d5dabe8a726a' has 7,073 rows (98% of crawled_pages table) - PostgreSQL uses sequential scan instead of index for large deletes - Operation takes 13.4s but frontend timeout was 10s - Results in frontend errors while backend eventually succeeds This prevents timeout errors during knowledge item deletion until we implement proper batch deletion or database optimization. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * refactor: complete simplification of ETag handling (Option 3) - Remove all explicit ETag handling code from apiWithEtag.ts - Let browser handle ETags and 304 responses automatically - Remove NotModifiedError class and associated retry logic - Simplify QueryClient retry configuration in App.tsx - Add comprehensive tests documenting browser caching behavior - Fix missing generic type in knowledgeService searchKnowledgeBase This completes Phase 1 of the frontend state management refactor. TanStack Query is now the single source of truth for caching, while browser handles HTTP cache/ETags transparently. Benefits: - 50+ lines of code removed - Zero complexity for 304 handling - Bandwidth optimization maintained (70-90% reduction) - Data freshness guaranteed - Perfect alignment with TanStack Query philosophy * fix: resolve DOM nesting validation error in ProjectCard Changed ProjectCard from motion.li to motion.div since it's already wrapped in an li element by ProjectList. This fixes the React warning about li elements being nested inside other li elements. * fix: properly unwrap task mutation responses from backend The backend returns wrapped responses for mutations: { message: string, task: Task } But the frontend was expecting just the Task object, causing description and other fields to not persist properly. Fixed by: - Updated createTask to unwrap response.task - Updated updateTask to unwrap response.task - Updated updateTaskStatus to unwrap response.task This ensures all task data including descriptions persist correctly. * test: add comprehensive tests for task service response unwrapping Added 15 tests covering: - createTask with response unwrapping - updateTask with response unwrapping - updateTaskStatus with response unwrapping - deleteTask (no unwrapping needed) - getTasksByProject (direct response) - Error handling for all methods - Regression tests ensuring description persistence - Full field preservation when unwrapping responses These tests verify that the backend's wrapped mutation responses { message: string, task: Task } are properly unwrapped to return just the Task object to consumers. * fix: add explicit event propagation stopping in ProjectCard Added e.stopPropagation() at the ProjectCard level when passing handlers to ProjectCardActions for pin and delete operations. This provides defense in depth even though ProjectCardActions already stops propagation internally. Ensures clicking action buttons never triggers card selection. * refactor: consolidate error handling into shared module - Create shared/errors.ts with APIServiceError, ValidationError, MCPToolError - Move error classes and utilities from projects/shared/api to shared location - Update all imports to use shared error module - Fix cross-feature dependencies (knowledge no longer depends on projects) - Apply biome formatting to all modified files This establishes a clean architecture where common errors are properly located in the shared module, eliminating feature coupling. 🤖 Generated with Claude Code Co-Authored-By: Claude * test: improve test isolation and clean up assertions - Preserve and restore global AbortSignal and fetch to prevent test pollution - Rename test suite from "Simplified API Client (Option 3)" to "apiWithEtag" - Optimize duplicate assertions by capturing promises once - Use toThrowError with specific error instances for better assertions This ensures tests don't affect each other and improves test maintainability. 🤖 Generated with Claude Code Co-Authored-By: Claude * refactor: Remove unused callAPI function and document 304 handling approach - Delete unused callAPI function from projects/shared/api.ts (56 lines of dead code) - Keep only the formatRelativeTime utility that's actively used - Add comprehensive documentation explaining why we don't handle 304s explicitly - Document that browser handles ETags/304s transparently and we use TanStack Query for cache control - Update apiWithEtag.ts header to clarify the simplification strategy This follows our beta principle of removing dead code immediately and maintains our simplified approach to HTTP caching where the browser handles 304s automatically. * docs: Fix comment drift and clarify ETag/304 handling documentation - Update header comment to be more technically accurate about Fetch API behavior - Clarify that fetch (not browser generically) returns cached responses for 304s - Explicitly document that we don't add If-None-Match headers - Add note about browser's automatic ETag revalidation These documentation updates prevent confusion about our simplified HTTP caching approach. --------- Co-authored-by: Claude --- .../layout/hooks/useBackendHealth.ts | 2 +- .../components/AddKnowledgeDialog.tsx | 71 ++- .../knowledge/components/KnowledgeCard.tsx | 14 +- .../components/KnowledgeCardTags.tsx | 6 +- .../components/KnowledgeCardTitle.tsx | 25 +- .../components/KnowledgeCardType.tsx | 20 +- .../components/KnowledgeTypeSelector.tsx | 59 +-- .../knowledge/components/LevelSelector.tsx | 138 +++--- .../knowledge/components/TagInput.tsx | 25 +- .../knowledge/hooks/useKnowledgeQueries.ts | 14 +- .../components/KnowledgeInspector.tsx | 2 +- .../progress/services/progressService.ts | 2 +- .../knowledge/services/knowledgeService.ts | 32 +- .../knowledge/utils/providerErrorHandler.ts | 30 +- .../src/features/mcp/services/mcpApi.ts | 2 +- .../projects/components/ProjectCard.tsx | 14 +- .../projects/services/projectService.ts | 16 +- .../src/features/projects/shared/api.ts | 116 +---- .../features/projects/shared/apiWithEtag.ts | 224 ---------- .../projects/tasks/components/TaskCard.tsx | 2 +- .../projects/tasks/services/taskService.ts | 36 +- .../tasks/services/tests/taskService.test.ts | 391 +++++++++++++++++ .../src/features/shared/apiWithEtag.test.ts | 412 ++++++++++++++++++ .../src/features/shared/apiWithEtag.ts | 120 +++++ archon-ui-main/src/features/shared/errors.ts | 83 ++++ 25 files changed, 1224 insertions(+), 632 deletions(-) delete mode 100644 archon-ui-main/src/features/projects/shared/apiWithEtag.ts create mode 100644 archon-ui-main/src/features/projects/tasks/services/tests/taskService.test.ts create mode 100644 archon-ui-main/src/features/shared/apiWithEtag.test.ts create mode 100644 archon-ui-main/src/features/shared/apiWithEtag.ts create mode 100644 archon-ui-main/src/features/shared/errors.ts diff --git a/archon-ui-main/src/components/layout/hooks/useBackendHealth.ts b/archon-ui-main/src/components/layout/hooks/useBackendHealth.ts index 4851dee8..e3fcbd9b 100644 --- a/archon-ui-main/src/components/layout/hooks/useBackendHealth.ts +++ b/archon-ui-main/src/components/layout/hooks/useBackendHealth.ts @@ -1,5 +1,5 @@ import { useQuery } from "@tanstack/react-query"; -import { callAPIWithETag } from "../../../features/projects/shared/apiWithEtag"; +import { callAPIWithETag } from "../../../features/shared/apiWithEtag"; import type { HealthResponse } from "../types"; /** diff --git a/archon-ui-main/src/features/knowledge/components/AddKnowledgeDialog.tsx b/archon-ui-main/src/features/knowledge/components/AddKnowledgeDialog.tsx index 26834e98..10d30d06 100644 --- a/archon-ui-main/src/features/knowledge/components/AddKnowledgeDialog.tsx +++ b/archon-ui-main/src/features/knowledge/components/AddKnowledgeDialog.tsx @@ -7,8 +7,8 @@ import { Globe, Loader2, Upload } from "lucide-react"; import { useId, useState } from "react"; import { useToast } from "../../ui/hooks/useToast"; import { Button, Input, Label } from "../../ui/primitives"; -import { cn } from "../../ui/primitives/styles"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "../../ui/primitives/dialog"; +import { cn } from "../../ui/primitives/styles"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/primitives/tabs"; import { useCrawlUrl, useUploadDocument } from "../hooks"; import type { CrawlRequest, UploadMetadata } from "../types"; @@ -145,7 +145,7 @@ export const AddKnowledgeDialog: React.FC = ({ "backdrop-blur-md border-2 font-medium text-sm", activeTab === "crawl" ? "bg-gradient-to-b from-cyan-100/70 via-cyan-50/40 to-white/80 dark:from-cyan-900/40 dark:via-cyan-800/25 dark:to-black/50 border-cyan-400/60 text-cyan-700 dark:text-cyan-300 shadow-[0_0_20px_rgba(34,211,238,0.25)]" - : "bg-gradient-to-b from-white/40 via-white/30 to-white/60 dark:from-gray-800/40 dark:via-gray-800/30 dark:to-black/60 border-gray-300/40 dark:border-gray-600/40 text-gray-600 dark:text-gray-300 hover:border-cyan-300/50 hover:text-cyan-600 dark:hover:text-cyan-400 hover:shadow-[0_0_15px_rgba(34,211,238,0.15)]" + : "bg-gradient-to-b from-white/40 via-white/30 to-white/60 dark:from-gray-800/40 dark:via-gray-800/30 dark:to-black/60 border-gray-300/40 dark:border-gray-600/40 text-gray-600 dark:text-gray-300 hover:border-cyan-300/50 hover:text-cyan-600 dark:hover:text-cyan-400 hover:shadow-[0_0_15px_rgba(34,211,238,0.15)]", )} > {/* Top accent glow for active state */} @@ -155,10 +155,7 @@ export const AddKnowledgeDialog: React.FC = ({
)} - +
Crawl Website Scan web pages @@ -174,7 +171,7 @@ export const AddKnowledgeDialog: React.FC = ({ "backdrop-blur-md border-2 font-medium text-sm", activeTab === "upload" ? "bg-gradient-to-b from-purple-100/70 via-purple-50/40 to-white/80 dark:from-purple-900/40 dark:via-purple-800/25 dark:to-black/50 border-purple-400/60 text-purple-700 dark:text-purple-300 shadow-[0_0_20px_rgba(147,51,234,0.25)]" - : "bg-gradient-to-b from-white/40 via-white/30 to-white/60 dark:from-gray-800/40 dark:via-gray-800/30 dark:to-black/60 border-gray-300/40 dark:border-gray-600/40 text-gray-600 dark:text-gray-300 hover:border-purple-300/50 hover:text-purple-600 dark:hover:text-purple-400 hover:shadow-[0_0_15px_rgba(147,51,234,0.15)]" + : "bg-gradient-to-b from-white/40 via-white/30 to-white/60 dark:from-gray-800/40 dark:via-gray-800/30 dark:to-black/60 border-gray-300/40 dark:border-gray-600/40 text-gray-600 dark:text-gray-300 hover:border-purple-300/50 hover:text-purple-600 dark:hover:text-purple-400 hover:shadow-[0_0_15px_rgba(147,51,234,0.15)]", )} > {/* Top accent glow for active state */} @@ -184,10 +181,7 @@ export const AddKnowledgeDialog: React.FC = ({
)} - +
Upload Document Add local files @@ -204,7 +198,7 @@ export const AddKnowledgeDialog: React.FC = ({
- +
= ({
- + - +
= ({ disabled={isProcessing} className="absolute inset-0 w-full h-full opacity-0 cursor-pointer disabled:cursor-not-allowed z-10" /> -
- +
+
{selectedFile ? (
-

- {selectedFile.name} -

+

{selectedFile.name}

{Math.round(selectedFile.size / 1024)} KB

) : (
-

- Click to browse or drag & drop -

+

Click to browse or drag & drop

PDF, DOC, DOCX, TXT, MD files supported

@@ -317,11 +300,7 @@ export const AddKnowledgeDialog: React.FC = ({
- + = ({ {isUrl ? "Web Page" : "Document"}
- +
{/* Actions */} @@ -300,7 +297,9 @@ export const KnowledgeCard: React.FC = ({
{/* Right: pills */}
- +
{ @@ -321,10 +320,7 @@ export const KnowledgeCard: React.FC = ({ content={`${codeExamplesCount} code example${codeExamplesCount !== 1 ? "s" : ""} extracted - ${onViewCodeExamples ? "Click to view" : "No examples available"}`} >
{ e.stopPropagation(); if (onViewCodeExamples) { diff --git a/archon-ui-main/src/features/knowledge/components/KnowledgeCardTags.tsx b/archon-ui-main/src/features/knowledge/components/KnowledgeCardTags.tsx index 5eb4157b..3334bc0a 100644 --- a/archon-ui-main/src/features/knowledge/components/KnowledgeCardTags.tsx +++ b/archon-ui-main/src/features/knowledge/components/KnowledgeCardTags.tsx @@ -75,7 +75,7 @@ export const KnowledgeCardTags: React.FC = ({ sourceId, // If we're editing an existing tag, remove the original first if (originalTagBeingEdited) { - newTags = newTags.filter(tag => tag !== originalTagBeingEdited); + newTags = newTags.filter((tag) => tag !== originalTagBeingEdited); } // Add the new/modified tag if it doesn't already exist @@ -84,7 +84,7 @@ export const KnowledgeCardTags: React.FC = ({ sourceId, } // Save directly without updating local state first - const updatedTags = newTags.filter(tag => tag.trim().length > 0); + const updatedTags = newTags.filter((tag) => tag.trim().length > 0); try { await updateMutation.mutateAsync({ @@ -128,7 +128,7 @@ export const KnowledgeCardTags: React.FC = ({ sourceId, // If we're editing an existing tag, remove the original first if (originalTagBeingEdited) { - newTags = newTags.filter(tag => tag !== originalTagBeingEdited); + newTags = newTags.filter((tag) => tag !== originalTagBeingEdited); } // Add the new/modified tag if it doesn't already exist diff --git a/archon-ui-main/src/features/knowledge/components/KnowledgeCardTitle.tsx b/archon-ui-main/src/features/knowledge/components/KnowledgeCardTitle.tsx index 63ee41c1..a019156c 100644 --- a/archon-ui-main/src/features/knowledge/components/KnowledgeCardTitle.tsx +++ b/archon-ui-main/src/features/knowledge/components/KnowledgeCardTitle.tsx @@ -7,7 +7,7 @@ import { Info } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { Input } from "../../ui/primitives"; import { cn } from "../../ui/primitives/styles"; -import { SimpleTooltip, Tooltip, TooltipTrigger, TooltipContent } from "../../ui/primitives/tooltip"; +import { SimpleTooltip, Tooltip, TooltipContent, TooltipTrigger } from "../../ui/primitives/tooltip"; import { useUpdateKnowledgeItem } from "../hooks"; // Centralized color class mappings @@ -23,12 +23,15 @@ const ICON_COLOR_CLASSES: Record = { const TOOLTIP_COLOR_CLASSES: Record = { cyan: "border-cyan-500/50 shadow-[0_0_15px_rgba(34,211,238,0.5)] dark:border-cyan-400/50 dark:shadow-[0_0_15px_rgba(34,211,238,0.7)]", - purple: "border-purple-500/50 shadow-[0_0_15px_rgba(168,85,247,0.5)] dark:border-purple-400/50 dark:shadow-[0_0_15px_rgba(168,85,247,0.7)]", + purple: + "border-purple-500/50 shadow-[0_0_15px_rgba(168,85,247,0.5)] dark:border-purple-400/50 dark:shadow-[0_0_15px_rgba(168,85,247,0.7)]", blue: "border-blue-500/50 shadow-[0_0_15px_rgba(59,130,246,0.5)] dark:border-blue-400/50 dark:shadow-[0_0_15px_rgba(59,130,246,0.7)]", pink: "border-pink-500/50 shadow-[0_0_15px_rgba(236,72,153,0.5)] dark:border-pink-400/50 dark:shadow-[0_0_15px_rgba(236,72,153,0.7)]", red: "border-red-500/50 shadow-[0_0_15px_rgba(239,68,68,0.5)] dark:border-red-400/50 dark:shadow-[0_0_15px_rgba(239,68,68,0.7)]", - yellow: "border-yellow-500/50 shadow-[0_0_15px_rgba(234,179,8,0.5)] dark:border-yellow-400/50 dark:shadow-[0_0_15px_rgba(234,179,8,0.7)]", - default: "border-cyan-500/50 shadow-[0_0_15px_rgba(34,211,238,0.5)] dark:border-cyan-400/50 dark:shadow-[0_0_15px_rgba(34,211,238,0.7)]", + yellow: + "border-yellow-500/50 shadow-[0_0_15px_rgba(234,179,8,0.5)] dark:border-yellow-400/50 dark:shadow-[0_0_15px_rgba(234,179,8,0.7)]", + default: + "border-cyan-500/50 shadow-[0_0_15px_rgba(34,211,238,0.5)] dark:border-cyan-400/50 dark:shadow-[0_0_15px_rgba(34,211,238,0.7)]", }; interface KnowledgeCardTitleProps { @@ -144,16 +147,16 @@ export const KnowledgeCardTitle: React.FC = ({ disabled={updateMutation.isPending} className={cn( "text-base font-semibold bg-transparent border-cyan-400 dark:border-cyan-600", - "focus:ring-1 focus:ring-cyan-400 px-2 py-1" + "focus:ring-1 focus:ring-cyan-400 px-2 py-1", )} /> - {(description && description.trim()) && ( + {description && description.trim() && ( @@ -173,20 +176,20 @@ export const KnowledgeCardTitle: React.FC = ({ className={cn( "text-base font-semibold text-gray-900 dark:text-white/90 line-clamp-2 cursor-pointer", "hover:text-gray-700 dark:hover:text-white transition-colors", - updateMutation.isPending && "opacity-50" + updateMutation.isPending && "opacity-50", )} onClick={handleClick} > {title} - {(description && description.trim()) && ( + {description && description.trim() && ( @@ -197,4 +200,4 @@ export const KnowledgeCardTitle: React.FC = ({ )}
); -}; \ No newline at end of file +}; diff --git a/archon-ui-main/src/features/knowledge/components/KnowledgeCardType.tsx b/archon-ui-main/src/features/knowledge/components/KnowledgeCardType.tsx index 107a8d4a..ac2f8afe 100644 --- a/archon-ui-main/src/features/knowledge/components/KnowledgeCardType.tsx +++ b/archon-ui-main/src/features/knowledge/components/KnowledgeCardType.tsx @@ -15,10 +15,7 @@ interface KnowledgeCardTypeProps { knowledgeType: "technical" | "business"; } -export const KnowledgeCardType: React.FC = ({ - sourceId, - knowledgeType, -}) => { +export const KnowledgeCardType: React.FC = ({ sourceId, knowledgeType }) => { const [isEditing, setIsEditing] = useState(false); const updateMutation = useUpdateKnowledgeItem(); @@ -61,10 +58,7 @@ export const KnowledgeCardType: React.FC = ({ if (isEditing) { return ( -
e.stopPropagation()} - onKeyDown={(e) => e.stopPropagation()} - > +
e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()}>