diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index d5bb56e..b316f3b 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -1,7 +1,7 @@ import js from '@eslint/js'; import tseslint from '@typescript-eslint/eslint-plugin'; import tsparser from '@typescript-eslint/parser'; - +import reactHooks from 'eslint-plugin-react-hooks'; export default [ { files: ['src/**/*.{ts,tsx}'], @@ -16,6 +16,7 @@ export default [ }, plugins: { '@typescript-eslint': tseslint, + 'react-hooks': reactHooks, }, rules: { // Error rules @@ -42,6 +43,8 @@ export default [ 'no-var': 'error', 'eqeqeq': ['error', 'always'], 'curly': ['error', 'multi-line'], + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', }, }, { diff --git a/frontend/package.json b/frontend/package.json index 943b80a..fef666d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,6 +28,7 @@ "@typescript-eslint/parser": "^8.30.0", "@vitejs/plugin-react": "^4.4.1", "eslint": "^9.24.0", + "eslint-plugin-react-hooks": "^5.0.0", "typescript": "^5.8.3", "vite": "^6.3.3" } diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 651fc3d..e5e7ccb 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -1,5 +1,4 @@ -import axios, { type AxiosError } from 'axios' -import type { InternalAxiosRequestConfig } from 'axios' +import axios, { type AxiosError, type InternalAxiosRequestConfig } from 'axios' import { useAuthStore } from '../store/authStore' import type { FleetStatus, diff --git a/frontend/src/hooks/useJobWebSocket.ts b/frontend/src/hooks/useJobWebSocket.ts index c11b7e1..2fff8e0 100644 --- a/frontend/src/hooks/useJobWebSocket.ts +++ b/frontend/src/hooks/useJobWebSocket.ts @@ -111,7 +111,7 @@ export function useJobWebSocket(options: JobWsOptions = {}): JobWsState { ws.onopen = () => { if (!mountedRef.current) { ws.close(); return } - console.debug('[JobWS] Connected') + console.warn('[JobWS] Connected') backoffRef.current = BACKOFF_INITIAL_MS // reset backoff on successful connect setConnected(true) } @@ -134,7 +134,7 @@ export function useJobWebSocket(options: JobWsOptions = {}): JobWsState { ws.onclose = () => { if (!mountedRef.current) return - console.debug('[JobWS] Disconnected — scheduling reconnect') + console.warn('[JobWS] Disconnected — scheduling reconnect') setConnected(false) wsRef.current = null scheduleReconnect() @@ -147,7 +147,7 @@ export function useJobWebSocket(options: JobWsOptions = {}): JobWsState { clearRetryTimer() const delay = backoffRef.current backoffRef.current = Math.min(delay * BACKOFF_FACTOR, BACKOFF_MAX_MS) - console.debug(`[JobWS] Reconnecting in ${delay} ms`) + console.warn(`[JobWS] Reconnecting in ${delay} ms`) retryTimerRef.current = setTimeout(() => { if (mountedRef.current) connect() }, delay) diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 38d3698..f4b7305 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -4,7 +4,9 @@ import { BrowserRouter } from 'react-router-dom' import App from './App' import './index.css' -ReactDOM.createRoot(document.getElementById('root')!).render( +const root = document.getElementById('root') +if (!root) throw new Error('Root element not found') +ReactDOM.createRoot(root).render( diff --git a/frontend/src/pages/HostDetailPage.tsx b/frontend/src/pages/HostDetailPage.tsx index fa20541..3c274d6 100644 --- a/frontend/src/pages/HostDetailPage.tsx +++ b/frontend/src/pages/HostDetailPage.tsx @@ -64,11 +64,11 @@ function scheduleDescription(w: MaintenanceWindow): string { case 'daily': return `Every day at ${time} for ${dur}` case 'weekly': { - const day = w.recurrence_day != null ? DAY_NAMES[w.recurrence_day] ?? `Day ${w.recurrence_day}` : '?' + const day = w.recurrence_day !== null ? DAY_NAMES[w.recurrence_day] ?? `Day ${w.recurrence_day}` : '?' return `Every ${day} at ${time} for ${dur}` } case 'monthly': { - const day = w.recurrence_day != null ? w.recurrence_day : '?' + const day = w.recurrence_day !== null ? w.recurrence_day : '?' return `Monthly on day ${day} at ${time} for ${dur}` } } diff --git a/frontend/src/pages/HostsPage.tsx b/frontend/src/pages/HostsPage.tsx index 85e0f22..c6558a3 100644 --- a/frontend/src/pages/HostsPage.tsx +++ b/frontend/src/pages/HostsPage.tsx @@ -6,8 +6,7 @@ import { } from '@mui/material' import { Add as AddIcon, Refresh as RefreshIcon, Delete as DeleteIcon } from '@mui/icons-material' import { useNavigate } from 'react-router-dom' -import { apiClient } from '../api/client' -import { hostsApi } from '../api/client' +import { apiClient, hostsApi } from '../api/client' import type { Host, HostHealthStatus } from '../types' const statusColor = (s: HostHealthStatus) => diff --git a/frontend/src/pages/MaintenanceWindowsPage.tsx b/frontend/src/pages/MaintenanceWindowsPage.tsx index 2f59041..ac4af3c 100644 --- a/frontend/src/pages/MaintenanceWindowsPage.tsx +++ b/frontend/src/pages/MaintenanceWindowsPage.tsx @@ -81,11 +81,11 @@ function scheduleDescription(w: MaintenanceWindow): string { case 'daily': return `Every day at ${time} for ${dur}` case 'weekly': { - const day = w.recurrence_day != null ? DAY_NAMES[w.recurrence_day] ?? `Day ${w.recurrence_day}` : '?' + const day = w.recurrence_day !== null ? DAY_NAMES[w.recurrence_day] ?? `Day ${w.recurrence_day}` : '?' return `Every ${day} at ${time} for ${dur}` } case 'monthly': { - const day = w.recurrence_day != null ? w.recurrence_day : '?' + const day = w.recurrence_day !== null ? w.recurrence_day : '?' return `Monthly on day ${day} at ${time} for ${dur}` } } diff --git a/frontend/src/pages/ReportsPage.tsx b/frontend/src/pages/ReportsPage.tsx index 5827b41..2e1f18b 100644 --- a/frontend/src/pages/ReportsPage.tsx +++ b/frontend/src/pages/ReportsPage.tsx @@ -126,7 +126,7 @@ export default function ReportsPage() { link.click() link.remove() window.URL.revokeObjectURL(url) - } catch (err: unknown) { + } catch (_err: unknown) { setError('Failed to generate report. Please try again.') } finally { setDownloading(false)