Skip to content

refactor: remove ETag Map cache layer for TanStack Query single source of truth#676

Merged
Wirasm merged 12 commits intomainfrom
refactor/remove-etag-cache-layer
Sep 17, 2025
Merged

refactor: remove ETag Map cache layer for TanStack Query single source of truth#676
Wirasm merged 12 commits intomainfrom
refactor/remove-etag-cache-layer

Conversation

@Wirasm
Copy link
Copy Markdown
Collaborator

@Wirasm Wirasm commented Sep 16, 2025

Pull Request

Summary

Phase 1 of frontend state management refactor: eliminates double-caching anti-pattern by removing Map-based cache from apiWithEtag.ts, making TanStack Query the single source of truth for caching decisions while preserving HTTP 304 bandwidth optimization.

Changes Made

  • Removed Map-based cache (etagCache, dataCache) from apiWithEtag.ts
  • Moved apiWithEtag.ts to src/features/shared/ since used across multiple features
  • Implemented NotModifiedError for 304 responses to work with TanStack Query
  • Removed all invalidateETagCache calls from service files (6 services affected)
  • Preserved browser ETag headers for bandwidth optimization (70-90% reduction maintained)
  • Added comprehensive test coverage (10 test cases)
  • Increased API timeout to 20s for large DELETE operations (temp fix for DB performance)

Type of Change

  • Code refactoring
  • Performance improvement
  • Bug fix (timeout issues on large deletes)

Affected Services

  • Frontend (React UI)

Testing

  • All existing tests pass
  • Added new tests for new functionality
  • Manually tested affected user flows

Test Evidence

# Unit tests for refactored apiWithEtag
npm run test:run src/features/shared/apiWithEtag.test.ts
# Result: ✓ 10 tests passed

# All project feature tests
npm run test:run src/features/projects  
# Result: ✓ 23 tests passed

# All knowledge feature tests  
npm run test:run src/features/knowledge
# Result: ✓ 8 tests passed

# Biome linting passed
npm run biome src/features/shared/apiWithEtag.ts
# Result: No issues found

Checklist

  • My code follows the service architecture patterns
  • If using an AI coding assistant, I used the CLAUDE.md rules
  • I have added tests that prove my fix/feature works
  • All new and existing tests pass locally
  • My changes generate no new warnings
  • I have verified no regressions in existing features

Breaking Changes

None - all existing functionality maintained with zero breaking changes.

Additional Notes

Root Cause Analysis for Timeout Fix

Database investigation revealed DELETE operations on archon_crawled_pages with 7K+ rows take 13+ seconds due to PostgreSQL sequential scan. The 20-second timeout increase is a temporary fix until batch deletion is implemented.

Key Benefits

  • ✅ Single cache layer (TanStack Query only)
  • ✅ No more cache synchronization issues
  • ✅ Proper staleTime calculations work correctly
  • ✅ HTTP 304 bandwidth optimization preserved
  • ✅ Foundation ready for Phase 2 (standardize query keys)

Architecture Impact

This addresses the double-layer caching anti-pattern identified in the frontend state management refactor PRP, where ETag Map cache conflicted with TanStack Query cache, causing stale data issues and bypassing cache freshness logic.

Summary by CodeRabbit

  • Refactor
    • Consolidated API and error utilities into shared modules; client-side cache invalidation removed so freshness is managed server-side.
  • Tests
    • Added comprehensive tests for the shared API wrapper (success, no-content, HTTP/API and network errors, timeouts, and caching interactions).
  • Bug Fixes
    • Improved input validation and clearer validation errors for project create/update flows.
  • UI
    • Minor UI tweaks: project card container element adjusted; pin/delete actions no longer trigger unintended card clicks; small improvements to knowledge card editing/tooltips and type/level selectors.
  • Style
    • Widespread formatting and JSX refinements with no behavioral changes.

Wirasm and others added 2 commits September 16, 2025 14:23
…e 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Sep 16, 2025

Walkthrough

Replaces a project-scoped ETag API client with a new shared features/shared/apiWithEtag, removes legacy project-scoped API/error helpers, updates imports across many modules, removes client-side ETag invalidation calls, adds shared error types and tests, unwraps some mutation responses, and applies UI/component tweaks.

Changes

