diff --git a/CHANGELOG.md b/CHANGELOG.md index bb03bb62..bf7f3cc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Added:** `isValidStackName`, `isValidRemoteUrl`, `isPathWithinBase` extracted to `backend/src/utils/validation.ts` for reuse and testability. ### Fixed +- **Fixed:** ESLint CI step now passes with zero errors — replaced all `any` type annotations with proper types or `unknown` casts, fixed unused catch variables, suppressed `react-refresh/only-export-components` on files that intentionally export both a component and a hook/constant (contexts, badge, button), added file-level `eslint-disable` on animate-ui third-party primitives, and added `eslint-disable-next-line react-hooks/set-state-in-effect` for the LogViewer state reset pattern. - **Fixed:** Four empty `catch {}` blocks in `EditorLayout` (mark-all-read, delete notification, clear-all notifications, image update fetch) now surface errors via `toast.error()` instead of silently swallowing them. - **Fixed:** `ErrorBoundary` component existed but was not connected — it now wraps the root `` in `main.tsx`, catching crashes in any context provider or route component. - **Fixed:** `AlertDialogContent` used `asChild` on `AlertDialogPrimitive.Content` with a `motion.div` wrapper — Radix internally injects a second child (`DescriptionWarning`), causing `React.Children.only` to throw and the ErrorBoundary to trigger whenever the delete-stack confirmation dialog opened. Replaced with CSS keyframe animations (`data-[state=open]:animate-in` / `data-[state=closed]:animate-out`) which don't require `asChild`. diff --git a/frontend/src/components/AppStoreView.tsx b/frontend/src/components/AppStoreView.tsx index fbd304b5..61401dbe 100644 --- a/frontend/src/components/AppStoreView.tsx +++ b/frontend/src/components/AppStoreView.tsx @@ -17,6 +17,11 @@ export interface TemplateEnv { default?: string; } +interface TemplateVolume { + container?: string; + bind?: string; +} + export interface Template { type?: number; title: string; @@ -24,7 +29,7 @@ export interface Template { logo?: string; image?: string; ports?: string[]; - volumes?: any[]; + volumes?: TemplateVolume[]; env?: TemplateEnv[]; categories?: string[]; github_url?: string; @@ -68,8 +73,8 @@ export function AppStoreView({ onDeploySuccess }: AppStoreViewProps) { if (!res.ok) throw new Error('Failed to fetch templates'); const data = await res.json(); setTemplates(data || []); - } catch (err: any) { - toast.error(err.message || "Failed to load App Shop"); + } catch (err) { + toast.error((err as Error).message || "Failed to load App Shop"); } finally { setLoading(false); } @@ -95,7 +100,7 @@ export function AppStoreView({ onDeploySuccess }: AppStoreViewProps) { // Initialize Volumes const initVols: Record = {}; - t.volumes?.forEach((v: any) => { + t.volumes?.forEach((v) => { if (v.container) { initVols[v.container] = v.bind || `./${v.container.split('/').filter(Boolean).pop() || 'data'}`; } @@ -146,7 +151,7 @@ export function AppStoreView({ onDeploySuccess }: AppStoreViewProps) { // Process Volumes if (modifiedTemplate.volumes) { - modifiedTemplate.volumes = modifiedTemplate.volumes.map((v: any) => { + modifiedTemplate.volumes = modifiedTemplate.volumes.map((v) => { if (v.container && volVars[v.container] !== undefined) { return { ...v, bind: volVars[v.container] }; } @@ -175,8 +180,8 @@ export function AppStoreView({ onDeploySuccess }: AppStoreViewProps) { toast.success(`${selectedTemplate?.title} deployed successfully!`); setIsSheetOpen(false); onDeploySuccess(stackName.trim()); - } catch (err: any) { - toast.error(err.message || 'Deployment failed'); + } catch (err) { + toast.error((err as Error).message || 'Deployment failed'); } finally { setIsDeploying(false); } @@ -399,14 +404,15 @@ export function AppStoreView({ onDeploySuccess }: AppStoreViewProps) { {selectedTemplate.volumes && selectedTemplate.volumes.length > 0 && (

Volumes (Host : Container)

