diff --git a/frontend/src/components/MarketsPanel.tsx b/frontend/src/components/MarketsPanel.tsx index 3d59f59..8d026ec 100644 --- a/frontend/src/components/MarketsPanel.tsx +++ b/frontend/src/components/MarketsPanel.tsx @@ -3,8 +3,9 @@ import React, { useState } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { ArrowUpRight, ArrowDownRight, TrendingUp, Droplet, ChevronDown, ChevronUp } from 'lucide-react'; +import type { DashboardData } from "@/types/dashboard"; -const MarketsPanel = React.memo(function MarketsPanel({ data }: { data: any }) { +const MarketsPanel = React.memo(function MarketsPanel({ data }: { data: DashboardData }) { const [isMinimized, setIsMinimized] = useState(true); const stocks = data?.stocks || {}; diff --git a/frontend/src/components/NewsFeed.tsx b/frontend/src/components/NewsFeed.tsx index 1a69731..ad22a04 100644 --- a/frontend/src/components/NewsFeed.tsx +++ b/frontend/src/components/NewsFeed.tsx @@ -6,6 +6,7 @@ import { AlertTriangle, Clock, ChevronDown, ChevronUp } from 'lucide-react'; import React, { useEffect, useRef, useCallback } from 'react'; import Hls from 'hls.js'; import WikiImage from '@/components/WikiImage'; +import type { DashboardData, SelectedEntity, RegionDossier } from "@/types/dashboard"; // HLS video player — uses hls.js on Chrome/Firefox, native on Safari function HlsVideo({ url, className }: { url: string; className?: string }) { @@ -154,7 +155,7 @@ const VESSEL_TYPE_WIKI: Record = { 'military_vessel': 'https://en.wikipedia.org/wiki/Warship', }; -function NewsFeedInner({ data, selectedEntity, regionDossier, regionDossierLoading }: { data: any, selectedEntity?: { type: string, id: string | number, name?: string, callsign?: string, media_url?: string, extra?: any } | null, regionDossier?: any, regionDossierLoading?: boolean }) { +function NewsFeedInner({ data, selectedEntity, regionDossier, regionDossierLoading }: { data: DashboardData, selectedEntity?: SelectedEntity | null, regionDossier?: RegionDossier | null, regionDossierLoading?: boolean }) { const [isMinimized, setIsMinimized] = useState(false); const [expandedIndexes, setExpandedIndexes] = useState([]); const itemRefs = useRef<(HTMLDivElement | null)[]>([]); @@ -431,7 +432,7 @@ function NewsFeedInner({ data, selectedEntity, regionDossier, regionDossierLoadi airline = "PRIVATE JET"; } else if (selectedEntity.type === 'private_flight') { airline = "PRIVATE / GA"; - } else if (flight.airline_code) { + } else if ('airline_code' in flight && flight.airline_code) { // Use the airline code resolved from adsb.lol routeset API const codeMap: Record = { "UAL": "UNITED AIRLINES", "DAL": "DELTA AIR LINES", "SWA": "SOUTHWEST AIRLINES", @@ -603,7 +604,7 @@ function NewsFeedInner({ data, selectedEntity, regionDossier, regionDossierLoadi {ship.callsign} )} - {ship.imo > 0 && ( + {(ship.imo ?? 0) > 0 && (
IMO NUMBER {ship.imo} diff --git a/frontend/src/components/RadioInterceptPanel.tsx b/frontend/src/components/RadioInterceptPanel.tsx index 15f6fe4..d595479 100644 --- a/frontend/src/components/RadioInterceptPanel.tsx +++ b/frontend/src/components/RadioInterceptPanel.tsx @@ -4,11 +4,12 @@ import { API_BASE } from "@/lib/api"; import { useState, useEffect, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { RadioReceiver, Activity, Play, Square, FastForward, ChevronDown, ChevronUp } from 'lucide-react'; +import type { DashboardData, SelectedEntity, RadioFeed } from "@/types/dashboard"; -export default function RadioInterceptPanel({ data, isEavesdropping, setIsEavesdropping, eavesdropLocation, cameraCenter, selectedEntity }: { data: any, isEavesdropping?: boolean, setIsEavesdropping?: (val: boolean) => void, eavesdropLocation?: { lat: number, lng: number } | null, cameraCenter?: { lat: number, lng: number } | null, selectedEntity?: { type: string, id: string | number, extra?: any } | null }) { +export default function RadioInterceptPanel({ data, isEavesdropping, setIsEavesdropping, eavesdropLocation, cameraCenter, selectedEntity }: { data: DashboardData, isEavesdropping?: boolean, setIsEavesdropping?: (val: boolean) => void, eavesdropLocation?: { lat: number, lng: number } | null, cameraCenter?: { lat: number, lng: number } | null, selectedEntity?: SelectedEntity | null }) { const [isMinimized, setIsMinimized] = useState(true); - const [feeds, setFeeds] = useState([]); - const [activeFeed, setActiveFeed] = useState(null); + const [feeds, setFeeds] = useState([]); + const [activeFeed, setActiveFeed] = useState(null); const [isPlaying, setIsPlaying] = useState(false); const [isScanning, setIsScanning] = useState(false); const audioRef = useRef(null); @@ -113,7 +114,7 @@ export default function RadioInterceptPanel({ data, isEavesdropping, setIsEavesd } }, [eavesdropLocation]); - const playFeed = (feed: any) => { + const playFeed = (feed: RadioFeed) => { if (isScanning && scanTimeoutRef.current) { clearTimeout(scanTimeoutRef.current); setIsScanning(false); @@ -135,10 +136,10 @@ export default function RadioInterceptPanel({ data, isEavesdropping, setIsEavesd useEffect(() => { if (activeFeed && isPlaying) { if (!audioRef.current) { - const audio = new Audio(activeFeed.stream_url); + const audio = new Audio(activeFeed.stream_url || ''); audioRef.current = audio; } else { - audioRef.current.src = activeFeed.stream_url; + audioRef.current.src = activeFeed.stream_url || ''; } audioRef.current.volume = volume; audioRef.current.play().catch(e => console.log("Audio play blocked", e)); @@ -382,7 +383,7 @@ export default function RadioInterceptPanel({ data, isEavesdropping, setIsEavesd {feeds.length === 0 ? (
SEARCHING FREQUENCIES...
) : ( - feeds.map((feed: any, idx: number) => ( + feeds.map((feed: RadioFeed, idx: number) => (
playFeed(feed)} diff --git a/frontend/src/components/WorldviewLeftPanel.tsx b/frontend/src/components/WorldviewLeftPanel.tsx index b9d4bfd..a8f838f 100644 --- a/frontend/src/components/WorldviewLeftPanel.tsx +++ b/frontend/src/components/WorldviewLeftPanel.tsx @@ -32,6 +32,7 @@ const FRESHNESS_MAP: Record = { ships_cargo: "ships", ships_civilian: "ships", ships_passenger: "ships", + ships_tracked_yachts: "ships", ukraine_frontline: "frontlines", global_incidents: "gdelt", cctv: "cctv", @@ -59,8 +60,9 @@ const POTUS_ICAOS: Record = { 'AE5E77': { label: 'Marine One (VH-92A)', type: 'M1' }, 'AE5E79': { label: 'Marine One (VH-92A)', type: 'M1' }, }; +import type { DashboardData, ActiveLayers, SelectedEntity } from "@/types/dashboard"; -const WorldviewLeftPanel = React.memo(function WorldviewLeftPanel({ data, activeLayers, setActiveLayers, onSettingsClick, onLegendClick, gibsDate, setGibsDate, gibsOpacity, setGibsOpacity, onEntityClick, onFlyTo }: { data: any; activeLayers: any; setActiveLayers: any; onSettingsClick?: () => void; onLegendClick?: () => void; gibsDate?: string; setGibsDate?: (d: string) => void; gibsOpacity?: number; setGibsOpacity?: (o: number) => void; onEntityClick?: (entity: { type: string; id: number; extra?: any }) => void; onFlyTo?: (lat: number, lng: number) => void }) { +const WorldviewLeftPanel = React.memo(function WorldviewLeftPanel({ data, activeLayers, setActiveLayers, onSettingsClick, onLegendClick, gibsDate, setGibsDate, gibsOpacity, setGibsOpacity, onEntityClick, onFlyTo }: { data: DashboardData; activeLayers: ActiveLayers; setActiveLayers: React.Dispatch>; onSettingsClick?: () => void; onLegendClick?: () => void; gibsDate?: string; setGibsDate?: (d: string) => void; gibsOpacity?: number; setGibsOpacity?: (o: number) => void; onEntityClick?: (entity: SelectedEntity) => void; onFlyTo?: (lat: number, lng: number) => void }) { const [isMinimized, setIsMinimized] = useState(false); const { theme, toggleTheme } = useTheme(); const [gibsPlaying, setGibsPlaying] = useState(false); @@ -92,18 +94,19 @@ const WorldviewLeftPanel = React.memo(function WorldviewLeftPanel({ data, active }, [gibsPlaying, gibsDate, setGibsDate]); // Compute ship category counts (memoized — ships array can be 1000+ items) - const { militaryShipCount, cargoShipCount, passengerShipCount, civilianShipCount } = useMemo(() => { + const { militaryShipCount, cargoShipCount, passengerShipCount, civilianShipCount, trackedYachtCount } = useMemo(() => { const ships = data?.ships; - if (!ships || !ships.length) return { militaryShipCount: 0, cargoShipCount: 0, passengerShipCount: 0, civilianShipCount: 0 }; - let military = 0, cargo = 0, passenger = 0, civilian = 0; + if (!ships || !ships.length) return { militaryShipCount: 0, cargoShipCount: 0, passengerShipCount: 0, civilianShipCount: 0, trackedYachtCount: 0 }; + let military = 0, cargo = 0, passenger = 0, civilian = 0, trackedYacht = 0; for (const s of ships) { + if (s.yacht_alert) { trackedYacht++; continue; } const t = s.type; if (t === 'carrier' || t === 'military_vessel') military++; else if (t === 'tanker' || t === 'cargo') cargo++; else if (t === 'passenger') passenger++; else civilian++; } - return { militaryShipCount: military, cargoShipCount: cargo, passengerShipCount: passenger, civilianShipCount: civilian }; + return { militaryShipCount: military, cargoShipCount: cargo, passengerShipCount: passenger, civilianShipCount: civilian, trackedYachtCount: trackedYacht }; }, [data?.ships]); // Find POTUS fleet planes currently airborne from tracked flights @@ -133,6 +136,7 @@ const WorldviewLeftPanel = React.memo(function WorldviewLeftPanel({ data, active { id: "ships_cargo", name: "Cargo / Tankers", source: "AIS Stream", count: cargoShipCount, icon: Ship }, { id: "ships_civilian", name: "Civilian Vessels", source: "AIS Stream", count: civilianShipCount, icon: Anchor }, { id: "ships_passenger", name: "Cruise / Passenger", source: "AIS Stream", count: passengerShipCount, icon: Anchor }, + { id: "ships_tracked_yachts", name: "Tracked Yachts", source: "Yacht-Alert DB", count: trackedYachtCount, icon: Eye }, { id: "ukraine_frontline", name: "Ukraine Frontline", source: "DeepStateMap", count: data?.frontlines ? 1 : 0, icon: AlertTriangle }, { id: "global_incidents", name: "Global Incidents", source: "GDELT", count: data?.gdelt?.length || 0, icon: Activity }, { id: "cctv", name: "CCTV Mesh", source: "CCTV Mesh + Street View", count: data?.cctv?.length || 0, icon: Cctv }, @@ -314,8 +318,8 @@ const WorldviewLeftPanel = React.memo(function WorldviewLeftPanel({ data, active
- {active && layer.count > 0 && ( - {layer.count.toLocaleString()} + {active && (layer.count ?? 0) > 0 && ( + {(layer.count ?? 0).toLocaleString()} )}
void; setUiVisible: (v: boolean) => void }) { const [isMinimized, setIsMinimized] = useState(true); const [currentTime, setCurrentTime] = useState({ date: "XXXX-XX-XX", time: "00:00:00" }); diff --git a/frontend/src/types/dashboard.ts b/frontend/src/types/dashboard.ts new file mode 100644 index 0000000..2be91b5 --- /dev/null +++ b/frontend/src/types/dashboard.ts @@ -0,0 +1,478 @@ +// ─── ShadowBroker Dashboard Data Types ───────────────────────────────────── +// Canonical type definitions for all data flowing from backend → frontend. +// Every `any` in the codebase should eventually be replaced with these types. + +// ─── FLIGHTS ──────────────────────────────────────────────────────────────── + +export interface FlightBase { + callsign: string; + country: string; + lat: number; + lng: number; + alt: number; + heading: number; + speed_knots: number | null; + registration: string; + model: string; + icao24: string; + squawk?: string; + aircraft_category?: string; + nac_p?: number; + _seen_at?: number; + origin_loc?: [number, number] | null; + dest_loc?: [number, number] | null; + origin_name?: string; + dest_name?: string; + trail?: Array<{ lat: number; lng: number; alt?: number; ts?: number }>; + holding?: boolean; +} + +export interface CommercialFlight extends FlightBase { + type: "commercial_flight"; + airline_code?: string; + supplemental_source?: string; +} + +export interface PrivateFlight extends FlightBase { + type: "private_ga" | "private_flight"; +} + +export interface PrivateJet extends FlightBase { + type: "private_jet"; +} + +export interface MilitaryFlight extends FlightBase { + type: "military_flight"; + military_type?: "heli" | "fighter" | "tanker" | "cargo" | "recon" | "default"; +} + +export interface TrackedFlight extends FlightBase { + type: "tracked_flight"; + alert_category?: string; + alert_operator?: string; + alert_special?: string; + alert_flag?: string; + alert_color?: string; + alert_wiki?: string; + alert_type?: string; + alert_tags?: string[]; + alert_link?: string; + tracked_name?: string; + operator?: string; + owner?: string; + name?: string; +} + +export interface UAV extends FlightBase { + type: "uav"; + uav_type?: string; + aircraft_model?: string; + wiki?: string; +} + +export type Flight = CommercialFlight | PrivateFlight | PrivateJet | MilitaryFlight | TrackedFlight | UAV; + +// ─── SHIPS / MARITIME ─────────────────────────────────────────────────────── + +export interface Ship { + mmsi: number; + name: string; + type: "carrier" | "military_vessel" | "tanker" | "cargo" | "passenger" | "yacht" | "other" | "unknown"; + lat: number; + lng: number; + heading: number; + sog: number; + cog: number; + callsign?: string; + destination?: string; + imo?: number; + country: string; + ais_type_code?: number; + _updated?: number; + estimated?: boolean; + source?: string; + source_url?: string; + last_osint_update?: string; + desc?: string; + // Tracked yacht enrichment + yacht_alert?: boolean; + yacht_owner?: string; + yacht_name?: string; + yacht_category?: string; + yacht_color?: string; + yacht_builder?: string; + yacht_length?: number; + yacht_year?: number; + yacht_link?: string; + // Carrier enrichment + wiki?: string; + homeport?: string; + homeport_lat?: number; + homeport_lng?: number; + fallback_lat?: number; + fallback_lng?: number; + fallback_heading?: number; + fallback_desc?: string; +} + +// ─── SATELLITES ───────────────────────────────────────────────────────────── + +export type SatelliteMission = + | "military_recon" | "military_sar" | "military_ew" + | "sar" | "commercial_imaging" | "navigation" + | "early_warning" | "space_station" | "sigint" | "general"; + +export interface Satellite { + id: number; + name: string; + mission: SatelliteMission; + sat_type: string; + country: string; + wiki?: string; + lat: number; + lng: number; + alt_km: number; + speed_knots: number; + heading: number; +} + +// ─── EARTHQUAKES ──────────────────────────────────────────────────────────── + +export interface Earthquake { + id: string; + mag: number; + lat: number; + lng: number; + place: string; + title?: string; +} + +// ─── GPS JAMMING ──────────────────────────────────────────────────────────── + +export interface GPSJammingZone { + lat: number; + lng: number; + severity: "high" | "medium" | "low"; + ratio: number; + degraded: number; + total: number; +} + +// ─── FIRE HOTSPOTS (NASA FIRMS) ───────────────────────────────────────────── + +export interface FireHotspot { + lat: number; + lng: number; + frp: number; + brightness: number; + confidence: string; + daynight: string; + acq_date: string; + acq_time: string; +} + +// ─── CCTV CAMERAS ─────────────────────────────────────────────────────────── + +export interface CCTVCamera { + id: string | number; + lat: number; + lon: number; + direction_facing?: string; + source_agency?: string; + media_url?: string; + media_type?: "image" | "hls" | "mjpeg"; +} + +// ─── KIWISDR RECEIVERS ───────────────────────────────────────────────────── + +export interface KiwiSDR { + lat: number; + lon: number; + name: string; + url?: string; + users?: number; + users_max?: number; + bands?: string; + antenna?: string; + location?: string; +} + +// ─── INTERNET OUTAGES (IODA) ──────────────────────────────────────────────── + +export interface InternetOutage { + region_code: string; + region_name: string; + country_code: string; + country_name: string; + level: string; + datasource: string; + severity: number; + lat: number; + lng: number; +} + +// ─── DATA CENTERS ─────────────────────────────────────────────────────────── + +export interface DataCenter { + name: string; + company: string; + street?: string; + city?: string; + country?: string; + zip?: string; + lat: number; + lng: number; +} + +// ─── NEWS / GLOBAL INCIDENTS ──────────────────────────────────────────────── + +export interface NewsArticle { + id: number | string; + title: string; + summary: string; + source: string; + link: string; + pub_date: string; + risk_score: number; + lat: number; + lng: number; + region?: string; + coords?: [number, number]; + machine_assessment?: string; +} + +// ─── UKRAINE FRONTLINE ────────────────────────────────────────────────────── + +export interface FrontlineGeoJSON { + type: "FeatureCollection"; + features: Array<{ + type: "Feature"; + geometry: { + type: "Polygon"; + coordinates: [number, number][][]; + }; + properties: { + name: string; + zone_id: number; + }; + }>; +} + +// ─── GDELT INCIDENTS ──────────────────────────────────────────────────────── + +export interface GDELTIncident { + type: "Feature"; + geometry: { + type: "Point"; + coordinates: [number, number]; + }; + properties: { + name: string; + count: number; + _urls_list: string[]; + _headlines_list: string[]; + }; +} + +// ─── LIVEUAMAP ────────────────────────────────────────────────────────────── + +export interface LiveUAmapIncident { + id: string | number; + lat: number; + lng: number; + title: string; + description?: string; + date: string; + timestamp?: number; + link?: string; + category?: string; + region?: string; +} + +// ─── STOCKS & COMMODITIES ─────────────────────────────────────────────────── + +export interface StockTicker { + price: number; + change_percent: number; + up: boolean; +} + +export type StocksData = Record; +export type OilData = Record; + +// ─── SPACE WEATHER ────────────────────────────────────────────────────────── + +export interface SpaceWeatherEvent { + type: string; + begin: string; + end: string; + classtype: string; +} + +export interface SpaceWeather { + kp_index: number | null; + kp_text: string; + events: SpaceWeatherEvent[]; +} + +// ─── WEATHER (RAINVIEWER) ─────────────────────────────────────────────────── + +export interface Weather { + time: number; + host: string; +} + +// ─── AIRPORTS ─────────────────────────────────────────────────────────────── + +export interface Airport { + id: string; + name: string; + iata: string; + lat: number; + lng: number; + type: "airport"; +} + +// ─── RADIO FEEDS ──────────────────────────────────────────────────────────── + +export interface RadioFeed { + id: string; + name: string; + location: string; + category: string; + listeners: number; + stream_url?: string; +} + +// ─── ROUTE ────────────────────────────────────────────────────────────────── + +export interface FlightRoute { + orig_loc: [number, number]; + dest_loc: [number, number]; + origin_name: string; + dest_name: string; +} + +// ─── REGION DOSSIER ───────────────────────────────────────────────────────── + +export interface RegionDossier { + lat: number; + lng: number; + admin_regions?: string[]; + populated_places?: string[]; + // Dynamic properties from backend (sentinel2, weather, etc.) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; +} + +// ─── FRESHNESS METADATA ───────────────────────────────────────────────────── + +export type FreshnessMap = Record; + +// ─── ROOT DATA OBJECT ─────────────────────────────────────────────────────── + +export interface DashboardData { + // Metadata + last_updated?: string | null; + freshness?: FreshnessMap; + satellite_source?: string; + + // Fast tier + commercial_flights?: CommercialFlight[]; + private_flights?: PrivateFlight[]; + private_jets?: PrivateJet[]; + military_flights?: MilitaryFlight[]; + tracked_flights?: TrackedFlight[]; + uavs?: UAV[]; + ships?: Ship[]; + cctv?: CCTVCamera[]; + liveuamap?: LiveUAmapIncident[]; + gps_jamming?: GPSJammingZone[]; + satellites?: Satellite[]; + + // Slow tier + news?: NewsArticle[]; + stocks?: StocksData; + oil?: OilData; + weather?: Weather | null; + earthquakes?: Earthquake[]; + frontlines?: FrontlineGeoJSON | null; + gdelt?: GDELTIncident[]; + airports?: Airport[]; + kiwisdr?: KiwiSDR[]; + space_weather?: SpaceWeather | null; + internet_outages?: InternetOutage[]; + firms_fires?: FireHotspot[]; + datacenters?: DataCenter[]; +} + +// ─── COMPONENT PROPS ──────────────────────────────────────────────────────── + +export interface ActiveLayers { + flights: boolean; + private: boolean; + jets: boolean; + military: boolean; + tracked: boolean; + satellites: boolean; + ships_military: boolean; + ships_cargo: boolean; + ships_civilian: boolean; + ships_passenger: boolean; + ships_tracked_yachts: boolean; + earthquakes: boolean; + cctv: boolean; + ukraine_frontline: boolean; + global_incidents: boolean; + day_night: boolean; + gps_jamming: boolean; + gibs_imagery: boolean; + highres_satellite: boolean; + kiwisdr: boolean; + firms: boolean; + internet_outages: boolean; + datacenters: boolean; +} + +export interface SelectedEntity { + id: string | number; + type: string; + name?: string; + media_url?: string; + // Dynamic bag — varies by entity type (flight, ship, cctv, region_dossier, etc.) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + extra?: Record; +} + +export interface MeasurePoint { + lat: number; + lng: number; +} + +export interface MapEffects { + bloom: boolean; + style?: string; +} + +export interface MaplibreViewerProps { + data: DashboardData; + activeLayers: ActiveLayers; + activeFilters?: Record; + effects?: MapEffects; + onEntityClick: (entity: SelectedEntity | null) => void; + flyToLocation: { lat: number; lng: number; zoom?: number; ts?: number } | null; + selectedEntity: SelectedEntity | null; + onMouseCoords: (coords: { lat: number; lng: number }) => void; + onRightClick: (coords: { lat: number; lng: number }) => void; + regionDossier: RegionDossier | null; + regionDossierLoading: boolean; + onViewStateChange?: (vs: { zoom: number; latitude: number }) => void; + measureMode: boolean; + onMeasureClick: (coords: { lat: number; lng: number }) => void; + measurePoints: MeasurePoint[]; + gibsDate: string; + gibsOpacity: number; + isEavesdropping?: boolean; + onEavesdropClick?: (coords: { lat: number; lng: number }) => void; + onCameraMove?: (coords: { lat: number; lng: number }) => void; +}