Cybersecurity-Projects/PROJECTS/advanced/monitor-the-situation-dashb.../frontend/src/pages/dashboard/lifecycle.ts

239 lines
5.6 KiB
TypeScript

// ©AngelaMos | 2026
// lifecycle.ts
import { type QueryClient, useQueryClient } from '@tanstack/react-query'
import { useEffect, useRef } from 'react'
import { SNAPSHOT_KEY, useSnapshot } from '@/api/snapshot'
import { browserDriver, createDashboardWS, type WSEvent } from '@/api/ws'
import { unlockOnFirstGesture } from '@/lib/audio'
import { type CveEvent, useCveStore } from '@/stores/cve'
import { useGlobeEvents } from '@/stores/globeEvents'
import { type KevEntry, useKevStore } from '@/stores/kev'
import { usePrices } from '@/stores/prices'
import { type RansomwareVictim, useRansomwareStore } from '@/stores/ransomware'
import { useTicker } from '@/stores/ticker'
const ALL_TOPICS: readonly string[] = [
'heartbeat',
'scan_firehose',
'internet_outage',
'bgp_hijack',
'cve_new',
'kev_added',
'ransomware_victim',
'coinbase_price',
'earthquake',
'space_weather',
'wiki_itn',
'gdelt_spike',
'iss_position',
]
const WS_URL = '/api/v1/ws'
const GLOBE_RING_TTL_MS = 4_000
const GLOBE_EVICT_INTERVAL_MS = 5 * 60_000
interface CoinbaseTickPayload {
symbol: string
ts: string
price: string
volume_24h?: string
}
interface IssPositionPayload {
latitude: number
longitude: number
altitude: number
velocity: number
timestamp: number
fetched_at?: string
}
interface EarthquakePayload {
id: string
geometry?: { coordinates?: number[] }
properties?: Record<string, unknown>
}
interface WikiItnPayload {
text: string
slug: string
}
interface GdeltSpikePayload {
theme: string
time: string
count: number
zscore: number
}
export function useDashboardLifecycle(): void {
const { data: snapshot, isSuccess } = useSnapshot()
const queryClient = useQueryClient()
const wsRef = useRef<ReturnType<typeof createDashboardWS> | null>(null)
useEffect(() => {
unlockOnFirstGesture()
}, [])
useEffect(() => {
if (!isSuccess) return
const ws = createDashboardWS({
driver: () => browserDriver(WS_URL, [...ALL_TOPICS]),
topics: [...ALL_TOPICS],
onEvent: (ev) => routeEvent(ev, queryClient),
})
wsRef.current = ws
ws.connect()
ws.setReady()
return () => {
ws.disconnect()
wsRef.current = null
}
}, [isSuccess, queryClient])
useEffect(() => {
const id = setInterval(() => {
useGlobeEvents.getState().evict(Date.now())
}, GLOBE_EVICT_INTERVAL_MS)
return () => clearInterval(id)
}, [])
void snapshot
}
function routeEvent(ev: WSEvent, queryClient: QueryClient): void {
const data = ev.payload
if (data === undefined || data === null) return
switch (ev.topic) {
case 'cve_new':
handleCve(data as CveEvent)
break
case 'kev_added':
handleKev(data as KevEntry)
break
case 'ransomware_victim':
handleRansomware(data as RansomwareVictim)
break
case 'coinbase_price':
handleCoinbase(data as CoinbaseTickPayload)
break
case 'earthquake':
handleEarthquake(data as EarthquakePayload)
break
case 'iss_position':
handleIss(data as IssPositionPayload, queryClient)
break
case 'wiki_itn':
handleWiki(data as WikiItnPayload)
break
case 'gdelt_spike':
handleGdelt(data as GdeltSpikePayload)
break
case 'space_weather':
case 'internet_outage':
case 'bgp_hijack':
case 'scan_firehose':
mergeIntoSnapshot(queryClient, ev.topic, data)
break
default:
break
}
}
function handleCve(p: CveEvent): void {
if (!p.CveID) return
useCveStore.getState().push(p)
}
function handleKev(p: KevEntry): void {
if (!p.cveID) return
useKevStore.getState().push(p)
}
function handleRansomware(p: RansomwareVictim): void {
if (!p.post_title) return
useRansomwareStore.getState().push(p)
}
function handleCoinbase(p: CoinbaseTickPayload): void {
if (!p.symbol || !p.ts) return
usePrices.getState().pushTick({
symbol: p.symbol,
ts: new Date(p.ts).getTime(),
price: p.price,
volume24h: p.volume_24h,
})
}
function handleEarthquake(p: EarthquakePayload): void {
const coords = p.geometry?.coordinates
if (!Array.isArray(coords)) return
const lng = coords[0]
const lat = coords[1]
if (typeof lat !== 'number' || typeof lng !== 'number') return
const now = Date.now()
useGlobeEvents.getState().pushPoint({
id: `eq-${p.id}`,
type: 'earthquake',
lat,
lng,
emittedAt: now,
meta: p.properties,
})
useGlobeEvents.getState().pushRing({
id: `eq-ring-${p.id}-${now}`,
lat,
lng,
emittedAt: now,
ttlMs: GLOBE_RING_TTL_MS,
})
}
function handleIss(p: IssPositionPayload, queryClient: QueryClient): void {
if (typeof p.latitude !== 'number' || typeof p.longitude !== 'number') return
useGlobeEvents.getState().pushPoint({
id: 'iss-current',
type: 'iss',
lat: p.latitude,
lng: p.longitude,
emittedAt: Date.now(),
})
mergeIntoSnapshot(queryClient, 'iss_position', p)
}
function handleWiki(p: WikiItnPayload): void {
if (!p.text) return
const id = `wiki-${p.slug || p.text}`
useTicker.getState().push({
id,
source: 'Wikipedia',
headline: p.text,
ts: Date.now(),
})
}
function handleGdelt(p: GdeltSpikePayload): void {
if (!p.theme) return
useTicker.getState().push({
id: `gdelt-${p.theme}-${p.time}`,
source: 'GDELT',
headline: `${p.theme} spike (z=${p.zscore.toFixed(2)}, n=${p.count})`,
ts: Date.now(),
})
}
function mergeIntoSnapshot(
queryClient: QueryClient,
topic: string,
data: unknown
): void {
queryClient.setQueryData<Record<string, unknown>>(SNAPSHOT_KEY, (prev) => ({
...(prev ?? {}),
[topic]: data,
}))
}