feat: POTUS Fleet tracker, Docker secrets, route fix, SQLite->JSON migration

- Add Docker Swarm secrets _FILE support (AIS_API_KEY_FILE, etc.)
- Fix flight route lookup: pass lat/lng to adsb.lol routeset API, return airport names
- Replace SQLite plane_alert DB with JSON file + O(1) category color mapping
- Add POTUS Fleet (AF1, AF2, Marine One) with hardcoded ICAO overrides
- Add tracked_names enrichment from Excel data with POTUS protection
- Add oversized gold-ringed POTUS SVG icons on map
- Add POTUS Fleet tracker panel in WorldviewLeftPanel with fly-to
- Overhaul tracked flight labels: zoom-gated, PIA hidden, color-mapped
- Add orange color to trackedIconMap, soften white icon strokes
- Fix NewsFeed Wikipedia links to use alert_wiki slug


Former-commit-id: 6f952104c1
This commit is contained in:
anoracleofra-code
2026-03-11 12:28:04 -06:00
parent ac62e4763f
commit 5ab02e821f
9 changed files with 423 additions and 121 deletions
-1
View File
@@ -1 +0,0 @@
{}
@@ -0,0 +1 @@
eb32e2f229fad1d5a14fe81e071e71591f875673
+46 -9
View File
@@ -1,3 +1,40 @@
import os
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Docker Swarm Secrets support
# For each VAR below, if VAR_FILE is set (e.g. AIS_API_KEY_FILE=/run/secrets/AIS_API_KEY),
# the file is read and its trimmed content is placed into VAR.
# This MUST run before service imports — modules read os.environ at import time.
# ---------------------------------------------------------------------------
_SECRET_VARS = [
"AIS_API_KEY",
"OPENSKY_CLIENT_ID",
"OPENSKY_CLIENT_SECRET",
"LTA_ACCOUNT_KEY",
"CORS_ORIGINS",
]
for _var in _SECRET_VARS:
_file_var = f"{_var}_FILE"
_file_path = os.environ.get(_file_var)
if _file_path:
try:
with open(_file_path, "r") as _f:
_value = _f.read().strip()
if _value:
os.environ[_var] = _value
logger.info(f"Loaded secret {_var} from {_file_path}")
else:
logger.warning(f"Secret file {_file_path} for {_var} is empty")
except FileNotFoundError:
logger.error(f"Secret file {_file_path} for {_var} not found")
except Exception as _e:
logger.error(f"Failed to read secret file {_file_path} for {_var}: {_e}")
from fastapi import FastAPI, Request, Response
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
@@ -5,14 +42,10 @@ from services.data_fetcher import start_scheduler, stop_scheduler, get_latest_da
from services.ais_stream import start_ais_stream, stop_ais_stream
from services.carrier_tracker import start_carrier_tracker, stop_carrier_tracker
import uvicorn
import logging
import hashlib
import json as json_mod
import os
import socket
logging.basicConfig(level=logging.INFO)
def _build_cors_origins():
"""Build a CORS origins whitelist: localhost + LAN IPs + env overrides.
@@ -187,9 +220,9 @@ async def api_get_nearest_radios_list(lat: float, lng: float, limit: int = 5):
from services.network_utils import fetch_with_curl
@app.get("/api/route/{callsign}")
async def get_flight_route(callsign: str):
r = fetch_with_curl("https://api.adsb.lol/api/0/routeset", method="POST", json_data={"planes": [{"callsign": callsign}]}, timeout=10)
if r.status_code == 200:
async def get_flight_route(callsign: str, lat: float = 0.0, lng: float = 0.0):
r = fetch_with_curl("https://api.adsb.lol/api/0/routeset", method="POST", json_data={"planes": [{"callsign": callsign, "lat": lat, "lng": lng}]}, timeout=10)
if r and r.status_code == 200:
data = r.json()
route_list = []
if isinstance(data, dict):
@@ -201,9 +234,13 @@ async def get_flight_route(callsign: str):
route = route_list[0]
airports = route.get("_airports", [])
if len(airports) >= 2:
orig = airports[0]
dest = airports[-1]
return {
"orig_loc": [airports[0].get("lon", 0), airports[0].get("lat", 0)],
"dest_loc": [airports[-1].get("lon", 0), airports[-1].get("lat", 0)]
"orig_loc": [orig.get("lon", 0), orig.get("lat", 0)],
"dest_loc": [dest.get("lon", 0), dest.get("lat", 0)],
"origin_name": f"{orig.get('iata', '') or orig.get('icao', '')}: {orig.get('name', 'Unknown')}",
"dest_name": f"{dest.get('iata', '') or dest.get('icao', '')}: {dest.get('name', 'Unknown')}",
}
return {}
+121 -40
View File
@@ -142,56 +142,132 @@ def _mark_fresh(*keys):
_data_lock = threading.Lock()
# ---------------------------------------------------------------------------
# Plane-Alert DB — load tracked aircraft from CSV on startup
# Plane-Alert DB — load tracked aircraft from JSON on startup
# ---------------------------------------------------------------------------
# Category → color mapping
_PINK_CATEGORIES = {
"Dictator Alert", "Head of State", "Da Comrade", "Oligarch",
"Governments", "Royal Aircraft", "Quango",
}
_RED_CATEGORIES = {
"Don't you know who I am?", "As Seen on TV", "Joe Cool",
"Vanity Plate", "Football", "Bizjets",
}
_DARKBLUE_CATEGORIES = {
"USAF", "United States Navy", "United States Marine Corps",
"Special Forces", "Hired Gun", "Oxcart", "Gunship", "Nuclear",
"CAP", "Zoomies",
# Exact category → color mapping for all 53 known categories.
# O(1) dict lookup — no keyword scanning, no false positives.
_CATEGORY_COLOR: dict[str, str] = {
# YELLOW — Military / Intelligence / Defense
"USAF": "yellow",
"Other Air Forces": "yellow",
"Toy Soldiers": "yellow",
"Oxcart": "yellow",
"United States Navy": "yellow",
"GAF": "yellow",
"Hired Gun": "yellow",
"United States Marine Corps": "yellow",
"Gunship": "yellow",
"RAF": "yellow",
"Other Navies": "yellow",
"Special Forces": "yellow",
"Zoomies": "yellow",
"Royal Navy Fleet Air Arm": "yellow",
"Army Air Corps": "yellow",
"Aerobatic Teams": "yellow",
"UAV": "yellow",
"Ukraine": "yellow",
"Nuclear": "yellow",
# LIME — Emergency / Medical / Rescue / Fire
"Flying Doctors": "#32cd32",
"Aerial Firefighter": "#32cd32",
"Coastguard": "#32cd32",
# BLUE — Government / Law Enforcement / Civil
"Police Forces": "blue",
"Governments": "blue",
"Quango": "blue",
"UK National Police Air Service": "blue",
"CAP": "blue",
# BLACK — Privacy / PIA
"PIA": "black",
# RED — Dictator / Oligarch
"Dictator Alert": "red",
"Da Comrade": "red",
"Oligarch": "red",
# HOT PINK — High Value Assets / VIP / Celebrity
"Head of State": "#ff1493",
"Royal Aircraft": "#ff1493",
"Don't you know who I am?": "#ff1493",
"As Seen on TV": "#ff1493",
"Bizjets": "#ff1493",
"Vanity Plate": "#ff1493",
"Football": "#ff1493",
# ORANGE — Joe Cool
"Joe Cool": "orange",
# WHITE — Climate Crisis
"Climate Crisis": "white",
# PURPLE — General Tracked / Other Notable
"Historic": "purple",
"Jump Johnny Jump": "purple",
"Ptolemy would be proud": "purple",
"Distinctive": "purple",
"Dogs with Jobs": "purple",
"You came here in that thing?": "purple",
"Big Hello": "purple",
"Watch Me Fly": "purple",
"Perfectly Serviceable Aircraft": "purple",
"Jesus he Knows me": "purple",
"Gas Bags": "purple",
"Radiohead": "purple",
}
def _category_to_color(cat: str) -> str:
if cat in _PINK_CATEGORIES:
return "pink"
if cat in _RED_CATEGORIES:
return "red"
if cat in _DARKBLUE_CATEGORIES:
return "darkblue"
return "white"
"""O(1) exact lookup. Unknown categories default to purple."""
return _CATEGORY_COLOR.get(cat, "purple")
# Load once on module import
_PLANE_ALERT_DB: dict = {} # uppercase ICAO hex → dict of aircraft info
_PLANE_ALERT_DB: dict = {}
# ---------------------------------------------------------------------------
# POTUS Fleet — override colors and operator names for presidential aircraft.
# These are hardcoded ICAO hexes verified against FAA registry + plane-alert.
# ---------------------------------------------------------------------------
_POTUS_FLEET: dict[str, dict] = {
# Air Force One — Boeing VC-25A (747-200B)
"ADFDF8": {"color": "#ff1493", "operator": "Air Force One (82-8000)", "category": "Head of State", "wiki": "Air_Force_One", "fleet": "AF1"},
"ADFDF9": {"color": "#ff1493", "operator": "Air Force One (92-9000)", "category": "Head of State", "wiki": "Air_Force_One", "fleet": "AF1"},
# Air Force Two — Boeing C-32A (757-200)
"ADFEB7": {"color": "blue", "operator": "Air Force Two (98-0001)", "category": "Governments", "wiki": "Air_Force_Two", "fleet": "AF2"},
"ADFEB8": {"color": "blue", "operator": "Air Force Two (98-0002)", "category": "Governments", "wiki": "Air_Force_Two", "fleet": "AF2"},
"ADFEB9": {"color": "blue", "operator": "Air Force Two (99-0003)", "category": "Governments", "wiki": "Air_Force_Two", "fleet": "AF2"},
"ADFEBA": {"color": "blue", "operator": "Air Force Two (99-0004)", "category": "Governments", "wiki": "Air_Force_Two", "fleet": "AF2"},
"AE4AE6": {"color": "blue", "operator": "Air Force Two (09-0015)", "category": "Governments", "wiki": "Air_Force_Two", "fleet": "AF2"},
"AE4AE8": {"color": "blue", "operator": "Air Force Two (09-0016)", "category": "Governments", "wiki": "Air_Force_Two", "fleet": "AF2"},
"AE4AEA": {"color": "blue", "operator": "Air Force Two (09-0017)", "category": "Governments", "wiki": "Air_Force_Two", "fleet": "AF2"},
"AE4AEC": {"color": "blue", "operator": "Air Force Two (19-0018)", "category": "Governments", "wiki": "Air_Force_Two", "fleet": "AF2"},
# Marine One — VH-3D Sea King / VH-92A Patriot
"AE0865": {"color": "#ff1493", "operator": "Marine One (VH-3D)", "category": "Head of State", "wiki": "Marine_One", "fleet": "M1"},
"AE5E76": {"color": "#ff1493", "operator": "Marine One (VH-92A)", "category": "Head of State", "wiki": "Marine_One", "fleet": "M1"},
"AE5E77": {"color": "#ff1493", "operator": "Marine One (VH-92A)", "category": "Head of State", "wiki": "Marine_One", "fleet": "M1"},
"AE5E79": {"color": "#ff1493", "operator": "Marine One (VH-92A)", "category": "Head of State", "wiki": "Marine_One", "fleet": "M1"},
}
def _load_plane_alert_db():
"""Parse plane_alert_db.json into a dict keyed by uppercase ICAO hex."""
"""Load plane_alert_db.json (exported from SQLite) into memory."""
global _PLANE_ALERT_DB
import json
json_path = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"data", "plane_alert_db.json"
)
if not os.path.exists(json_path):
logger.warning(f"Plane-Alert JSON DB not found at {json_path}")
logger.warning(f"Plane-Alert DB not found at {json_path}")
return
try:
with open(json_path, "r", encoding="utf-8") as fh:
data = json.load(fh)
for icao_hex, info in data.items():
info["color"] = _category_to_color(info.get("category", ""))
_PLANE_ALERT_DB[icao_hex] = info
logger.info(f"Plane-Alert JSON DB loaded: {len(_PLANE_ALERT_DB)} aircraft")
raw = json.load(fh)
for icao_hex, info in raw.items():
info["color"] = _category_to_color(info.get("category", ""))
# Apply POTUS fleet overrides (correct colors + clean operator names)
override = _POTUS_FLEET.get(icao_hex)
if override:
info["color"] = override["color"]
info["operator"] = override["operator"]
info["category"] = override["category"]
info["wiki"] = override.get("wiki", "")
info["potus_fleet"] = override.get("fleet", "")
_PLANE_ALERT_DB[icao_hex] = info
logger.info(f"Plane-Alert DB loaded: {len(_PLANE_ALERT_DB)} aircraft")
except Exception as e:
logger.error(f"Failed to load Plane-Alert JSON DB: {e}")
logger.error(f"Failed to load Plane-Alert DB: {e}")
_load_plane_alert_db()
@@ -204,11 +280,12 @@ def enrich_with_plane_alert(flight: dict) -> dict:
flight["alert_color"] = info["color"]
flight["alert_operator"] = info["operator"]
flight["alert_type"] = info["ac_type"]
flight["alert_tag1"] = info["tag1"]
flight["alert_tag2"] = info["tag2"]
flight["alert_tag3"] = info["tag3"]
flight["alert_tags"] = info["tags"]
flight["alert_link"] = info["link"]
# Override registration if DB has a better one
if info.get("wiki"):
flight["alert_wiki"] = info["wiki"]
if info.get("potus_fleet"):
flight["potus_fleet"] = info["potus_fleet"]
if info["registration"]:
flight["registration"] = info["registration"]
@@ -245,18 +322,22 @@ _load_tracked_names()
def enrich_with_tracked_names(flight: dict) -> dict:
"""If flight's registration matches our Excel extraction, tag it as tracked."""
# POTUS fleet overrides are authoritative — never let Excel overwrite them
icao = flight.get("icao24", "").strip().upper()
if icao in _POTUS_FLEET:
return flight
reg = flight.get("registration", "").strip().upper()
callsign = flight.get("callsign", "").strip().upper()
match = None
if reg and reg in _TRACKED_NAMES_DB:
match = _TRACKED_NAMES_DB[reg]
elif callsign and callsign in _TRACKED_NAMES_DB:
match = _TRACKED_NAMES_DB[callsign]
if match:
# Don't overwrite Plane-Alert DB operator if it exists unless we want Excel to take precedence.
# Let's let Excel take precedence as it has cleaner individual names (e.g. Elon Musk instead of FALCON LANDING LLC).
# Let Excel take precedence as it has cleaner individual names (e.g. Elon Musk instead of FALCON LANDING LLC).
flight["alert_operator"] = match["name"]
flight["alert_category"] = match["category"]
if "alert_color" not in flight:
+26 -10
View File
@@ -298,23 +298,32 @@ export default function Dashboard() {
const slowEtag = useRef<string | null>(null);
useEffect(() => {
// Track whether we've received substantial data yet (backend may still be starting up)
let hasData = false;
let fastTimerId: ReturnType<typeof setTimeout> | null = null;
let slowTimerId: ReturnType<typeof setTimeout> | null = null;
const fetchFastData = async () => {
try {
const headers: Record<string, string> = {};
if (fastEtag.current) headers['If-None-Match'] = fastEtag.current;
const res = await fetch(`${API_BASE}/api/live-data/fast`, { headers });
if (res.status === 304) { setBackendStatus('connected'); return; }
if (res.status === 304) { setBackendStatus('connected'); scheduleNext('fast'); return; }
if (res.ok) {
setBackendStatus('connected');
fastEtag.current = res.headers.get('etag') || null;
const json = await res.json();
dataRef.current = { ...dataRef.current, ...json };
setDataVersion(v => v + 1);
// Check if we got real data (backend finished loading)
const flights = json.commercial_flights?.length || 0;
if (flights > 100) hasData = true;
}
} catch (e) {
console.error("Failed fetching fast live data", e);
setBackendStatus('disconnected');
}
scheduleNext('fast');
};
const fetchSlowData = async () => {
@@ -322,7 +331,7 @@ export default function Dashboard() {
const headers: Record<string, string> = {};
if (slowEtag.current) headers['If-None-Match'] = slowEtag.current;
const res = await fetch(`${API_BASE}/api/live-data/slow`, { headers });
if (res.status === 304) return;
if (res.status === 304) { scheduleNext('slow'); return; }
if (res.ok) {
slowEtag.current = res.headers.get('etag') || null;
const json = await res.json();
@@ -332,19 +341,26 @@ export default function Dashboard() {
} catch (e) {
console.error("Failed fetching slow live data", e);
}
scheduleNext('slow');
};
// Adaptive polling: retry every 3s during startup, back off to normal cadence once data arrives
const scheduleNext = (tier: 'fast' | 'slow') => {
if (tier === 'fast') {
const delay = hasData ? 60000 : 3000; // 3s startup retry → 60s steady state
fastTimerId = setTimeout(fetchFastData, delay);
} else {
const delay = hasData ? 120000 : 5000; // 5s startup retry → 120s steady state
slowTimerId = setTimeout(fetchSlowData, delay);
}
};
fetchFastData();
fetchSlowData();
// Fast polling: 60s (matches backend update cadence — was 15s, wasting 75% on 304s)
// Slow polling: 120s (backend updates every 30min)
const fastInterval = setInterval(fetchFastData, 60000);
const slowInterval = setInterval(fetchSlowData, 120000);
return () => {
clearInterval(fastInterval);
clearInterval(slowInterval);
if (fastTimerId) clearTimeout(fastTimerId);
if (slowTimerId) clearTimeout(slowTimerId);
};
}, []);
@@ -418,7 +434,7 @@ export default function Dashboard() {
{/* LEFT HUD CONTAINER */}
<div className="absolute left-6 top-24 bottom-6 w-80 flex flex-col gap-6 z-[200] pointer-events-none">
{/* LEFT PANEL - DATA LAYERS */}
<WorldviewLeftPanel data={data} activeLayers={activeLayers} setActiveLayers={setActiveLayers} onSettingsClick={() => setSettingsOpen(true)} onLegendClick={() => setLegendOpen(true)} gibsDate={gibsDate} setGibsDate={setGibsDate} gibsOpacity={gibsOpacity} setGibsOpacity={setGibsOpacity} />
<WorldviewLeftPanel data={data} activeLayers={activeLayers} setActiveLayers={setActiveLayers} onSettingsClick={() => setSettingsOpen(true)} onLegendClick={() => setLegendOpen(true)} gibsDate={gibsDate} setGibsDate={setGibsDate} gibsOpacity={gibsOpacity} setGibsOpacity={setGibsOpacity} onEntityClick={setSelectedEntity} onFlyTo={(lat, lng) => setFlyToLocation({ lat, lng, ts: Date.now() })} />
{/* LEFT BOTTOM - DISPLAY CONFIG */}
<WorldviewRightPanel effects={effects} setEffects={setEffects} setUiVisible={setUiVisible} />
+88 -14
View File
@@ -26,10 +26,12 @@ const svgPlanePink = `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="
const svgPlaneAlertRed = `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="#FF2020" stroke="black"><path d="M21 16v-2l-8-5V3.5c0-.83-.67-1.5-1.5-1.5S10 2.67 10 3.5V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z" /></svg>`)}`;
const svgPlaneDarkBlue = `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="#1A3A8A" stroke="#4A80D0"><path d="M21 16v-2l-8-5V3.5c0-.83-.67-1.5-1.5-1.5S10 2.67 10 3.5V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z" /></svg>`)}`;
const svgPlaneWhiteAlert = `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="white" stroke="#ff0000" stroke-width="2"><path d="M21 16v-2l-8-5V3.5c0-.83-.67-1.5-1.5-1.5S10 2.67 10 3.5V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z" /></svg>`)}`;
const svgHeliPink = `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="#ff66b2" stroke="black"><path d="M10 6L10 14L8 16L8 18L10 17L12 22L14 17L16 18L16 16L14 14L14 6C14 4 13 2 12 2C11 2 10 4 10 6Z"/><circle cx="12" cy="12" r="8" fill="none" stroke="#ff66b2" stroke-dasharray="2 2" stroke-width="1"/></svg>`)}`;
const svgHeliPink = `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="#FF1493" stroke="black"><path d="M10 6L10 14L8 16L8 18L10 17L12 22L14 17L16 18L16 16L14 14L14 6C14 4 13 2 12 2C11 2 10 4 10 6Z"/><circle cx="12" cy="12" r="8" fill="none" stroke="#FF1493" stroke-dasharray="2 2" stroke-width="1"/></svg>`)}`;
const svgHeliAlertRed = `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="#ff0000" stroke="black"><path d="M10 6L10 14L8 16L8 18L10 17L12 22L14 17L16 18L16 16L14 14L14 6C14 4 13 2 12 2C11 2 10 4 10 6Z"/><circle cx="12" cy="12" r="8" fill="none" stroke="#ff0000" stroke-dasharray="2 2" stroke-width="1"/></svg>`)}`;
const svgHeliDarkBlue = `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="#000080" stroke="#4A80D0"><path d="M10 6L10 14L8 16L8 18L10 17L12 22L14 17L16 18L16 16L14 14L14 6C14 4 13 2 12 2C11 2 10 4 10 6Z"/><circle cx="12" cy="12" r="8" fill="none" stroke="#4A80D0" stroke-dasharray="2 2" stroke-width="1"/></svg>`)}`;
const svgHeliWhiteAlert = `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="white" stroke="#ff0000"><path d="M10 6L10 14L8 16L8 18L10 17L12 22L14 17L16 18L16 16L14 14L14 6C14 4 13 2 12 2C11 2 10 4 10 6Z"/><circle cx="12" cy="12" r="8" fill="none" stroke="#ff0000" stroke-dasharray="2 2" stroke-width="1"/></svg>`)}`;
const svgHeliBlue = `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="#3b82f6" stroke="black"><path d="M10 6L10 14L8 16L8 18L10 17L12 22L14 17L16 18L16 16L14 14L14 6C14 4 13 2 12 2C11 2 10 4 10 6Z"/><circle cx="12" cy="12" r="8" fill="none" stroke="#3b82f6" stroke-dasharray="2 2" stroke-width="1"/></svg>`)}`;
const svgHeliLime = `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="#32CD32" stroke="black"><path d="M10 6L10 14L8 16L8 18L10 17L12 22L14 17L16 18L16 16L14 14L14 6C14 4 13 2 12 2C11 2 10 4 10 6Z"/><circle cx="12" cy="12" r="8" fill="none" stroke="#32CD32" stroke-dasharray="2 2" stroke-width="1"/></svg>`)}`;
const svgHeliWhiteAlert = `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="white" stroke="#666"><path d="M10 6L10 14L8 16L8 18L10 17L12 22L14 17L16 18L16 16L14 14L14 6C14 4 13 2 12 2C11 2 10 4 10 6Z"/><circle cx="12" cy="12" r="8" fill="none" stroke="#999" stroke-dasharray="2 2" stroke-width="1"/></svg>`)}`;
const svgPlaneBlack = `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="#222" stroke="#444"><path d="M21 16v-2l-8-5V3.5c0-.83-.67-1.5-1.5-1.5S10 2.67 10 3.5V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z" /></svg>`)}`;
const svgHeliBlack = `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="#222" stroke="#444"><path d="M10 6L10 14L8 16L8 18L10 17L12 22L14 17L16 18L16 16L14 14L14 6C14 4 13 2 12 2C11 2 10 4 10 6Z"/><circle cx="12" cy="12" r="8" fill="none" stroke="#444" stroke-dasharray="2 2" stroke-width="1"/></svg>`)}`;
const svgDrone = `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="orange" stroke="black"><path d="M12 2L15 8H9L12 2Z" /><rect x="8" y="8" width="8" height="2" /><path d="M4 10L10 14H14L20 10V12L14 16H10L4 12V10Z" /><circle cx="12" cy="14" r="2" fill="red"/></svg>`)}`;
@@ -88,6 +90,13 @@ function makeAircraftSvg(type: 'airliner' | 'turboprop' | 'bizjet' | 'generic',
return `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="${fill}" stroke="${stroke}"><path d="${p}"/>${extras}</svg>`)}`;
}
// POTUS fleet — oversized hot pink with yellow halo ring
const svgPotusPlane = `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(4,4)"><path d="${AIRLINER_PATH}" fill="#FF1493" stroke="black"/><circle cx="7" cy="12.5" r="1.2" fill="#FF1493" stroke="black" stroke-width="0.5"/><circle cx="17" cy="12.5" r="1.2" fill="#FF1493" stroke="black" stroke-width="0.5"/></g></svg>`)}`;
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']);
// Pre-built aircraft SVGs by type & color
const svgAirlinerCyan = makeAircraftSvg('airliner', 'cyan');
const svgAirlinerOrange = makeAircraftSvg('airliner', '#FF8C00');
@@ -96,7 +105,10 @@ const svgAirlinerYellow = makeAircraftSvg('airliner', 'yellow');
const svgAirlinerPink = makeAircraftSvg('airliner', '#FF1493', 'black', 22);
const svgAirlinerRed = makeAircraftSvg('airliner', '#FF2020', 'black', 22);
const svgAirlinerDarkBlue = makeAircraftSvg('airliner', '#1A3A8A', '#4A80D0', 22);
const svgAirlinerWhite = makeAircraftSvg('airliner', 'white', '#ff0000', 22);
const svgAirlinerBlue = makeAircraftSvg('airliner', '#3b82f6', 'black', 22);
const svgAirlinerLime = makeAircraftSvg('airliner', '#32CD32', 'black', 22);
const svgAirlinerBlack = makeAircraftSvg('airliner', '#222', '#555', 22);
const svgAirlinerWhite = makeAircraftSvg('airliner', 'white', '#666', 22);
const svgTurbopropCyan = makeAircraftSvg('turboprop', 'cyan');
const svgTurbopropOrange = makeAircraftSvg('turboprop', '#FF8C00');
@@ -105,7 +117,10 @@ const svgTurbopropYellow = makeAircraftSvg('turboprop', 'yellow');
const svgTurbopropPink = makeAircraftSvg('turboprop', '#FF1493', 'black', 22);
const svgTurbopropRed = makeAircraftSvg('turboprop', '#FF2020', 'black', 22);
const svgTurbopropDarkBlue = makeAircraftSvg('turboprop', '#1A3A8A', '#4A80D0', 22);
const svgTurbopropWhite = makeAircraftSvg('turboprop', 'white', '#ff0000', 22);
const svgTurbopropBlue = makeAircraftSvg('turboprop', '#3b82f6', 'black', 22);
const svgTurbopropLime = makeAircraftSvg('turboprop', '#32CD32', 'black', 22);
const svgTurbopropBlack = makeAircraftSvg('turboprop', '#222', '#555', 22);
const svgTurbopropWhite = makeAircraftSvg('turboprop', 'white', '#666', 22);
const svgBizjetCyan = makeAircraftSvg('bizjet', 'cyan');
const svgBizjetOrange = makeAircraftSvg('bizjet', '#FF8C00');
@@ -114,7 +129,10 @@ const svgBizjetYellow = makeAircraftSvg('bizjet', 'yellow');
const svgBizjetPink = makeAircraftSvg('bizjet', '#FF1493', 'black', 22);
const svgBizjetRed = makeAircraftSvg('bizjet', '#FF2020', 'black', 22);
const svgBizjetDarkBlue = makeAircraftSvg('bizjet', '#1A3A8A', '#4A80D0', 22);
const svgBizjetWhite = makeAircraftSvg('bizjet', 'white', '#ff0000', 22);
const svgBizjetBlue = makeAircraftSvg('bizjet', '#3b82f6', 'black', 22);
const svgBizjetLime = makeAircraftSvg('bizjet', '#32CD32', 'black', 22);
const svgBizjetBlack = makeAircraftSvg('bizjet', '#222', '#555', 22);
const svgBizjetWhite = makeAircraftSvg('bizjet', 'white', '#666', 22);
// Grey variants for grounded/parked aircraft (altitude 0)
const svgAirlinerGrey = makeAircraftSvg('airliner', '#555', '#333');
@@ -334,21 +352,26 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
let isMounted = true;
let callsign = null;
let entityLat = 0;
let entityLng = 0;
if (selectedEntity && data) {
let entity = null;
if (selectedEntity.type === 'flight') entity = data?.commercial_flights?.[selectedEntity.id as number];
else if (selectedEntity.type === 'private_flight') entity = data?.private_flights?.[selectedEntity.id as number];
else if (selectedEntity.type === 'military_flight') entity = data?.military_flights?.[selectedEntity.id as number];
else if (selectedEntity.type === 'private_jet') entity = data?.private_jets?.[selectedEntity.id as number];
else if (selectedEntity.type === 'tracked_flight') entity = data?.tracked_flights?.[selectedEntity.id as number];
if (entity && entity.callsign) {
callsign = entity.callsign;
entityLat = entity.lat ?? 0;
entityLng = entity.lng ?? 0;
}
}
if (callsign && callsign !== prevCallsign.current) {
prevCallsign.current = callsign;
fetch(`${API_BASE}/api/route/${callsign}`)
fetch(`${API_BASE}/api/route/${callsign}?lat=${entityLat}&lng=${entityLng}`)
.then(res => res.json())
.then(routeData => {
if (isMounted) setDynamicRoute(routeData);
@@ -595,6 +618,8 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
loadImg('svgHeliCyan', svgHeliCyan);
loadImg('svgHeliOrange', svgHeliOrange);
loadImg('svgHeliPurple', svgHeliPurple);
loadImg('svgHeliBlue', svgHeliBlue);
loadImg('svgHeliLime', svgHeliLime);
loadImg('svgFighter', svgFighter);
loadImg('svgTanker', svgTanker);
loadImg('svgRecon', svgRecon);
@@ -636,17 +661,28 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
loadImg('svgHeliDarkBlue', svgHeliDarkBlue);
loadImg('svgHeliWhiteAlert', svgHeliWhiteAlert);
loadImg('svgHeliBlack', svgHeliBlack);
loadImg('svgPotusPlane', svgPotusPlane);
loadImg('svgPotusHeli', svgPotusHeli);
loadImg('svgAirlinerPink', svgAirlinerPink);
loadImg('svgAirlinerRed', svgAirlinerRed);
loadImg('svgAirlinerDarkBlue', svgAirlinerDarkBlue);
loadImg('svgAirlinerBlue', svgAirlinerBlue);
loadImg('svgAirlinerLime', svgAirlinerLime);
loadImg('svgAirlinerBlack', svgAirlinerBlack);
loadImg('svgAirlinerWhite', svgAirlinerWhite);
loadImg('svgTurbopropPink', svgTurbopropPink);
loadImg('svgTurbopropRed', svgTurbopropRed);
loadImg('svgTurbopropDarkBlue', svgTurbopropDarkBlue);
loadImg('svgTurbopropBlue', svgTurbopropBlue);
loadImg('svgTurbopropLime', svgTurbopropLime);
loadImg('svgTurbopropBlack', svgTurbopropBlack);
loadImg('svgTurbopropWhite', svgTurbopropWhite);
loadImg('svgBizjetPink', svgBizjetPink);
loadImg('svgBizjetRed', svgBizjetRed);
loadImg('svgBizjetDarkBlue', svgBizjetDarkBlue);
loadImg('svgBizjetBlue', svgBizjetBlue);
loadImg('svgBizjetLime', svgBizjetLime);
loadImg('svgBizjetBlack', svgBizjetBlack);
loadImg('svgBizjetWhite', svgBizjetWhite);
loadImg('svgDrone', svgDrone);
loadImg('svgCctv', svgCctv);
@@ -976,6 +1012,7 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
else if (selectedEntity.type === 'private_flight') entity = data?.private_flights?.[selectedEntity.id as number];
else if (selectedEntity.type === 'military_flight') entity = data?.military_flights?.[selectedEntity.id as number];
else if (selectedEntity.type === 'private_jet') entity = data?.private_jets?.[selectedEntity.id as number];
else if (selectedEntity.type === 'tracked_flight') entity = data?.tracked_flights?.[selectedEntity.id as number];
else if (selectedEntity.type === 'ship') entity = data?.ships?.[selectedEntity.id as number];
if (!entity) return null;
@@ -987,6 +1024,9 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
if (dynamicRoute && dynamicRoute.orig_loc && dynamicRoute.dest_loc) {
originLoc = dynamicRoute.orig_loc;
destLoc = dynamicRoute.dest_loc;
// Also override display names so NewsFeed shows the resolved airport info
if (dynamicRoute.origin_name) entity.origin_name = dynamicRoute.origin_name;
if (dynamicRoute.dest_name) entity.dest_name = dynamicRoute.dest_name;
}
const features = [];
@@ -1150,10 +1190,10 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
// Tracked icon maps by aircraft shape and alert color
const trackedIconMap: Record<string, Record<string, string>> = {
heli: { pink: 'svgHeliPink', red: 'svgHeliAlertRed', darkblue: 'svgHeliDarkBlue', white: 'svgHeliWhiteAlert' },
airliner: { pink: 'svgAirlinerPink', red: 'svgAirlinerRed', darkblue: 'svgAirlinerDarkBlue', white: 'svgAirlinerWhite' },
turboprop: { pink: 'svgTurbopropPink', red: 'svgTurbopropRed', darkblue: 'svgTurbopropDarkBlue', white: 'svgTurbopropWhite' },
bizjet: { pink: 'svgBizjetPink', red: 'svgBizjetRed', darkblue: 'svgBizjetDarkBlue', white: 'svgBizjetWhite' },
heli: { '#ff1493': 'svgHeliPink', pink: 'svgHeliPink', red: 'svgHeliAlertRed', blue: 'svgHeliBlue', darkblue: 'svgHeliDarkBlue', yellow: 'svgHeli', orange: 'svgHeliOrange', purple: 'svgHeliPurple', '#32cd32': 'svgHeliLime', black: 'svgHeliBlack', white: 'svgHeliWhiteAlert' },
airliner: { '#ff1493': 'svgAirlinerPink', pink: 'svgAirlinerPink', red: 'svgAirlinerRed', blue: 'svgAirlinerBlue', darkblue: 'svgAirlinerDarkBlue', yellow: 'svgAirlinerYellow', orange: 'svgAirlinerOrange', purple: 'svgAirlinerPurple', '#32cd32': 'svgAirlinerLime', black: 'svgAirlinerBlack', white: 'svgAirlinerWhite' },
turboprop: { '#ff1493': 'svgTurbopropPink', pink: 'svgTurbopropPink', red: 'svgTurbopropRed', blue: 'svgTurbopropBlue', darkblue: 'svgTurbopropDarkBlue', yellow: 'svgTurbopropYellow', orange: 'svgTurbopropOrange', purple: 'svgTurbopropPurple', '#32cd32': 'svgTurbopropLime', black: 'svgTurbopropBlack', white: 'svgTurbopropWhite' },
bizjet: { '#ff1493': 'svgBizjetPink', pink: 'svgBizjetPink', red: 'svgBizjetRed', blue: 'svgBizjetBlue', darkblue: 'svgBizjetDarkBlue', yellow: 'svgBizjetYellow', orange: 'svgBizjetOrange', purple: 'svgBizjetPurple', '#32cd32': 'svgBizjetLime', black: 'svgBizjetBlack', white: 'svgBizjetWhite' },
};
return {
@@ -1164,7 +1204,11 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
const alertColor = f.alert_color || 'white';
const acType = classifyAircraft(f.model, f.aircraft_category);
const grounded = f.alt != null && f.alt <= 100;
const iconId = grounded ? GROUNDED_ICON_MAP[acType] : (trackedIconMap[acType]?.[alertColor] || trackedIconMap.airliner[alertColor] || 'svgAirlinerWhite');
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 displayName = f.alert_operator || f.operator || f.owner || f.name || f.callsign || f.icao24 || "UNKNOWN";
@@ -1696,16 +1740,46 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
</Marker>
))}
{/* HTML labels for tracked flights (pink names, grey when grounded) */}
{/* HTML labels for tracked flights — color-matched, zoom-gated for non-HVA */}
{trackedFlightsGeoJSON && !selectedEntity && data?.tracked_flights?.map((f: any, i: number) => {
if (f.lat == null || f.lng == null) return null;
if (!inView(f.lat, f.lng)) return null;
const displayName = f.alert_operator || f.operator || f.owner || f.name || f.callsign || f.icao24 || "UNKNOWN";
const alertColor = f.alert_color || '#ff1493';
// Always hide military labels (yellow) — too many, clutters map
if (alertColor === 'yellow') return null;
// Hide black (PIA) labels — they want to stay hidden
if (alertColor === 'black') return null;
// Only show non-HVA/non-red labels when zoomed in (~2000mi or closer = zoom >= 5)
const isHighPriority = alertColor === '#ff1493' || alertColor === 'pink' || alertColor === 'red';
if (!isHighPriority && viewState.zoom < 5) return null;
let displayName = f.alert_operator || f.operator || f.owner || f.name || f.callsign || f.icao24 || "UNKNOWN";
// Strip redundant "Private" labels — tells you nothing
if (displayName === 'Private' || displayName === 'private') return null;
// Map alert_color to a visible label color (some hex colors render near-white)
const labelColorMap: Record<string, string> = {
'#ff1493': '#ff1493', pink: '#ff1493', red: '#ff4444',
blue: '#3b82f6', orange: '#FF8C00', '#32cd32': '#32cd32',
purple: '#b266ff', white: '#cccccc',
};
const grounded = f.alt != null && f.alt <= 100;
const labelColor = grounded ? '#888' : (labelColorMap[alertColor] || alertColor);
const [iLng, iLat] = interpFlight(f);
return (
<Marker key={`tf-label-${i}`} longitude={iLng} latitude={iLat} anchor="top" offset={[0, 10]} style={{ zIndex: 2 }}>
<div style={{ color: grounded ? '#888' : '#ff1493', fontSize: '10px', fontFamily: 'monospace', fontWeight: 'bold', textShadow: '0 0 3px #000, 0 0 3px #000, 1px 1px 2px #000', whiteSpace: 'nowrap', pointerEvents: 'none' }}>
<div style={{
color: labelColor,
fontSize: '10px',
fontFamily: 'monospace',
fontWeight: 'bold',
textShadow: '0 0 3px #000, 0 0 3px #000, 1px 1px 2px #000',
whiteSpace: 'nowrap',
pointerEvents: 'none'
}}>
{String(displayName)}
</div>
</Marker>
+51 -43
View File
@@ -260,28 +260,40 @@ function NewsFeedInner({ data, selectedEntity, regionDossier, regionDossierLoadi
if (flight) {
const callsign = flight.callsign || "UNKNOWN";
const alertColorMap: Record<string, string> = {
'pink': 'text-pink-400', 'red': 'text-red-400',
'darkblue': 'text-blue-400', 'white': 'text-white'
'#ff1493': 'text-[#ff1493]', pink: 'text-[#ff1493]', red: 'text-red-400', yellow: 'text-yellow-400',
blue: 'text-blue-400', orange: 'text-orange-400', '#32cd32': 'text-[#32cd32]', purple: 'text-purple-400',
black: 'text-gray-400', white: 'text-white'
};
const alertBorderMap: Record<string, string> = {
'pink': 'border-pink-500/30', 'red': 'border-red-500/30',
'darkblue': 'border-blue-500/30', 'white': 'border-[var(--border-primary)]/30'
'#ff1493': 'border-[#ff1493]/30', pink: 'border-[#ff1493]/30', red: 'border-red-500/30', yellow: 'border-yellow-500/30',
blue: 'border-blue-500/30', orange: 'border-orange-500/30', '#32cd32': 'border-[#32cd32]/30', purple: 'border-purple-500/30',
black: 'border-gray-500/30', white: 'border-[var(--border-primary)]/30'
};
const alertBgMap: Record<string, string> = {
'pink': 'bg-pink-950/40', 'red': 'bg-red-950/40',
'darkblue': 'bg-blue-950/40', 'white': 'bg-[var(--bg-panel)]'
'#ff1493': 'bg-[#ff1493]/10', pink: 'bg-[#ff1493]/10', red: 'bg-red-950/40', yellow: 'bg-yellow-950/40',
blue: 'bg-blue-950/40', orange: 'bg-orange-950/40', '#32cd32': 'bg-lime-950/40', purple: 'bg-purple-950/40',
black: 'bg-gray-900/40', white: 'bg-[var(--bg-panel)]'
};
const ac = flight.alert_color || 'white';
const headerColor = alertColorMap[ac] || 'text-white';
const borderColor = alertBorderMap[ac] || 'border-[var(--border-primary)]/30';
const bgColor = alertBgMap[ac] || 'bg-[var(--bg-panel)]';
const shadowColor = (ac === 'pink' || ac === '#ff1493') ? 'rgba(255,20,147,0.4)'
: ac === 'red' ? 'rgba(255,32,32,0.2)'
: ac === 'yellow' ? 'rgba(255,255,0,0.2)'
: ac === 'blue' ? 'rgba(59,130,246,0.2)'
: ac === 'orange' ? 'rgba(255,140,0,0.3)'
: ac === '#32cd32' ? 'rgba(50,205,50,0.2)'
: ac === 'purple' ? 'rgba(155,89,182,0.2)'
: 'rgba(255,255,255,0.1)';
return (
<motion.div
initial={{ y: 50, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.4 }}
className={`w-full bg-black/60 backdrop-blur-md border ${ac === 'pink' ? 'border-pink-800' : ac === 'red' ? 'border-red-800' : ac === 'darkblue' ? 'border-blue-800' : 'border-[var(--border-secondary)]'} rounded-xl flex flex-col z-10 font-mono shadow-[0_4px_30px_rgba(255,20,147,0.2)] pointer-events-auto overflow-hidden flex-shrink-0`}
className={`w-full bg-black/60 backdrop-blur-md border ${(ac === 'pink' || ac === '#ff1493') ? 'border-[#ff1493]' : ac === 'red' ? 'border-red-800' : ac === 'yellow' ? 'border-yellow-800' : ac === 'blue' ? 'border-blue-800' : ac === 'orange' ? 'border-orange-800' : ac === '#32cd32' ? 'border-lime-800' : ac === 'purple' ? 'border-purple-800' : 'border-[var(--border-secondary)]'} rounded-xl flex flex-col z-10 font-mono shadow-[0_4px_30px_${shadowColor}] pointer-events-auto overflow-hidden flex-shrink-0`}
>
<div className={`p-3 border-b ${borderColor} ${bgColor} flex justify-between items-center`}>
<h2 className={`text-xs tracking-widest font-bold ${headerColor} flex items-center gap-2`}>
@@ -293,31 +305,39 @@ function NewsFeedInner({ data, selectedEntity, regionDossier, regionDossierLoadi
<div className="p-4 flex flex-col gap-3">
<div className="flex justify-between items-center border-b border-[var(--border-primary)] pb-2">
<span className="text-[var(--text-muted)] text-[10px]">OPERATOR</span>
{flight.alert_operator && flight.alert_operator !== "UNKNOWN" ? (
<a
href={`https://en.wikipedia.org/wiki/${encodeURIComponent(flight.alert_operator.replace(/ /g, '_'))}`}
target="_blank"
rel="noreferrer"
className={`text-xs font-bold underline ${headerColor} hover:opacity-80 transition-opacity`}
title={`Search Wikipedia for ${flight.alert_operator}`}
>
{flight.alert_operator}
</a>
) : (
{flight.alert_operator && flight.alert_operator !== "UNKNOWN" ? (() => {
const wikiSlug = flight.alert_wiki || flight.alert_operator.replace(/\s*\(.*?\)\s*/g, '').trim().replace(/ /g, '_');
const wikiHref = `https://en.wikipedia.org/wiki/${encodeURIComponent(wikiSlug)}`;
return (
<a
href={wikiHref}
target="_blank"
rel="noreferrer"
className={`text-xs font-bold underline ${headerColor} hover:opacity-80 transition-opacity`}
title={`Search Wikipedia for ${flight.alert_operator}`}
>
{flight.alert_operator}
</a>
);
})() : (
<span className={`text-xs font-bold ${headerColor}`}>UNKNOWN</span>
)}
</div>
{/* Owner/Operator Wikipedia photo */}
{flight.alert_operator && flight.alert_operator !== "UNKNOWN" && (
<div className="border-b border-[var(--border-primary)] pb-2">
<WikiImage
wikiUrl={`https://en.wikipedia.org/wiki/${encodeURIComponent(flight.alert_operator.replace(/ /g, '_'))}`}
label={flight.alert_operator}
maxH="max-h-36"
accent={ac === 'pink' ? 'hover:border-pink-500/50' : ac === 'red' ? 'hover:border-red-500/50' : 'hover:border-cyan-500/50'}
/>
</div>
)}
{flight.alert_operator && flight.alert_operator !== "UNKNOWN" && (() => {
const wikiSlug = flight.alert_wiki || flight.alert_operator.replace(/\s*\(.*?\)\s*/g, '').trim().replace(/ /g, '_');
const wikiHref = `https://en.wikipedia.org/wiki/${encodeURIComponent(wikiSlug)}`;
return (
<div className="border-b border-[var(--border-primary)] pb-2">
<WikiImage
wikiUrl={wikiHref}
label={flight.alert_operator}
maxH="max-h-36"
accent={ac === 'pink' ? 'hover:border-pink-500/50' : ac === 'red' ? 'hover:border-red-500/50' : 'hover:border-cyan-500/50'}
/>
</div>
);
})()}
{/* Aircraft model Wikipedia photo */}
{aircraftImgUrl && (
<div className="border-b border-[var(--border-primary)] pb-2">
@@ -348,22 +368,10 @@ function NewsFeedInner({ data, selectedEntity, regionDossier, regionDossierLoadi
<span className="text-[var(--text-muted)] text-[10px]">REGISTRATION</span>
<span className="text-[var(--text-primary)] text-xs font-bold">{flight.registration || "N/A"}</span>
</div>
{flight.alert_tag1 && (
{flight.alert_tags && (
<div className="flex justify-between items-center border-b border-[var(--border-primary)] pb-2">
<span className="text-[var(--text-muted)] text-[10px]">INTEL TAG</span>
<span className={`text-xs font-bold ${headerColor}`}>{flight.alert_tag1}</span>
</div>
)}
{flight.alert_tag2 && (
<div className="flex justify-between items-center border-b border-[var(--border-primary)] pb-2">
<span className="text-[var(--text-muted)] text-[10px]">SECONDARY</span>
<span className="text-[var(--text-primary)] text-xs font-bold">{flight.alert_tag2}</span>
</div>
)}
{flight.alert_tag3 && (
<div className="flex justify-between items-center border-b border-[var(--border-primary)] pb-2">
<span className="text-[var(--text-muted)] text-[10px]">DETAIL</span>
<span className="text-[var(--text-secondary)] text-xs">{flight.alert_tag3}</span>
<span className="text-[var(--text-muted)] text-[10px]">INTEL TAGS</span>
<span className={`text-xs font-bold text-right max-w-[200px] ${headerColor}`}>{flight.alert_tags}</span>
</div>
)}
<div className="flex justify-between items-center border-b border-[var(--border-primary)] pb-2">
+87 -2
View File
@@ -2,7 +2,7 @@
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 } 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 } from "lucide-react";
import { useTheme } from "@/lib/ThemeContext";
function relativeTime(iso: string | undefined): string {
@@ -40,7 +40,25 @@ const FRESHNESS_MAP: Record<string, string> = {
datacenters: "datacenters",
};
const WorldviewLeftPanel = React.memo(function WorldviewLeftPanel({ data, activeLayers, setActiveLayers, onSettingsClick, onLegendClick, gibsDate, setGibsDate, gibsOpacity, setGibsOpacity }: { data: any; activeLayers: any; setActiveLayers: any; onSettingsClick?: () => void; onLegendClick?: () => void; gibsDate?: string; setGibsDate?: (d: string) => void; gibsOpacity?: number; setGibsOpacity?: (o: number) => void }) {
// POTUS fleet ICAO hex codes for client-side filtering
const POTUS_ICAOS: Record<string, { label: string; type: string }> = {
'ADFDF8': { label: 'Air Force One (82-8000)', type: 'AF1' },
'ADFDF9': { label: 'Air Force One (92-9000)', type: 'AF1' },
'ADFEB7': { label: 'Air Force Two (98-0001)', type: 'AF2' },
'ADFEB8': { label: 'Air Force Two (98-0002)', type: 'AF2' },
'ADFEB9': { label: 'Air Force Two (99-0003)', type: 'AF2' },
'ADFEBA': { label: 'Air Force Two (99-0004)', type: 'AF2' },
'AE4AE6': { label: 'Air Force Two (09-0015)', type: 'AF2' },
'AE4AE8': { label: 'Air Force Two (09-0016)', type: 'AF2' },
'AE4AEA': { label: 'Air Force Two (09-0017)', type: 'AF2' },
'AE4AEC': { label: 'Air Force Two (19-0018)', type: 'AF2' },
'AE0865': { label: 'Marine One (VH-3D)', type: 'M1' },
'AE5E76': { label: 'Marine One (VH-92A)', type: 'M1' },
'AE5E77': { label: 'Marine One (VH-92A)', type: 'M1' },
'AE5E79': { label: 'Marine One (VH-92A)', type: 'M1' },
};
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 [isMinimized, setIsMinimized] = useState(false);
const { theme, toggleTheme } = useTheme();
const [gibsPlaying, setGibsPlaying] = useState(false);
@@ -84,6 +102,21 @@ const WorldviewLeftPanel = React.memo(function WorldviewLeftPanel({ data, active
return { importantShipCount: important, passengerShipCount: passenger, civilianShipCount: civilian };
}, [data?.ships]);
// Find POTUS fleet planes currently airborne from tracked flights
const potusFlights = useMemo(() => {
const tracked = data?.tracked_flights;
if (!tracked) return [];
const results: { index: number; flight: any; meta: { label: string; type: string } }[] = [];
for (let i = 0; i < tracked.length; i++) {
const f = tracked[i];
const icao = (f.icao24 || '').toUpperCase();
if (POTUS_ICAOS[icao]) {
results.push({ index: i, flight: f, meta: POTUS_ICAOS[icao] });
}
}
return results;
}, [data?.tracked_flights]);
const layers = [
{ id: "flights", name: "Commercial Flights", source: "adsb.lol", count: data?.commercial_flights?.length || 0, icon: Plane },
{ id: "private", name: "Private Flights", source: "adsb.lol", count: data?.private_flights?.length || 0, icon: Plane },
@@ -260,6 +293,58 @@ const WorldviewLeftPanel = React.memo(function WorldviewLeftPanel({ data, active
</div>
)
})}
{/* 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>
)}
</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>
)}
+3 -2
View File
@@ -1,6 +1,7 @@
// All API calls use relative paths (e.g. /api/flights).
// Next.js rewrites them at the server level to BACKEND_URL (set in docker-compose
// or .env.local for dev). This means:
// The catch-all route handler at src/app/api/[...path]/route.ts proxies them
// to BACKEND_URL at runtime (set in docker-compose or .env.local for dev).
// This means:
// - No build-time baking of the backend URL into the client bundle
// - BACKEND_URL=http://backend:8000 works via Docker internal networking
// - Only port 3000 needs to be exposed externally