diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt index d974d0bd..bce6e475 100644 --- a/backend/requirements-dev.txt +++ b/backend/requirements-dev.txt @@ -4,7 +4,7 @@ # Testing pytest==7.4.3 pytest-asyncio==0.23.0 -httpx==0.26.0 +httpx>=0.27.0 pytest-cov==4.1.0 # Code quality diff --git a/frontend/src/components/admin/EmptyState.tsx b/frontend/src/components/admin/EmptyState.tsx index 4e58f7a8..1fd1c8a8 100644 --- a/frontend/src/components/admin/EmptyState.tsx +++ b/frontend/src/components/admin/EmptyState.tsx @@ -1,2 +1 @@ export { EmptyState } from '@/components/common/EmptyState' -export { default } from '@/components/common/EmptyState' diff --git a/frontend/src/components/analytics/FlowAnalyticsPanel.tsx b/frontend/src/components/analytics/FlowAnalyticsPanel.tsx index 7c5823ea..64afac14 100644 --- a/frontend/src/components/analytics/FlowAnalyticsPanel.tsx +++ b/frontend/src/components/analytics/FlowAnalyticsPanel.tsx @@ -37,7 +37,9 @@ export function FlowAnalyticsPanel({ treeId }: FlowAnalyticsPanelProps) { const [error, setError] = useState(false) useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect setLoading(true) + // eslint-disable-next-line react-hooks/set-state-in-effect setError(false) analyticsApi .getFlowAnalytics(treeId, period) diff --git a/frontend/src/components/common/Modal.tsx b/frontend/src/components/common/Modal.tsx index f798e4bb..58262b47 100644 --- a/frontend/src/components/common/Modal.tsx +++ b/frontend/src/components/common/Modal.tsx @@ -29,7 +29,9 @@ export function Modal({ isOpen, onClose, title, children, footer, size = 'md', a setIsFullScreen(next) try { localStorage.setItem('rf-editor-fullscreen', String(next)) - } catch {} + } catch { + // localStorage unavailable — ignore + } } // Close on Escape key diff --git a/frontend/src/components/layout/QuickLaunch.tsx b/frontend/src/components/layout/QuickLaunch.tsx index 76da29fe..11337487 100644 --- a/frontend/src/components/layout/QuickLaunch.tsx +++ b/frontend/src/components/layout/QuickLaunch.tsx @@ -40,6 +40,7 @@ export function QuickLaunch({ open, onClose }: QuickLaunchProps) { useEffect(() => { if (open) { + // eslint-disable-next-line react-hooks/set-state-in-effect setSelectedIndex(0) treesApi.list({ sort_by: 'updated_at' }) .then(trees => setRecentTrees(trees.slice(0, 4))) diff --git a/frontend/src/components/session/CSATModal.tsx b/frontend/src/components/session/CSATModal.tsx index b99bcc74..6611bb6a 100644 --- a/frontend/src/components/session/CSATModal.tsx +++ b/frontend/src/components/session/CSATModal.tsx @@ -3,6 +3,7 @@ import { Star } from 'lucide-react' import { Modal } from '@/components/common/Modal' import { analyticsApi } from '@/api' import { cn } from '@/lib/utils' +import { markSessionRated } from './csatUtils' interface CSATModalProps { isOpen: boolean @@ -10,26 +11,6 @@ interface CSATModalProps { sessionId: string } -const RATED_SESSIONS_KEY = 'rf-rated-sessions' - -function getRatedSessions(): string[] { - try { - return JSON.parse(localStorage.getItem(RATED_SESSIONS_KEY) || '[]') - } catch { - return [] - } -} - -function markSessionRated(sessionId: string) { - const rated = getRatedSessions() - rated.push(sessionId) - localStorage.setItem(RATED_SESSIONS_KEY, JSON.stringify(rated.slice(-100))) -} - -export function hasBeenRated(sessionId: string): boolean { - return getRatedSessions().includes(sessionId) -} - export function CSATModal({ isOpen, onClose, sessionId }: CSATModalProps) { const [rating, setRating] = useState(0) const [hoveredRating, setHoveredRating] = useState(0) diff --git a/frontend/src/components/session/csatUtils.ts b/frontend/src/components/session/csatUtils.ts new file mode 100644 index 00000000..8ffceea5 --- /dev/null +++ b/frontend/src/components/session/csatUtils.ts @@ -0,0 +1,19 @@ +const RATED_SESSIONS_KEY = 'rf-rated-sessions' + +export function getRatedSessions(): string[] { + try { + return JSON.parse(localStorage.getItem(RATED_SESSIONS_KEY) || '[]') + } catch { + return [] + } +} + +export function markSessionRated(sessionId: string) { + const rated = getRatedSessions() + rated.push(sessionId) + localStorage.setItem(RATED_SESSIONS_KEY, JSON.stringify(rated.slice(-100))) +} + +export function hasBeenRated(sessionId: string): boolean { + return getRatedSessions().includes(sessionId) +} diff --git a/frontend/src/components/tree-editor/NodeEditorPanel.tsx b/frontend/src/components/tree-editor/NodeEditorPanel.tsx index a6d76349..c1362b06 100644 --- a/frontend/src/components/tree-editor/NodeEditorPanel.tsx +++ b/frontend/src/components/tree-editor/NodeEditorPanel.tsx @@ -21,6 +21,7 @@ const TYPE_CONFIG: Record, { icon: typeof HelpCircle } function cloneWithoutChildren(node: TreeStructure): TreeStructure { + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { children: _children, ...rest } = node return structuredClone(rest) as TreeStructure } @@ -44,8 +45,11 @@ export function NodeEditorPanel({ nodeId, onClose, onSelectType }: NodeEditorPan // (e.g., answer stub → decision/action/solution via type picker) useEffect(() => { if (node) { + // eslint-disable-next-line react-hooks/set-state-in-effect setDraft(cloneWithoutChildren(node)) + // eslint-disable-next-line react-hooks/set-state-in-effect setIsDirty(false) + // eslint-disable-next-line react-hooks/set-state-in-effect setShowDeleteConfirm(false) } }, [nodeId, node?.type]) // eslint-disable-line react-hooks/exhaustive-deps @@ -57,6 +61,7 @@ export function NodeEditorPanel({ nodeId, onClose, onSelectType }: NodeEditorPan const handleSave = useCallback(() => { if (!draft || !node) return + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { children: _children, ...draftWithoutChildren } = draft updateNode(nodeId, draftWithoutChildren) diff --git a/frontend/src/components/tree-editor/useTreeLayout.ts b/frontend/src/components/tree-editor/useTreeLayout.ts index d0c146be..c600c70e 100644 --- a/frontend/src/components/tree-editor/useTreeLayout.ts +++ b/frontend/src/components/tree-editor/useTreeLayout.ts @@ -60,6 +60,7 @@ export function useTreeLayout(): UseTreeLayoutResult { if (!treeStructure) return { rawNodes: nodes, rawEdges: edges } + // eslint-disable-next-line @typescript-eslint/no-unused-vars function walk(node: TreeStructure, _parentId?: string | null) { const isCollapsed = collapsedNodeIds.has(node.id) const hasChildren = (node.children?.length ?? 0) > 0 diff --git a/frontend/src/hooks/useCachedQuota.ts b/frontend/src/hooks/useCachedQuota.ts index 32f3175f..28a41f00 100644 --- a/frontend/src/hooks/useCachedQuota.ts +++ b/frontend/src/hooks/useCachedQuota.ts @@ -16,7 +16,9 @@ export function useCachedQuota() { useEffect(() => { if (cachedResult && Date.now() - cachedResult.timestamp < CACHE_TTL_MS) { + // eslint-disable-next-line react-hooks/set-state-in-effect setAiEnabled(cachedResult.aiEnabled) + // eslint-disable-next-line react-hooks/set-state-in-effect setIsLoading(false) return } diff --git a/frontend/src/hooks/usePaginationParams.ts b/frontend/src/hooks/usePaginationParams.ts index 3a643bac..f4c02a67 100644 --- a/frontend/src/hooks/usePaginationParams.ts +++ b/frontend/src/hooks/usePaginationParams.ts @@ -1,5 +1,5 @@ import { useSearchParams } from 'react-router-dom' -import { useCallback, useMemo, useRef } from 'react' +import { useCallback, useMemo } from 'react' type PageSize = number | 'all' @@ -13,10 +13,6 @@ const DEFAULT_ALLOWED: PageSize[] = [10, 25, 50, 'all'] export function usePaginationParams(options: UsePaginationParamsOptions = {}) { const { defaultPageSize = 10, allowedPageSizes = DEFAULT_ALLOWED } = options - // Stabilize the array reference to avoid re-render loops - const allowedRef = useRef(allowedPageSizes) - allowedRef.current = allowedPageSizes - const [searchParams, setSearchParams] = useSearchParams() const page = useMemo(() => { @@ -27,11 +23,11 @@ export function usePaginationParams(options: UsePaginationParamsOptions = {}) { const pageSize = useMemo((): PageSize => { const raw = searchParams.get('size') - if (raw === 'all' && allowedRef.current.includes('all')) return 'all' + if (raw === 'all' && allowedPageSizes.includes('all')) return 'all' const n = raw ? parseInt(raw, 10) : defaultPageSize - if (Number.isFinite(n) && allowedRef.current.includes(n)) return n + if (Number.isFinite(n) && allowedPageSizes.includes(n)) return n return defaultPageSize - }, [searchParams, defaultPageSize]) + }, [searchParams, defaultPageSize, allowedPageSizes]) const setPage = useCallback( (newPage: number) => { diff --git a/frontend/src/pages/MyAnalyticsPage.tsx b/frontend/src/pages/MyAnalyticsPage.tsx index a7b7d88a..e5aa3564 100644 --- a/frontend/src/pages/MyAnalyticsPage.tsx +++ b/frontend/src/pages/MyAnalyticsPage.tsx @@ -35,6 +35,7 @@ export default function MyAnalyticsPage() { const [loading, setLoading] = useState(true) useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect setLoading(true) analyticsApi .getPersonalAnalytics(period) diff --git a/frontend/src/pages/ProceduralNavigationPage.tsx b/frontend/src/pages/ProceduralNavigationPage.tsx index 4d313ace..8f416d2a 100644 --- a/frontend/src/pages/ProceduralNavigationPage.tsx +++ b/frontend/src/pages/ProceduralNavigationPage.tsx @@ -14,7 +14,8 @@ import { Spinner } from '@/components/common/Spinner' import { cn } from '@/lib/utils' import { toast } from '@/lib/toast' import { StepFeedback } from '@/components/session/StepFeedback' -import { CSATModal, hasBeenRated } from '@/components/session/CSATModal' +import { CSATModal } from '@/components/session/CSATModal' +import { hasBeenRated } from '@/components/session/csatUtils' interface StepState { notes: string diff --git a/frontend/src/pages/TeamAnalyticsPage.tsx b/frontend/src/pages/TeamAnalyticsPage.tsx index f2a27342..950533aa 100644 --- a/frontend/src/pages/TeamAnalyticsPage.tsx +++ b/frontend/src/pages/TeamAnalyticsPage.tsx @@ -37,6 +37,7 @@ export default function TeamAnalyticsPage() { useEffect(() => { if (!isAccountOwner && !isSuperAdmin) return + // eslint-disable-next-line react-hooks/set-state-in-effect setLoading(true) analyticsApi .getTeamAnalytics(period) diff --git a/frontend/src/pages/TreeNavigationPage.tsx b/frontend/src/pages/TreeNavigationPage.tsx index 00f31bf8..848ae3af 100644 --- a/frontend/src/pages/TreeNavigationPage.tsx +++ b/frontend/src/pages/TreeNavigationPage.tsx @@ -15,7 +15,8 @@ import { Spinner } from '@/components/common/Spinner' import { toast } from '@/lib/toast' import { Modal } from '@/components/common/Modal' import { ShareSessionModal } from '@/components/session/ShareSessionModal' -import { CSATModal, hasBeenRated } from '@/components/session/CSATModal' +import { CSATModal } from '@/components/session/CSATModal' +import { hasBeenRated } from '@/components/session/csatUtils' import { StepFeedback } from '@/components/session/StepFeedback' import { buildSessionShareUrl, getLatestActiveShareForSession } from '@/lib/sessionShare'