+ {sections.map((s) => {
+ const isActive = s.key === active
+ const cls = [
+ 'section-row',
+ isActive ? 'active' : '',
+ s.disabled ? 'disabled' : '',
+ ]
+ .filter(Boolean)
+ .join(' ')
+
+ return (
+
onSelect(s.key)}
+ role={s.disabled ? undefined : 'button'}
+ tabIndex={s.disabled ? undefined : 0}
+ onKeyDown={
+ s.disabled
+ ? undefined
+ : (e) => {
+ if (e.key === 'Enter' || e.key === ' ') onSelect(s.key)
+ }
+ }
+ >
+ {s.label}
+ {s.tag != null && (
+ {s.tag}
+ )}
+
+ )
+ })}
+
+ )
+}
diff --git a/web/src/features/settings/useSavedSearches.test.ts b/web/src/features/settings/useSavedSearches.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cb87f77e20886bdd3f6b08270a40b930b400c207
--- /dev/null
+++ b/web/src/features/settings/useSavedSearches.test.ts
@@ -0,0 +1,94 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import { renderHook, act, waitFor } from '@testing-library/react'
+import React from 'react'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { useSavedSearches, useCreateSavedSearch } from './useSavedSearches'
+import type { SavedSearchDTO } from '../../api/adapters'
+
+// ── Mock apiFetch ─────────────────────────────────────────────────────────────
+
+const mockApiFetch = vi.fn()
+
+vi.mock('../../api/client', () => ({
+ apiFetch: (...args: unknown[]) => mockApiFetch(...args),
+ apiFetchVoid: vi.fn().mockResolvedValue(undefined),
+ AuthError: class AuthError extends Error {
+ name = 'AuthError'
+ constructor(message: string) { super(message) }
+ },
+ APIError: class APIError extends Error {
+ name = 'APIError'
+ status: number
+ code: string
+ constructor(message: string, status: number, code: string) {
+ super(message)
+ this.status = status
+ this.code = code
+ }
+ },
+}))
+
+// ── Test wrapper ──────────────────────────────────────────────────────────────
+
+function makeWrapper(queryClient: QueryClient) {
+ return function Wrapper({ children }: { children: React.ReactNode }) {
+ return React.createElement(QueryClientProvider, { client: queryClient }, children)
+ }
+}
+
+// ── Tests ─────────────────────────────────────────────────────────────────────
+
+describe('useSavedSearches + useCreateSavedSearch — cache invalidation', () => {
+ let queryClient: QueryClient
+
+ beforeEach(() => {
+ queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false },
+ mutations: { retry: false },
+ },
+ })
+ mockApiFetch.mockReset()
+ })
+
+ it('invalidates saved-searches after createSavedSearch succeeds', async () => {
+ const emptyResponse = { saved_searches: [] as SavedSearchDTO[] }
+ const filledResponse = {
+ saved_searches: [
+ { name: 'a', query: 'q', created_at: 0, updated_at: 0 } satisfies SavedSearchDTO,
+ ],
+ }
+ // POST returns the created row
+ const createdRow: SavedSearchDTO = { name: 'a', query: 'q', created_at: 0, updated_at: 0 }
+
+ // First call → initial list fetch (empty)
+ // Second call → re-fetch after invalidation (one row)
+ // Third call may come from the mutation POST
+ mockApiFetch
+ .mockResolvedValueOnce(emptyResponse) // initial GET
+ .mockResolvedValueOnce(createdRow) // POST create
+ .mockResolvedValueOnce(filledResponse) // re-fetch after invalidation
+
+ const wrapper = makeWrapper(queryClient)
+
+ const { result: listResult } = renderHook(() => useSavedSearches(), { wrapper })
+ const { result: mutResult } = renderHook(() => useCreateSavedSearch(), { wrapper })
+
+ // Wait for initial fetch to settle
+ await waitFor(() => expect(listResult.current.isSuccess).toBe(true))
+ expect(listResult.current.data).toEqual([])
+
+ // Call mutate
+ act(() => {
+ mutResult.current.mutate({ name: 'a', query: 'q' })
+ })
+
+ // Wait for mutation success + re-fetch
+ await waitFor(() => expect(mutResult.current.isSuccess).toBe(true))
+ await waitFor(() => {
+ expect(listResult.current.data?.length).toBe(1)
+ })
+
+ expect(listResult.current.data?.[0].name).toBe('a')
+ })
+})
diff --git a/web/src/features/settings/useSavedSearches.ts b/web/src/features/settings/useSavedSearches.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a1cc965bfb752861f5c349245f59c497c17e195a
--- /dev/null
+++ b/web/src/features/settings/useSavedSearches.ts
@@ -0,0 +1,79 @@
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
+import type { UseQueryResult, UseMutationResult } from '@tanstack/react-query'
+import { apiFetch, apiFetchVoid } from '../../api/client'
+import { adaptSavedSearch } from '../../api/adapters'
+import type { SavedSearch, SavedSearchDTO } from '../../api/adapters'
+
+interface SavedSearchesResponse {
+ saved_searches: SavedSearchDTO[]
+}
+
+export function useSavedSearches(): UseQueryResult