Cohort / File(s) Summary
New shared ETag client, errors & tests
archon-ui-main/src/features/shared/apiWithEtag.ts, archon-ui-main/src/features/shared/apiWithEtag.test.ts, archon-ui-main/src/features/shared/errors.ts
Adds callAPIWithETag<T> (20s timeout, test-env URL builder, JSON parsing and error normalization), introduces APIServiceError/ValidationError/MCPToolError and formatting helpers, and adds comprehensive Vitest tests for the client.
Remove legacy project-scoped ETag client
archon-ui-main/src/features/projects/shared/apiWithEtag.ts
Deletes the previous project-scoped ETag client and removed cache utilities (clearETagCache, invalidateETagCache, getETagCacheStats) and internal caches.
Import/path migrations (no logic changes)
archon-ui-main/src/components/layout/hooks/useBackendHealth.ts, archon-ui-main/src/features/knowledge/progress/services/progressService.ts, archon-ui-main/src/features/mcp/services/mcpApi.ts
Updated imports to features/shared/apiWithEtag; no other logic changes.
Knowledge service: remove cache invalidation & error tweaks
archon-ui-main/src/features/knowledge/services/knowledgeService.ts
Removed invalidateETagCache calls and import; added APIServiceError import; improved uploadDocument error construction; search call now uses generic typing.
Projects service: validation, errors, remove invalidation, API refactor
archon-ui-main/src/features/projects/services/projectService.ts, archon-ui-main/src/features/projects/shared/api.ts
Switched to shared client and shared/errors, added input validation that throws ValidationError for create/update, removed invalidateETagCache usages, and removed legacy project-scoped API/error helpers (leaving formatRelativeTime).
Tasks service: remove invalidation, unwrap mutation responses, tests
archon-ui-main/src/features/projects/tasks/services/taskService.ts, archon-ui-main/src/features/projects/tasks/services/tests/taskService.test.ts
Removed invalidateETagCache calls; mutation responses typed as { message: string; task: Task } and unwrapped to return task; added unit tests covering CRUD and unwrapping logic.
UI/component tweaks and formatting
archon-ui-main/src/features/projects/components/ProjectCard.tsx, archon-ui-main/src/features/knowledge/components/*, archon-ui-main/src/features/projects/tasks/components/TaskCard.tsx, ...
ProjectCard root changed from <motion.li> to <motion.div> and action handlers now stopPropagation; multiple knowledge UI components had formatting changes and small behavior updates (e.g., inline tooltip in KnowledgeCardTitle, KnowledgeCardType edit lifecycle, selection glow in KnowledgeTypeSelector).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor UI
  participant Client as callAPIWithETag (features/shared)
  participant Fetch as fetch

  rect #E8F1FF
    note right of UI: All reads/writes use shared client
    UI->>Client: callAPIWithETag(endpoint, options)
    Client->>Fetch: fetch(fullUrl, headers, signal/timeout)
    Fetch-->>Client: 200 JSON / 204 / 4xx-5xx / AbortError
    alt 200 & payload.error
      Client-->>UI: throw APIServiceError(API_ERROR, status)
    else 200 & OK
      Client-->>UI: parsed JSON
    else 204
      Client-->>UI: undefined
    else network/timeout
      Client-->>UI: throw APIServiceError(NETWORK_ERROR)
    end
  end

  rect #FFF5E6
    note left of Client: Mutations no longer invalidate client-side caches
    UI->>Client: POST/PUT/DELETE (mutation)
    Client->>Fetch: send mutation
    Fetch-->>Client: response (may be { message, task } or raw payload)
    Client-->>UI: parsed response (no invalidate calls)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • leex279
  • coleam00

Poem

"I nibble headers, hop the wire,
Shared calls hum — the old cache retired.
Tests thump softly, fetches cheer,
Responses unwrap — the path is clear.
🐇✨ Down the burrow, code and carrot near."

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title accurately summarizes the primary change by indicating the removal of the ETag Map cache layer and positioning TanStack Query as the single source of truth, which aligns directly with the pull request’s main objective; it is concise, specific, and uses a conventional commit prefix without extraneous detail.
Description Check ✅ Passed The pull request description closely follows the repository template by including a clear Summary, a detailed Changes Made list, Type of Change checkboxes, Affected Services, Testing declarations with concrete evidence, a Checklist, Breaking Changes, and Additional Notes; each section is fully populated and provides the required information.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/remove-etag-cache-layer

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 59521b1 and 9edfe22.

📒 Files selected for processing (1)
  • archon-ui-main/src/features/shared/apiWithEtag.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • archon-ui-main/src/features/shared/apiWithEtag.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
archon-ui-main/src/features/knowledge/services/knowledgeService.ts (1)

208-213: Missing generic breaks strict typing.

callAPIWithETag defaults to unknown; returning it as SearchResultsResponse will fail type-checking.

-  async searchKnowledgeBase(options: SearchOptions): Promise<SearchResultsResponse> {
-    return callAPIWithETag("/api/knowledge-items/search", {
+  async searchKnowledgeBase(options: SearchOptions): Promise<SearchResultsResponse> {
+    return callAPIWithETag<SearchResultsResponse>("/api/knowledge-items/search", {
       method: "POST",
       body: JSON.stringify(options),
     });
   },
🧹 Nitpick comments (15)
archon-ui-main/src/components/layout/hooks/useBackendHealth.ts (2)

22-27: Prevent 304 from surfacing as an error in React Query.

callAPIWithETag throws NotModifiedError on 304. Uncaught, React Query will mark the query as errored on normal revalidations. Catch 304 and return previous data.

Apply:

-import { useQuery } from "@tanstack/react-query";
+import { useQuery, useQueryClient } from "@tanstack/react-query";
+import { NotModifiedError } from "../../../features/shared/apiWithEtag";
@@
 export function useBackendHealth() {
-  return useQuery<HealthResponse>({
+  const queryClient = useQueryClient();
+  return useQuery<HealthResponse>({
     queryKey: ["backend", "health"],
-    queryFn: ({ signal }) => {
+    placeholderData: (prev) => prev,
+    queryFn: async ({ signal, queryKey }) => {
@@
-      return callAPIWithETag<HealthResponse>("/api/health", {
-        signal: controller.signal,
-      }).finally(() => {
-        clearTimeout(timeoutId);
-      });
+      try {
+        return await callAPIWithETag<HealthResponse>("/api/health", {
+          signal: controller.signal,
+        });
+      } catch (e) {
+        if (e instanceof NotModifiedError) {
+          const prev = queryClient.getQueryData<HealthResponse>(queryKey);
+          if (prev) return prev;
+        }
+        throw e;
+      } finally {
+        clearTimeout(timeoutId);
+      }
     },

Also applies to: 9-12


14-21: Optional: simplify signal chaining.

Use AbortSignal.any to avoid manual listeners and timeouts.

-  const controller = new AbortController();
-  const timeoutId = setTimeout(() => controller.abort(), 5000);
-  if (signal) {
-    signal.addEventListener("abort", () => controller.abort());
-  }
+  const combined = typeof AbortSignal.any === "function"
+    ? AbortSignal.any([signal as AbortSignal, AbortSignal.timeout(5000)].filter(Boolean) as AbortSignal[])
+    : (() => {
+        const c = new AbortController();
+        const id = setTimeout(() => c.abort(), 5000);
+        signal?.addEventListener("abort", () => c.abort());
+        return Object.assign(c.signal, { __cleanup: () => clearTimeout(id) });
+      })();

If you take this path, ensure to clear __cleanup() in finally for the fallback.

archon-ui-main/src/features/knowledge/services/knowledgeService.ts (1)

135-138: Harden error parsing for non‑JSON error bodies.

response.json() will throw if the server returns text/HTML on errors.

-    if (!response.ok) {
-      const error = await response.json();
-      throw new Error(error.error || `HTTP ${response.status}`);
-    }
+    if (!response.ok) {
+      const contentType = response.headers.get("content-type") || "";
+      const payload = contentType.includes("application/json") ? await response.json().catch(() => ({})) : { error: await response.text().catch(() => "") };
+      throw new Error((payload as any).error || `HTTP ${response.status}`);
+    }
archon-ui-main/src/features/projects/tasks/services/taskService.ts (1)

175-179: Avoid masking server issues with default {}.

If /api/projects/task-counts unexpectedly returns 204/empty, we should surface that rather than silently returning {}.

-      const response = await callAPIWithETag<Record<string, TaskCounts>>("/api/projects/task-counts");
-      return response || {};
+      const response = await callAPIWithETag<Record<string, TaskCounts>>("/api/projects/task-counts");
+      return response;
archon-ui-main/src/features/shared/apiWithEtag.ts (3)

58-63: Only set Content‑Type when a body exists.

Sending Content-Type: application/json on GET can trigger unnecessary preflight/CORS behavior and is semantically wrong.

-    const headers: Record<string, string> = {
-      "Content-Type": "application/json",
-      ...((options.headers as Record<string, string>) || {}),
-    };
+    const headers: Record<string, string> = {
+      ...((options.headers as Record<string, string>) || {}),
+    };
+    if (options.body && !("Content-Type" in headers)) {
+      headers["Content-Type"] = "application/json";
+    }

28-38: Unify test URL construction with existing env knobs.

Elsewhere (knowledgeService.uploadDocument) uses VITE_HOST/ARCHON_SERVER_PORT. Mirror those here for consistency.

-  if (isTestEnv && !fullUrl.startsWith("http")) {
-    const testHost = "localhost";
-    const testPort = process.env?.ARCHON_SERVER_PORT || "8181";
-    fullUrl = `http://${testHost}:${testPort}${fullUrl}`;
+  if (isTestEnv && !fullUrl.startsWith("http")) {
+    const testHost = process.env?.VITE_HOST || "localhost";
+    const testPort = process.env?.ARCHON_SERVER_PORT || "8181";
+    fullUrl = `http://${testHost}:${testPort}${fullUrl}`;

112-118: Guard JSON parsing for empty bodies.

Some 200 responses may legitimately return no body; response.json() would throw.

-    const result = await response.json();
+    const text = await response.text();
+    const result = text ? JSON.parse(text) : ({} as T);
archon-ui-main/src/features/shared/apiWithEtag.test.ts (8)

6-20: Restore stubbed globals; avoid test pollution and prefer vi.stubGlobal.

Directly assigning global.AbortSignal/global.fetch without restoring can leak across tests. Use vi.stubGlobal and restore in afterEach; also use vi.restoreAllMocks instead of reset+clear combo.

Apply this diff and the top-level declarations:

@@
-  beforeEach(() => {
-    vi.clearAllMocks();
-    vi.resetAllMocks();
+  let originalAbortSignal: typeof globalThis.AbortSignal;
+  let originalFetch: typeof globalThis.fetch;
+
+  beforeEach(() => {
+    vi.restoreAllMocks();
+    vi.clearAllMocks();
+    // Save originals for safety (if present)
+    originalAbortSignal = globalThis.AbortSignal;
+    originalFetch = globalThis.fetch as any;
@@
-    global.AbortSignal = {
+    // Prefer stubbing over direct assignment
+    vi.stubGlobal("AbortSignal", {
       timeout: vi.fn((ms: number) => ({
         aborted: false,
         addEventListener: vi.fn(),
         removeEventListener: vi.fn(),
         reason: undefined,
       })),
-    } as any;
+    } as any);
   });
+
+  afterEach(() => {
+    // Restore globals to avoid cross-file interference
+    globalThis.AbortSignal = originalAbortSignal;
+    globalThis.fetch = originalFetch;
+  });

Also replace direct fetch assignments below with vi.stubGlobal("fetch", ...) (see next comment).


32-33: Use vi.stubGlobal for fetch to keep typings and simplify teardown.

Replace direct global.fetch assignments with vi.stubGlobal and rely on afterEach restoration. This avoids TS type mismatches and reduces flakiness.

Apply this pattern at each occurrence:

-      global.fetch = vi.fn().mockResolvedValue(mockResponse);
+      vi.stubGlobal("fetch", vi.fn().mockResolvedValue(mockResponse));
-      global.fetch = vi.fn().mockRejectedValue(networkError);
+      vi.stubGlobal("fetch", vi.fn().mockRejectedValue(networkError));

Also applies to: 54-55, 68-69, 81-83, 90-91, 105-106, 121-122, 130-131, 145-146


60-72: Verify error shape (code/statusCode) for HTTP errors.

Assert the structured fields to prevent regressions.

Apply this diff:

-      await expect(callAPIWithETag("/test-endpoint")).rejects.toThrow(ProjectServiceError);
-      await expect(callAPIWithETag("/test-endpoint")).rejects.toThrow("Bad request");
+      await expect(callAPIWithETag("/test-endpoint")).rejects.toThrow(ProjectServiceError);
+      await expect(callAPIWithETag("/test-endpoint")).rejects.toThrow("Bad request");
+      await expect(callAPIWithETag("/test-endpoint")).rejects.toMatchObject({
+        name: "ProjectServiceError",
+        code: "HTTP_ERROR",
+        statusCode: 400,
+        message: "Bad request",
+      });

88-95: Also assert ProjectServiceError metadata for network errors.

Check code and statusCode to cement the contract.

Apply this diff:

       await expect(callAPIWithETag("/test-endpoint")).rejects.toThrow(ProjectServiceError);
       await expect(callAPIWithETag("/test-endpoint")).rejects.toThrow("Failed to call API /test-endpoint: Network error");
+      await expect(callAPIWithETag("/test-endpoint")).rejects.toMatchObject({
+        code: "NETWORK_ERROR",
+        statusCode: 500,
+      });

96-109: Assert API_ERROR code and status passthrough on 200 with error body.

This clarifies intended behavior when server encodes errors in a 200 response.

Apply this diff:

       await expect(callAPIWithETag("/test-endpoint")).rejects.toThrow(ProjectServiceError);
       await expect(callAPIWithETag("/test-endpoint")).rejects.toThrow("Database connection failed");
+      await expect(callAPIWithETag("/test-endpoint")).rejects.toMatchObject({
+        code: "API_ERROR",
+        statusCode: 200,
+      });

111-125: Also check code/status for nested backend errors.

Apply this diff:

       await expect(callAPIWithETag("/test-endpoint")).rejects.toThrow(ProjectServiceError);
       await expect(callAPIWithETag("/test-endpoint")).rejects.toThrow("Validation failed");
+      await expect(callAPIWithETag("/test-endpoint")).rejects.toMatchObject({
+        code: "HTTP_ERROR",
+        statusCode: 422,
+      });

127-134: Timeout test: assert error metadata too.

Apply this diff:

       await expect(callAPIWithETag("/test-endpoint")).rejects.toThrow(ProjectServiceError);
       await expect(callAPIWithETag("/test-endpoint")).rejects.toThrow("Failed to call API /test-endpoint: Request timeout");
+      await expect(callAPIWithETag("/test-endpoint")).rejects.toMatchObject({
+        code: "NETWORK_ERROR",
+        statusCode: 500,
+      });

22-22: Add a test: provided AbortSignal should be honored (no extra timeout).

Covers the branch where options.signal bypasses AbortSignal.timeout.

Add this test in the callAPIWithETag describe block:

it("should respect provided signal and not create a new timeout", async () => {
  const mockData = { ok: true };
  const controller = new AbortController();
  vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
    ok: true,
    status: 200,
    json: () => Promise.resolve(mockData),
    headers: new Headers(),
  }));
  await callAPIWithETag("/test-endpoint", { signal: controller.signal });
  expect((globalThis.AbortSignal as any).timeout).not.toHaveBeenCalled();
  expect(globalThis.fetch).toHaveBeenCalledWith(
    expect.any(String),
    expect.objectContaining({ signal: controller.signal }),
  );
});
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b2ec7df and f6977c5.

📒 Files selected for processing (9)
  • archon-ui-main/src/components/layout/hooks/useBackendHealth.ts (1 hunks)
  • archon-ui-main/src/features/knowledge/progress/services/progressService.ts (1 hunks)
  • archon-ui-main/src/features/knowledge/services/knowledgeService.ts (1 hunks)
  • archon-ui-main/src/features/mcp/services/mcpApi.ts (1 hunks)
  • archon-ui-main/src/features/projects/services/projectService.ts (1 hunks)
  • archon-ui-main/src/features/projects/shared/apiWithEtag.ts (0 hunks)
  • archon-ui-main/src/features/projects/tasks/services/taskService.ts (1 hunks)
  • archon-ui-main/src/features/shared/apiWithEtag.test.ts (1 hunks)
  • archon-ui-main/src/features/shared/apiWithEtag.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • archon-ui-main/src/features/projects/shared/apiWithEtag.ts
🧰 Additional context used
📓 Path-based instructions (7)
archon-ui-main/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

archon-ui-main/src/**/*.{ts,tsx}: Use TanStack Query for all data fetching; avoid prop drilling
TypeScript: strict mode with no implicit any in frontend code
State naming: is[Action]ing for loading flags, [resource]Error for errors, selected[Resource] for current selection
Use HTTP polling with ETag caching; do not introduce WebSocket-based updates in the frontend

archon-ui-main/src/**/*.{ts,tsx}: WebSocket event failures (if any) should be logged and not crash the client; continue serving others
Frontend data fetching must use TanStack Query (no prop drilling) with query key factories, smart polling, and optimistic updates with rollback
Use vertical slice architecture: place UI under src/features/[feature]/(components|hooks|services|types)
State naming: use is[Action]ing for loading, [resource]Error for errors, selected[Resource] for selections
Service method names: get[Resource]sByProject(projectId), getResource, create/update/delete patterns
Frontend TypeScript should be strict (no implicit any)

Files:

  • archon-ui-main/src/components/layout/hooks/useBackendHealth.ts
  • archon-ui-main/src/features/projects/tasks/services/taskService.ts
  • archon-ui-main/src/features/knowledge/progress/services/progressService.ts
  • archon-ui-main/src/features/knowledge/services/knowledgeService.ts
  • archon-ui-main/src/features/shared/apiWithEtag.test.ts
  • archon-ui-main/src/features/projects/services/projectService.ts
  • archon-ui-main/src/features/mcp/services/mcpApi.ts
  • archon-ui-main/src/features/shared/apiWithEtag.ts
