feat: wire TypeScript interfaces into all component props, fix 12 lint errors

Former-commit-id: 04b30a9e7af32b644140c45333f55c20afec45f2
This commit is contained in:
anoracleofra-code
2026-03-14 08:07:45 -06:00
parent 17c41d7ddf
commit 60c90661d4
6 changed files with 505 additions and 19 deletions
+2 -1
View File
@@ -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 || {};
+4 -3
View File
@@ -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<string, string> = {
'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<number[]>([]);
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<string, string> = {
"UAL": "UNITED AIRLINES", "DAL": "DELTA AIR LINES", "SWA": "SOUTHWEST AIRLINES",
@@ -603,7 +604,7 @@ function NewsFeedInner({ data, selectedEntity, regionDossier, regionDossierLoadi
<span className="text-[var(--text-primary)] text-xs font-bold">{ship.callsign}</span>
</div>
)}
{ship.imo > 0 && (
{(ship.imo ?? 0) > 0 && (
<div className="flex justify-between items-center border-b border-[var(--border-primary)] pb-2">
<span className="text-[var(--text-muted)] text-[10px]">IMO NUMBER</span>
<span className="text-[var(--text-primary)] text-xs font-bold">{ship.imo}</span>
@@ -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<any[]>([]);
const [activeFeed, setActiveFeed] = useState<any | null>(null);
const [feeds, setFeeds] = useState<RadioFeed[]>([]);
const [activeFeed, setActiveFeed] = useState<RadioFeed | null>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [isScanning, setIsScanning] = useState(false);
const audioRef = useRef<HTMLAudioElement | null>(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 ? (
<div className="text-[10px] text-cyan-700 font-mono text-center p-4">SEARCHING FREQUENCIES...</div>
) : (
feeds.map((feed: any, idx: number) => (
feeds.map((feed: RadioFeed, idx: number) => (
<div
key={feed.id}
onClick={() => playFeed(feed)}
+11 -7
View File
@@ -32,6 +32,7 @@ const FRESHNESS_MAP: Record<string, string> = {
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<string, { label: string; type: string }> = {
'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<React.SetStateAction<ActiveLayers>>; 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
</div>
</div>
<div className="flex items-center gap-3">
{active && layer.count > 0 && (
<span className="text-[10px] text-gray-300 font-mono">{layer.count.toLocaleString()}</span>
{active && (layer.count ?? 0) > 0 && (
<span className="text-[10px] text-gray-300 font-mono">{(layer.count ?? 0).toLocaleString()}</span>
)}
<div className={`text-[9px] font-mono tracking-wider px-2 py-0.5 rounded-full border ${active
? 'border-cyan-500/50 text-cyan-400 bg-cyan-950/30 shadow-[0_0_10px_rgba(34,211,238,0.2)]'
@@ -3,8 +3,9 @@
import React, { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { ChevronDown, ChevronUp } from "lucide-react";
import type { MapEffects } from "@/types/dashboard";
const WorldviewRightPanel = React.memo(function WorldviewRightPanel({ effects, setEffects, setUiVisible }: { effects: any; setEffects: any; setUiVisible: any }) {
const WorldviewRightPanel = React.memo(function WorldviewRightPanel({ effects, setEffects, setUiVisible }: { effects: MapEffects; setEffects: (e: MapEffects) => void; setUiVisible: (v: boolean) => void }) {
const [isMinimized, setIsMinimized] = useState(true);
const [currentTime, setCurrentTime] = useState({ date: "XXXX-XX-XX", time: "00:00:00" });
+478
View File
@@ -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<string, StockTicker>;
export type OilData = Record<string, StockTicker>;
// ─── 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<string, string>;
// ─── 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<string, any>;
}
export interface MeasurePoint {
lat: number;
lng: number;
}
export interface MapEffects {
bloom: boolean;
style?: string;
}
export interface MaplibreViewerProps {
data: DashboardData;
activeLayers: ActiveLayers;
activeFilters?: Record<string, string[]>;
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;
}