v0.8.0: POTUS fleet tracking, full aircraft color-coding, carrier fidelity, UI overhaul

New features:
- POTUS fleet (AF1, AF2, Marine One) with hot-pink icons + gold halo ring
- 9-color aircraft system: military, medical, police, VIP, privacy, dictators
- Sentinel-2 fullscreen overlay with download/copy/open buttons (green themed)
- Carrier homeport deconfliction — distinct pier positions instead of stacking
- Toggle all data layers button (cyan when active, excludes MODIS Terra)
- Version badge + update checker + Discussions shortcut in UI
- Overhauled MapLegend with POTUS fleet, wildfires, infrastructure sections
- Data center map layer with ~700 global DCs from curated dataset

Fixes:
- All Air Force Two ICAO hex codes now correctly identified
- POTUS icon priority over grounded state
- Sentinel-2 no longer overlaps bottom coordinate bar
- Region dossier Nominatim 429 rate-limit retry/backoff
- Docker ENV legacy format warnings resolved
- UI buttons cyan in dark mode, grey in light mode
- Circuit breaker for flaky upstream APIs

Community: @suranyami — parallel multi-arch Docker builds + runtime BACKEND_URL fix (PR #35, #44)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Former-commit-id: 7c523df70a2d26f675603166e3513d29230592cd
This commit is contained in:
anoracleofra-code
2026-03-12 09:30:51 -06:00
parent a0d0a449eb
commit 34db99deaf
20 changed files with 907 additions and 553 deletions
+59 -24
View File
@@ -2,45 +2,60 @@
import React, { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { X, Zap, Gauge, Anchor, Layers, Bug } from "lucide-react";
import { X, Zap, Shield, Satellite, MapPin, Palette, ToggleRight, Bug, Heart } from "lucide-react";
const CURRENT_VERSION = "0.7";
const CURRENT_VERSION = "0.8";
const STORAGE_KEY = `shadowbroker_changelog_v${CURRENT_VERSION}`;
const NEW_FEATURES = [
{
icon: <Gauge size={14} className="text-green-400" />,
title: "Parallelized Data Fetches",
desc: "Stock and oil ticker fetches now run in parallel via ThreadPoolExecutor — backend data updates ~4x faster (~2s vs ~8s serial).",
color: "green",
icon: <Shield size={14} className="text-pink-400" />,
title: "POTUS Fleet Tracking",
desc: "Air Force One, Air Force Two, and Marine One aircraft now display with oversized hot-pink icons and a gold dashed halo ring — instantly recognizable on the map.",
color: "pink",
},
{
icon: <Anchor size={14} className="text-blue-400" />,
title: "AIS WebSocket Stability",
desc: "Exponential backoff now properly resets after 200 consecutive successes. Removed lock-contention vessel pruning — replaced with time-based logging every 60s.",
color: "blue",
},
{
icon: <Zap size={14} className="text-yellow-400" />,
title: "Deferred Icon Loading",
desc: "~35 critical map icons load immediately on startup. ~50 non-critical icons (fire markers, satellites, color variants) are deferred — faster initial map render.",
icon: <Palette size={14} className="text-yellow-400" />,
title: "Full Aircraft Color-Coding",
desc: "9-color system: military (yellow), medical/rescue (lime), police/government (blue), privacy (black), VIPs (hot pink), dictators/oligarchs (red), and more — all enriched from plane_alert_db.",
color: "yellow",
},
{
icon: <Layers size={14} className="text-cyan-400" />,
title: "Smarter Data Tiering",
desc: "Satellites removed from fast endpoint (was duplicated). Geopolitics polling reduced from 5min to 30min. Single-pass ETag serialization — clients get 304 Not Modified most of the time.",
icon: <Satellite size={14} className="text-green-400" />,
title: "Sentinel-2 Satellite Overhaul",
desc: "Replaced the tiny satellite popup with a fullscreen image overlay. Added Download, Copy to Clipboard, and Open Full Res buttons. Green dossier-themed UI.",
color: "green",
},
{
icon: <MapPin size={14} className="text-blue-400" />,
title: "Region Dossier & Carrier Fidelity",
desc: "Fixed Nominatim 429 rate-limit errors with retry/backoff. Carriers at shared homeports now dock at distinct pier positions instead of stacking.",
color: "blue",
},
{
icon: <Zap size={14} className="text-cyan-400" />,
title: "Overhauled Map Legend & Controls",
desc: "Full 9-color aircraft legend with POTUS fleet, wildfires, and infrastructure sections. New version badge, update checker, and Discussions shortcut in the UI.",
color: "cyan",
},
{
icon: <ToggleRight size={14} className="text-purple-400" />,
title: "Toggle All Data Layers",
desc: "One-click button to enable/disable all data layers at once. Turns cyan when active. MODIS Terra excluded from bulk toggle to prevent accidental imagery load.",
color: "purple",
},
];
const BUG_FIXES = [
"News feed entrance animations capped at 15 items — no more 100+ simultaneous Framer Motion instances",
"FIRMS fire hotspots and internet outages use heapq.nlargest() instead of full sort — faster processing of 60K+ records",
"Ship counts in left panel memoized with single-pass loop instead of 3 separate filter calls",
"Color map objects extracted to module-level constants — no allocation on every 2s tick",
"GDELT headline extraction improved — skips gibberish URL slugs and hex IDs",
"Multi-arch Docker images now available (amd64 + arm64) — runs on Raspberry Pi and Apple Silicon",
"POTUS fleet ICAO codes expanded — all Air Force Two (C-32A/B) airframes now correctly identified with gold halo",
"POTUS icon priority fixed — presidential aircraft always show the POTUS icon even when grounded",
"Sentinel-2 imagery no longer overlaps the bottom coordinate bar",
"Docker ENV format warnings resolved (legacy syntax → key=value)",
"Settings/Key/Version buttons now cyan in dark mode, grey only in light mode",
];
const CONTRIBUTORS = [
{ name: "@suranyami", desc: "Parallel multi-arch Docker builds (11min → 3min) + runtime BACKEND_URL fix", pr: "#35, #44" },
];
export function useChangelog() {
@@ -145,6 +160,26 @@ const ChangelogModal = React.memo(function ChangelogModal({ onClose }: Changelog
))}
</div>
</div>
{/* Contributors */}
<div>
<div className="text-[9px] font-mono tracking-[0.2em] text-pink-400 font-bold mb-3 flex items-center gap-2">
<Heart size={10} className="text-pink-400" />
COMMUNITY CONTRIBUTORS
</div>
<div className="space-y-1.5">
{CONTRIBUTORS.map((c, i) => (
<div key={i} className="flex items-start gap-2 px-3 py-2 rounded-lg border border-pink-500/20 bg-pink-500/5">
<span className="text-pink-400 text-[10px] mt-0.5 flex-shrink-0">&hearts;</span>
<div>
<span className="text-[10px] font-mono text-pink-300 font-bold">{c.name}</span>
<span className="text-[9px] font-mono text-[var(--text-muted)]"> {c.desc}</span>
<span className="text-[8px] font-mono text-[var(--text-muted)]"> (PR {c.pr})</span>
</div>
</div>
))}
</div>
</div>
</div>
{/* Footer */}
+6 -4
View File
@@ -89,12 +89,13 @@ export default function FindLocateBar({ data, onLocate, onFilter }: FindLocateBa
});
}
// Tracked flights
// Tracked flights — include tags/owner/name for broad search (first name, last name, etc.)
for (const f of data?.tracked_flights || []) {
const uid = f.icao24 || f.registration || f.callsign || '';
const operator = f.alert_operator || 'Unknown Operator';
const category = f.alert_category || 'Tracked';
const type = f.alert_type || f.model || 'Unknown';
const extras = [f.alert_tags, f.owner, f.name, f.callsign].filter(Boolean).join(' ');
results.push({
id: `tracked-${uid}`,
label: operator,
@@ -104,7 +105,8 @@ export default function FindLocateBar({ data, onLocate, onFilter }: FindLocateBa
lat: f.lat,
lng: f.lng,
entityType: "tracked_flight",
});
_extra: extras,
} as any);
}
// Ships
@@ -144,7 +146,7 @@ export default function FindLocateBar({ data, onLocate, onFilter }: FindLocateBa
const q = query.toLowerCase();
return allEntities
.filter(e => {
const searchable = `${e.label} ${e.sublabel} ${e.id}`.toLowerCase();
const searchable = `${e.label} ${e.sublabel} ${e.id} ${(e as any)._extra || ''}`.toLowerCase();
return searchable.includes(q);
})
.slice(0, 12);
@@ -177,7 +179,7 @@ export default function FindLocateBar({ data, onLocate, onFilter }: FindLocateBa
ref={inputRef}
type="text"
value={query}
placeholder="Find aircraft or vessel..."
placeholder="Find aircraft, person or vessel..."
className="flex-1 bg-transparent text-[10px] text-[var(--text-secondary)] font-mono tracking-wider outline-none placeholder:text-[var(--text-muted)]"
onChange={(e) => {
setQuery(e.target.value);
+183 -95
View File
@@ -95,7 +95,12 @@ const svgPotusPlane = `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns=
const svgPotusHeli = `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="16" r="15" fill="none" stroke="gold" stroke-width="2" stroke-dasharray="4 2"/><g transform="translate(6,4)"><path d="M10 6L10 14L8 16L8 18L10 17L12 22L14 17L16 18L16 16L14 14L14 6C14 4 13 2 12 2C11 2 10 4 10 6Z" fill="#FF1493" stroke="black"/><circle cx="12" cy="12" r="8" fill="none" stroke="#FF1493" stroke-dasharray="2 2" stroke-width="1"/></g></svg>`)}`;
// POTUS fleet ICAO hex codes (verified FAA registry)
const POTUS_ICAOS = new Set(['ADFDF8','ADFDF9','AE0865','AE5E76','AE5E77','AE5E79']);
const POTUS_ICAOS = new Set([
'ADFDF8','ADFDF9', // Air Force One (VC-25A)
'ADFEB7','ADFEB8','ADFEB9','ADFEBA', // Air Force Two (C-32A)
'AE4AE6','AE4AE8','AE4AEA','AE4AEC', // Air Force Two (C-32B)
'AE0865','AE5E76','AE5E77','AE5E79', // Marine One (VH-3D / VH-92A)
]);
// Pre-built aircraft SVGs by type & color
const svgAirlinerCyan = makeAircraftSvg('airliner', 'cyan');
@@ -334,10 +339,10 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
dataTimestamp.current = Date.now();
}, [data?.commercial_flights, data?.ships, data?.satellites]);
// Tick every 2s between data refreshes to animate positions
// Tick every 1s between data refreshes to animate positions
// Satellites move ~7km/s so need frequent updates for smooth motion
useEffect(() => {
const timer = setInterval(() => setInterpTick(t => t + 1), 2000);
const timer = setInterval(() => setInterpTick(t => t + 1), 1000);
return () => clearInterval(timer);
}, []);
@@ -566,8 +571,10 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
type: 'datacenter',
name: dc.name || 'Unknown',
company: dc.company || '',
street: dc.street || '',
city: dc.city || '',
country: dc.country || '',
zip: dc.zip || '',
},
geometry: { type: 'Point' as const, coordinates: [dc.lng, dc.lat] }
}))
@@ -733,10 +740,8 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
// Helper: interpolate a flight's position if airborne and has speed+heading
const interpFlight = (f: any): [number, number] => {
// Fast path: skip trig for stationary/grounded/no-speed aircraft
if (!f.speed_knots || f.speed_knots <= 0 || dtSeconds <= 0) return [f.lng, f.lat];
if (f.alt != null && f.alt <= 100) return [f.lng, f.lat];
// Only interpolate if enough time has passed to matter (>1s)
if (dtSeconds < 1) return [f.lng, f.lat];
const heading = f.true_track || f.heading || 0;
const [newLat, newLng] = interpolatePosition(f.lat, f.lng, heading, f.speed_knots, dtSeconds);
@@ -752,8 +757,6 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
};
// Helper: interpolate a satellite's position between API updates
// Satellites have deterministic orbits so linear interpolation over 60s is accurate
// maxDt=65 allows full interval coverage (60s update + 5s buffer)
const interpSat = (s: any): [number, number] => {
if (!s.speed_knots || s.speed_knots <= 0 || dtSeconds < 1) return [s.lng, s.lat];
const [newLat, newLng] = interpolatePosition(s.lat, s.lng, s.heading || 0, s.speed_knots, dtSeconds, 0, 65);
@@ -768,15 +771,9 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
features: data.satellites.filter((s: any) => s.lat != null && s.lng != null && inView(s.lat, s.lng)).map((s: any, i: number) => ({
type: 'Feature' as const,
properties: {
id: s.id || i,
type: 'satellite',
name: s.name,
mission: s.mission || 'general',
sat_type: s.sat_type || 'Satellite',
country: s.country || '',
alt_km: s.alt_km || 0,
wiki: s.wiki || '',
color: MISSION_COLORS[s.mission] || '#aaaaaa',
id: s.id || i, type: 'satellite', name: s.name, mission: s.mission || 'general',
sat_type: s.sat_type || 'Satellite', country: s.country || '', alt_km: s.alt_km || 0,
wiki: s.wiki || '', color: MISSION_COLORS[s.mission] || '#aaaaaa',
iconId: MISSION_ICON_MAP[s.mission] || 'sat-gen'
},
geometry: { type: 'Point' as const, coordinates: interpSat(s) }
@@ -784,8 +781,6 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
};
}, [activeLayers.satellites, data?.satellites, dtSeconds, inView]);
// Create GeoJSON collections dynamically (this runs ultra fast in pure JS)
const commFlightsGeoJSON = useMemo(() => {
if (!activeLayers.flights || !data?.commercial_flights) return null;
return {
@@ -848,7 +843,6 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
const milFlightsGeoJSON = useMemo(() => {
if (!activeLayers.military || !data?.military_flights) return null;
return {
type: 'FeatureCollection',
features: data.military_flights.map((f: any, i: number) => {
@@ -877,34 +871,21 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
const shipsGeoJSON = useMemo(() => {
if (!(activeLayers.ships_important || activeLayers.ships_civilian || activeLayers.ships_passenger) || !data?.ships) return null;
return {
type: 'FeatureCollection',
features: data.ships.map((s: any, i: number) => {
if (s.lat == null || s.lng == null) return null;
if (!inView(s.lat, s.lng)) return null;
const isImportant = s.type === 'carrier' || s.type === 'military_vessel' || s.type === 'tanker' || s.type === 'cargo';
const isPassenger = s.type === 'passenger';
// Carriers are now handled by a dedicated unclustered source
if (s.type === 'carrier') return null;
if (isImportant && activeLayers?.ships_important === false) return null;
if (isPassenger && activeLayers?.ships_passenger === false) return null;
if (!isImportant && !isPassenger && activeLayers?.ships_civilian === false) return null;
let iconId = 'svgShipBlue';
if (s.type === 'carrier') {
iconId = 'svgCarrier';
} else if (s.type === 'tanker' || s.type === 'cargo') {
iconId = 'svgShipRed';
} else if (s.type === 'yacht' || s.type === 'passenger') {
iconId = 'svgShipWhite';
} else if (s.type === 'military_vessel') {
iconId = 'svgShipYellow';
}
if (s.type === 'tanker' || s.type === 'cargo') iconId = 'svgShipRed';
else if (s.type === 'yacht' || s.type === 'passenger') iconId = 'svgShipWhite';
else if (s.type === 'military_vessel') iconId = 'svgShipYellow';
const [iLng, iLat] = interpShip(s);
return {
type: 'Feature',
@@ -994,11 +975,10 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
type: 'FeatureCollection',
features: data.ships.map((s: any, i: number) => {
if (s.type !== 'carrier' || s.lat == null || s.lng == null) return null;
const [iLng, iLat] = interpShip(s);
return {
type: 'Feature',
properties: { id: i, type: 'ship', name: s.name, rotation: s.heading || 0, iconId: 'svgCarrier' },
geometry: { type: 'Point', coordinates: [iLng, iLat] }
geometry: { type: 'Point', coordinates: [s.lng, s.lat] }
};
}).filter(Boolean)
};
@@ -1030,12 +1010,22 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
}
const features = [];
// Extract IATA codes from "IATA: Airport Name" format
const originCode = (entity.origin_name || '').split(':')[0]?.trim() || '';
const destCode = (entity.dest_name || '').split(':')[0]?.trim() || '';
if (originLoc) {
features.push({
type: 'Feature',
properties: { type: 'route-origin' },
geometry: { type: 'LineString', coordinates: [currentLoc, originLoc] }
});
// Airport dot at origin
features.push({
type: 'Feature',
properties: { type: 'airport', code: originCode, role: 'DEP' },
geometry: { type: 'Point', coordinates: originLoc }
});
}
if (destLoc) {
features.push({
@@ -1043,6 +1033,12 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
properties: { type: 'route-dest' },
geometry: { type: 'LineString', coordinates: [currentLoc, destLoc] }
});
// Airport dot at destination
features.push({
type: 'Feature',
properties: { type: 'airport', code: destCode, role: 'ARR' },
geometry: { type: 'Point', coordinates: destLoc }
});
}
if (features.length === 0) return null;
@@ -1185,6 +1181,7 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
}));
}, [data?.news, Math.round(viewState.zoom)]);
// Tracked flights GeoJSON with interpolation
const trackedFlightsGeoJSON = useMemo(() => {
if (!activeLayers.tracked || !data?.tracked_flights) return null;
@@ -1196,30 +1193,28 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
bizjet: { '#ff1493': 'svgBizjetPink', pink: 'svgBizjetPink', red: 'svgBizjetRed', blue: 'svgBizjetBlue', darkblue: 'svgBizjetDarkBlue', yellow: 'svgBizjetYellow', orange: 'svgBizjetOrange', purple: 'svgBizjetPurple', '#32cd32': 'svgBizjetLime', black: 'svgBizjetBlack', white: 'svgBizjetWhite' },
};
return {
type: 'FeatureCollection',
features: data.tracked_flights.map((f: any, i: number) => {
if (f.lat == null || f.lng == null) return null;
const features: any[] = [];
for (let i = 0; i < data.tracked_flights.length; i++) {
const f = data.tracked_flights[i];
if (f.lat == null || f.lng == null) continue;
const alertColor = f.alert_color || 'white';
const acType = classifyAircraft(f.model, f.aircraft_category);
const grounded = f.alt != null && f.alt <= 100;
const icaoHex = (f.icao24 || '').toUpperCase();
// POTUS fleet gets oversized gold-ringed icon
const isPotus = POTUS_ICAOS.has(icaoHex);
const potusIcon = acType === 'heli' ? 'svgPotusHeli' : 'svgPotusPlane';
const iconId = grounded ? GROUNDED_ICON_MAP[acType] : isPotus ? potusIcon : (trackedIconMap[acType]?.[alertColor] || trackedIconMap.airliner[alertColor] || 'svgAirlinerWhite');
const [lng, lat] = interpFlight(f);
const alertColor = f.alert_color || 'white';
const acType = classifyAircraft(f.model, f.aircraft_category);
const grounded = f.alt != null && f.alt <= 100;
const icaoHex = (f.icao24 || '').toUpperCase();
const isPotus = POTUS_ICAOS.has(icaoHex);
const potusIcon = acType === 'heli' ? 'svgPotusHeli' : 'svgPotusPlane';
const iconId = isPotus ? potusIcon : grounded ? GROUNDED_ICON_MAP[acType] : (trackedIconMap[acType]?.[alertColor] || trackedIconMap.airliner[alertColor] || 'svgAirlinerWhite');
const displayName = f.alert_operator || f.operator || f.owner || f.name || f.callsign || f.icao24 || "UNKNOWN";
const displayName = f.alert_operator || f.operator || f.owner || f.name || f.callsign || f.icao24 || "UNKNOWN";
const [iLng, iLat] = interpFlight(f);
return {
type: 'Feature',
properties: { id: i, type: 'tracked_flight', callsign: String(displayName), rotation: f.heading || 0, iconId },
geometry: { type: 'Point', coordinates: [iLng, iLat] }
};
}).filter(Boolean)
};
features.push({
type: 'Feature',
properties: { id: i, type: 'tracked_flight', callsign: String(displayName), rotation: f.heading || 0, iconId },
geometry: { type: 'Point', coordinates: [lng, lat] }
});
}
return { type: 'FeatureCollection', features };
}, [activeLayers.tracked, data?.tracked_flights, dtSeconds]);
const uavGeoJSON = useMemo(() => {
@@ -1294,6 +1289,7 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
// Interactive layer IDs for click handling
const activeInteractiveLayerIds = [
commFlightsGeoJSON && 'commercial-flights-layer',
privFlightsGeoJSON && 'private-flights-layer',
@@ -1317,19 +1313,15 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
].filter(Boolean) as string[];
// --- Imperative source updates for high-volume layers ---
// Bypasses React reconciliation of huge GeoJSON FeatureCollections.
// The <Source data={EMPTY_FC}> mounts the source; the hook pushes real data.
// --- Imperative source updates: bypass React reconciliation for GeoJSON layers ---
const mapForHook = mapReady ? mapRef.current : null;
// Flights & UAVs: immediate (they move fast, stale = visually wrong)
useImperativeSource(mapForHook, 'commercial-flights', commFlightsGeoJSON);
useImperativeSource(mapForHook, 'private-flights', privFlightsGeoJSON);
useImperativeSource(mapForHook, 'private-jets', privJetsGeoJSON);
useImperativeSource(mapForHook, 'military-flights', milFlightsGeoJSON);
useImperativeSource(mapForHook, 'tracked-flights', trackedFlightsGeoJSON);
useImperativeSource(mapForHook, 'uavs', uavGeoJSON);
// Satellites & fires: 2s debounce (slow-changing, high feature count)
useImperativeSource(mapForHook, 'satellites', satellitesGeoJSON, 2000);
useImperativeSource(mapForHook, 'satellites', satellitesGeoJSON);
useImperativeSource(mapForHook, 'firms-fires', firmsGeoJSON, 2000);
const handleMouseMove = useCallback((evt: any) => {
@@ -1353,7 +1345,7 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
onViewStateChange?.({ zoom: evt.viewState.zoom, latitude: evt.viewState.latitude });
// Debounce bounds update to avoid thrashing during drag
if (boundsTimerRef.current) clearTimeout(boundsTimerRef.current);
boundsTimerRef.current = setTimeout(updateBounds, 300);
boundsTimerRef.current = setTimeout(updateBounds, 500);
}}
onMouseMove={handleMouseMove}
onContextMenu={(evt) => {
@@ -1409,6 +1401,25 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
/>
</Source>
)}
{/* Esri Reference Overlay — borders, labels, cities on top of satellite imagery */}
{activeLayers.highres_satellite && (
<Source
id="esri-reference-overlay"
type="raster"
tiles={['https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}']}
tileSize={256}
maxzoom={18}
>
<Layer
id="esri-reference-overlay-layer"
type="raster"
paint={{
'raster-opacity': 0.9,
'raster-fade-duration': 300
}}
/>
</Source>
)}
{/* NASA GIBS MODIS Terra — daily satellite imagery overlay */}
{activeLayers.gibs_imagery && gibsDate && (
@@ -1635,12 +1646,13 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
<Layer
id="active-route-layer"
type="line"
filter={['in', ['get', 'type'], ['literal', ['route-origin', 'route-dest']]]}
paint={{
'line-color': [
'match',
['get', 'type'],
'route-origin', '#38bdf8', // light blue
'route-dest', '#fcd34d', // yellow
'route-origin', '#38bdf8',
'route-dest', '#fcd34d',
'#ffffff'
],
'line-width': 2,
@@ -1648,6 +1660,38 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
'line-opacity': 0.8
}}
/>
{/* Airport dots at origin/destination */}
<Layer
id="airport-dots"
type="circle"
filter={['==', ['get', 'type'], 'airport']}
paint={{
'circle-radius': 5,
'circle-color': ['match', ['get', 'role'], 'DEP', '#38bdf8', 'ARR', '#fcd34d', '#ffffff'],
'circle-stroke-color': '#000',
'circle-stroke-width': 1.5,
'circle-opacity': 0.9
}}
/>
{/* IATA code labels at airports */}
<Layer
id="airport-labels"
type="symbol"
filter={['==', ['get', 'type'], 'airport']}
layout={{
'text-field': ['get', 'code'],
'text-font': ['Noto Sans Bold'],
'text-size': 11,
'text-offset': [0, -1.4],
'text-anchor': 'bottom',
'text-allow-overlap': true,
}}
paint={{
'text-color': ['match', ['get', 'role'], 'DEP', '#38bdf8', 'ARR', '#fcd34d', '#ffffff'],
'text-halo-color': '#000',
'text-halo-width': 1.5,
}}
/>
</Source>
)}
@@ -1668,12 +1712,33 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
{/* tracked-flights & UAVs: data pushed imperatively */}
<Source id="tracked-flights" type="geojson" data={EMPTY_FC as any}>
{/* Gold halo ring — POTUS aircraft only (Air Force One/Two, Marine One) */}
<Layer
id="tracked-flights-halo"
type="circle"
filter={['any',
['==', ['get', 'iconId'], 'svgPotusPlane'],
['==', ['get', 'iconId'], 'svgPotusHeli'],
]}
paint={{
'circle-radius': 18,
'circle-color': 'transparent',
'circle-stroke-width': 2,
'circle-stroke-color': 'gold',
'circle-stroke-opacity': opacityFilter,
'circle-opacity': 0,
}}
/>
<Layer
id="tracked-flights-layer"
type="symbol"
layout={{
'icon-image': ['get', 'iconId'],
'icon-size': 0.8,
'icon-size': ['case',
['==', ['get', 'iconId'], 'svgPotusPlane'], 1.3,
['==', ['get', 'iconId'], 'svgPotusHeli'], 1.3,
0.8
],
'icon-allow-overlap': true,
'icon-rotate': ['get', 'rotation'],
'icon-rotation-alignment': 'map'
@@ -2463,12 +2528,30 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
Speed: <span style={{ color: '#00e5ff' }}>{ship.sog.toFixed(1)} kn</span>
</div>
)}
{ship.heading != null && (
<div style={{ marginBottom: 4 }}>
Heading: <span style={{ color: '#888' }}>{Math.round(ship.heading)}°</span>
<div style={{ marginBottom: 4 }}>
Heading: <span style={{ color: ship.heading != null ? '#888' : '#ff6644' }}>
{ship.heading != null ? `${Math.round(ship.heading)}°` : 'UNKNOWN'}
</span>
</div>
{ship.type === 'carrier' && ship.source && (
<div style={{ marginTop: 6, padding: '5px 7px', background: 'rgba(255,170,0,0.08)', border: '1px solid rgba(255,170,0,0.3)', borderRadius: 4, fontSize: 9, letterSpacing: 1 }}>
<div style={{ color: '#ffaa00', marginBottom: 3 }}>
SOURCE: {ship.source_url ? (
<a href={ship.source_url} target="_blank" rel="noopener noreferrer"
style={{ color: '#00e5ff', textDecoration: 'underline' }}>{ship.source}</a>
) : (
<span style={{ color: '#fff' }}>{ship.source}</span>
)}
</div>
{ship.last_osint_update && (
<div style={{ color: '#888' }}>LAST OSINT UPDATE: {new Date(ship.last_osint_update).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })}</div>
)}
{ship.desc && (
<div style={{ color: '#aaa', marginTop: 3, fontSize: 8, lineHeight: 1.3 }}>{ship.desc}</div>
)}
</div>
)}
{ship.last_osint_update && (
{ship.type !== 'carrier' && ship.last_osint_update && (
<div style={{ marginBottom: 4 }}>
Last OSINT Update: <span style={{ color: '#888' }}>{new Date(ship.last_osint_update).toLocaleDateString()}</span>
</div>
@@ -2505,6 +2588,11 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
Operator: <span style={{ color: '#c4b5fd' }}>{dc.company}</span>
</div>
)}
{dc.street && (
<div style={{ marginBottom: 4 }}>
Address: <span style={{ color: '#fff' }}>{dc.street}{dc.zip ? ` ${dc.zip}` : ''}</span>
</div>
)}
{dc.city && (
<div style={{ marginBottom: 4 }}>
Location: <span style={{ color: '#fff' }}>{dc.city}{dc.country ? `, ${dc.country}` : ''}</span>
@@ -2741,7 +2829,7 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '60px 20px 20px 20px',
padding: '60px 20px 80px 20px',
}}
onClick={(e) => { if (e.target === e.currentTarget) onEntityClick(null); }}
onKeyDown={(e: any) => { if (e.key === 'Escape') onEntityClick(null); }}
@@ -2750,14 +2838,14 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
>
<div style={{
background: 'rgba(0,0,0,0.95)',
border: '1px solid rgba(59,130,246,0.5)',
border: '1px solid rgba(34,197,94,0.5)',
borderRadius: 12,
overflow: 'hidden',
maxWidth: 'calc(100vw - 40px)',
maxHeight: 'calc(100vh - 80px)',
display: 'flex',
flexDirection: 'column',
boxShadow: '0 0 60px rgba(59,130,246,0.3)',
boxShadow: '0 0 60px rgba(34,197,94,0.3)',
}}>
{/* Header bar */}
<div style={{
@@ -2765,17 +2853,17 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
alignItems: 'center',
justifyContent: 'space-between',
padding: '10px 16px',
background: 'rgba(30,58,138,0.4)',
borderBottom: '1px solid rgba(59,130,246,0.3)',
background: 'rgba(20,83,45,0.4)',
borderBottom: '1px solid rgba(34,197,94,0.3)',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ width: 6, height: 6, borderRadius: '50%', background: '#60a5fa', animation: 'pulse 2s infinite' }} />
<span style={{ fontSize: 11, color: '#60a5fa', fontFamily: 'monospace', letterSpacing: '0.2em', fontWeight: 'bold' }}>
<div style={{ width: 6, height: 6, borderRadius: '50%', background: '#4ade80', animation: 'pulse 2s infinite' }} />
<span style={{ fontSize: 11, color: '#4ade80', fontFamily: 'monospace', letterSpacing: '0.2em', fontWeight: 'bold' }}>
SENTINEL-2 IMAGERY
</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<span style={{ fontSize: 10, color: 'rgba(147,197,253,0.6)', fontFamily: 'monospace' }}>
<span style={{ fontSize: 10, color: 'rgba(134,239,172,0.6)', fontFamily: 'monospace' }}>
{selectedEntity.extra.lat.toFixed(4)}, {selectedEntity.extra.lng.toFixed(4)}
</span>
<button
@@ -2807,11 +2895,11 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
padding: '8px 16px',
fontSize: 11,
fontFamily: 'monospace',
borderBottom: '1px solid rgba(30,58,138,0.4)',
borderBottom: '1px solid rgba(20,83,45,0.4)',
}}>
<span style={{ color: '#93c5fd' }}>{s2.platform}</span>
<span style={{ color: '#22d3ee', fontWeight: 'bold' }}>{s2.datetime?.slice(0, 10)}</span>
<span style={{ color: '#93c5fd' }}>{s2.cloud_cover?.toFixed(0)}% cloud</span>
<span style={{ color: '#86efac' }}>{s2.platform}</span>
<span style={{ color: '#4ade80', fontWeight: 'bold' }}>{s2.datetime?.slice(0, 10)}</span>
<span style={{ color: '#86efac' }}>{s2.cloud_cover?.toFixed(0)}% cloud</span>
</div>
{/* Image */}
@@ -2829,7 +2917,7 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
/>
</div>
) : (
<div style={{ padding: '40px 16px', fontSize: 11, color: 'rgba(147,197,253,0.5)', fontFamily: 'monospace', textAlign: 'center' }}>
<div style={{ padding: '40px 16px', fontSize: 11, color: 'rgba(134,239,172,0.5)', fontFamily: 'monospace', textAlign: 'center' }}>
Scene found no preview available
</div>
)}
@@ -2842,8 +2930,8 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
justifyContent: 'center',
gap: 12,
padding: '10px 16px',
background: 'rgba(30,58,138,0.3)',
borderTop: '1px solid rgba(59,130,246,0.2)',
background: 'rgba(20,83,45,0.3)',
borderTop: '1px solid rgba(34,197,94,0.2)',
}}>
<a
href={imgUrl}
@@ -2851,10 +2939,10 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
target="_blank"
rel="noopener noreferrer"
style={{
background: 'rgba(59,130,246,0.2)',
border: '1px solid rgba(59,130,246,0.5)',
background: 'rgba(34,197,94,0.2)',
border: '1px solid rgba(34,197,94,0.5)',
borderRadius: 6,
color: '#60a5fa',
color: '#4ade80',
fontSize: 10,
fontFamily: 'monospace',
padding: '6px 16px',
@@ -2880,10 +2968,10 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
}
}}
style={{
background: 'rgba(34,211,238,0.15)',
border: '1px solid rgba(34,211,238,0.4)',
background: 'rgba(34,197,94,0.15)',
border: '1px solid rgba(34,197,94,0.4)',
borderRadius: 6,
color: '#22d3ee',
color: '#4ade80',
fontSize: 10,
fontFamily: 'monospace',
padding: '6px 16px',
@@ -2918,7 +3006,7 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
)}
</>
) : (
<div style={{ padding: '40px 16px', fontSize: 11, color: 'rgba(147,197,253,0.5)', fontFamily: 'monospace', textAlign: 'center' }}>
<div style={{ padding: '40px 16px', fontSize: 11, color: 'rgba(134,239,172,0.5)', fontFamily: 'monospace', textAlign: 'center' }}>
No clear imagery in last 30 days
</div>
)}
+2 -2
View File
@@ -14,7 +14,7 @@ const _cache: Record<string, { url: string | null; done: boolean }> = {};
* maxH: Max height class (default "max-h-32")
* accent: Border hover color class (default "hover:border-cyan-500/50")
*/
export default function WikiImage({ wikiUrl, label, maxH = 'max-h-32', accent = 'hover:border-cyan-500/50' }: {
export default function WikiImage({ wikiUrl, label, maxH = 'max-h-52', accent = 'hover:border-cyan-500/50' }: {
wikiUrl: string;
label?: string;
maxH?: string;
@@ -56,7 +56,7 @@ export default function WikiImage({ wikiUrl, label, maxH = 'max-h-32', accent =
<img
src={imgUrl}
alt={label || title.replace(/_/g, ' ')}
className={`w-full h-auto ${maxH} object-cover rounded border border-[var(--border-primary)]/50 ${accent} transition-colors`}
className={`w-full h-auto ${maxH} object-contain rounded border border-[var(--border-primary)]/50 ${accent} transition-colors`}
/>
</a>
)}
+108 -60
View File
@@ -2,7 +2,8 @@
import React, { useState, useEffect, useRef, useMemo } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Plane, AlertTriangle, Activity, Satellite, Cctv, ChevronDown, ChevronUp, Ship, Eye, Anchor, Settings, Sun, Moon, BookOpen, Radio, Play, Pause, Globe, Flame, Wifi, Server, Shield } from "lucide-react";
import { Plane, AlertTriangle, Activity, Satellite, Cctv, ChevronDown, ChevronUp, Ship, Eye, Anchor, Settings, Sun, Moon, BookOpen, Radio, Play, Pause, Globe, Flame, Wifi, Server, Shield, ToggleLeft, ToggleRight } from "lucide-react";
import packageJson from "../../package.json";
import { useTheme } from "@/lib/ThemeContext";
function relativeTime(iso: string | undefined): string {
@@ -62,6 +63,7 @@ const WorldviewLeftPanel = React.memo(function WorldviewLeftPanel({ data, active
const [isMinimized, setIsMinimized] = useState(false);
const { theme, toggleTheme } = useTheme();
const [gibsPlaying, setGibsPlaying] = useState(false);
const [potusEnabled, setPotusEnabled] = useState(true);
const gibsIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
// GIBS time slider play/pause animation
@@ -124,7 +126,7 @@ const WorldviewLeftPanel = React.memo(function WorldviewLeftPanel({ data, active
{ id: "military", name: "Military Flights", source: "adsb.lol", count: data?.military_flights?.length || 0, icon: AlertTriangle },
{ id: "tracked", name: "Tracked Aircraft", source: "Plane-Alert DB", count: data?.tracked_flights?.length || 0, icon: Eye },
{ id: "earthquakes", name: "Earthquakes (24h)", source: "USGS", count: data?.earthquakes?.length || 0, icon: Activity },
{ id: "satellites", name: "Satellites", source: "CelesTrak SGP4", count: data?.satellites?.length || 0, icon: Satellite },
{ id: "satellites", name: "Satellites", source: data?.satellite_source === "celestrak" ? "CelesTrak SGP4" : data?.satellite_source === "tle_api" ? "TLE API · SGP4" : data?.satellite_source === "disk_cache" ? "Cached · SGP4 (est.)" : "CelesTrak SGP4", count: data?.satellites?.length || 0, icon: Satellite },
{ id: "ships_important", name: "Carriers / Mil / Cargo", source: "AIS Stream", count: importantShipCount, 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 },
@@ -158,7 +160,7 @@ const WorldviewLeftPanel = React.memo(function WorldviewLeftPanel({ data, active
<h1 className="text-2xl font-bold tracking-[0.2em] text-[var(--text-heading)]">FLIR</h1>
<button
onClick={toggleTheme}
className="w-7 h-7 rounded-lg border border-[var(--border-primary)] hover:border-cyan-500/50 flex items-center justify-center text-[var(--text-muted)] hover:text-cyan-400 transition-all hover:bg-[var(--hover-accent)]"
className={`w-7 h-7 rounded-lg border border-[var(--border-primary)] hover:border-cyan-500/50 flex items-center justify-center ${theme === 'dark' ? 'text-cyan-400' : 'text-[var(--text-muted)]'} hover:text-cyan-300 transition-all hover:bg-[var(--hover-accent)]`}
title={theme === 'dark' ? 'Switch to Light Mode' : 'Switch to Dark Mode'}
>
{theme === 'dark' ? <Sun size={14} /> : <Moon size={14} />}
@@ -166,7 +168,7 @@ const WorldviewLeftPanel = React.memo(function WorldviewLeftPanel({ data, active
{onSettingsClick && (
<button
onClick={onSettingsClick}
className="w-7 h-7 rounded-lg border border-[var(--border-primary)] hover:border-cyan-500/50 flex items-center justify-center text-[var(--text-muted)] hover:text-cyan-400 transition-all hover:bg-[var(--hover-accent)] group"
className={`w-7 h-7 rounded-lg border border-[var(--border-primary)] hover:border-cyan-500/50 flex items-center justify-center ${theme === 'dark' ? 'text-cyan-400' : 'text-[var(--text-muted)]'} hover:text-cyan-300 transition-all hover:bg-[var(--hover-accent)] group`}
title="System Settings"
>
<Settings size={14} className="group-hover:rotate-90 transition-transform duration-300" />
@@ -175,13 +177,16 @@ const WorldviewLeftPanel = React.memo(function WorldviewLeftPanel({ data, active
{onLegendClick && (
<button
onClick={onLegendClick}
className="h-7 px-2 rounded-lg border border-[var(--border-primary)] hover:border-cyan-500/50 flex items-center justify-center gap-1 text-[var(--text-muted)] hover:text-cyan-400 transition-all hover:bg-[var(--hover-accent)]"
className={`h-7 px-2 rounded-lg border border-[var(--border-primary)] hover:border-cyan-500/50 flex items-center justify-center gap-1 ${theme === 'dark' ? 'text-cyan-400' : 'text-[var(--text-muted)]'} hover:text-cyan-300 transition-all hover:bg-[var(--hover-accent)]`}
title="Map Legend / Icon Key"
>
<BookOpen size={12} />
<span className="text-[8px] font-mono tracking-widest font-bold">KEY</span>
</button>
)}
<span className={`h-7 px-2 rounded-lg border border-[var(--border-primary)] flex items-center justify-center text-[8px] ${theme === 'dark' ? 'text-cyan-400' : 'text-[var(--text-muted)]'} font-mono tracking-widest select-none`}>
v{packageJson.version}
</span>
</div>
</div>
@@ -191,12 +196,30 @@ const WorldviewLeftPanel = React.memo(function WorldviewLeftPanel({ data, active
{/* Header / Toggle */}
<div
className="flex justify-between items-center p-4 cursor-pointer hover:bg-[var(--bg-secondary)]/50 transition-colors border-b border-[var(--border-primary)]/50"
onClick={() => setIsMinimized(!isMinimized)}
>
<span className="text-[10px] text-[var(--text-muted)] font-mono tracking-widest">DATA LAYERS</span>
<button className="text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors">
{isMinimized ? <ChevronDown size={14} /> : <ChevronUp size={14} />}
</button>
<span className="text-[10px] text-[var(--text-muted)] font-mono tracking-widest" onClick={() => setIsMinimized(!isMinimized)}>DATA LAYERS</span>
<div className="flex items-center gap-2">
<button
title={Object.entries(activeLayers).filter(([k]) => k !== 'gibs_imagery').every(([, v]) => v) ? "Disable all layers" : "Enable all layers"}
className={`${Object.entries(activeLayers).filter(([k]) => k !== 'gibs_imagery').every(([, v]) => v) ? 'text-cyan-400' : 'text-[var(--text-muted)]'} hover:text-cyan-400 transition-colors`}
onClick={(e) => {
e.stopPropagation();
const allOn = Object.entries(activeLayers).filter(([k]) => k !== 'gibs_imagery').every(([, v]) => v);
setActiveLayers((prev: any) => {
const next: any = {};
for (const k of Object.keys(prev)) {
next[k] = k === 'gibs_imagery' ? false : !allOn;
}
return next;
});
}}
>
{Object.entries(activeLayers).filter(([k]) => k !== 'gibs_imagery').every(([, v]) => v) ? <ToggleRight size={16} /> : <ToggleLeft size={16} />}
</button>
<button className="text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors" onClick={() => setIsMinimized(!isMinimized)}>
{isMinimized ? <ChevronDown size={14} /> : <ChevronUp size={14} />}
</button>
</div>
</div>
<AnimatePresence>
@@ -208,6 +231,61 @@ const WorldviewLeftPanel = React.memo(function WorldviewLeftPanel({ data, active
className="overflow-y-auto styled-scrollbar"
>
<div className="flex flex-col gap-6 p-4 pt-2 pb-6">
{/* POTUS Fleet — pinned to TOP when aircraft are active */}
{potusEnabled && potusFlights.length > 0 && (
<div className="bg-[#ff1493]/5 border border-[#ff1493]/30 rounded-lg p-3 -mt-1">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Shield size={14} className="text-[#ff1493]" />
<span className="text-[10px] text-[#ff1493] font-mono tracking-widest font-bold">POTUS FLEET</span>
<span className="text-[9px] font-mono px-1.5 py-0.5 rounded-full bg-[#ff1493]/20 border border-[#ff1493]/40 text-[#ff1493] animate-pulse">
{potusFlights.length} ACTIVE
</span>
</div>
<button
onClick={(e) => { e.stopPropagation(); setPotusEnabled(false); }}
className="text-[8px] font-mono text-[var(--text-muted)] hover:text-[#ff1493] border border-[var(--border-primary)] hover:border-[#ff1493]/40 rounded px-1.5 py-0.5 transition-colors"
title="Hide POTUS Fleet tracker"
>
HIDE
</button>
</div>
<div className="flex flex-col gap-2">
{potusFlights.map((pf) => {
const color = pf.meta.type === 'AF1' ? '#ff1493' : pf.meta.type === 'M1' ? '#ff1493' : '#3b82f6';
const alt = pf.flight.alt_baro || pf.flight.alt || 0;
const speed = pf.flight.gs || pf.flight.speed || 0;
return (
<div
key={pf.flight.icao24}
className="flex items-center justify-between p-2 rounded-lg border cursor-pointer transition-all hover:bg-[var(--bg-secondary)]/60"
style={{ borderColor: `${color}40`, background: `${color}10` }}
onClick={() => {
if (onFlyTo && pf.flight.lat != null && pf.flight.lng != null) {
onFlyTo(pf.flight.lat, pf.flight.lng);
}
if (onEntityClick) {
onEntityClick({ type: 'tracked_flight', id: pf.index });
}
}}
>
<div className="flex flex-col">
<span className="text-[10px] font-bold font-mono" style={{ color }}>{pf.meta.label}</span>
<span className="text-[8px] text-[var(--text-muted)] font-mono mt-0.5">
{alt > 0 ? `${Math.round(alt).toLocaleString()} ft` : 'GND'} · {speed > 0 ? `${Math.round(speed)} kts` : 'STATIC'}
</span>
</div>
<div className="flex items-center gap-1.5">
<div className="w-1.5 h-1.5 rounded-full animate-pulse" style={{ backgroundColor: color }} />
<span className="text-[8px] font-mono" style={{ color }}>TRACK</span>
</div>
</div>
);
})}
</div>
</div>
)}
{layers.map((layer, idx) => {
const Icon = layer.icon;
const active = activeLayers[layer.id as keyof typeof activeLayers] || false;
@@ -294,57 +372,27 @@ const WorldviewLeftPanel = React.memo(function WorldviewLeftPanel({ data, active
)
})}
{/* POTUS Fleet Tracker */}
<div className="border-t border-[var(--border-primary)]/50 pt-4 mt-2">
<div className="flex items-center gap-2 mb-3">
<Shield size={14} className="text-[#ff1493]" />
<span className="text-[10px] text-[#ff1493] font-mono tracking-widest font-bold">POTUS FLEET</span>
{potusFlights.length > 0 && (
<span className="text-[9px] font-mono px-1.5 py-0.5 rounded-full bg-[#ff1493]/20 border border-[#ff1493]/40 text-[#ff1493] animate-pulse">
{potusFlights.length} ACTIVE
</span>
)}
{/* POTUS Fleet — bottom section when inactive or hidden */}
{(potusFlights.length === 0 || !potusEnabled) && (
<div className="border-t border-[var(--border-primary)]/50 pt-4 mt-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Shield size={14} className="text-[var(--text-muted)]" />
<span className="text-[10px] text-[var(--text-muted)] font-mono tracking-widest">POTUS FLEET</span>
</div>
{!potusEnabled ? (
<button
onClick={(e) => { e.stopPropagation(); setPotusEnabled(true); }}
className="text-[8px] font-mono text-[var(--text-muted)] hover:text-[#ff1493] border border-[var(--border-primary)] hover:border-[#ff1493]/40 rounded px-1.5 py-0.5 transition-colors"
>
SHOW
</button>
) : (
<span className="text-[8px] font-mono text-[var(--text-muted)]">NO ACTIVE AIRCRAFT</span>
)}
</div>
</div>
{potusFlights.length === 0 ? (
<div className="ml-5 text-[9px] text-[var(--text-muted)] font-mono">
No POTUS fleet aircraft currently airborne
</div>
) : (
<div className="flex flex-col gap-2 ml-1">
{potusFlights.map((pf) => {
const color = pf.meta.type === 'AF1' ? '#ff1493' : pf.meta.type === 'M1' ? '#ff1493' : '#3b82f6';
const alt = pf.flight.alt_baro || pf.flight.alt || 0;
const speed = pf.flight.gs || pf.flight.speed || 0;
return (
<div
key={pf.flight.icao24}
className="flex items-center justify-between p-2 rounded-lg border cursor-pointer transition-all hover:bg-[var(--bg-secondary)]/60"
style={{ borderColor: `${color}40`, background: `${color}10` }}
onClick={() => {
if (onFlyTo && pf.flight.lat != null && pf.flight.lng != null) {
onFlyTo(pf.flight.lat, pf.flight.lng);
}
if (onEntityClick) {
onEntityClick({ type: 'tracked_flight', id: pf.index });
}
}}
>
<div className="flex flex-col">
<span className="text-[10px] font-bold font-mono" style={{ color }}>{pf.meta.label}</span>
<span className="text-[8px] text-[var(--text-muted)] font-mono mt-0.5">
{alt > 0 ? `${Math.round(alt).toLocaleString()} ft` : 'GND'} · {speed > 0 ? `${Math.round(speed)} kts` : 'STATIC'}
</span>
</div>
<div className="flex items-center gap-1.5">
<div className="w-1.5 h-1.5 rounded-full animate-pulse" style={{ backgroundColor: color }} />
<span className="text-[8px] font-mono" style={{ color }}>TRACK</span>
</div>
</div>
);
})}
</div>
)}
</div>
)}
</div>
</motion.div>
)}