- {selectedTemplate.volumes.map((v: any, idx: number) => { - if (!v.container) return null; + {selectedTemplate.volumes.map((v, idx: number) => { + const containerPath = v.container; + if (!containerPath) return null; return (
- + setVolVars(prev => ({ ...prev, [v.container]: e.target.value }))} + value={volVars[containerPath] !== undefined ? volVars[containerPath] : ''} + onChange={(e) => setVolVars(prev => ({ ...prev, [containerPath]: e.target.value }))} placeholder={`/path/to/host/dir`} />
diff --git a/frontend/src/components/BashExecModal.tsx b/frontend/src/components/BashExecModal.tsx index fa172c85..f72fcd27 100644 --- a/frontend/src/components/BashExecModal.tsx +++ b/frontend/src/components/BashExecModal.tsx @@ -5,6 +5,8 @@ import { Terminal } from '@xterm/xterm'; import { FitAddon } from '@xterm/addon-fit'; import '@xterm/xterm/css/xterm.css'; +type TerminalContainer = HTMLDivElement & { __resizeObserver?: ResizeObserver }; + interface BashExecModalProps { isOpen: boolean; onClose: () => void; @@ -191,14 +193,15 @@ export default function BashExecModal({ isOpen, onClose, containerId, containerN resizeObserver.observe(containerEl); // Store observer cleanup on the container element for later - (containerEl as any).__resizeObserver = resizeObserver; + (containerEl as TerminalContainer).__resizeObserver = resizeObserver; } return () => { // Clean up ResizeObserver - if (terminalRef.current && (terminalRef.current as any).__resizeObserver) { - (terminalRef.current as any).__resizeObserver.disconnect(); - delete (terminalRef.current as any).__resizeObserver; + const el = terminalRef.current as TerminalContainer | null; + if (el?.__resizeObserver) { + el.__resizeObserver.disconnect(); + delete el.__resizeObserver; } }; }, [isOpen, containerId, cleanup]); diff --git a/frontend/src/components/EditorLayout.tsx b/frontend/src/components/EditorLayout.tsx index 40406563..89838f72 100644 --- a/frontend/src/components/EditorLayout.tsx +++ b/frontend/src/components/EditorLayout.tsx @@ -492,7 +492,7 @@ export default function EditorLayout() { let currentRx = 0; let currentTx = 0; if (data.networks) { - Object.values(data.networks).forEach((net: any) => { + Object.values(data.networks as Record).forEach((net) => { currentRx += net.rx_bytes || 0; currentTx += net.tx_bytes || 0; }); @@ -713,9 +713,9 @@ export default function EditorLayout() { const conts = await containersRes.json(); setContainers(Array.isArray(conts) ? conts : []); await refreshStacks(true); - } catch (error: any) { + } catch (error) { console.error('Failed to deploy:', error); - toast.error(error.message || 'Failed to deploy stack'); + toast.error((error as Error).message || 'Failed to deploy stack'); } finally { setLoadingAction(null); } @@ -741,9 +741,9 @@ export default function EditorLayout() { const conts = await containersRes.json(); setContainers(Array.isArray(conts) ? conts : []); await refreshStacks(true); - } catch (error: any) { + } catch (error) { console.error('Failed to stop:', error); - toast.error(error.message || 'Failed to stop stack'); + toast.error((error as Error).message || 'Failed to stop stack'); } finally { setLoadingAction(null); } @@ -769,9 +769,9 @@ export default function EditorLayout() { const conts = await containersRes.json(); setContainers(Array.isArray(conts) ? conts : []); await refreshStacks(true); - } catch (error: any) { + } catch (error) { console.error('Failed to restart:', error); - toast.error(error.message || 'Failed to restart stack'); + toast.error((error as Error).message || 'Failed to restart stack'); } finally { setLoadingAction(null); } @@ -797,9 +797,9 @@ export default function EditorLayout() { const conts = await containersRes.json(); setContainers(Array.isArray(conts) ? conts : []); await refreshStacks(true); - } catch (error: any) { + } catch (error) { console.error('Failed to update:', error); - toast.error(error.message || 'Failed to update stack'); + toast.error((error as Error).message || 'Failed to update stack'); } finally { setLoadingAction(null); } @@ -830,9 +830,9 @@ export default function EditorLayout() { setIsEditing(false); } await refreshStacks(); - } catch (error: any) { + } catch (error) { console.error('Failed to delete stack:', error); - toast.error(error.message || 'Failed to delete stack'); + toast.error((error as Error).message || 'Failed to delete stack'); } finally { setLoadingAction(null); } @@ -860,9 +860,9 @@ export default function EditorLayout() { await refreshStacks(); // Auto-load the new stack in the editor pane await loadFile(stackName); - } catch (error: any) { + } catch (error) { console.error('Failed to create stack:', error); - toast.error(error.message || 'Failed to create stack'); + toast.error((error as Error).message || 'Failed to create stack'); } }; diff --git a/frontend/src/components/GlobalObservabilityView.tsx b/frontend/src/components/GlobalObservabilityView.tsx index 13f7fdc4..a4d16d62 100644 --- a/frontend/src/components/GlobalObservabilityView.tsx +++ b/frontend/src/components/GlobalObservabilityView.tsx @@ -95,7 +95,7 @@ export function GlobalObservabilityView() { const entry: LogEntry = JSON.parse(event.data); entry._id = ++logIdRef.current; bufferRef.current.push(entry); - } catch (e) { /* ignore parse errors */ } + } catch { /* ignore parse errors */ } }; eventSource.onerror = () => { @@ -243,7 +243,7 @@ export function GlobalObservabilityView() { - setStreamFilter(val as 'ALL' | 'STDOUT' | 'STDERR')}> diff --git a/frontend/src/components/HomeDashboard.tsx b/frontend/src/components/HomeDashboard.tsx index a71b45f6..0c12a009 100644 --- a/frontend/src/components/HomeDashboard.tsx +++ b/frontend/src/components/HomeDashboard.tsx @@ -18,6 +18,12 @@ interface Stats { inactive: number; } +interface MetricPoint { + timestamp: number; + cpu_percent: number; + memory_mb: number; +} + interface SystemStats { cpu: { usage: string; @@ -62,7 +68,7 @@ export default function HomeDashboard() { const [newStackName, setNewStackName] = useState(''); const [stats, setStats] = useState({ active: 0, exited: 0, total: 0, inactive: 0 }); const [systemStats, setSystemStats] = useState(null); - const [metrics, setMetrics] = useState([]); + const [metrics, setMetrics] = useState([]); // Fetch container stats - re-runs when active node changes so stale data is cleared immediately useEffect(() => { @@ -194,9 +200,9 @@ export default function HomeDashboard() { setConvertedYaml(''); setDockerRunInput(''); window.location.reload(); // Refresh to show new stack - } catch (error: any) { + } catch (error) { console.error('Failed to create stack:', error); - toast.error(error?.message || error?.error || 'Failed to create stack'); + toast.error((error as Error).message || 'Failed to create stack'); } }; diff --git a/frontend/src/components/LogViewer.tsx b/frontend/src/components/LogViewer.tsx index 13125f7e..fd5f43f0 100644 --- a/frontend/src/components/LogViewer.tsx +++ b/frontend/src/components/LogViewer.tsx @@ -24,6 +24,7 @@ export function LogViewer({ containerId, containerName, isOpen, onClose }: LogVi useEffect(() => { if (!isOpen || !containerId) return; + // eslint-disable-next-line react-hooks/set-state-in-effect setLogs([]); setIsConnected(false); diff --git a/frontend/src/components/NodeManager.tsx b/frontend/src/components/NodeManager.tsx index 5c1523f0..87e864a2 100644 --- a/frontend/src/components/NodeManager.tsx +++ b/frontend/src/components/NodeManager.tsx @@ -42,7 +42,7 @@ export function NodeManager() { const [editingNodeId, setEditingNodeId] = useState(null); const [deletingNode, setDeletingNode] = useState(null); const [testing, setTesting] = useState(null); - const [testResult, setTestResult] = useState<{ nodeId: number; info: any } | null>(null); + const [testResult, setTestResult] = useState<{ nodeId: number; info: { serverVersion?: string; os?: string; architecture?: string; containers?: number; images?: number; cpus?: number } } | null>(null); // Node token generation state const [generatedToken, setGeneratedToken] = useState(null); @@ -84,8 +84,8 @@ export function NodeManager() { } await refreshNodes(); - } catch (error: any) { - toast.error(error.message || 'Failed to create node'); + } catch (error) { + toast.error((error as Error).message || 'Failed to create node'); } }; @@ -105,8 +105,8 @@ export function NodeManager() { setEditingNodeId(null); setFormData(defaultFormData); await refreshNodes(); - } catch (error: any) { - toast.error(error.message || 'Failed to update node'); + } catch (error) { + toast.error((error as Error).message || 'Failed to update node'); } }; @@ -135,8 +135,8 @@ export function NodeManager() { setDeleteOpen(false); setDeletingNode(null); await refreshNodes(); - } catch (error: any) { - toast.error(error.message || 'Failed to delete node'); + } catch (error) { + toast.error((error as Error).message || 'Failed to delete node'); } }; @@ -153,8 +153,8 @@ export function NodeManager() { toast.error(`Failed to connect: ${result.error}`); } await refreshNodes(); - } catch (error: any) { - toast.error(error.message || 'Connection test failed'); + } catch (error) { + toast.error((error as Error).message || 'Connection test failed'); } finally { setTesting(null); } @@ -169,8 +169,8 @@ export function NodeManager() { const { token } = await res.json(); setGeneratedToken(token); toast.success('Node token generated'); - } catch (error: any) { - toast.error(error.message || 'Failed to generate token'); + } catch (error) { + toast.error((error as Error).message || 'Failed to generate token'); } finally { setGeneratingToken(false); } diff --git a/frontend/src/components/ResourcesView.tsx b/frontend/src/components/ResourcesView.tsx index f66d3ff8..86369c00 100644 --- a/frontend/src/components/ResourcesView.tsx +++ b/frontend/src/components/ResourcesView.tsx @@ -78,8 +78,8 @@ export default function ResourcesView() { setNetworks(await networksRes.json()); setOrphans(await orphansRes.json()); setSelectedOrphans([]); - } catch (error) { - console.error('Failed to fetch data', error); + } catch (err) { + console.error('Failed to fetch data', err); toast.error('Failed to load resources data'); } finally { setIsLoading(false); @@ -105,7 +105,7 @@ export default function ResourcesView() { toast.success(`Pruned ${confirmPruneType}.`); } await fetchAllData(); - } catch (error) { + } catch { toast.error(`Failed to prune ${confirmPruneType}`); } finally { setIsActioning(false); @@ -124,7 +124,7 @@ export default function ResourcesView() { if (!res.ok) throw new Error(); toast.success(`Deleted ${confirmDelete.type.slice(0, -1)}`); await fetchAllData(); - } catch (error) { + } catch { toast.error(`Failed to delete ${confirmDelete.type.slice(0, -1)}`); } finally { setIsActioning(false); @@ -154,7 +154,7 @@ export default function ResourcesView() { toast.success(`Purged ${selectedOrphans.length} ghost container(s)`); setBulkPurgeConfirm(false); await fetchAllData(); - } catch (error) { + } catch { toast.error('Failed to purge selected containers.'); } finally { setIsActioning(false); @@ -169,7 +169,7 @@ export default function ResourcesView() { const totalReclaimable = chartData.reduce((acc, curr) => acc + curr.value, 0); - const CustomTooltip = ({ active, payload }: any) => { + const CustomTooltip = ({ active, payload }: { active?: boolean; payload?: Array<{ name: string; value: number }> }) => { if (active && payload && payload.length) { return (
diff --git a/frontend/src/components/StackAlertSheet.tsx b/frontend/src/components/StackAlertSheet.tsx index 7835fdd2..0c1cb629 100644 --- a/frontend/src/components/StackAlertSheet.tsx +++ b/frontend/src/components/StackAlertSheet.tsx @@ -147,7 +147,7 @@ export function StackAlertSheet({ isOpen, onClose, stackName }: StackAlertSheetP const err = await res.json().catch(() => ({})); toast.error(err?.error || 'Failed to delete alert rule.'); } - } catch (e) { + } catch { toast.error('Network error. Could not reach the node.'); } finally { setIsLoading(false); diff --git a/frontend/src/components/animate-ui/primitives/animate/slot.tsx b/frontend/src/components/animate-ui/primitives/animate/slot.tsx index 3142cc40..35b9e0c9 100644 --- a/frontend/src/components/animate-ui/primitives/animate/slot.tsx +++ b/frontend/src/components/animate-ui/primitives/animate/slot.tsx @@ -1,3 +1,4 @@ +/* eslint-disable */ 'use client'; import * as React from 'react'; diff --git a/frontend/src/components/animate-ui/primitives/effects/highlight.tsx b/frontend/src/components/animate-ui/primitives/effects/highlight.tsx index 96ccc952..f7e7058d 100644 --- a/frontend/src/components/animate-ui/primitives/effects/highlight.tsx +++ b/frontend/src/components/animate-ui/primitives/effects/highlight.tsx @@ -1,3 +1,4 @@ +/* eslint-disable */ 'use client'; import * as React from 'react'; diff --git a/frontend/src/components/animate-ui/primitives/radix/dialog.tsx b/frontend/src/components/animate-ui/primitives/radix/dialog.tsx index 2efa06da..3730aead 100644 --- a/frontend/src/components/animate-ui/primitives/radix/dialog.tsx +++ b/frontend/src/components/animate-ui/primitives/radix/dialog.tsx @@ -1,3 +1,4 @@ +/* eslint-disable */ 'use client'; import * as React from 'react'; @@ -204,4 +205,4 @@ export { type DialogDescriptionProps, type DialogContextType, type DialogFlipDirection, -}; +}; \ No newline at end of file diff --git a/frontend/src/components/animate-ui/primitives/radix/dropdown-menu.tsx b/frontend/src/components/animate-ui/primitives/radix/dropdown-menu.tsx index 0ee15e5f..b828b906 100644 --- a/frontend/src/components/animate-ui/primitives/radix/dropdown-menu.tsx +++ b/frontend/src/components/animate-ui/primitives/radix/dropdown-menu.tsx @@ -1,3 +1,4 @@ +/* eslint-disable */ 'use client'; import * as React from 'react'; @@ -560,4 +561,4 @@ export { type DropdownMenuRadioGroupProps, type DropdownMenuContextType, type DropdownMenuSubContextType, -}; +}; \ No newline at end of file diff --git a/frontend/src/components/animate-ui/primitives/radix/hover-card.tsx b/frontend/src/components/animate-ui/primitives/radix/hover-card.tsx index 239843b5..b2ce5ca7 100644 --- a/frontend/src/components/animate-ui/primitives/radix/hover-card.tsx +++ b/frontend/src/components/animate-ui/primitives/radix/hover-card.tsx @@ -1,3 +1,4 @@ +/* eslint-disable */ 'use client'; import * as React from 'react'; @@ -204,4 +205,4 @@ export { type HoverCardContentProps, type HoverCardArrowProps, type HoverCardContextType, -}; +}; \ No newline at end of file diff --git a/frontend/src/components/animate-ui/primitives/radix/popover.tsx b/frontend/src/components/animate-ui/primitives/radix/popover.tsx index 99ecf2a3..c022ac4b 100644 --- a/frontend/src/components/animate-ui/primitives/radix/popover.tsx +++ b/frontend/src/components/animate-ui/primitives/radix/popover.tsx @@ -1,3 +1,4 @@ +/* eslint-disable */ 'use client'; import * as React from 'react'; @@ -159,4 +160,4 @@ export { type PopoverCloseProps, type PopoverArrowProps, type PopoverContextType, -}; +}; \ No newline at end of file diff --git a/frontend/src/components/animate-ui/primitives/radix/sheet.tsx b/frontend/src/components/animate-ui/primitives/radix/sheet.tsx index b789c2e2..59f7a0ae 100644 --- a/frontend/src/components/animate-ui/primitives/radix/sheet.tsx +++ b/frontend/src/components/animate-ui/primitives/radix/sheet.tsx @@ -1,3 +1,4 @@ +/* eslint-disable */ 'use client'; import * as React from 'react'; @@ -188,4 +189,4 @@ export { type SheetFooterProps, type SheetTitleProps, type SheetDescriptionProps, -}; +}; \ No newline at end of file diff --git a/frontend/src/components/animate-ui/primitives/radix/switch.tsx b/frontend/src/components/animate-ui/primitives/radix/switch.tsx index 800a28fd..c414bf99 100644 --- a/frontend/src/components/animate-ui/primitives/radix/switch.tsx +++ b/frontend/src/components/animate-ui/primitives/radix/switch.tsx @@ -1,3 +1,4 @@ +/* eslint-disable */ 'use client'; import * as React from 'react'; @@ -152,4 +153,4 @@ export { type SwitchIconProps, type SwitchIconPosition, type SwitchContextType, -}; +}; \ No newline at end of file diff --git a/frontend/src/components/animate-ui/primitives/radix/tooltip.tsx b/frontend/src/components/animate-ui/primitives/radix/tooltip.tsx index ee7413ba..39e21586 100644 --- a/frontend/src/components/animate-ui/primitives/radix/tooltip.tsx +++ b/frontend/src/components/animate-ui/primitives/radix/tooltip.tsx @@ -1,3 +1,4 @@ +/* eslint-disable */ 'use client'; import * as React from 'react'; @@ -217,4 +218,4 @@ export { type TooltipContentProps, type TooltipArrowProps, type TooltipContextType, -}; +}; \ No newline at end of file diff --git a/frontend/src/components/animate-ui/primitives/texts/sliding-number.tsx b/frontend/src/components/animate-ui/primitives/texts/sliding-number.tsx index 561173f5..b4750d2f 100644 --- a/frontend/src/components/animate-ui/primitives/texts/sliding-number.tsx +++ b/frontend/src/components/animate-ui/primitives/texts/sliding-number.tsx @@ -1,3 +1,4 @@ +/* eslint-disable */ 'use client'; import * as React from 'react'; @@ -350,4 +351,4 @@ function SlidingNumber({ ); } -export { SlidingNumber, type SlidingNumberProps }; +export { SlidingNumber, type SlidingNumberProps }; \ No newline at end of file diff --git a/frontend/src/components/ui/badge.tsx b/frontend/src/components/ui/badge.tsx index e87d62bf..644e9dd5 100644 --- a/frontend/src/components/ui/badge.tsx +++ b/frontend/src/components/ui/badge.tsx @@ -33,4 +33,5 @@ function Badge({ className, variant, ...props }: BadgeProps) { ) } +// eslint-disable-next-line react-refresh/only-export-components export { Badge, badgeVariants } diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx index 65d4fcd9..d8f7202c 100644 --- a/frontend/src/components/ui/button.tsx +++ b/frontend/src/components/ui/button.tsx @@ -54,4 +54,5 @@ const Button = React.forwardRef( ) Button.displayName = "Button" +// eslint-disable-next-line react-refresh/only-export-components export { Button, buttonVariants } diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx index 55b308ea..81691514 100644 --- a/frontend/src/context/AuthContext.tsx +++ b/frontend/src/context/AuthContext.tsx @@ -108,6 +108,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { ); } +// eslint-disable-next-line react-refresh/only-export-components export function useAuth() { const context = useContext(AuthContext); if (context === undefined) { diff --git a/frontend/src/context/NodeContext.tsx b/frontend/src/context/NodeContext.tsx index 2d368ca4..1a297861 100644 --- a/frontend/src/context/NodeContext.tsx +++ b/frontend/src/context/NodeContext.tsx @@ -98,6 +98,7 @@ export function NodeProvider({ children }: { children: React.ReactNode }) { ); } +// eslint-disable-next-line react-refresh/only-export-components export function useNodes() { const context = useContext(NodeContext); if (!context) { diff --git a/frontend/src/hooks/use-data-state.tsx b/frontend/src/hooks/use-data-state.tsx index 7166f3ef..59decef4 100644 --- a/frontend/src/hooks/use-data-state.tsx +++ b/frontend/src/hooks/use-data-state.tsx @@ -51,4 +51,5 @@ function useDataState( return [value, localRef]; } +// eslint-disable-next-line react-refresh/only-export-components export { useDataState, type DataStateValue }; diff --git a/frontend/src/hooks/use-is-in-view.tsx b/frontend/src/hooks/use-is-in-view.tsx index 3738c569..0648e4e5 100644 --- a/frontend/src/hooks/use-is-in-view.tsx +++ b/frontend/src/hooks/use-is-in-view.tsx @@ -22,4 +22,5 @@ function useIsInView( return { ref: localRef, isInView }; } +// eslint-disable-next-line react-refresh/only-export-components export { useIsInView, type UseIsInViewOptions }; diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 7fbd64b4..3282f165 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -39,7 +39,7 @@ export async function apiFetch( if (errData.error && errData.error.includes('not found') && errData.error.includes('Node')) { window.dispatchEvent(new Event('node-not-found')); } - } catch (e) { + } catch { // Ignore JSON parse errors, caller handles standard 404s } }