chore(frontend): fix lint errors and update biome schema

Fix exhaustive deps, non-null assertion, a11y roles, and error boundary
type. Migrate biome config to 2.4.4.
This commit is contained in:
CarterPerez-dev 2026-02-28 12:29:20 -05:00
parent cda830a4e5
commit 65e01d055c
21 changed files with 2699 additions and 90 deletions

View File

@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.8/schema.json",
"$schema": "https://biomejs.dev/schemas/2.4.4/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@
// index.ts
// ===================
export * from './useThreats'
export * from './useStats'
export * from './useModels'
export * from './useAlerts'
export * from './useModels'
export * from './useStats'
export * from './useThreats'

View File

@ -5,8 +5,8 @@
import { useEffect, useRef } from 'react'
import { create } from 'zustand'
import { type WebSocketAlert, WebSocketAlertSchema } from '@/api/types'
import { ALERTS, WS_ENDPOINTS } from '@/config'
import { WebSocketAlertSchema, type WebSocketAlert } from '@/api/types'
interface AlertState {
alerts: WebSocketAlert[]
@ -31,11 +31,9 @@ const useAlertStore = create<AlertState>()((set) => ({
setConnected: (connected) =>
set({ isConnected: connected, connectionError: null }),
setError: (error) =>
set({ isConnected: false, connectionError: error }),
setError: (error) => set({ isConnected: false, connectionError: error }),
clear: () =>
set({ alerts: [], isConnected: false, connectionError: null }),
clear: () => set({ alerts: [], isConnected: false, connectionError: null }),
}))
function getWsUrl(): string {
@ -82,7 +80,7 @@ export function useAlerts() {
function scheduleReconnect() {
const delay = Math.min(
ALERTS.RECONNECT_BASE_MS * 2 ** retryCountRef.current,
ALERTS.RECONNECT_MAX_MS,
ALERTS.RECONNECT_MAX_MS
)
retryCountRef.current += 1
retryTimerRef.current = setTimeout(connect, delay)

View File

@ -5,16 +5,16 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { QUERY_KEYS, API_ENDPOINTS } from '@/config'
import { apiClient, QUERY_STRATEGIES } from '@/core/api'
import type { ModelStatus, RetrainResponse } from '@/api/types'
import { API_ENDPOINTS, QUERY_KEYS } from '@/config'
import { apiClient, QUERY_STRATEGIES } from '@/core/api'
export function useModelStatus() {
return useQuery<ModelStatus>({
queryKey: QUERY_KEYS.MODELS.STATUS(),
queryFn: async () => {
const { data } = await apiClient.get<ModelStatus>(
API_ENDPOINTS.MODELS.STATUS,
API_ENDPOINTS.MODELS.STATUS
)
return data
},
@ -28,7 +28,7 @@ export function useRetrain() {
return useMutation<RetrainResponse>({
mutationFn: async () => {
const { data } = await apiClient.post<RetrainResponse>(
API_ENDPOINTS.MODELS.RETRAIN,
API_ENDPOINTS.MODELS.RETRAIN
)
return data
},

View File

@ -4,18 +4,17 @@
// ===================
import { useQuery } from '@tanstack/react-query'
import { QUERY_KEYS, API_ENDPOINTS } from '@/config'
import { apiClient, QUERY_STRATEGIES } from '@/core/api'
import type { StatsResponse } from '@/api/types'
import { API_ENDPOINTS, QUERY_KEYS } from '@/config'
import { apiClient, QUERY_STRATEGIES } from '@/core/api'
export function useStats(range = '24h') {
return useQuery<StatsResponse>({
queryKey: QUERY_KEYS.STATS.BY_RANGE(range),
queryFn: async () => {
const { data } = await apiClient.get<StatsResponse>(
API_ENDPOINTS.STATS,
{ params: { range } },
)
const { data } = await apiClient.get<StatsResponse>(API_ENDPOINTS.STATS, {
params: { range },
})
return data
},
...QUERY_STRATEGIES.frequent,

View File

@ -4,9 +4,9 @@
// ===================
import { useQuery } from '@tanstack/react-query'
import { QUERY_KEYS, API_ENDPOINTS, PAGINATION } from '@/config'
import type { ThreatEvent, ThreatList } from '@/api/types'
import { API_ENDPOINTS, PAGINATION, QUERY_KEYS } from '@/config'
import { apiClient, QUERY_STRATEGIES } from '@/core/api'
import type { ThreatList, ThreatEvent } from '@/api/types'
interface ThreatParams {
limit?: number
@ -29,7 +29,7 @@ export function useThreats(params: ThreatParams = {}) {
queryFn: async () => {
const { data } = await apiClient.get<ThreatList>(
API_ENDPOINTS.THREATS.LIST,
{ params: queryParams },
{ params: queryParams }
)
return data
},
@ -42,7 +42,7 @@ export function useThreat(id: string | null) {
queryKey: QUERY_KEYS.THREATS.BY_ID(id ?? ''),
queryFn: async () => {
const { data } = await apiClient.get<ThreatEvent>(
API_ENDPOINTS.THREATS.BY_ID(id!),
API_ENDPOINTS.THREATS.BY_ID(id as string)
)
return data
},

View File

@ -3,7 +3,7 @@
// index.ts
// ===================
export * from './threats.types'
export * from './stats.types'
export * from './models.types'
export * from './stats.types'
export * from './threats.types'
export * from './websocket.types'

View File

@ -5,8 +5,8 @@
import { useEffect, useRef } from 'react'
import type { WebSocketAlert } from '@/api/types'
import { SeverityBadge } from './severity-badge'
import styles from './alert-feed.module.scss'
import { SeverityBadge } from './severity-badge'
interface AlertFeedProps {
alerts: WebSocketAlert[]
@ -25,11 +25,13 @@ export function AlertFeed({
}: AlertFeedProps): React.ReactElement {
const listRef = useRef<HTMLDivElement>(null)
const alertCount = alerts.length
// biome-ignore lint/correctness/useExhaustiveDependencies: scroll on new alerts
useEffect(() => {
if (listRef.current) {
listRef.current.scrollTop = 0
}
}, [alerts.length])
}, [alertCount])
return (
<div className={styles.feed}>
@ -50,9 +52,7 @@ export function AlertFeed({
) : (
alerts.map((alert, i) => (
<div key={`${alert.timestamp}-${i}`} className={styles.row}>
<span className={styles.time}>
{formatTime(alert.timestamp)}
</span>
<span className={styles.time}>{formatTime(alert.timestamp)}</span>
<span className={styles.ip}>{alert.source_ip}</span>
<span className={styles.path}>{alert.request_path}</span>
<SeverityBadge

View File

@ -3,7 +3,7 @@
// index.tsx
// ===================
export { AlertFeed } from './alert-feed'
export { SeverityBadge } from './severity-badge'
export { StatCard } from './stat-card'
export { AlertFeed } from './alert-feed'
export { ThreatDetail } from './threat-detail'

View File

@ -24,8 +24,15 @@ export function ThreatDetail({
if (!threat) return null
return (
<div className={styles.overlay} onClick={onClose} onKeyDown={() => {}}>
// biome-ignore lint/a11y/noStaticElementInteractions: modal overlay dismiss
<div
role="presentation"
className={styles.overlay}
onClick={onClose}
onKeyDown={() => {}}
>
<div
role="dialog"
className={styles.panel}
onClick={(e) => e.stopPropagation()}
onKeyDown={() => {}}
@ -80,9 +87,7 @@ export function ThreatDetail({
</div>
<div className={styles.field}>
<span className={styles.fieldLabel}>Method</span>
<span className={styles.fieldValue}>
{threat.request_method}
</span>
<span className={styles.fieldValue}>{threat.request_method}</span>
</div>
<div className={styles.field}>
<span className={styles.fieldLabel}>Path</span>
@ -90,9 +95,7 @@ export function ThreatDetail({
</div>
<div className={styles.field}>
<span className={styles.fieldLabel}>Status</span>
<span className={styles.fieldValue}>
{threat.status_code}
</span>
<span className={styles.fieldValue}>{threat.status_code}</span>
</div>
<div className={styles.field}>
<span className={styles.fieldLabel}>Response Size</span>
@ -131,16 +134,12 @@ export function ThreatDetail({
<div className={styles.grid}>
<div className={styles.field}>
<span className={styles.fieldLabel}>Country</span>
<span className={styles.fieldValue}>
{threat.geo.country}
</span>
<span className={styles.fieldValue}>{threat.geo.country}</span>
</div>
{threat.geo.city && (
<div className={styles.field}>
<span className={styles.fieldLabel}>City</span>
<span className={styles.fieldValue}>
{threat.geo.city}
</span>
<span className={styles.fieldValue}>{threat.geo.city}</span>
</div>
)}
</div>

View File

@ -26,13 +26,11 @@ export const QUERY_KEYS = {
ALL: ['threats'] as const,
LIST: (params: Record<string, unknown>) =>
[...QUERY_KEYS.THREATS.ALL, 'list', params] as const,
BY_ID: (id: string) =>
[...QUERY_KEYS.THREATS.ALL, 'detail', id] as const,
BY_ID: (id: string) => [...QUERY_KEYS.THREATS.ALL, 'detail', id] as const,
},
STATS: {
ALL: ['stats'] as const,
BY_RANGE: (range: string) =>
[...QUERY_KEYS.STATS.ALL, range] as const,
BY_RANGE: (range: string) => [...QUERY_KEYS.STATS.ALL, range] as const,
},
MODELS: {
ALL: ['models'] as const,

View File

@ -20,5 +20,5 @@ apiClient.interceptors.response.use(
(response) => response,
(error: AxiosError): Promise<never> => {
return Promise.reject(transformAxiosError(error))
},
}
)

View File

@ -26,7 +26,7 @@ export class ApiError extends Error {
message: string,
code: ApiErrorCode,
statusCode: number,
details?: Record<string, string[]>,
details?: Record<string, string[]>
) {
super(message)
this.name = 'ApiError'

View File

@ -30,7 +30,7 @@ const calculateRetryDelay = (attemptIndex: number): number => {
const handleQueryCacheError = (
error: Error,
query: { state: { data: unknown } },
query: { state: { data: unknown } }
): void => {
if (query.state.data !== undefined) {
const message =
@ -45,7 +45,7 @@ const handleMutationCacheError = (
error: Error,
_variables: unknown,
_context: unknown,
mutation: { options: { onError?: unknown } },
mutation: { options: { onError?: unknown } }
): void => {
if (mutation.options.onError === undefined) {
const message =

View File

@ -3,7 +3,7 @@
// routers.tsx
// ===================
import { Navigate, createBrowserRouter, type RouteObject } from 'react-router-dom'
import { createBrowserRouter, Navigate, type RouteObject } from 'react-router-dom'
import { ROUTES } from '@/config'
import { Shell } from './shell'

View File

@ -5,7 +5,14 @@
import { Suspense } from 'react'
import { ErrorBoundary } from 'react-error-boundary'
import { LuChevronLeft, LuChevronRight, LuCpu, LuLayoutDashboard, LuMenu, LuShield } from 'react-icons/lu'
import {
LuChevronLeft,
LuChevronRight,
LuCpu,
LuLayoutDashboard,
LuMenu,
LuShield,
} from 'react-icons/lu'
import { NavLink, Outlet, useLocation } from 'react-router-dom'
import { ROUTES } from '@/config'
import { useUIStore } from '@/core/lib'
@ -17,11 +24,12 @@ const NAV_ITEMS = [
{ path: ROUTES.MODELS, label: 'Models', icon: LuCpu },
]
function ShellErrorFallback({ error }: { error: Error }): React.ReactElement {
function ShellErrorFallback({ error }: { error: unknown }): React.ReactElement {
const message = error instanceof Error ? error.message : String(error)
return (
<div className={styles.error}>
<h2>Something went wrong</h2>
<pre>{error.message}</pre>
<pre>{message}</pre>
</div>
)
}

View File

@ -3,8 +3,8 @@
// index.tsx
// ===================
import { useStats, useModelStatus, useAlerts } from '@/api/hooks'
import { StatCard, AlertFeed } from '@/components'
import { useAlerts, useModelStatus, useStats } from '@/api/hooks'
import { AlertFeed, StatCard } from '@/components'
import styles from './dashboard.module.scss'
function SeverityBar({
@ -106,14 +106,8 @@ export function Component(): React.ReactElement {
return (
<div className={styles.page}>
<div className={styles.statRow}>
<StatCard
label="Threats Detected"
value={stats.threats_detected}
/>
<StatCard
label="Threats Stored"
value={stats.threats_stored}
/>
<StatCard label="Threats Detected" value={stats.threats_detected} />
<StatCard label="Threats Stored" value={stats.threats_stored} />
<StatCard
label="High Severity"
value={sb.high}
@ -122,9 +116,7 @@ export function Component(): React.ReactElement {
<StatCard
label="Detection Mode"
value={modelStatus?.detection_mode ?? '...'}
sublabel={
modelStatus?.models_loaded ? 'Models loaded' : 'Rules only'
}
sublabel={modelStatus?.models_loaded ? 'Models loaded' : 'Rules only'}
/>
</div>
@ -134,11 +126,7 @@ export function Component(): React.ReactElement {
</div>
<div className={styles.bottomRow}>
<AlertFeed
alerts={alerts}
isConnected={isConnected}
maxHeight="360px"
/>
<AlertFeed alerts={alerts} isConnected={isConnected} maxHeight="360px" />
<div className={styles.lists}>
<RankedList
@ -158,9 +146,7 @@ export function Component(): React.ReactElement {
</div>
</div>
{connectionError && (
<div className={styles.wsError}>{connectionError}</div>
)}
{connectionError && <div className={styles.wsError}>{connectionError}</div>}
</div>
)
}

View File

@ -26,9 +26,7 @@ function ModelCard({ model }: { model: ActiveModel }): React.ReactElement {
{model.threshold !== null && (
<div className={styles.stat}>
<span className={styles.statLabel}>Threshold</span>
<span className={styles.statValue}>
{model.threshold.toFixed(4)}
</span>
<span className={styles.statValue}>{model.threshold.toFixed(4)}</span>
</div>
)}
</div>
@ -77,9 +75,7 @@ export function Component(): React.ReactElement {
disabled={retrain.isPending}
onClick={() => retrain.mutate()}
>
<LuRefreshCw
className={retrain.isPending ? styles.spinning : ''}
/>
<LuRefreshCw className={retrain.isPending ? styles.spinning : ''} />
{retrain.isPending ? 'Retraining...' : 'Retrain Models'}
</button>
</div>

View File

@ -121,9 +121,7 @@ export function Component(): React.ReactElement {
<div className={styles.pagination}>
<span className={styles.paginationInfo}>
{total === 0
? 'No results'
: `${offset + 1}${showing} of ${total}`}
{total === 0 ? 'No results' : `${offset + 1}${showing} of ${total}`}
</span>
<div className={styles.paginationButtons}>
<button

View File

@ -12,9 +12,7 @@ export default defineConfig(({ mode }) => {
const env = loadEnv(mode, path.resolve(__dirname, '..'), '')
const isDev = mode === 'development'
const apiTarget =
process.env.VITE_API_TARGET ||
env.VITE_API_TARGET ||
'http://localhost:8000'
process.env.VITE_API_TARGET || env.VITE_API_TARGET || 'http://localhost:8000'
return {
plugins: [react(), tsconfigPaths()],