From 3b4f6ce8a67c0c1355de0f263bd008c45a768946 Mon Sep 17 00:00:00 2001 From: CarterPerez-dev Date: Sat, 28 Feb 2026 11:32:23 -0500 Subject: [PATCH] feat(frontend): add React Query hooks and WebSocket alert feed Hooks for threats list/detail, stats, model status/retrain, and live WebSocket alert stream with auto-reconnect and ring buffer. --- .../frontend/src/api/hooks/index.ts | 5 + .../frontend/src/api/hooks/useAlerts.ts | 105 ++++++++++++++++++ .../frontend/src/api/hooks/useModels.ts | 43 +++++++ .../frontend/src/api/hooks/useStats.ts | 23 ++++ .../frontend/src/api/hooks/useThreats.ts | 52 +++++++++ 5 files changed, 228 insertions(+) create mode 100644 PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useAlerts.ts create mode 100644 PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useModels.ts create mode 100644 PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useStats.ts create mode 100644 PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useThreats.ts diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/index.ts b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/index.ts index 6221af9a..ea2609d4 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/index.ts +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/index.ts @@ -2,3 +2,8 @@ // © AngelaMos | 2026 // index.ts // =================== + +export * from './useThreats' +export * from './useStats' +export * from './useModels' +export * from './useAlerts' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useAlerts.ts b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useAlerts.ts new file mode 100644 index 00000000..f6402929 --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useAlerts.ts @@ -0,0 +1,105 @@ +// =================== +// © AngelaMos | 2026 +// useAlerts.ts +// =================== + +import { useEffect, useRef } from 'react' +import { create } from 'zustand' +import { ALERTS, WS_ENDPOINTS } from '@/config' +import { WebSocketAlertSchema, type WebSocketAlert } from '@/api/types' + +interface AlertState { + alerts: WebSocketAlert[] + isConnected: boolean + connectionError: string | null + addAlert: (alert: WebSocketAlert) => void + setConnected: (connected: boolean) => void + setError: (error: string | null) => void + clear: () => void +} + +const useAlertStore = create()((set) => ({ + alerts: [], + isConnected: false, + connectionError: null, + + addAlert: (alert) => + set((state) => ({ + alerts: [alert, ...state.alerts].slice(0, ALERTS.MAX_ITEMS), + })), + + setConnected: (connected) => + set({ isConnected: connected, connectionError: null }), + + setError: (error) => + set({ isConnected: false, connectionError: error }), + + clear: () => + set({ alerts: [], isConnected: false, connectionError: null }), +})) + +function getWsUrl(): string { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' + return `${protocol}//${window.location.host}${WS_ENDPOINTS.ALERTS}` +} + +export function useAlerts() { + const wsRef = useRef(null) + const retryCountRef = useRef(0) + const retryTimerRef = useRef | null>(null) + + const { alerts, isConnected, connectionError } = useAlertStore() + const { addAlert, setConnected, setError, clear } = useAlertStore() + + useEffect(() => { + function connect() { + const ws = new WebSocket(getWsUrl()) + wsRef.current = ws + + ws.onopen = () => { + retryCountRef.current = 0 + setConnected(true) + } + + ws.onmessage = (event) => { + const parsed = WebSocketAlertSchema.safeParse(JSON.parse(event.data)) + if (parsed.success) { + addAlert(parsed.data) + } + } + + ws.onclose = () => { + setConnected(false) + scheduleReconnect() + } + + ws.onerror = () => { + setError('WebSocket connection failed') + ws.close() + } + } + + function scheduleReconnect() { + const delay = Math.min( + ALERTS.RECONNECT_BASE_MS * 2 ** retryCountRef.current, + ALERTS.RECONNECT_MAX_MS, + ) + retryCountRef.current += 1 + retryTimerRef.current = setTimeout(connect, delay) + } + + connect() + + return () => { + if (retryTimerRef.current) { + clearTimeout(retryTimerRef.current) + } + if (wsRef.current) { + wsRef.current.close() + } + clear() + } + }, [addAlert, setConnected, setError, clear]) + + return { alerts, isConnected, connectionError } +} diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useModels.ts b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useModels.ts new file mode 100644 index 00000000..088f3d6e --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useModels.ts @@ -0,0 +1,43 @@ +// =================== +// © AngelaMos | 2026 +// useModels.ts +// =================== + +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' + +export function useModelStatus() { + return useQuery({ + queryKey: QUERY_KEYS.MODELS.STATUS(), + queryFn: async () => { + const { data } = await apiClient.get( + API_ENDPOINTS.MODELS.STATUS, + ) + return data + }, + ...QUERY_STRATEGIES.standard, + }) +} + +export function useRetrain() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async () => { + const { data } = await apiClient.post( + API_ENDPOINTS.MODELS.RETRAIN, + ) + return data + }, + onSuccess: () => { + toast.success('Retraining started') + queryClient.invalidateQueries({ queryKey: QUERY_KEYS.MODELS.ALL }) + }, + onError: () => { + toast.error('Failed to start retraining') + }, + }) +} diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useStats.ts b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useStats.ts new file mode 100644 index 00000000..56851082 --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useStats.ts @@ -0,0 +1,23 @@ +// =================== +// © AngelaMos | 2026 +// useStats.ts +// =================== + +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' + +export function useStats(range = '24h') { + return useQuery({ + queryKey: QUERY_KEYS.STATS.BY_RANGE(range), + queryFn: async () => { + const { data } = await apiClient.get( + API_ENDPOINTS.STATS, + { params: { range } }, + ) + return data + }, + ...QUERY_STRATEGIES.frequent, + }) +} diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useThreats.ts b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useThreats.ts new file mode 100644 index 00000000..1a52c8a6 --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useThreats.ts @@ -0,0 +1,52 @@ +// =================== +// © AngelaMos | 2026 +// useThreats.ts +// =================== + +import { useQuery } from '@tanstack/react-query' +import { QUERY_KEYS, API_ENDPOINTS, PAGINATION } from '@/config' +import { apiClient, QUERY_STRATEGIES } from '@/core/api' +import type { ThreatList, ThreatEvent } from '@/api/types' + +interface ThreatParams { + limit?: number + offset?: number + severity?: 'HIGH' | 'MEDIUM' | 'LOW' + source_ip?: string + since?: string + until?: string +} + +export function useThreats(params: ThreatParams = {}) { + const queryParams = { + limit: params.limit ?? PAGINATION.DEFAULT_LIMIT, + offset: params.offset ?? 0, + ...params, + } + + return useQuery({ + queryKey: QUERY_KEYS.THREATS.LIST(queryParams), + queryFn: async () => { + const { data } = await apiClient.get( + API_ENDPOINTS.THREATS.LIST, + { params: queryParams }, + ) + return data + }, + ...QUERY_STRATEGIES.frequent, + }) +} + +export function useThreat(id: string | null) { + return useQuery({ + queryKey: QUERY_KEYS.THREATS.BY_ID(id ?? ''), + queryFn: async () => { + const { data } = await apiClient.get( + API_ENDPOINTS.THREATS.BY_ID(id!), + ) + return data + }, + enabled: id !== null, + ...QUERY_STRATEGIES.standard, + }) +}