archon-ui-main/src/components/**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Legacy UI code should adhere to standard React ESLint rules

Files:

  • archon-ui-main/src/components/layout/hooks/useBackendHealth.ts
**/*.{py,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Never return None/null to indicate failure; raise an exception with details instead

Files:

  • archon-ui-main/src/components/layout/hooks/useBackendHealth.ts
  • archon-ui-main/src/features/projects/tasks/services/taskService.ts
  • archon-ui-main/src/features/knowledge/progress/services/progressService.ts
  • archon-ui-main/src/features/knowledge/services/knowledgeService.ts
  • archon-ui-main/src/features/shared/apiWithEtag.test.ts
  • archon-ui-main/src/features/projects/services/projectService.ts
  • archon-ui-main/src/features/mcp/services/mcpApi.ts
  • archon-ui-main/src/features/shared/apiWithEtag.ts
archon-ui-main/src/components/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Legacy UI under /components should follow ESLint standard React rules

Files:

  • archon-ui-main/src/components/layout/hooks/useBackendHealth.ts
archon-ui-main/src/features/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

archon-ui-main/src/features/**/*.{ts,tsx}: Follow TanStack Query patterns: query-keys factory, smart polling via useSmartPolling, optimistic updates with rollback
Biome formatting in features: 120-character lines, double quotes, trailing commas

archon-ui-main/src/features/**/*.{ts,tsx}: Use Biome formatting/conventions in /src/features: 120-char lines, double quotes, trailing commas
Use useSmartPolling and polling intervals (1–2s active, 5–10s background) with smart pausing on tab inactivity
Expose progress via dedicated hooks (e.g., useCrawlProgressPolling, useProjectTasks) instead of ad-hoc timers
Do not use prop drilling for data fetching/state; rely on TanStack Query caches/selectors

Files:

  • archon-ui-main/src/features/projects/tasks/services/taskService.ts
  • archon-ui-main/src/features/knowledge/progress/services/progressService.ts
  • archon-ui-main/src/features/knowledge/services/knowledgeService.ts
  • archon-ui-main/src/features/shared/apiWithEtag.test.ts
  • archon-ui-main/src/features/projects/services/projectService.ts
  • archon-ui-main/src/features/mcp/services/mcpApi.ts
  • archon-ui-main/src/features/shared/apiWithEtag.ts
archon-ui-main/src/features/**/services/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Frontend service method naming: get[Resource]sByProject(projectId), getResource, createResource, update[Resource](id, updates), deleteResource

Files:

  • archon-ui-main/src/features/projects/tasks/services/taskService.ts
  • archon-ui-main/src/features/knowledge/progress/services/progressService.ts
  • archon-ui-main/src/features/knowledge/services/knowledgeService.ts
  • archon-ui-main/src/features/projects/services/projectService.ts
  • archon-ui-main/src/features/mcp/services/mcpApi.ts
archon-ui-main/src/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Write frontend tests with Vitest and React Testing Library

Files:

  • archon-ui-main/src/features/shared/apiWithEtag.test.ts
🧠 Learnings (10)
📓 Common learnings
Learnt from: CR
PR: coleam00/Archon#0
File: AGENTS.md:0-0
Timestamp: 2025-09-12T13:47:04.535Z
Learning: Applies to archon-ui-main/src/**/*.{ts,tsx} : Use HTTP polling with ETag caching; do not introduce WebSocket-based updates in the frontend
Learnt from: CR
PR: coleam00/Archon#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-13T15:53:40.741Z
Learning: Applies to archon-ui-main/src/features/**/*.{ts,tsx} : Do not use prop drilling for data fetching/state; rely on TanStack Query caches/selectors
Learnt from: CR
PR: coleam00/Archon#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-13T15:53:40.741Z
Learning: Applies to archon-ui-main/src/**/*.{ts,tsx} : Frontend data fetching must use TanStack Query (no prop drilling) with query key factories, smart polling, and optimistic updates with rollback
Learnt from: Wirasm
PR: coleam00/Archon#514
File: archon-ui-main/src/services/crawlProgressService.ts:35-39
Timestamp: 2025-08-28T13:07:24.810Z
Learning: The crawlProgressService.ts in the Archon codebase should be deprecated in favor of the existing useCrawlProgressPolling hook from usePolling.ts, which already includes ETag support, 304 handling, tab visibility detection, and proper React lifecycle integration. This consolidation reduces code duplication and improves performance.
📚 Learning: 2025-09-12T13:47:04.535Z
Learnt from: CR
PR: coleam00/Archon#0
File: AGENTS.md:0-0
Timestamp: 2025-09-12T13:47:04.535Z
Learning: Applies to archon-ui-main/src/**/*.{ts,tsx} : Use HTTP polling with ETag caching; do not introduce WebSocket-based updates in the frontend

Applied to files:

  • archon-ui-main/src/components/layout/hooks/useBackendHealth.ts
  • archon-ui-main/src/features/projects/tasks/services/taskService.ts
  • archon-ui-main/src/features/shared/apiWithEtag.test.ts
  • archon-ui-main/src/features/projects/services/projectService.ts
  • archon-ui-main/src/features/mcp/services/mcpApi.ts
  • archon-ui-main/src/features/shared/apiWithEtag.ts
📚 Learning: 2025-09-12T13:47:04.534Z
Learnt from: CR
PR: coleam00/Archon#0
File: AGENTS.md:0-0
Timestamp: 2025-09-12T13:47:04.534Z
Learning: Applies to archon-ui-main/src/features/**/*.{ts,tsx} : Follow TanStack Query patterns: query-keys factory, smart polling via useSmartPolling, optimistic updates with rollback

Applied to files:

  • archon-ui-main/src/components/layout/hooks/useBackendHealth.ts
  • archon-ui-main/src/features/shared/apiWithEtag.ts
📚 Learning: 2025-08-28T13:07:24.810Z
Learnt from: Wirasm
PR: coleam00/Archon#514
File: archon-ui-main/src/services/crawlProgressService.ts:35-39
Timestamp: 2025-08-28T13:07:24.810Z
Learning: The crawlProgressService.ts in the Archon codebase should be deprecated in favor of the existing useCrawlProgressPolling hook from usePolling.ts, which already includes ETag support, 304 handling, tab visibility detection, and proper React lifecycle integration. This consolidation reduces code duplication and improves performance.

Applied to files:

  • archon-ui-main/src/components/layout/hooks/useBackendHealth.ts
  • archon-ui-main/src/features/knowledge/progress/services/progressService.ts
  • archon-ui-main/src/features/projects/services/projectService.ts
  • archon-ui-main/src/features/mcp/services/mcpApi.ts
  • archon-ui-main/src/features/shared/apiWithEtag.ts
📚 Learning: 2025-09-13T15:53:40.741Z
Learnt from: CR
PR: coleam00/Archon#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-13T15:53:40.741Z
Learning: Applies to archon-ui-main/src/**/*.{ts,tsx} : Frontend data fetching must use TanStack Query (no prop drilling) with query key factories, smart polling, and optimistic updates with rollback

Applied to files:

  • archon-ui-main/src/components/layout/hooks/useBackendHealth.ts
  • archon-ui-main/src/features/shared/apiWithEtag.ts
📚 Learning: 2025-08-29T08:57:47.558Z
Learnt from: Wirasm
PR: coleam00/Archon#514
File: archon-ui-main/src/services/progressService.ts:0-0
Timestamp: 2025-08-29T08:57:47.558Z
Learning: The progressService.ts file was removed from the codebase in PR #514 as part of the consolidation effort to use the usePolling hook pattern from usePolling.ts instead of maintaining separate polling services. This aligns with the architectural decision to deprecate individual polling services in favor of the centralized hook-based approach.

Applied to files:

  • archon-ui-main/src/features/knowledge/progress/services/progressService.ts
📚 Learning: 2025-09-13T15:53:40.741Z
Learnt from: CR
PR: coleam00/Archon#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-13T15:53:40.741Z
Learning: Applies to archon-ui-main/src/features/**/*.{ts,tsx} : Expose progress via dedicated hooks (e.g., useCrawlProgressPolling, useProjectTasks) instead of ad-hoc timers

Applied to files:

  • archon-ui-main/src/features/knowledge/progress/services/progressService.ts
📚 Learning: 2025-09-13T15:53:40.741Z
Learnt from: CR
PR: coleam00/Archon#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-13T15:53:40.741Z
Learning: Applies to archon-ui-main/src/**/*.{test,spec}.{ts,tsx} : Write frontend tests with Vitest and React Testing Library

Applied to files:

  • archon-ui-main/src/features/shared/apiWithEtag.test.ts
📚 Learning: 2025-09-13T15:53:40.741Z
Learnt from: CR
PR: coleam00/Archon#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-13T15:53:40.741Z
Learning: Applies to archon-ui-main/src/**/*.{ts,tsx} : Service method names: get[Resource]sByProject(projectId), get[Resource](id), create/update/delete patterns

Applied to files:

  • archon-ui-main/src/features/projects/services/projectService.ts
📚 Learning: 2025-09-12T13:47:04.535Z
Learnt from: CR
PR: coleam00/Archon#0
File: AGENTS.md:0-0
Timestamp: 2025-09-12T13:47:04.535Z
Learning: Applies to archon-ui-main/src/features/**/services/**/*.ts : Frontend service method naming: get[Resource]sByProject(projectId), get[Resource](id), create[Resource](data), update[Resource](id, updates), delete[Resource](id)

Applied to files:

  • archon-ui-main/src/features/projects/services/projectService.ts
🧬 Code graph analysis (2)
archon-ui-main/src/features/shared/apiWithEtag.test.ts (2)
archon-ui-main/src/features/shared/apiWithEtag.ts (2)
  • callAPIWithETag (50-144)
  • NotModifiedError (9-14)
archon-ui-main/src/features/projects/shared/api.ts (1)
  • ProjectServiceError (10-19)
archon-ui-main/src/features/shared/apiWithEtag.ts (2)
archon-ui-main/src/config/api.ts (1)
  • API_BASE_URL (47-47)
archon-ui-main/src/features/projects/shared/api.ts (1)
  • ProjectServiceError (10-19)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Backend Tests (Python + pytest)
🔇 Additional comments (17)
archon-ui-main/src/components/layout/hooks/useBackendHealth.ts (1)

2-2: Path update looks good.

Import now correctly points to the shared client.

archon-ui-main/src/features/mcp/services/mcpApi.ts (2)

1-1: Path update to shared client is correct.


5-13: Ensure query boundaries handle 304.

These service calls will throw NotModifiedError on 304 via callAPIWithETag. Confirm any useQuery wrappers that call these methods catch NotModifiedError and return previous data, as shown in the useBackendHealth suggestion.

Also applies to: 15-23, 25-33, 35-44

archon-ui-main/src/features/knowledge/progress/services/progressService.ts (2)

6-6: Import path change LGTM.


13-15: 304 handling at consumers.

getProgress/listActiveOperations will propagate NotModifiedError. Ensure polling hooks (e.g., useCrawlProgressPolling) catch it and keep prior data instead of surfacing an error state.

Also applies to: 20-23

archon-ui-main/src/features/knowledge/services/knowledgeService.ts (2)

6-6: Confirm cache invalidation moved to hooks.

With client-side ETag invalidation removed, ensure mutations that call this service still invalidate the relevant TanStack Query keys (e.g., summaries, item detail, sources).


56-63: DELETE return type vs 204 No Content — incorrect for this endpoint

callAPIWithETag explicitly returns undefined for 204 (archon-ui-main/src/features/shared/apiWithEtag.ts), but the server's DELETE /knowledge-items/{source_id} returns JSON { success: True, message: "..." } (python/src/server/api_routes/knowledge_api.py). deleteKnowledgeItem's Promise<{ success: boolean; message: string }> matches the backend response — no change required. If the server is changed to return 204, update the client to Promise or handle undefined.

Likely an incorrect or invalid review comment.

archon-ui-main/src/features/projects/tasks/services/taskService.ts (2)

7-7: Shared client import is correct.


16-22: Ensure 304 handling at query boundaries.

These service methods will throw NotModifiedError on 304. Confirm all useQuery callers catch it and keep prior data.

Also applies to: 31-35, 55-62, 78-86, 105-113, 123-128, 176-179

archon-ui-main/src/features/projects/services/projectService.ts (2)

8-8: Import path update LGTM.


15-22: 304 handling: verify callers are wrapping service calls.

If these are used inside useQuery, ensure NotModifiedError is caught and previous data returned to avoid transient error states on revalidation.

Also applies to: 51-54, 125-129, 153-157, 168-174

archon-ui-main/src/features/shared/apiWithEtag.ts (1)

64-66: ETag assumptions.

Browsers only send If-None-Match if responses are cacheable (e.g., Cache-Control: no-cache/ETag). Verify server headers are configured accordingly; otherwise you won’t see 304s.

archon-ui-main/src/features/shared/apiWithEtag.test.ts (5)

36-45: Also assert timeout/signal usage to lock in the 20s default.

Ensure AbortSignal.timeout(20000) is invoked and a signal is passed to fetch.

Apply this diff:

       expect(global.fetch).toHaveBeenCalledWith(
         expect.stringContaining("/test-endpoint"),
         expect.objectContaining({
           headers: expect.objectContaining({
             "Content-Type": "application/json",
           }),
+          signal: expect.anything(),
         })
       );
+      expect((globalThis.AbortSignal as any).timeout).toHaveBeenCalledWith(20000);

47-58: LGTM: 304 path correctly surfaces NotModifiedError.


74-86: LGTM: 204 returns undefined for No Content.


136-164: LGTM: custom headers propagate alongside default Content-Type.


167-176: LGTM: NotModifiedError shape is validated.

Comment on lines +77 to +84
// Handle 304 Not Modified - let TanStack Query handle caching
if (response.status === 304) {
if (ETAG_DEBUG) {
console.log(`%c[ETag] 304 Not Modified for ${cleanEndpoint}`, "color: #10b981; font-weight: bold");
}
throw new NotModifiedError();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Revisit 304 strategy: throwing at the client boundary pushes complexity to every caller.

Uncaught, this degrades React Query UX. Provide an escape hatch to return previous data directly, or document/enforce a wrapper.

Option A (preferred): add an optional onNotModified callback.

-export async function callAPIWithETag<T = unknown>(endpoint: string, options: RequestInit = {}): Promise<T> {
+export async function callAPIWithETag<T = unknown>(
+  endpoint: string,
+  options: RequestInit & { onNotModified?: () => T } = {},
+): Promise<T> {
@@
-    if (response.status === 304) {
+    if (response.status === 304) {
       if (ETAG_DEBUG) {
         console.log(`%c[ETag] 304 Not Modified for ${cleanEndpoint}`, "color: #10b981; font-weight: bold");
       }
-      throw new NotModifiedError();
+      if (typeof options.onNotModified === "function") {
+        // Return prior data supplied by caller (e.g., from QueryClient)
+        return options.onNotModified();
+      }
+      throw new NotModifiedError();
     }

Then callers in queryFns can pass onNotModified: () => queryClient.getQueryData(queryKey)!.

Option B: keep throwing, but mandate a shared wrapQueryFn helper that catches NotModifiedError and returns prior data. If you prefer this, add that helper in features/shared/ and update query hooks.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Handle 304 Not Modified - let TanStack Query handle caching
if (response.status === 304) {
if (ETAG_DEBUG) {
console.log(`%c[ETag] 304 Not Modified for ${cleanEndpoint}`, "color: #10b981; font-weight: bold");
}
throw new NotModifiedError();
}
export async function callAPIWithETag<T = unknown>(
endpoint: string,
options: RequestInit & { onNotModified?: () => T } = {},
): Promise<T> {
// ... (earlier code)
// Handle 304 Not Modified - let TanStack Query handle caching
if (response.status === 304) {
if (ETAG_DEBUG) {
console.log("%c[ETag] 304 Not Modified for " + cleanEndpoint, "color: #10b981; font-weight: bold");
}
if (typeof options.onNotModified === "function") {
// Return prior data supplied by caller (e.g., from QueryClient)
return options.onNotModified();
}
throw new NotModifiedError();
}
// ... (later code)
}

- 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
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.
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.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (10)
archon-ui-main/src/features/projects/components/ProjectCard.tsx (2)

31-31: Switching from li to div: confirm parent semantics or add list semantics.

If this component is rendered inside a UL/OL, replacing LI with DIV breaks list semantics and screen-reader navigation. Either revert to LI or keep DIV and add role="listitem".

Option A (revert):

-    <motion.div
+    <motion.li
...
-    </motion.div>
+    </motion.li>

Option B (keep div, preserve semantics):

-    <motion.div
+    <motion.div
+      role="listitem"

Also applies to: 265-265


261-263: Prevent action button clicks from also selecting the card.

Stop event propagation locally so onSelect isn’t triggered when clicking Pin/Delete.

-          onPin={(e) => onPin(e, project.id)}
-          onDelete={(e) => onDelete(e, project.id, project.title)}
+          onPin={(e) => {
+            e.stopPropagation();
+            onPin(e, project.id);
+          }}
+          onDelete={(e) => {
+            e.stopPropagation();
+            onDelete(e, project.id, project.title);
+          }}
archon-ui-main/src/features/projects/tasks/services/taskService.ts (2)

16-26: Support request cancellation (React Query’s signal).

Expose an optional AbortSignal to propagate cancellations; apply similarly to other methods.

-  async getTasksByProject(projectId: string): Promise<Task[]> {
+  async getTasksByProject(projectId: string, signal?: AbortSignal): Promise<Task[]> {
     try {
-      const tasks = await callAPIWithETag<Task[]>(`/api/projects/${projectId}/tasks`);
+      const tasks = await callAPIWithETag<Task[]>(`/api/projects/${projectId}/tasks`, { signal });
       return tasks;
     } catch (error) {
       console.error(`Failed to get tasks for project ${projectId}:`, error);
       throw error;
     }
   },

174-181: Avoid masking unexpected undefined results.

Returning {} on falsy response can hide server/typing issues. Prefer returning the actual response and let callers handle absence.

-      const response = await callAPIWithETag<Record<string, TaskCounts>>("/api/projects/task-counts");
-      return response || {};
+      const response = await callAPIWithETag<Record<string, TaskCounts>>("/api/projects/task-counts");
+      return response;
archon-ui-main/src/features/knowledge/services/knowledgeService.ts (2)

120-142: Unify error type on uploads with the rest of the API errors.

Throw ProjectServiceError for upload failures to keep consumers handling a single error shape.

-    if (!response.ok) {
-      const error = await response.json();
-      throw new Error(error.error || `HTTP ${response.status}`);
-    }
+    if (!response.ok) {
+      const err = await response.json().catch(() => ({}));
+      throw new ProjectServiceError(err.error || `HTTP ${response.status}`, "HTTP_ERROR", response.status);
+    }

Add import (outside the shown range):

import { ProjectServiceError } from "../../projects/shared/api";

27-44: Optional: propagate AbortSignal to enable cancellations.

Let callers (TanStack Query) pass a signal; forward it to callAPIWithETag/fetch.

Pattern example:

-  async getKnowledgeSummaries(filter?: KnowledgeItemsFilter): Promise<KnowledgeItemsResponse> {
+  async getKnowledgeSummaries(filter?: KnowledgeItemsFilter, signal?: AbortSignal): Promise<KnowledgeItemsResponse> {
...
-    return callAPIWithETag<KnowledgeItemsResponse>(endpoint);
+    return callAPIWithETag<KnowledgeItemsResponse>(endpoint, { signal });

Apply similarly to other methods.

Also applies to: 49-51, 81-89, 148-151, 178-179, 202-203, 209-213, 219-220

archon-ui-main/src/features/shared/apiWithEtag.test.ts (4)

1-4: Restore globals to avoid test pollution.

Preserve and restore original AbortSignal (and fetch if desired) so other tests aren’t affected.

-import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+const originalAbortSignal = global.AbortSignal as any;
+const originalFetch = global.fetch;

...
   beforeEach(() => {
-    vi.clearAllMocks();
-    vi.resetAllMocks();
+    vi.resetAllMocks();
     // Reset fetch to undefined to ensure clean state
     if (global.fetch) {
       delete (global as any).fetch;
     }
...
-    global.AbortSignal = {
+    global.AbortSignal = {
       timeout: vi.fn((ms: number) => ({
         aborted: false,
         addEventListener: vi.fn(),
         removeEventListener: vi.fn(),
         reason: undefined,
       })),
     } as any;
   });
 
   afterEach(() => {
-    vi.restoreAllMocks();
+    vi.restoreAllMocks();
+    global.AbortSignal = originalAbortSignal;
+    if (originalFetch) global.fetch = originalFetch;
   });

Also applies to: 6-24, 26-28


5-5: Test suite title nit.

Consider a neutral suite name (e.g., "apiWithEtag") to avoid referencing internal option numbers.


65-67: Avoid duplicate assertions triggering extra fetch calls.

Each test calls callAPIWithETag twice for the same condition. Capture the thrown error once or assert once to keep tests lean.

Also applies to: 87-89


2-4: Reduce cross-feature coupling of ProjectServiceError import.

Importing the error class from projects/shared in a shared test creates an odd dependency. Consider relocating ProjectServiceError to features/shared (and re-export from projects) to reflect its actual scope.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f6977c5 and 0e34961.

📒 Files selected for processing (5)
  • archon-ui-main/src/features/knowledge/services/knowledgeService.ts (2 hunks)
  • archon-ui-main/src/features/projects/components/ProjectCard.tsx (2 hunks)
  • archon-ui-main/src/features/projects/tasks/services/taskService.ts (1 hunks)
  • archon-ui-main/src/features/shared/apiWithEtag.test.ts (1 hunks)
  • archon-ui-main/src/features/shared/apiWithEtag.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • archon-ui-main/src/features/shared/apiWithEtag.ts
🧰 Additional context used
📓 Path-based instructions (5)
archon-ui-main/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

archon-ui-main/src/**/*.{ts,tsx}: Use TanStack Query for all data fetching; avoid prop drilling
TypeScript: strict mode with no implicit any in frontend code
State naming: is[Action]ing for loading flags, [resource]Error for errors, selected[Resource] for current selection
Use HTTP polling with ETag caching; do not introduce WebSocket-based updates in the frontend

archon-ui-main/src/**/*.{ts,tsx}: WebSocket event failures (if any) should be logged and not crash the client; continue serving others
Frontend data fetching must use TanStack Query (no prop drilling) with query key factories, smart polling, and optimistic updates with rollback
Use vertical slice architecture: place UI under src/features/[feature]/(components|hooks|services|types)
State naming: use is[Action]ing for loading, [resource]Error for errors, selected[Resource] for selections
Service method names: get[Resource]sByProject(projectId), getResource, create/update/delete patterns
Frontend TypeScript should be strict (no implicit any)

Files:

  • archon-ui-main/src/features/projects/components/ProjectCard.tsx
  • archon-ui-main/src/features/shared/apiWithEtag.test.ts
  • archon-ui-main/src/features/knowledge/services/knowledgeService.ts
  • archon-ui-main/src/features/projects/tasks/services/taskService.ts
archon-ui-main/src/features/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

archon-ui-main/src/features/**/*.{ts,tsx}: Follow TanStack Query patterns: query-keys factory, smart polling via useSmartPolling, optimistic updates with rollback
Biome formatting in features: 120-character lines, double quotes, trailing commas

archon-ui-main/src/features/**/*.{ts,tsx}: Use Biome formatting/conventions in /src/features: 120-char lines, double quotes, trailing commas
Use useSmartPolling and polling intervals (1–2s active, 5–10s background) with smart pausing on tab inactivity
Expose progress via dedicated hooks (e.g., useCrawlProgressPolling, useProjectTasks) instead of ad-hoc timers
Do not use prop drilling for data fetching/state; rely on TanStack Query caches/selectors

Files:

  • archon-ui-main/src/features/projects/components/ProjectCard.tsx
  • archon-ui-main/src/features/shared/apiWithEtag.test.ts
  • archon-ui-main/src/features/knowledge/services/knowledgeService.ts
  • archon-ui-main/src/features/projects/tasks/services/taskService.ts
**/*.{py,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Never return None/null to indicate failure; raise an exception with details instead

Files:

  • archon-ui-main/src/features/projects/components/ProjectCard.tsx
  • archon-ui-main/src/features/shared/apiWithEtag.test.ts
  • archon-ui-main/src/features/knowledge/services/knowledgeService.ts
  • archon-ui-main/src/features/projects/tasks/services/taskService.ts
archon-ui-main/src/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Write frontend tests with Vitest and React Testing Library

Files:

  • archon-ui-main/src/features/shared/apiWithEtag.test.ts
archon-ui-main/src/features/**/services/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Frontend service method naming: get[Resource]sByProject(projectId), getResource, createResource, update[Resource](id, updates), deleteResource

Files:

  • archon-ui-main/src/features/knowledge/services/knowledgeService.ts
  • archon-ui-main/src/features/projects/tasks/services/taskService.ts
🧠 Learnings (4)
📓 Common learnings
Learnt from: CR
PR: coleam00/Archon#0
File: AGENTS.md:0-0
Timestamp: 2025-09-12T13:47:04.535Z
Learning: Applies to archon-ui-main/src/**/*.{ts,tsx} : Use HTTP polling with ETag caching; do not introduce WebSocket-based updates in the frontend
📚 Learning: 2025-08-28T12:56:47.840Z
Learnt from: Wirasm
PR: coleam00/Archon#514
File: archon-ui-main/src/pages/ProjectPage.tsx:329-331
Timestamp: 2025-08-28T12:56:47.840Z
Learning: In the ProjectPage.tsx polling refactor, temporary project creation logic with progress cards was removed in favor of simpler modal loading states, as the complexity of managing in-flight temporary projects wasn't justified when HTTP polling would show new projects within seconds anyway.

Applied to files:

  • archon-ui-main/src/features/projects/components/ProjectCard.tsx
📚 Learning: 2025-09-13T15:53:40.741Z
Learnt from: CR
PR: coleam00/Archon#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-13T15:53:40.741Z
Learning: Applies to archon-ui-main/src/**/*.{test,spec}.{ts,tsx} : Write frontend tests with Vitest and React Testing Library

Applied to files:

  • archon-ui-main/src/features/shared/apiWithEtag.test.ts
📚 Learning: 2025-09-12T13:47:04.535Z
Learnt from: CR
PR: coleam00/Archon#0
File: AGENTS.md:0-0
Timestamp: 2025-09-12T13:47:04.535Z
Learning: Applies to archon-ui-main/src/**/*.{ts,tsx} : Use HTTP polling with ETag caching; do not introduce WebSocket-based updates in the frontend

Applied to files:

  • archon-ui-main/src/features/shared/apiWithEtag.test.ts
🧬 Code graph analysis (2)
archon-ui-main/src/features/shared/apiWithEtag.test.ts (2)
archon-ui-main/src/features/shared/apiWithEtag.ts (1)
  • callAPIWithETag (36-109)
archon-ui-main/src/features/projects/shared/api.ts (1)
  • ProjectServiceError (10-19)
archon-ui-main/src/features/knowledge/services/knowledgeService.ts (2)
archon-ui-main/src/features/shared/apiWithEtag.ts (1)
  • callAPIWithETag (36-109)
archon-ui-main/src/features/knowledge/types/knowledge.ts (1)
  • SearchResultsResponse (182-187)
🔇 Additional comments (2)
archon-ui-main/src/features/projects/tasks/services/taskService.ts (1)

51-66: TanStack Query invalidations moved out: verify hooks invalidate affected queries.

After create/update/status/delete, ensure your mutations invalidate or update query keys (e.g., tasks by project, task counts) since service-level ETag invalidation was removed.

Example (in your hooks):

const qc = useQueryClient();
useMutation({
  mutationFn: (data) => taskService.createTask(data),
  onSuccess: (_task, vars) => {
    qc.invalidateQueries({ queryKey: ["projects", vars.project_id, "tasks"] });
    qc.invalidateQueries({ queryKey: ["projects", "task-counts"] });
  },
});

Also applies to: 78-89, 104-117, 122-132

archon-ui-main/src/features/knowledge/services/knowledgeService.ts (1)

56-63: After removing client-side invalidation, ensure mutations invalidate knowledge queries.

Confirm mutation hooks invalidate list, summary, and detail query keys (e.g., ["knowledge","summaries"], ["knowledge","items",sourceId]) so UI stays consistent.

I can provide a small query-keys factory if helpful.

Also applies to: 68-76, 81-89, 94-101, 106-143

Wirasm and others added 4 commits September 17, 2025 14:26
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.
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.
- 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 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
archon-ui-main/src/features/shared/apiWithEtag.ts (1)

60-80: Handle HTTP 304 explicitly to avoid treating cache revalidations as errors.

Currently, 304 falls into !response.ok and becomes an error. Either return prior data via a callback or throw a dedicated NotModifiedError and catch it in a shared query wrapper.

Option A (preferred): onNotModified callback.

-export async function callAPIWithETag<T = unknown>(endpoint: string, options: RequestInit = {}): Promise<T> {
+export async function callAPIWithETag<T = unknown>(
+  endpoint: string,
+  options: RequestInit & { onNotModified?: () => T } = {},
+): Promise<T> {
@@
-    if (!response.ok) {
+    if (response.status === 304) {
+      if (typeof (options as any).onNotModified === "function") {
+        return (options as any).onNotModified();
+      }
+      throw new APIServiceError("Not Modified", "NOT_MODIFIED", 304);
+    }
+    if (!response.ok) {

Option B: keep throwing on 304 here, but add a shared wrapQueryFn helper to catch code === "NOT_MODIFIED" and return queryClient.getQueryData(key)!.

🧹 Nitpick comments (9)
archon-ui-main/src/features/projects/components/ProjectCard.tsx (1)

31-41: Add proper button semantics for a11y (role and state).

Clickable div with keyboard handlers should declare role and state. Replace aria-current (for nav) with aria-selected or aria-pressed depending on semantics.

-    <motion.div
+    <motion.div
+      role="button"
       tabIndex={0}
-      aria-label={`Select project ${project.title}`}
-      aria-current={isSelected ? "true" : undefined}
+      aria-label={`Select project ${project.title}`}
+      aria-selected={isSelected || undefined}
       onKeyDown={(e) => {
         if (e.key === "Enter" || e.key === " ") {
           e.preventDefault();
           onSelect(project);
         }
       }}
archon-ui-main/src/features/projects/services/projectService.ts (1)

34-37: Guard against missing updated_at to avoid "Invalid Date".

If updated_at is undefined/null, formatRelativeTime will produce NaN. Provide a safe fallback.

-          updated: project.updated || formatRelativeTime(project.updated_at),
+          updated:
+            project.updated_at ? formatRelativeTime(project.updated_at) : (project.updated || "just now"),
-        updated: project.updated || formatRelativeTime(project.updated_at),
+        updated: project.updated_at ? formatRelativeTime(project.updated_at) : (project.updated || "just now"),

Also applies to: 56-60

archon-ui-main/src/features/projects/tasks/services/tests/taskService.test.ts (2)

7-9: Prefer typed vi.mocked to avoid casts.

Use vi.mocked(callAPIWithETag) for type-safe mocks and remove as any.

- (callAPIWithETag as any).mockResolvedValueOnce(mockResponse);
+ vi.mocked(callAPIWithETag).mockResolvedValueOnce(mockResponse);

Also applies to: 49-52, 101-111


200-208: DELETE: also assert 204 handling path.

Consider asserting the returned value is undefined to codify the 204 contract.

archon-ui-main/src/features/knowledge/services/knowledgeService.ts (2)

132-135: Return richer error detail on upload failures.

Backends often return { detail } or { detail: { error } }. Mirror the JSON parsing used in callAPIWithETag for consistency.

-  const err = await response.json().catch(() => ({}));
-  throw new APIServiceError(err.error || `HTTP ${response.status}`, "HTTP_ERROR", response.status);
+  const err = await response.json().catch(() => ({}));
+  const message =
+    (typeof err?.detail === "object" && err?.detail?.error) || err?.detail || err?.error || `HTTP ${response.status}`;
+  throw new APIServiceError(message, "HTTP_ERROR", response.status);

118-125: Cross-origin in tests: consider credentials if auth is cookie-based.

Absolute URL in tests may drop cookies (same-origin default). If tests or CI rely on auth cookies, add credentials: "include" guarded for tests.

Also applies to: 126-131

archon-ui-main/src/features/shared/errors.ts (1)

20-28: Add NotModifiedError (304) to align with PR description and 304 flow.

Expose a specific error type for cache revalidation handling in callers.

 export class ValidationError extends APIServiceError {
   constructor(message: string) {
     super(message, "VALIDATION_ERROR", 400);
     this.name = "ValidationError";
   }
 }
 
+/**
+ * 304 Not Modified error used when leveraging HTTP cache revalidation
+ */
+export class NotModifiedError extends APIServiceError {
+  constructor(message = "Not Modified") {
+    super(message, "NOT_MODIFIED", 304);
+    this.name = "NotModifiedError";
+  }
+}
+
archon-ui-main/src/features/projects/shared/api.ts (2)

8-10: Unify API base and timeout with shared client.

This local API_BASE_URL duplicates config and still uses 10s timeout. For consistency with callAPIWithETag (20s), import the shared base/timeout.

-import { APIServiceError } from "../../shared/errors";
+import { APIServiceError } from "../../shared/errors";
+import { API_BASE_URL } from "../../config/api";
@@
-// API configuration - use relative URL to go through Vite proxy
-const API_BASE_URL = "/api";
+// Use shared API base URL (proxied via Vite in dev)
+// NOTE: Keep aligned with features/shared/apiWithEtag.ts
@@
-      signal: options.signal ?? AbortSignal.timeout(10000), // 10 second timeout
+      signal: options.signal ?? AbortSignal.timeout(20000), // 20 second timeout (temp)

Also applies to: 11-23


25-39: Consistent error extraction.

Mirror the richer error parsing used in callAPIWithETag (detail.error -> detail -> error).

Also applies to: 46-51, 59-63

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 01e8b17 and 1547cf9.

📒 Files selected for processing (9)
  • archon-ui-main/src/features/knowledge/services/knowledgeService.ts (3 hunks)
  • archon-ui-main/src/features/projects/components/ProjectCard.tsx (2 hunks)
  • archon-ui-main/src/features/projects/services/projectService.ts (1 hunks)
  • archon-ui-main/src/features/projects/shared/api.ts (3 hunks)
  • archon-ui-main/src/features/projects/tasks/services/taskService.ts (4 hunks)
  • archon-ui-main/src/features/projects/tasks/services/tests/taskService.test.ts (1 hunks)
  • archon-ui-main/src/features/shared/apiWithEtag.test.ts (1 hunks)
  • archon-ui-main/src/features/shared/apiWithEtag.ts (1 hunks)
  • archon-ui-main/src/features/shared/errors.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • archon-ui-main/src/features/projects/tasks/services/taskService.ts
  • archon-ui-main/src/features/shared/apiWithEtag.test.ts
🧰 Additional context used
📓 Path-based instructions (5)
archon-ui-main/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

archon-ui-main/src/**/*.{ts,tsx}: Use TanStack Query for all data fetching; avoid prop drilling
TypeScript: strict mode with no implicit any in frontend code
State naming: is[Action]ing for loading flags, [resource]Error for errors, selected[Resource] for current selection
Use HTTP polling with ETag caching; do not introduce WebSocket-based updates in the frontend

archon-ui-main/src/**/*.{ts,tsx}: WebSocket event failures (if any) should be logged and not crash the client; continue serving others
Frontend data fetching must use TanStack Query (no prop drilling) with query key factories, smart polling, and optimistic updates with rollback
Use vertical slice architecture: place UI under src/features/[feature]/(components|hooks|services|types)
State naming: use is[Action]ing for loading, [resource]Error for errors, selected[Resource] for selections
Service method names: get[Resource]sByProject(projectId), getResource, create/update/delete patterns
Frontend TypeScript should be strict (no implicit any)

Files:

  • archon-ui-main/src/features/projects/tasks/services/tests/taskService.test.ts
  • archon-ui-main/src/features/projects/components/ProjectCard.tsx
  • archon-ui-main/src/features/shared/errors.ts
  • archon-ui-main/src/features/projects/services/projectService.ts
  • archon-ui-main/src/features/knowledge/services/knowledgeService.ts
  • archon-ui-main/src/features/shared/apiWithEtag.ts
  • archon-ui-main/src/features/projects/shared/api.ts
archon-ui-main/src/features/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

archon-ui-main/src/features/**/*.{ts,tsx}: Follow TanStack Query patterns: query-keys factory, smart polling via useSmartPolling, optimistic updates with rollback
Biome formatting in features: 120-character lines, double quotes, trailing commas

archon-ui-main/src/features/**/*.{ts,tsx}: Use Biome formatting/conventions in /src/features: 120-char lines, double quotes, trailing commas
Use useSmartPolling and polling intervals (1–2s active, 5–10s background) with smart pausing on tab inactivity
Expose progress via dedicated hooks (e.g., useCrawlProgressPolling, useProjectTasks) instead of ad-hoc timers
Do not use prop drilling for data fetching/state; rely on TanStack Query caches/selectors

Files:

  • archon-ui-main/src/features/projects/tasks/services/tests/taskService.test.ts
  • archon-ui-main/src/features/projects/components/ProjectCard.tsx
  • archon-ui-main/src/features/shared/errors.ts
  • archon-ui-main/src/features/projects/services/projectService.ts
  • archon-ui-main/src/features/knowledge/services/knowledgeService.ts
  • archon-ui-main/src/features/shared/apiWithEtag.ts
  • archon-ui-main/src/features/projects/shared/api.ts
archon-ui-main/src/features/**/services/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Frontend service method naming: get[Resource]sByProject(projectId), getResource, createResource, update[Resource](id, updates), deleteResource

Files:

  • archon-ui-main/src/features/projects/tasks/services/tests/taskService.test.ts
  • archon-ui-main/src/features/projects/services/projectService.ts
  • archon-ui-main/src/features/knowledge/services/knowledgeService.ts
**/*.{py,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Never return None/null to indicate failure; raise an exception with details instead

Files:

  • archon-ui-main/src/features/projects/tasks/services/tests/taskService.test.ts
  • archon-ui-main/src/features/projects/components/ProjectCard.tsx
  • archon-ui-main/src/features/shared/errors.ts
  • archon-ui-main/src/features/projects/services/projectService.ts
  • archon-ui-main/src/features/knowledge/services/knowledgeService.ts
  • archon-ui-main/src/features/shared/apiWithEtag.ts
  • archon-ui-main/src/features/projects/shared/api.ts
archon-ui-main/src/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Write frontend tests with Vitest and React Testing Library

Files:

  • archon-ui-main/src/features/projects/tasks/services/tests/taskService.test.ts
🧠 Learnings (7)
📓 Common learnings
Learnt from: CR
PR: coleam00/Archon#0
File: AGENTS.md:0-0
Timestamp: 2025-09-12T13:47:04.535Z
Learning: Applies to archon-ui-main/src/**/*.{ts,tsx} : Use HTTP polling with ETag caching; do not introduce WebSocket-based updates in the frontend
Learnt from: Wirasm
PR: coleam00/Archon#514
File: archon-ui-main/src/services/crawlProgressService.ts:35-39
Timestamp: 2025-08-28T13:07:24.810Z
Learning: The crawlProgressService.ts in the Archon codebase should be deprecated in favor of the existing useCrawlProgressPolling hook from usePolling.ts, which already includes ETag support, 304 handling, tab visibility detection, and proper React lifecycle integration. This consolidation reduces code duplication and improves performance.
Learnt from: CR
PR: coleam00/Archon#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-13T15:53:40.741Z
Learning: Applies to archon-ui-main/src/**/*.{ts,tsx} : Frontend data fetching must use TanStack Query (no prop drilling) with query key factories, smart polling, and optimistic updates with rollback
Learnt from: CR
PR: coleam00/Archon#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-13T15:53:40.741Z
Learning: Applies to archon-ui-main/src/features/**/*.{ts,tsx} : Do not use prop drilling for data fetching/state; rely on TanStack Query caches/selectors
Learnt from: CR
PR: coleam00/Archon#0
File: AGENTS.md:0-0
Timestamp: 2025-09-12T13:47:04.534Z
Learning: Applies to archon-ui-main/src/features/**/*.{ts,tsx} : Follow TanStack Query patterns: query-keys factory, smart polling via useSmartPolling, optimistic updates with rollback
📚 Learning: 2025-09-13T15:53:40.741Z
Learnt from: CR
PR: coleam00/Archon#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-13T15:53:40.741Z
Learning: Applies to archon-ui-main/src/**/*.{test,spec}.{ts,tsx} : Write frontend tests with Vitest and React Testing Library

Applied to files:

  • archon-ui-main/src/features/projects/tasks/services/tests/taskService.test.ts
📚 Learning: 2025-08-28T12:56:47.840Z
Learnt from: Wirasm
PR: coleam00/Archon#514
File: archon-ui-main/src/pages/ProjectPage.tsx:329-331
Timestamp: 2025-08-28T12:56:47.840Z
Learning: In the ProjectPage.tsx polling refactor, temporary project creation logic with progress cards was removed in favor of simpler modal loading states, as the complexity of managing in-flight temporary projects wasn't justified when HTTP polling would show new projects within seconds anyway.

Applied to files:

  • archon-ui-main/src/features/projects/components/ProjectCard.tsx
📚 Learning: 2025-09-13T15:53:40.741Z
Learnt from: CR
PR: coleam00/Archon#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-13T15:53:40.741Z
Learning: Applies to archon-ui-main/src/**/*.{ts,tsx} : Service method names: get[Resource]sByProject(projectId), get[Resource](id), create/update/delete patterns

Applied to files:

  • archon-ui-main/src/features/projects/services/projectService.ts
📚 Learning: 2025-09-12T13:47:04.535Z
Learnt from: CR
PR: coleam00/Archon#0
File: AGENTS.md:0-0
Timestamp: 2025-09-12T13:47:04.535Z
Learning: Applies to archon-ui-main/src/features/**/services/**/*.ts : Frontend service method naming: get[Resource]sByProject(projectId), get[Resource](id), create[Resource](data), update[Resource](id, updates), delete[Resource](id)

Applied to files:

  • archon-ui-main/src/features/projects/services/projectService.ts
📚 Learning: 2025-09-12T13:47:04.535Z
Learnt from: CR
PR: coleam00/Archon#0
File: AGENTS.md:0-0
Timestamp: 2025-09-12T13:47:04.535Z
Learning: Applies to archon-ui-main/src/**/*.{ts,tsx} : Use HTTP polling with ETag caching; do not introduce WebSocket-based updates in the frontend

Applied to files:

  • archon-ui-main/src/features/shared/apiWithEtag.ts
📚 Learning: 2025-09-04T16:30:05.227Z
Learnt from: stevepresley
PR: coleam00/Archon#573
File: archon-ui-main/src/config/api.ts:15-25
Timestamp: 2025-09-04T16:30:05.227Z
Learning: Archon UI API config: Prefer lazy getters getApiFullUrl() and getWsUrl() over module-load constants to avoid SSR/test crashes. Avoid CommonJS exports patterns (Object.defineProperty(exports,…)) in ESM. Add typeof window guards with VITE_API_URL fallback inside getApiUrl()/getWebSocketUrl() when SSR safety is required.

Applied to files:

  • archon-ui-main/src/features/shared/apiWithEtag.ts
🧬 Code graph analysis (4)
archon-ui-main/src/features/projects/tasks/services/tests/taskService.test.ts (1)
archon-ui-main/src/features/shared/apiWithEtag.ts (1)
  • callAPIWithETag (35-107)
archon-ui-main/src/features/knowledge/services/knowledgeService.ts (3)
archon-ui-main/src/features/shared/errors.ts (1)
  • APIServiceError (9-18)
archon-ui-main/src/features/shared/apiWithEtag.ts (1)
  • callAPIWithETag (35-107)
archon-ui-main/src/features/knowledge/types/knowledge.ts (1)
  • SearchResultsResponse (182-187)
archon-ui-main/src/features/shared/apiWithEtag.ts (2)
archon-ui-main/src/config/api.ts (1)
  • API_BASE_URL (47-47)
archon-ui-main/src/features/shared/errors.ts (1)
  • APIServiceError (9-18)
archon-ui-main/src/features/projects/shared/api.ts (1)
archon-ui-main/src/features/shared/errors.ts (1)
  • APIServiceError (9-18)
🔇 Additional comments (3)
archon-ui-main/src/features/projects/components/ProjectCard.tsx (1)

261-268: Good: stop propagation to prevent accidental card selection.

Wrapping onPin/onDelete with e.stopPropagation() avoids unintended parent onClick. Solid UX fix.

archon-ui-main/src/features/projects/services/projectService.ts (1)

114-122: Ensure TanStack Query invalidation covers mutations (cache layer removed).

With invalidateETagCache removed, verify that mutation hooks invalidate/refetch the relevant query keys (listProjects, getProject) after create/update to keep UI in sync.

Also applies to: 123-139

archon-ui-main/src/features/shared/apiWithEtag.ts (1)

13-26: URL base: prefer single source of truth.

If API_BASE_URL may change, ensure config/api.ts is the only source. This is fine now; just confirm no other modules redefine base URLs differently.

…proach

- 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.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
archon-ui-main/src/features/knowledge/utils/providerErrorHandler.ts (1)

16-41: Fix unsafe unknown access to satisfy TS strict mode and keep messages.

Directly reading properties from unknown (e.g., error.statusCode, error.message) violates strict mode and can break builds. Also, casting unknown to ProviderError without a fallback loses the message when error isn’t an Error.

Apply a safe narrow + fallback:

 export function parseProviderError(error: unknown): ProviderError {
-  const providerError = error as ProviderError;
-
-  // Check if this is a structured provider error from backend
-  if (error && typeof error === "object") {
-    if (error.statusCode || error.status) {
-      providerError.statusCode = error.statusCode || error.status;
-    }
-
-    // Parse backend error structure
-    if (error.message && error.message.includes("detail")) {
-      try {
-        const parsed = JSON.parse(error.message);
-        if (parsed.detail && parsed.detail.error_type) {
-          providerError.isProviderError = true;
-          providerError.provider = parsed.detail.provider || "LLM";
-          providerError.errorType = parsed.detail.error_type;
-          providerError.message = parsed.detail.message || error.message;
-        }
-      } catch {
-        // If parsing fails, use message as-is
-      }
-    }
-  }
-
-  return providerError;
+  // Preserve message for non-Error inputs
+  const base: ProviderError =
+    error instanceof Error
+      ? (error as ProviderError)
+      : (new Error(typeof error === "string" ? error : JSON.stringify(error)) as ProviderError);
+
+  if (error && typeof error === "object") {
+    const anyErr = error as Record<string, unknown>;
+
+    // Status code
+    if (typeof anyErr.statusCode === "number") base.statusCode = anyErr.statusCode as number;
+    else if (typeof anyErr.status === "number") base.statusCode = anyErr.status as number;
+
+    // Structured provider error in stringified message
+    const msg = typeof anyErr.message === "string" ? (anyErr.message as string) : base.message;
+    if (typeof msg === "string" && msg.includes("detail")) {
+      try {
+        const parsed = JSON.parse(msg);
+        const detail = parsed?.detail;
+        if (detail?.error_type) {
+          base.isProviderError = true;
+          base.provider = detail.provider ?? "LLM";
+          base.errorType = detail.error_type;
+          base.message = detail.message ?? base.message;
+        }
+      } catch {
+        // ignore parse errors
+      }
+    }
+  }
+  return base;
 }
 
 export function getProviderErrorMessage(error: unknown): string {
   const parsed = parseProviderError(error);
 
   if (parsed.isProviderError) {
     const provider = parsed.provider || "LLM";
 
     switch (parsed.errorType) {
       case "authentication_failed":
         return `Please verify your ${provider} API key in Settings.`;
       case "quota_exhausted":
         return `${provider} quota exhausted. Please check your billing settings.`;
       case "rate_limit":
         return `${provider} rate limit exceeded. Please wait and try again.`;
       default:
         return `${provider} API error. Please check your configuration.`;
     }
   }
 
   // Handle status codes for non-structured errors
-  if (parsed.statusCode === 401) {
+  if (parsed.statusCode === 401) {
     return "Please verify your API key in Settings.";
   }
+  if (parsed.statusCode === 429) {
+    return "Rate limit exceeded. Please wait and try again.";
+  }
 
   return parsed.message || "An error occurred.";
 }

Also applies to: 47-71

🧹 Nitpick comments (20)
archon-ui-main/src/features/knowledge/components/KnowledgeTypeSelector.tsx (2)

101-109: Use accent‑aware shadow instead of hardcoded cyan.

The selected card shadow uses a fixed cyan glow and won’t match the Business (pink) theme. Suggest deriving the glow from the card’s accent, similar to the top bar/smear.

Example tweak (conceptual; pick a consistent approach):

  • add shadow token to each type (e.g., shadow-[0_0_20px_rgba(236,72,153,0.15)] for pink)
  • or replace shadow with a ring derived from type.border.selected.

104-107: Minor: avoid allocating empty objects for motion states.

Using {} when disabled still creates a new object each render. Prefer undefined to skip the prop entirely.

- whileHover={!disabled ? { scale: 1.02 } : {}}
- whileTap={!disabled ? { scale: 0.98 } : {}}
+ whileHover={disabled ? undefined : { scale: 1.02 }}
+ whileTap={disabled ? undefined : { scale: 0.98 }}
archon-ui-main/src/features/knowledge/components/TagInput.tsx (1)

29-35: Case-insensitive dedupe to prevent visually duplicate tags (“Tag” vs “tag”).

Current checks are case-sensitive. Consider normalizing when checking/inserting to avoid duplicates that differ only by case.

- if (trimmedTag && !tags.includes(trimmedTag) && tags.length < maxTags) {
+ const hasTag = tags.some((t) => t.localeCompare(trimmedTag, undefined, { sensitivity: "accent" }) === 0);
+ if (trimmedTag && !hasTag && tags.length < maxTags) {
     onTagsChange([...tags, trimmedTag]);
     setInputValue("");
   }

Also normalize pasted candidates:

- const newCandidates = value
-   .split(",")
-   .map((tag) => tag.trim())
-   .filter(Boolean);
+ const newCandidates = value
+   .split(",")
+   .map((tag) => tag.trim())
+   .filter(Boolean);
+ // Dedupe in a case-insensitive manner
+ const seen = new Set<string>();
+ const deduped = newCandidates.filter((t) => {
+   const key = t.toLocaleLowerCase();
+   if (seen.has(key)) return false;
+   seen.add(key);
+   return true;
+ });
- const combinedTags = new Set([...tags, ...newCandidates]);
+ const combinedTags = new Set([...tags, ...deduped]);

Also applies to: 51-67

archon-ui-main/src/features/projects/shared/api.ts (1)

6-17: Handle invalid/future dates and singular units in relative time.

new Date(dateString) can be invalid; negatives yield “-3 minutes ago”; and pluralization is off.

 export function formatRelativeTime(dateString: string): string {
-  const date = new Date(dateString);
-  const now = new Date();
-  const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
-
-  if (diffInSeconds < 60) return "just now";
-  if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} minutes ago`;
-  if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hours ago`;
-  if (diffInSeconds < 604800) return `${Math.floor(diffInSeconds / 86400)} days ago`;
-
-  return `${Math.floor(diffInSeconds / 604800)} weeks ago`;
+  const date = new Date(dateString);
+  if (Number.isNaN(date.getTime())) return "unknown";
+  const diffSecRaw = Math.floor((Date.now() - date.getTime()) / 1000);
+  const diffSec = Math.max(0, diffSecRaw); // clamp future to "just now"
+  const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: "auto" });
+  if (diffSec < 60) return "just now";
+  const mins = Math.floor(diffSec / 60);
+  if (mins < 60) return rtf.format(-mins, "minute");
+  const hours = Math.floor(mins / 60);
+  if (hours < 24) return rtf.format(-hours, "hour");
+  const days = Math.floor(hours / 24);
+  if (days < 7) return rtf.format(-days, "day");
+  const weeks = Math.floor(days / 7);
+  return rtf.format(-weeks, "week");
 }
archon-ui-main/src/features/knowledge/components/KnowledgeCard.tsx (2)

233-237: Avoid any for description; extend type or use optional chaining.

(item as any).summary bypasses type safety. Prefer adding summary?: string to KnowledgeItem or guarding at use-site.

-              description={(item as any).summary}
+              description={"summary" in item ? (item as unknown as { summary?: string }).summary : undefined}

Or, update KnowledgeItem to include summary?: string and use item.summary.


300-339: Tooltips/pills LGTM; minor micro‑nit on cursor affordance.

When codeExamplesCount === 0, you already drop the handler; consider also forcing cursor-default for clearer affordance.

- className={cn("transition-transform", onViewCodeExamples && "cursor-pointer hover:scale-105")}
+ className={cn(
+   "transition-transform",
+   onViewCodeExamples ? "cursor-pointer hover:scale-105" : "cursor-default"
+ )}
archon-ui-main/src/features/knowledge/components/KnowledgeCardType.tsx (1)

105-123: Make the non-editing control keyboard-accessible.

The clickable div isn’t focusable and doesn’t react to Enter/Space. Add role, tabIndex, and key handling.

-  return (
-    <SimpleTooltip
+  return (
+    <SimpleTooltip
       content={`${isTechnical ? "Technical documentation" : "Business/general content"} - Click to change`}
     >
-      <div
+      <div
         className={cn(
           "flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-medium cursor-pointer",
           "hover:ring-1 hover:ring-cyan-400/50 transition-all",
           isTechnical
             ? "bg-blue-100 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400"
             : "bg-pink-100 text-pink-700 dark:bg-pink-500/10 dark:text-pink-400",
           updateMutation.isPending && "opacity-50 cursor-not-allowed",
         )}
-        onClick={handleClick}
+        role="button"
+        tabIndex={0}
+        onClick={handleClick}
+        onKeyDown={(e) => {
+          if (e.key === "Enter" || e.key === " ") {
+            e.preventDefault();
+            handleClick(e as unknown as React.MouseEvent);
+          }
+        }}
       >
         {getTypeIcon()}
         <span>{getTypeLabel()}</span>
       </div>
     </SimpleTooltip>
   );
archon-ui-main/src/features/shared/apiWithEtag.ts (7)

57-66: Recommended: Combine caller signal with default timeout using AbortSignal.any (when available).

Currently, providing a custom signal disables the 20s timeout. Merge both so either can abort first; fallback when .any is unavailable.

Apply this diff:

-    const response = await fetch(fullUrl, {
-      ...options,
-      headers,
-      signal: options.signal ?? AbortSignal.timeout(20000), // 20 second timeout (was 10s)
-    });
+    const defaultTimeout = AbortSignal.timeout(20000);
+    const combinedSignal =
+      options.signal != null
+        ? (typeof (AbortSignal as any).any === "function"
+            ? (AbortSignal as any).any([options.signal, defaultTimeout])
+            : options.signal)
+        : defaultTimeout;
+    const response = await fetch(fullUrl, {
+      ...options,
+      headers,
+      signal: combinedSignal,
+    });

21-34: Support absolute URLs and ensure leading slash for relative paths.

If a caller passes an absolute URL, the current code prepends API_BASE_URL, yielding an invalid URL. Also, relative paths without a leading slash would concatenate incorrectly.

Apply this diff:

-function buildFullUrl(cleanEndpoint: string): string {
-  let fullUrl = `${API_BASE_URL}${cleanEndpoint}`;
+function buildFullUrl(cleanEndpoint: string): string {
+  // Pass through absolute URLs
+  if (/^https?:\/\//i.test(cleanEndpoint)) {
+    return cleanEndpoint;
+  }
+  // Ensure leading slash for relative paths
+  const path = cleanEndpoint.startsWith("/") ? cleanEndpoint : `/${cleanEndpoint}`;
+  let fullUrl = `${API_BASE_URL}${path}`;

98-102: Guard against non-object JSON before accessing result.error.

If the server returns a primitive/array, result.error can throw. Guard the shape.

Apply this diff:

-    if (result.error) {
-      throw new APIServiceError(result.error, "API_ERROR", response.status);
-    }
+    if (result && typeof result === "object" && "error" in result && (result as any).error) {
+      throw new APIServiceError((result as any).error as string, "API_ERROR", response.status);
+    }

5-12: Doc accuracy: clarify 304 handling under the Fetch spec.

The gist is right, but it’s fetch (not “the browser” generically) that returns the cached stored response to JS on revalidation when the network replies 304; the 304 isn’t exposed. Consider tightening the wording and citing the Fetch revalidation step for future readers. (whatpr.org)

Proposed comment tweak:

  • “When the server returns 304, fetch returns the cached stored response to JS (typically 200) and updates cache headers in the background. Configure freshness via TanStack Query’s staleTime.”

90-93: API shape: returning undefined as T for 204 can be surprising.

Either type the function as Promise<T | undefined> or document clearly that DELETE/204 resolve to undefined.


46-49: Endpoint cleaning: ensure consistency for bare paths.

If callers pass "health" (no leading slash), this will produce /apihealth. Consider normalizing with a leading slash before buildFullUrl.

Example:

const cleaned = endpoint.startsWith("/api") ? endpoint.substring(4) : endpoint;
const cleanEndpoint = cleaned.startsWith("/") ? cleaned : `/${cleaned}`;

51-51: Comment drift: mention we don’t add If-None-Match.

The nearby comment implies ETag header handling; after the refactor, we intentionally don’t add it.

archon-ui-main/src/features/shared/apiWithEtag.test.ts (6)

94-101: Make timeout/network error assertions resilient to platform messages.

Relying on exact message text is brittle across runtimes (“AbortError” vs “TimeoutError”). Assert on shape instead.

Apply this diff:

-      await expect(callAPIWithETag("/test-endpoint")).rejects.toThrowError(
-        new APIServiceError("Failed to call API /test-endpoint: Network error", "NETWORK_ERROR", 500),
-      );
+      await expect(callAPIWithETag("/test-endpoint")).rejects.toMatchObject({
+        name: "APIServiceError",
+        code: "NETWORK_ERROR",
+        statusCode: 500,
+        message: expect.stringContaining("Failed to call API /test-endpoint"),
+      });

139-147: Same as above for timeout: avoid brittle message matching.

Timeouts may surface as TimeoutError/AbortError with different messages. Validate error shape.

Apply this diff:

-      await expect(callAPIWithETag("/test-endpoint")).rejects.toThrowError(
-        new APIServiceError("Failed to call API /test-endpoint: Request timeout", "NETWORK_ERROR", 500),
-      );
+      await expect(callAPIWithETag("/test-endpoint")).rejects.toMatchObject({
+        name: "APIServiceError",
+        code: "NETWORK_ERROR",
+        statusCode: 500,
+      });

149-177: Add a test to cover Headers instance in options.headers.

Prevents regressions of header normalization (e.g., dropping Authorization when callers pass new Headers()).

Apply this diff (append after this block):

@@
     it("should pass custom headers correctly", async () => {
@@
     });
+
+    it("should accept a Headers instance in options.headers", async () => {
+      const mockResponse = {
+        ok: true,
+        status: 200,
+        json: () => Promise.resolve({ ok: true }),
+        headers: new Headers(),
+      };
+      global.fetch = vi.fn().mockResolvedValue(mockResponse);
+      const headers = new Headers({ Authorization: "Bearer h123", "Custom-Header": "v" });
+      await callAPIWithETag("/test-endpoint", { headers });
+      const [, options] = (global.fetch as any).mock.calls[0];
+      expect(options.headers.Authorization).toBe("Bearer h123");
+      expect(options.headers["Custom-Header"]).toBe("v");
+    });

40-63: Nit: Asserting on GET’s Content-Type header might be unnecessary.

Setting Content-Type: application/json on GETs isn’t required and can trigger CORS preflights across origins. Consider loosening this assertion if you adopt conditional Content-Type in the client.


1-3: Style nit: import grouping.

No action required; consistent with project style.


1-411: Optional: add one test for non-JSON success.

If any endpoint returns text (200), response.json() will throw. Either keep contract “JSON-only” and add a test to document it, or add tolerant parsing.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1547cf9 and 59521b1.

📒 Files selected for processing (17)
  • archon-ui-main/src/features/knowledge/components/AddKnowledgeDialog.tsx (9 hunks)
  • archon-ui-main/src/features/knowledge/components/KnowledgeCard.tsx (3 hunks)
  • archon-ui-main/src/features/knowledge/components/KnowledgeCardTags.tsx (3 hunks)
  • archon-ui-main/src/features/knowledge/components/KnowledgeCardTitle.tsx (5 hunks)
  • archon-ui-main/src/features/knowledge/components/KnowledgeCardType.tsx (5 hunks)
  • archon-ui-main/src/features/knowledge/components/KnowledgeTypeSelector.tsx (6 hunks)
  • archon-ui-main/src/features/knowledge/components/LevelSelector.tsx (4 hunks)
  • archon-ui-main/src/features/knowledge/components/TagInput.tsx (5 hunks)
  • archon-ui-main/src/features/knowledge/hooks/useKnowledgeQueries.ts (3 hunks)
  • archon-ui-main/src/features/knowledge/inspector/components/KnowledgeInspector.tsx (1 hunks)
  • archon-ui-main/src/features/knowledge/utils/providerErrorHandler.ts (3 hunks)
  • archon-ui-main/src/features/projects/shared/api.ts (1 hunks)
  • archon-ui-main/src/features/projects/tasks/components/TaskCard.tsx (1 hunks)
  • archon-ui-main/src/features/projects/tasks/services/tests/taskService.test.ts (1 hunks)
  • archon-ui-main/src/features/shared/apiWithEtag.test.ts (1 hunks)
  • archon-ui-main/src/features/shared/apiWithEtag.ts (1 hunks)
  • archon-ui-main/src/features/shared/errors.ts (1 hunks)
✅ Files skipped from review due to trivial changes (7)
  • archon-ui-main/src/features/knowledge/components/KnowledgeCardTags.tsx
  • archon-ui-main/src/features/knowledge/hooks/useKnowledgeQueries.ts
  • archon-ui-main/src/features/projects/tasks/components/TaskCard.tsx
  • archon-ui-main/src/features/knowledge/inspector/components/KnowledgeInspector.tsx
  • archon-ui-main/src/features/knowledge/components/AddKnowledgeDialog.tsx
  • archon-ui-main/src/features/knowledge/components/LevelSelector.tsx
  • archon-ui-main/src/features/knowledge/components/KnowledgeCardTitle.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • archon-ui-main/src/features/projects/tasks/services/tests/taskService.test.ts
  • archon-ui-main/src/features/shared/errors.ts
🧰 Additional context used
📓 Path-based instructions (4)
archon-ui-main/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

archon-ui-main/src/**/*.{ts,tsx}: Use TanStack Query for all data fetching; avoid prop drilling
TypeScript: strict mode with no implicit any in frontend code
State naming: is[Action]ing for loading flags, [resource]Error for errors, selected[Resource] for current selection
Use HTTP polling with ETag caching; do not introduce WebSocket-based updates in the frontend

archon-ui-main/src/**/*.{ts,tsx}: WebSocket event failures (if any) should be logged and not crash the client; continue serving others
Frontend data fetching must use TanStack Query (no prop drilling) with query key factories, smart polling, and optimistic updates with rollback
Use vertical slice architecture: place UI under src/features/[feature]/(components|hooks|services|types)
State naming: use is[Action]ing for loading, [resource]Error for errors, selected[Resource] for selections
Service method names: get[Resource]sByProject(projectId), getResource, create/update/delete patterns
Frontend TypeScript should be strict (no implicit any)

Files:

  • archon-ui-main/src/features/knowledge/components/KnowledgeCard.tsx
  • archon-ui-main/src/features/knowledge/components/TagInput.tsx
  • archon-ui-main/src/features/shared/apiWithEtag.ts
  • archon-ui-main/src/features/shared/apiWithEtag.test.ts
  • archon-ui-main/src/features/knowledge/components/KnowledgeTypeSelector.tsx
  • archon-ui-main/src/features/knowledge/components/KnowledgeCardType.tsx
  • archon-ui-main/src/features/projects/shared/api.ts
  • archon-ui-main/src/features/knowledge/utils/providerErrorHandler.ts
archon-ui-main/src/features/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

archon-ui-main/src/features/**/*.{ts,tsx}: Follow TanStack Query patterns: query-keys factory, smart polling via useSmartPolling, optimistic updates with rollback
Biome formatting in features: 120-character lines, double quotes, trailing commas

archon-ui-main/src/features/**/*.{ts,tsx}: Use Biome formatting/conventions in /src/features: 120-char lines, double quotes, trailing commas
Use useSmartPolling and polling intervals (1–2s active, 5–10s background) with smart pausing on tab inactivity
Expose progress via dedicated hooks (e.g., useCrawlProgressPolling, useProjectTasks) instead of ad-hoc timers
Do not use prop drilling for data fetching/state; rely on TanStack Query caches/selectors

Files:

  • archon-ui-main/src/features/knowledge/components/KnowledgeCard.tsx
  • archon-ui-main/src/features/knowledge/components/TagInput.tsx
  • archon-ui-main/src/features/shared/apiWithEtag.ts
  • archon-ui-main/src/features/shared/apiWithEtag.test.ts
  • archon-ui-main/src/features/knowledge/components/KnowledgeTypeSelector.tsx
  • archon-ui-main/src/features/knowledge/components/KnowledgeCardType.tsx
  • archon-ui-main/src/features/projects/shared/api.ts
  • archon-ui-main/src/features/knowledge/utils/providerErrorHandler.ts
**/*.{py,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Never return None/null to indicate failure; raise an exception with details instead

Files:

  • archon-ui-main/src/features/knowledge/components/KnowledgeCard.tsx
  • archon-ui-main/src/features/knowledge/components/TagInput.tsx
  • archon-ui-main/src/features/shared/apiWithEtag.ts
  • archon-ui-main/src/features/shared/apiWithEtag.test.ts
  • archon-ui-main/src/features/knowledge/components/KnowledgeTypeSelector.tsx
  • archon-ui-main/src/features/knowledge/components/KnowledgeCardType.tsx
  • archon-ui-main/src/features/projects/shared/api.ts
  • archon-ui-main/src/features/knowledge/utils/providerErrorHandler.ts
archon-ui-main/src/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Write frontend tests with Vitest and React Testing Library

Files:

  • archon-ui-main/src/features/shared/apiWithEtag.test.ts
🧠 Learnings (6)
📓 Common learnings
Learnt from: CR
PR: coleam00/Archon#0
File: AGENTS.md:0-0
Timestamp: 2025-09-12T13:47:04.535Z
Learning: Applies to archon-ui-main/src/**/*.{ts,tsx} : Use HTTP polling with ETag caching; do not introduce WebSocket-based updates in the frontend
Learnt from: Wirasm
PR: coleam00/Archon#514
File: archon-ui-main/src/services/crawlProgressService.ts:35-39
Timestamp: 2025-08-28T13:07:24.810Z
Learning: The crawlProgressService.ts in the Archon codebase should be deprecated in favor of the existing useCrawlProgressPolling hook from usePolling.ts, which already includes ETag support, 304 handling, tab visibility detection, and proper React lifecycle integration. This consolidation reduces code duplication and improves performance.
📚 Learning: 2025-09-13T15:53:40.741Z
Learnt from: CR
PR: coleam00/Archon#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-13T15:53:40.741Z
Learning: Applies to archon-ui-main/src/features/**/*.{ts,tsx} : Use Biome formatting/conventions in /src/features: 120-char lines, double quotes, trailing commas

Applied to files:

  • archon-ui-main/src/features/knowledge/components/KnowledgeCard.tsx
  • archon-ui-main/src/features/knowledge/components/TagInput.tsx
  • archon-ui-main/src/features/knowledge/utils/providerErrorHandler.ts
📚 Learning: 2025-09-12T13:47:04.534Z
Learnt from: CR
PR: coleam00/Archon#0
File: AGENTS.md:0-0
Timestamp: 2025-09-12T13:47:04.534Z
Learning: Applies to archon-ui-main/src/features/**/*.{ts,tsx} : Biome formatting in features: 120-character lines, double quotes, trailing commas

Applied to files:

  • archon-ui-main/src/features/knowledge/components/KnowledgeCard.tsx
  • archon-ui-main/src/features/knowledge/utils/providerErrorHandler.ts
📚 Learning: 2025-09-12T13:47:04.535Z
Learnt from: CR
PR: coleam00/Archon#0
File: AGENTS.md:0-0
Timestamp: 2025-09-12T13:47:04.535Z
Learning: Applies to archon-ui-main/src/**/*.{ts,tsx} : Use HTTP polling with ETag caching; do not introduce WebSocket-based updates in the frontend

Applied to files:

  • archon-ui-main/src/features/shared/apiWithEtag.ts
  • archon-ui-main/src/features/shared/apiWithEtag.test.ts
📚 Learning: 2025-09-04T16:30:05.227Z
Learnt from: stevepresley
PR: coleam00/Archon#573
File: archon-ui-main/src/config/api.ts:15-25
Timestamp: 2025-09-04T16:30:05.227Z
Learning: Archon UI API config: Prefer lazy getters getApiFullUrl() and getWsUrl() over module-load constants to avoid SSR/test crashes. Avoid CommonJS exports patterns (Object.defineProperty(exports,…)) in ESM. Add typeof window guards with VITE_API_URL fallback inside getApiUrl()/getWebSocketUrl() when SSR safety is required.

Applied to files:

  • archon-ui-main/src/features/shared/apiWithEtag.ts
📚 Learning: 2025-09-13T15:53:40.741Z
Learnt from: CR
PR: coleam00/Archon#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-13T15:53:40.741Z
Learning: Applies to archon-ui-main/src/**/*.{test,spec}.{ts,tsx} : Write frontend tests with Vitest and React Testing Library

Applied to files:

  • archon-ui-main/src/features/shared/apiWithEtag.test.ts
🧬 Code graph analysis (5)
archon-ui-main/src/features/knowledge/components/KnowledgeCard.tsx (3)
archon-ui-main/src/features/knowledge/components/KnowledgeCardType.tsx (1)
  • KnowledgeCardType (18-125)
archon-ui-main/src/features/ui/primitives/tooltip.tsx (1)
  • SimpleTooltip (62-77)
archon-ui-main/src/features/ui/primitives/styles.ts (1)
  • cn (139-141)
archon-ui-main/src/features/shared/apiWithEtag.ts (2)
archon-ui-main/src/config/api.ts (1)
  • API_BASE_URL (47-47)
archon-ui-main/src/features/shared/errors.ts (1)
  • APIServiceError (19-28)
archon-ui-main/src/features/shared/apiWithEtag.test.ts (2)
archon-ui-main/src/features/shared/apiWithEtag.ts (1)
  • callAPIWithETag (43-115)
archon-ui-main/src/features/shared/errors.ts (1)
  • APIServiceError (19-28)
archon-ui-main/src/features/knowledge/components/KnowledgeTypeSelector.tsx (1)
archon-ui-main/src/features/ui/primitives/styles.ts (1)
  • cn (139-141)
archon-ui-main/src/features/knowledge/components/KnowledgeCardType.tsx (2)
archon-ui-main/src/features/ui/primitives/tooltip.tsx (1)
  • SimpleTooltip (62-77)
archon-ui-main/src/features/ui/primitives/styles.ts (1)
  • cn (139-141)
🔇 Additional comments (7)
archon-ui-main/src/features/knowledge/components/KnowledgeTypeSelector.tsx (1)

122-129: Selection indicator looks good.

Consistent with accent color and positioned accessibly off the content. No issues.

archon-ui-main/src/features/shared/apiWithEtag.test.ts (5)

180-232: Note on 304: test description matches Fetch behavior.

Good to keep this as documentation—JS will not see 304 when revalidating via fetch; it receives the cached response (typically 200). Reference for future maintainers. (whatpr.org)


312-331: LGTM on HTTP error mapping.

Covers nested detail.error correctly and surfaces status as HTTP_ERROR.


9-38: Test scaffolding looks clean.

Globals are saved/restored; mocks reset between tests. Good hygiene.


203-231: Good documentation tests.

These clarify the caching contract between fetch and TanStack Query.


334-411: End-to-end “browser cache integration” scenario reads well.

Keeps expectations aligned with the simplified approach.

archon-ui-main/src/features/shared/apiWithEtag.ts (1)

43-115: No SSR usage detected — OK to rely on browser HTTP cache

callAPIWithETag (archon-ui-main/src/features/shared/apiWithEtag.ts) is only imported by client-side services/hooks and tests (examples: components/layout/hooks/useBackendHealth.ts; features/projects/services/; features/knowledge/services/). Repo scan found no getServerSideProps/getStaticProps/loader/handle/SSR occurrences.

Comment on lines +51 to +56
// Build headers with If-None-Match if we have an ETag
const headers: Record<string, string> = {
"Content-Type": "application/json",
...((options.headers as Record<string, string>) || {}),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Critical: Request headers handling drops valid headers (e.g., Authorization) when options.headers is a Headers instance or tuple.

RequestInit.headers is HeadersInit (supports Headers, [string, string][], or plain object). Casting to Record<string, string> loses headers in the first two cases, potentially stripping auth and breaking CORS. Fix by normalizing with new Headers() and then passing a plain object to fetch.

Apply this diff:

-    // Build headers with If-None-Match if we have an ETag
-    const headers: Record<string, string> = {
-      "Content-Type": "application/json",
-      ...((options.headers as Record<string, string>) || {}),
-    };
+    // Normalize headers (support Headers, string tuples, and plain objects).
+    // We do not add conditional headers; the browser handles ETag revalidation.
+    const mergedHeaders = new Headers(options.headers as HeadersInit);
+    if (!mergedHeaders.has("Content-Type")) {
+      mergedHeaders.set("Content-Type", "application/json");
+    }
+    const headers = Object.fromEntries(mergedHeaders.entries());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Build headers with If-None-Match if we have an ETag
const headers: Record<string, string> = {
"Content-Type": "application/json",
...((options.headers as Record<string, string>) || {}),
};
// Normalize headers (support Headers, string tuples, and plain objects).
// We do not add conditional headers; the browser handles ETag revalidation.
const mergedHeaders = new Headers(options.headers as HeadersInit);
if (!mergedHeaders.has("Content-Type")) {
mergedHeaders.set("Content-Type", "application/json");
}
const headers = Object.fromEntries(mergedHeaders.entries());
🤖 Prompt for AI Agents
In archon-ui-main/src/features/shared/apiWithEtag.ts around lines 51 to 56, the
current cast to Record<string,string> drops headers when options.headers is a
Headers instance or header tuples; replace the cast with normalization: create a
new Headers(options.headers || {}) and merge defaults into it (set Content-Type
if missing and set If-None-Match when we have an ETag), then convert the
normalized Headers back to a plain object via
Object.fromEntries(headers.entries()) and pass that to fetch so all header types
(Headers, tuples, or plain objects) are preserved.

- 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.
@Wirasm Wirasm merged commit b383c8c into main Sep 17, 2025
8 checks passed
@Wirasm Wirasm deleted the refactor/remove-etag-cache-layer branch September 17, 2025 13:45
@coderabbitai coderabbitai Bot mentioned this pull request Oct 10, 2025
12 tasks
leonj1 pushed a commit to leonj1/Archon that referenced this pull request Oct 13, 2025
…e of truth (coleam00#676)

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant