diff --git a/backend/api.py b/backend/api.py
index 141aecc..c5f5214 100644
--- a/backend/api.py
+++ b/backend/api.py
@@ -4,6 +4,7 @@
import json
import logging
from collections import deque
+from contextlib import asynccontextmanager
from pathlib import Path
from typing import Any, AsyncGenerator, Dict, List
@@ -20,10 +21,15 @@
from services.commodity_service import get_commodity_service
from services.conflict_service import get_conflict_service
from services.rag_service import get_rag_service
-from services.tracking_service import fetch_flights, get_ships, get_flights
+from services.tracking_service import fetch_flights, get_ships, get_flights, stream_ships
logger = logging.getLogger(__name__)
+_FRONTEND_DIR = Path(__file__).resolve().parent.parent / "frontend" / "web"
+_ASSETS_DIR = Path(__file__).resolve().parent.parent / "frontend" / "assets"
+
+latest_news: deque[Dict[str, Any]] = deque(maxlen=100)
+
@asynccontextmanager
async def lifespan(app):
# Start AIS ship stream in background
@@ -39,14 +45,6 @@ async def lifespan(app):
allow_headers=["*"],
)
-_FRONTEND_DIR = Path(__file__).resolve().parent.parent / "frontend" / "web"
-_ASSETS_DIR = Path(__file__).resolve().parent.parent / "frontend" / "assets"
-
-latest_news: deque[Dict[str, Any]] = deque(maxlen=100)
-
-from contextlib import asynccontextmanager
-from services.tracking_service import stream_ships
-
# ── Health ────────────────────────────────────────────────────────────
@app.get("/health", tags=["meta"])
@@ -235,15 +233,25 @@ async def receive_stream(data: Dict[str, Any]):
@app.get("/api/tracking/flights", tags=["tracking"])
-async def get_flight_data(military_only: bool = False):
+async def get_flight_data(military_only: bool = False, limit: int = 50):
+ """Get flight tracking data (limited and cached)"""
flights = await fetch_flights()
if military_only:
flights = [f for f in flights if f.get("military")]
+
+ # Limit results to prevent frontend lag
+ flights = flights[:limit]
+
return {"success": True, "count": len(flights), "flights": flights}
@app.get("/api/tracking/ships", tags=["tracking"])
-async def get_ship_data(tankers_only: bool = False):
+async def get_ship_data(tankers_only: bool = False, limit: int = 100):
+ """Get ship tracking data (limited and cached)"""
ships = get_ships(tankers_only)
+
+ # Limit results to prevent frontend lag
+ ships = ships[:limit]
+
return {"success": True, "count": len(ships), "ships": ships}
# ── Static mount — MUST BE LAST ───────────────────────────────────────
diff --git a/backend/config/auth_telegram.py b/backend/config/auth_telegram.py
index 74a3611..0875f2e 100644
--- a/backend/config/auth_telegram.py
+++ b/backend/config/auth_telegram.py
@@ -17,7 +17,7 @@
from dotenv import load_dotenv
# Load Telegram API credentials from environment
-load_dotenv() # Load variables from .env88
+load_dotenv() # Load variables from .env
api_id = os.getenv("TELEGRAM_API_ID")
api_hash = os.getenv("TELEGRAM_API_HASH")
phone = os.getenv("TELEGRAM_PHONE")
diff --git a/backend/init_infra.py b/backend/init_infra.py
index 596c249..84238c1 100644
--- a/backend/init_infra.py
+++ b/backend/init_infra.py
@@ -19,7 +19,6 @@ def check_postgresql():
try:
with engine.connect() as conn:
result = conn.execute(text("SELECT version();"))
- conn.execute(text("CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE;"))
version = result.fetchone()[0]
print(f"✅ PostgreSQL: {version[:50]}...")
return True
diff --git a/backend/models/__pycache__/__init__.cpython-310.pyc b/backend/models/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..9d924d8
Binary files /dev/null and b/backend/models/__pycache__/__init__.cpython-310.pyc differ
diff --git a/backend/models/__pycache__/database.cpython-310.pyc b/backend/models/__pycache__/database.cpython-310.pyc
new file mode 100644
index 0000000..e7cfad3
Binary files /dev/null and b/backend/models/__pycache__/database.cpython-310.pyc differ
diff --git a/backend/models/__pycache__/database.cpython-311.pyc b/backend/models/__pycache__/database.cpython-311.pyc
index b81653b..2968280 100644
Binary files a/backend/models/__pycache__/database.cpython-311.pyc and b/backend/models/__pycache__/database.cpython-311.pyc differ
diff --git a/backend/models/database.py b/backend/models/database.py
index 2a9830a..18fb0f9 100644
--- a/backend/models/database.py
+++ b/backend/models/database.py
@@ -120,28 +120,73 @@ def init_db():
def init_timescaledb():
- """Convert events table to TimescaleDB hypertable"""
+ """Convert events table to TimescaleDB hypertable with better error handling"""
+ from sqlalchemy import text
try:
with engine.connect() as conn:
# Check if TimescaleDB extension exists
- conn.execute("CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE;")
-
- # Convert events table to hypertable
- conn.execute("""
- SELECT create_hypertable('events', 'timestamp',
- if_not_exists => TRUE,
- chunk_time_interval => INTERVAL '1 day'
+ print("📊 Installing TimescaleDB extension...")
+ conn.execute(text("CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE;"))
+ conn.commit()
+
+ # Check if events table is already a hypertable
+ result = conn.execute(text("""
+ SELECT EXISTS (
+ SELECT 1 FROM timescaledb_information.hypertables
+ WHERE hypertable_name = 'events'
);
- """)
-
- # Convert commodities table to hypertable
- conn.execute("""
- SELECT create_hypertable('commodities', 'timestamp',
- if_not_exists => TRUE,
- chunk_time_interval => INTERVAL '1 hour'
+ """)).scalar()
+
+ if not result:
+ print("📊 Converting events table to hypertable...")
+ # Convert events table to hypertable (migrate existing data)
+ conn.execute(text("""
+ SELECT create_hypertable('events', 'timestamp',
+ if_not_exists => TRUE,
+ migrate_data => TRUE,
+ chunk_time_interval => INTERVAL '1 day'
+ );
+ """))
+ conn.commit()
+ print("✅ Events hypertable created successfully")
+ else:
+ print("ℹ️ Events table already a hypertable")
+
+ # Check if commodities table is already a hypertable
+ result = conn.execute(text("""
+ SELECT EXISTS (
+ SELECT 1 FROM timescaledb_information.hypertables
+ WHERE hypertable_name = 'commodities'
);
- """)
-
- print("✅ TimescaleDB hypertables configured")
+ """)).scalar()
+
+ if not result:
+ print("📊 Converting commodities table to hypertable...")
+ # Convert commodities table to hypertable (migrate existing data)
+ conn.execute(text("""
+ SELECT create_hypertable('commodities', 'timestamp',
+ if_not_exists => TRUE,
+ migrate_data => TRUE,
+ chunk_time_interval => INTERVAL '1 hour'
+ );
+ """))
+ conn.commit()
+ print("✅ Commodities hypertable created successfully")
+ else:
+ print("ℹ️ Commodities table already a hypertable")
+
+ print("✅ TimescaleDB hypertables configured successfully")
+
except Exception as e:
- print(f"⚠️ TimescaleDB setup skipped (requires extension): {e}")
+ error_msg = str(e)
+ if "table is not empty" in error_msg:
+ print(f"⚠️ TimescaleDB: Tables contain data. Use 'migrate_data => TRUE' to migrate.")
+ elif "does not exist" in error_msg:
+ print(f"⚠️ TimescaleDB extension not available. Install with: apt install timescaledb-postgresql")
+ elif "already exists" in error_msg:
+ print(f"ℹ️ TimescaleDB: Tables already converted to hypertables")
+ else:
+ print(f"⚠️ TimescaleDB setup error: {error_msg}")
+
+ # Don't fail the whole initialization - regular PostgreSQL still works
+ print("ℹ️ Continuing with regular PostgreSQL (performance may be reduced)")
diff --git a/backend/services/__pycache__/conflict_service.cpython-311.pyc b/backend/services/__pycache__/conflict_service.cpython-311.pyc
index 43462be..80962ff 100644
Binary files a/backend/services/__pycache__/conflict_service.cpython-311.pyc and b/backend/services/__pycache__/conflict_service.cpython-311.pyc differ
diff --git a/backend/services/__pycache__/rag_service.cpython-311.pyc b/backend/services/__pycache__/rag_service.cpython-311.pyc
index bf41163..af9a1bd 100644
Binary files a/backend/services/__pycache__/rag_service.cpython-311.pyc and b/backend/services/__pycache__/rag_service.cpython-311.pyc differ
diff --git a/backend/services/__pycache__/tracking_service.cpython-311.pyc b/backend/services/__pycache__/tracking_service.cpython-311.pyc
new file mode 100644
index 0000000..835efb6
Binary files /dev/null and b/backend/services/__pycache__/tracking_service.cpython-311.pyc differ
diff --git a/backend/services/rag_service.py b/backend/services/rag_service.py
index 1cc8db2..71474fc 100644
--- a/backend/services/rag_service.py
+++ b/backend/services/rag_service.py
@@ -14,7 +14,7 @@
from dotenv import load_dotenv
load_dotenv()
-_MODEL = "openrouter/free"
+OPENROUTER_API_BASE = "https://openrouter.ai/api/v1"
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
QDRANT_URL = os.getenv("QDRANT_URL", "http://localhost:6333")
EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "all-MiniLM-L6-v2")
@@ -51,10 +51,10 @@ def _initialize(self):
embedding=self.embeddings, # note: "embedding" not "embeddings"
)
- # Initialize OpenRouter LLM
+ # Initialize OpenRouter LLM - using less busy models
self.llm = ChatOpenAI(
- model="meta-llama/llama-3.3-70b-instruct:free",
- openai_api_base=_MODEL,
+ model="deepseek/deepseek-chat", # DeepSeek R1 - fast and reliable
+ openai_api_base=OPENROUTER_API_BASE,
openai_api_key=OPENROUTER_API_KEY,
temperature=0.7,
max_tokens=1000,
diff --git a/backend/services/tracking_service.py b/backend/services/tracking_service.py
index 3512129..e95b57e 100644
--- a/backend/services/tracking_service.py
+++ b/backend/services/tracking_service.py
@@ -1,4 +1,4 @@
-"""Real-time tracking service
+"""Real-time tracking service with caching
- Ships: aisstream.io WebSocket (free)
- Flights: OpenSky Network REST API (free, no key needed)
"""
@@ -8,8 +8,10 @@
import os
import httpx
import websockets
+import time
from datetime import datetime
from typing import Dict, List
+from functools import lru_cache
from dotenv import load_dotenv
load_dotenv()
@@ -18,6 +20,11 @@
OPENSKY_USER = os.getenv("OPENSKY_USERNAME", "")
OPENSKY_PASS = os.getenv("OPENSKY_PASSWORD", "")
+# Caching configuration
+CACHE_TTL = 120 # 2 minutes cache for tracking data
+flight_cache = {"data": [], "timestamp": 0}
+ship_cache = {"data": [], "timestamp": 0}
+
# Military callsign prefixes
MILITARY_CALLSIGN_PATTERNS = [
"RCH", # US Air Force (Reach)
@@ -102,7 +109,14 @@ async def stream_ships():
async def fetch_flights(region: str = "global") -> List[dict]:
- """Fetch flights from OpenSky Network — free, no key needed."""
+ """Fetch flights from OpenSky Network with caching — free, no key needed."""
+
+ # Check cache first
+ current_time = time.time()
+ if (current_time - flight_cache["timestamp"]) < CACHE_TTL and flight_cache["data"]:
+ print(f"🛩️ Using cached flight data ({len(flight_cache['data'])} flights)")
+ return flight_cache["data"]
+
# Bounding boxes for key regions
REGIONS = {
"middle_east": (12.0, 25.0, 42.0, 65.0),
@@ -115,7 +129,7 @@ async def fetch_flights(region: str = "global") -> List[dict]:
try:
auth = (OPENSKY_USER, OPENSKY_PASS) if OPENSKY_USER else None
- async with httpx.AsyncClient(timeout=15.0) as client:
+ async with httpx.AsyncClient(timeout=10.0) as client: # Reduced timeout
resp = await client.get(
"https://opensky-network.org/api/states/all",
params={"lamin": lamin, "lomin": lomin,
@@ -126,7 +140,7 @@ async def fetch_flights(region: str = "global") -> List[dict]:
data = resp.json()
flights = []
- for state in (data.get("states") or []):
+ for state in (data.get("states") or [])[:200]: # Limit to 200 flights
if len(state) < 17:
continue
icao24 = state[0] or ""
@@ -162,20 +176,41 @@ async def fetch_flights(region: str = "global") -> List[dict]:
for f in flights:
_flights[f["icao24"]] = f
+ # Update cache
+ flight_cache["data"] = flights
+ flight_cache["timestamp"] = current_time
+
military_count = sum(1 for f in flights if f["military"])
- print(f"✅ OpenSky: {len(flights)} flights, {military_count} military")
+ print(f"✅ OpenSky: {len(flights)} flights, {military_count} military (cached)")
return flights
except Exception as e:
print(f"⚠️ OpenSky error: {e}")
+ # Return cached data if available, otherwise empty list
+ if flight_cache["data"]:
+ print(f"🛩️ Using cached flight data due to error ({len(flight_cache['data'])} flights)")
+ return flight_cache["data"]
return list(_flights.values())
def get_ships(tankers_only: bool = False) -> List[dict]:
- ships = list(_ships.values())
+ """Get cached ship data with optional tanker filtering"""
+
+ # Check cache first
+ current_time = time.time()
+ if (current_time - ship_cache["timestamp"]) < CACHE_TTL and ship_cache["data"]:
+ ships = ship_cache["data"]
+ print(f"🚢 Using cached ship data ({len(ships)} ships)")
+ else:
+ ships = list(_ships.values())
+ # Update cache
+ ship_cache["data"] = ships
+ ship_cache["timestamp"] = current_time
+
if tankers_only:
ships = [s for s in ships if s.get("is_tanker")]
- return ships[:500] # cap at 500 for frontend performance
+
+ return ships[:100] # Reduced from 500 to 100 for performance
def get_flights(military_only: bool = False) -> List[dict]:
diff --git a/backend/workers/tasks/__pycache__/news_worker.cpython-311.pyc b/backend/workers/tasks/__pycache__/news_worker.cpython-311.pyc
index 425038f..e6550a9 100644
Binary files a/backend/workers/tasks/__pycache__/news_worker.cpython-311.pyc and b/backend/workers/tasks/__pycache__/news_worker.cpython-311.pyc differ
diff --git a/backend/workers/tasks/__pycache__/processor.cpython-311.pyc b/backend/workers/tasks/__pycache__/processor.cpython-311.pyc
index d38a8fc..4e47a76 100644
Binary files a/backend/workers/tasks/__pycache__/processor.cpython-311.pyc and b/backend/workers/tasks/__pycache__/processor.cpython-311.pyc differ
diff --git a/backend/workers/tasks/__pycache__/reddit_worker.cpython-311.pyc b/backend/workers/tasks/__pycache__/reddit_worker.cpython-311.pyc
index 6a64e79..91139ef 100644
Binary files a/backend/workers/tasks/__pycache__/reddit_worker.cpython-311.pyc and b/backend/workers/tasks/__pycache__/reddit_worker.cpython-311.pyc differ
diff --git a/backend/workers/tasks/__pycache__/rss_worker.cpython-311.pyc b/backend/workers/tasks/__pycache__/rss_worker.cpython-311.pyc
index 78a9c80..e49b1a3 100644
Binary files a/backend/workers/tasks/__pycache__/rss_worker.cpython-311.pyc and b/backend/workers/tasks/__pycache__/rss_worker.cpython-311.pyc differ
diff --git a/backend/workers/tasks/__pycache__/telegram_worker.cpython-311.pyc b/backend/workers/tasks/__pycache__/telegram_worker.cpython-311.pyc
index 1c9e64f..d9f7a87 100644
Binary files a/backend/workers/tasks/__pycache__/telegram_worker.cpython-311.pyc and b/backend/workers/tasks/__pycache__/telegram_worker.cpython-311.pyc differ
diff --git a/backend/workers/tasks/news_worker.py b/backend/workers/tasks/news_worker.py
index 4bd0a5e..32db8cd 100644
--- a/backend/workers/tasks/news_worker.py
+++ b/backend/workers/tasks/news_worker.py
@@ -13,6 +13,7 @@
from config.celery_config import celery_app
from models.redis_client import is_duplicate, RedisPubSub
from models.database import SessionLocal, Event
+from services.geo_extractor import extract_location
load_dotenv()
@@ -86,7 +87,15 @@ def fetch_news():
# Parse timestamp
published = article.get("publishedAt")
timestamp = datetime.fromisoformat(published.replace("Z", "+00:00")) if published else datetime.utcnow()
-
+
+ # Extract geo-location
+ geo = extract_location(full_text)
+ lat, lon, place = None, None, None
+ if geo:
+ lat = geo.get("lat")
+ lon = geo.get("lon")
+ place = geo.get("place")
+
# Create event
event = Event(
source="NewsAPI",
@@ -94,20 +103,28 @@ def fetch_news():
url=article.get("url", ""),
timestamp=timestamp,
bias="Varied",
- content_hash=content_hash
+ content_hash=content_hash,
+ lat=lat,
+ lon=lon,
+ place=place
)
-
+
db.add(event)
+ db.flush() # Get event ID
new_articles += 1
-
+
# Publish to stream
pubsub.publish({
"type": "event",
+ "id": event.id,
"source": "NewsAPI",
"text": full_text[:200] + "..." if len(full_text) > 200 else full_text,
"url": event.url,
"timestamp": timestamp.isoformat(),
- "bias": "Varied"
+ "bias": "Varied",
+ "lat": lat,
+ "lon": lon,
+ "place": place
})
# Queue for processing
diff --git a/backend/workers/tasks/processor.py b/backend/workers/tasks/processor.py
index 81ebd07..2587f7d 100644
--- a/backend/workers/tasks/processor.py
+++ b/backend/workers/tasks/processor.py
@@ -1,10 +1,9 @@
-"""Event Processor - Generate embeddings and extract entities
+"""Event Processor - Generate embeddings and store in Qdrant
Processes events through:
1. Text cleaning
-2. Entity extraction (NER using spaCy)
-3. Embedding generation (sentence-transformers)
-4. Vector storage (Qdrant)
+2. Embedding generation (sentence-transformers)
+3. Vector storage (Qdrant)
"""
from config.celery_config import celery_app
@@ -97,22 +96,10 @@ def process_event(event_id: int):
# Update event with embedding ID
event.embedding_id = point_id
-
- # Extract entities (placeholder - can add spaCy NER here)
- # TODO: Add NER extraction
- # import spacy
- # nlp = spacy.load("en_core_web_sm")
- # doc = nlp(event.text)
- # entities = {
- # "locations": [ent.text for ent in doc.ents if ent.label_ == "GPE"],
- # "organizations": [ent.text for ent in doc.ents if ent.label_ == "ORG"],
- # "persons": [ent.text for ent in doc.ents if ent.label_ == "PERSON"]
- # }
- # event.entities = entities
-
+
db.commit()
db.close()
-
+
print(f"✅ Processed event {event_id}")
return {"status": "success", "event_id": event_id, "embedding_id": point_id}
diff --git a/backend/workers/tasks/reddit_worker.py b/backend/workers/tasks/reddit_worker.py
index d41ea54..a742614 100644
--- a/backend/workers/tasks/reddit_worker.py
+++ b/backend/workers/tasks/reddit_worker.py
@@ -11,6 +11,7 @@
from config.celery_config import celery_app
from models.redis_client import is_duplicate, RedisPubSub
from models.database import SessionLocal, Event
+from services.geo_extractor import extract_location
def load_reddit_config():
@@ -79,7 +80,15 @@ def fetch_reddit():
# Check duplicate
if is_duplicate(content_hash):
continue
-
+
+ # Extract geo-location
+ geo = extract_location(full_text)
+ lat, lon, place = None, None, None
+ if geo:
+ lat = geo.get("lat")
+ lon = geo.get("lon")
+ place = geo.get("place")
+
# Create event
event = Event(
source="Reddit",
@@ -87,20 +96,28 @@ def fetch_reddit():
url=f"https://reddit.com{post.get('permalink')}",
timestamp=datetime.fromtimestamp(post.get('created_utc', 0)),
bias="Varied",
- content_hash=content_hash
+ content_hash=content_hash,
+ lat=lat,
+ lon=lon,
+ place=place
)
-
+
db.add(event)
+ db.flush() # Get event ID
new_posts += 1
-
+
# Publish to stream
pubsub.publish({
"type": "event",
+ "id": event.id,
"source": "Reddit",
"text": full_text[:200] + "..." if len(full_text) > 200 else full_text,
"url": event.url,
"timestamp": event.timestamp.isoformat(),
- "bias": "Varied"
+ "bias": "Varied",
+ "lat": lat,
+ "lon": lon,
+ "place": place
})
# Queue for processing
diff --git a/backend/workers/tasks/rss_worker.py b/backend/workers/tasks/rss_worker.py
index e5bb905..ef1711f 100644
--- a/backend/workers/tasks/rss_worker.py
+++ b/backend/workers/tasks/rss_worker.py
@@ -12,6 +12,7 @@
from config.celery_config import celery_app
from models.redis_client import is_duplicate, RedisPubSub
from models.database import SessionLocal, Event
+from services.geo_extractor import extract_location
import sys
sys.path.append(str(Path(__file__).parent.parent))
@@ -89,7 +90,15 @@ def fetch_single_rss(feed_config: dict):
# Parse timestamp
published = entry.get("published_parsed")
timestamp = datetime(*published[:6]) if published else datetime.utcnow()
-
+
+ # Extract geo-location
+ geo = extract_location(text)
+ lat, lon, place = None, None, None
+ if geo:
+ lat = geo.get("lat")
+ lon = geo.get("lon")
+ place = geo.get("place")
+
# Create event
event = Event(
source=name,
@@ -97,20 +106,28 @@ def fetch_single_rss(feed_config: dict):
url=link,
timestamp=timestamp,
bias=bias,
- content_hash=content_hash
+ content_hash=content_hash,
+ lat=lat,
+ lon=lon,
+ place=place
)
-
+
db.add(event)
+ db.flush() # Get event ID
new_items += 1
-
+
# Publish to real-time stream
pubsub.publish({
"type": "event",
+ "id": event.id,
"source": name,
"text": text[:200] + "..." if len(text) > 200 else text,
"url": link,
"timestamp": timestamp.isoformat(),
- "bias": bias
+ "bias": bias,
+ "lat": lat,
+ "lon": lon,
+ "place": place
})
# Queue for processing (embeddings, NER)
diff --git a/backend/workers/tasks/telegram_worker.py b/backend/workers/tasks/telegram_worker.py
index 423f3ba..e9ef38f 100644
--- a/backend/workers/tasks/telegram_worker.py
+++ b/backend/workers/tasks/telegram_worker.py
@@ -7,6 +7,7 @@
from config.celery_config import celery_app
from models.database import SessionLocal, Event
from models.redis_client import is_duplicate, RedisPubSub
+from services.geo_extractor import extract_location
import hashlib
from datetime import datetime
import os
@@ -75,7 +76,15 @@ async def handler(event):
# Get sender info
sender = await event.get_sender()
username = sender.username if sender else "Unknown"
-
+
+ # Extract geo-location
+ geo = extract_location(text)
+ lat, lon, place = None, None, None
+ if geo:
+ lat = geo.get("lat")
+ lon = geo.get("lon")
+ place = geo.get("place")
+
# Create event
db = SessionLocal()
event_obj = Event(
@@ -84,23 +93,31 @@ async def handler(event):
url=f"https://t.me/{username}/{event.id}",
timestamp=datetime.fromtimestamp(event.date.timestamp()),
bias=bias_tags.get(username, "Independent"),
- content_hash=content_hash
+ content_hash=content_hash,
+ lat=lat,
+ lon=lon,
+ place=place
)
-
+
db.add(event_obj)
- db.commit()
+ db.flush() # Get event ID
event_id = event_obj.id
+ db.commit()
db.close()
-
+
# Publish to stream
pubsub = RedisPubSub()
pubsub.publish({
"type": "event",
+ "id": event_id,
"source": f"Telegram/{username}",
"text": text[:200] + "..." if len(text) > 200 else text,
"url": event_obj.url,
"timestamp": event_obj.timestamp.isoformat(),
- "bias": event_obj.bias
+ "bias": event_obj.bias,
+ "lat": lat,
+ "lon": lon,
+ "place": place
})
# Queue for processing
diff --git a/data/commodity_cache.json b/data/commodity_cache.json
index 0ffb7b8..5087b8e 100644
--- a/data/commodity_cache.json
+++ b/data/commodity_cache.json
@@ -2,28 +2,28 @@
"prices": {
"XAU": {
"rate": 4492.200195,
- "timestamp": "2026-03-21T22:51:40.557619",
+ "timestamp": "2026-03-22T16:18:13.511176",
"unit": "troy oz",
"name": "Gold"
},
"XAG": {
"rate": 67.938004,
- "timestamp": "2026-03-21T22:51:40.557619",
+ "timestamp": "2026-03-22T16:18:13.511176",
"unit": "troy oz",
"name": "Silver"
},
"WTI_USD": {
"rate": 98.23,
- "timestamp": "2026-03-21T22:51:40.557619",
+ "timestamp": "2026-03-22T16:18:13.511176",
"unit": "barrel",
"name": "WTI Crude Oil"
},
"BRENT_USD": {
"rate": 112.19,
- "timestamp": "2026-03-21T22:51:40.557619",
+ "timestamp": "2026-03-22T16:18:13.511176",
"unit": "barrel",
"name": "Brent Crude Oil"
}
},
- "last_refresh": "2026-03-21T22:51:40.557619"
+ "last_refresh": "2026-03-22T16:18:13.511176"
}
\ No newline at end of file
diff --git a/data/conflicts.json b/data/conflicts.json
index d045c14..0c1a078 100644
--- a/data/conflicts.json
+++ b/data/conflicts.json
@@ -1,6 +1,202 @@
{
- "conflicts": [],
+ "conflicts": [
+ {
+ "id": 1,
+ "name": "Russia-Ukraine War",
+ "region": "Europe",
+ "status": "Worsening",
+ "impact_on_us": "Critical",
+ "severity": 9,
+ "coordinates": {
+ "lat": 48.5,
+ "lng": 35.0
+ },
+ "description": "Full-scale Russian invasion of Ukraine ongoing since February 2022."
+ },
+ {
+ "id": 2,
+ "name": "Israel-Hamas War",
+ "region": "Middle East",
+ "status": "Worsening",
+ "impact_on_us": "Critical",
+ "severity": 9,
+ "coordinates": {
+ "lat": 31.5,
+ "lng": 34.5
+ },
+ "description": "Conflict in Gaza following October 7 2023 attacks."
+ },
+ {
+ "id": 3,
+ "name": "Sudan Civil War",
+ "region": "Africa",
+ "status": "Worsening",
+ "impact_on_us": "Significant",
+ "severity": 8,
+ "coordinates": {
+ "lat": 15.6,
+ "lng": 32.5
+ },
+ "description": "RSF vs SAF conflict causing major humanitarian crisis in Darfur."
+ },
+ {
+ "id": 4,
+ "name": "Yemen Conflict",
+ "region": "Middle East",
+ "status": "Unchanging",
+ "impact_on_us": "Significant",
+ "severity": 7,
+ "coordinates": {
+ "lat": 15.5,
+ "lng": 44.0
+ },
+ "description": "Houthi attacks on Red Sea shipping continuing."
+ },
+ {
+ "id": 5,
+ "name": "Myanmar Civil War",
+ "region": "Asia",
+ "status": "Worsening",
+ "impact_on_us": "Limited",
+ "severity": 7,
+ "coordinates": {
+ "lat": 21.9,
+ "lng": 96.0
+ },
+ "description": "Military junta fighting resistance coalition forces."
+ },
+ {
+ "id": 6,
+ "name": "Taiwan Strait Tensions",
+ "region": "Asia",
+ "status": "Unchanging",
+ "impact_on_us": "Critical",
+ "severity": 7,
+ "coordinates": {
+ "lat": 23.7,
+ "lng": 121.0
+ },
+ "description": "PLA military exercises near Taiwan continuing."
+ },
+ {
+ "id": 7,
+ "name": "Iran Nuclear Standoff",
+ "region": "Middle East",
+ "status": "Worsening",
+ "impact_on_us": "Critical",
+ "severity": 8,
+ "coordinates": {
+ "lat": 32.4,
+ "lng": 53.7
+ },
+ "description": "Iran enrichment at 60%, IAEA access restricted."
+ },
+ {
+ "id": 8,
+ "name": "Sahel Insurgency",
+ "region": "Africa",
+ "status": "Worsening",
+ "impact_on_us": "Limited",
+ "severity": 6,
+ "coordinates": {
+ "lat": 14.0,
+ "lng": 2.0
+ },
+ "description": "Jihadist insurgency across Mali, Burkina Faso, Niger."
+ },
+ {
+ "id": 9,
+ "name": "North Korea Provocations",
+ "region": "Asia",
+ "status": "Worsening",
+ "impact_on_us": "Critical",
+ "severity": 7,
+ "coordinates": {
+ "lat": 39.0,
+ "lng": 127.0
+ },
+ "description": "Ballistic missile tests and nuclear program expansion."
+ },
+ {
+ "id": 10,
+ "name": "DRC Conflict",
+ "region": "Africa",
+ "status": "Worsening",
+ "impact_on_us": "Limited",
+ "severity": 7,
+ "coordinates": {
+ "lat": -4.0,
+ "lng": 21.8
+ },
+ "description": "M23 advances in eastern DRC backed by Rwanda."
+ },
+ {
+ "id": 11,
+ "name": "Ethiopia-Tigray",
+ "region": "Africa",
+ "status": "Unchanging",
+ "impact_on_us": "Limited",
+ "severity": 5,
+ "coordinates": {
+ "lat": 14.0,
+ "lng": 38.5
+ },
+ "description": "Fragile ceasefire holds but tensions remain high."
+ },
+ {
+ "id": 12,
+ "name": "South China Sea Tensions",
+ "region": "Asia",
+ "status": "Unchanging",
+ "impact_on_us": "Critical",
+ "severity": 6,
+ "coordinates": {
+ "lat": 12.0,
+ "lng": 115.0
+ },
+ "description": "China vs Philippines standoffs at disputed reefs."
+ },
+ {
+ "id": 13,
+ "name": "Lebanon Instability",
+ "region": "Middle East",
+ "status": "Worsening",
+ "impact_on_us": "Significant",
+ "severity": 6,
+ "coordinates": {
+ "lat": 33.9,
+ "lng": 35.5
+ },
+ "description": "Post-war reconstruction stalled, Hezbollah weakened."
+ },
+ {
+ "id": 14,
+ "name": "Haiti Gang Crisis",
+ "region": "Americas",
+ "status": "Worsening",
+ "impact_on_us": "Significant",
+ "severity": 7,
+ "coordinates": {
+ "lat": 18.9,
+ "lng": -72.3
+ },
+ "description": "Gang coalitions control most of Port-au-Prince."
+ },
+ {
+ "id": 15,
+ "name": "Venezuela Crisis",
+ "region": "Americas",
+ "status": "Unchanging",
+ "impact_on_us": "Significant",
+ "severity": 5,
+ "coordinates": {
+ "lat": 8.0,
+ "lng": -66.0
+ },
+ "description": "Political and economic crisis with mass migration continuing."
+ }
+ ],
"metadata": {
- "last_refresh": "2026-03-21T22:51:20.763134"
+ "last_refresh": "2026-03-22T16:17:59.421247"
}
}
\ No newline at end of file
diff --git a/debug-layers.html b/debug-layers.html
new file mode 100644
index 0000000..e610f48
--- /dev/null
+++ b/debug-layers.html
@@ -0,0 +1,292 @@
+
+
+
+
+
+ FlashPoint - Debug Layers
+
+
+
+
+
+
🔧 FlashPoint Layer Debug
+
+
+
📊 Layer Status
+
Loading...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/diagnostic_map.html b/diagnostic_map.html
new file mode 100644
index 0000000..fecd868
--- /dev/null
+++ b/diagnostic_map.html
@@ -0,0 +1,124 @@
+
+
+
+ FlashPoint Map Diagnostic
+
+
+
+
+ 🗺️ FlashPoint Map Diagnostic
+
+
+
+
+
+
+
diff --git a/frontend/web/app.js b/frontend/web/app.js
index 82d40f7..8641d45 100644
--- a/frontend/web/app.js
+++ b/frontend/web/app.js
@@ -5,17 +5,44 @@ import { initChat } from './js/chat.js?v=3';
import { initCommodities } from './js/commodities.js?v=3';
import { initConflicts } from './js/conflicts.js?v=3';
import { initReports } from './js/reports.js?v=3';
+import { initTracking } from './js/tracking.js?v=3';
+import { initTestData } from './js/test-data.js?v=3';
+import './js/debug.js?v=3'; // Load debug commands
function init() {
+ console.log("🚀 Starting FlashPoint initialization...");
+
updateClock();
setInterval(updateClock, 1000);
- initMap();
- initFeed();
+
+ // Initialize map first
+ console.log("🗺️ Initializing map...");
+ const mapReady = initMap();
+
+ console.log("🗺️ Map ready:", mapReady);
+
+ if (mapReady) {
+ // Wait a bit for map to fully initialize, then load data
+ setTimeout(() => {
+ console.log("📡 Loading data modules...");
+ initFeed();
+ initConflicts();
+ initTracking(); // Load flights & ships
+
+ // Add test data for immediate visualization
+ initTestData();
+ }, 1000);
+ } else {
+ console.error("❌ Map initialization failed - skipping data loading");
+ }
+
+ // Initialize other modules
initChat();
initCommodities();
- initConflicts();
initReports();
- console.log("FlashPoint operational");
+
+ console.log("✅ FlashPoint operational");
+ console.log("🔧 Debug commands available: FlashPointDebug.checkMap()");
}
if (document.readyState === "loading") {
diff --git a/frontend/web/index.html b/frontend/web/index.html
index 1ed2795..b8c9954 100644
--- a/frontend/web/index.html
+++ b/frontend/web/index.html
@@ -93,7 +93,9 @@ COMMODITIES
OPERATIONAL PICTURE
-
+
diff --git a/frontend/web/js/conflicts.js b/frontend/web/js/conflicts.js
index 62ed56a..ac71246 100644
--- a/frontend/web/js/conflicts.js
+++ b/frontend/web/js/conflicts.js
@@ -1,22 +1,28 @@
/**
- * conflicts.js - Global conflict tracker integration
+ * conflicts.js - Global conflict tracker integration with better timing
*/
import { API_BASE, ENDPOINTS } from './utils.js';
import { renderConflictMarkers } from './map.js';
/**
- * Fetch and render conflicts
+ * Fetch and render conflicts with retry logic
*/
async function fetchConflicts() {
try {
+ console.log("🔴 Fetching conflicts...");
const resp = await fetch(`${API_BASE}${ENDPOINTS.conflicts}`);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const data = await resp.json();
+
if (data.success && data.conflicts) {
- renderConflictMarkers(data.conflicts);
- console.log(`✅ Loaded ${data.conflicts.length} conflicts`);
+ console.log(`🔴 Received ${data.conflicts.length} conflicts`);
+
+ // Wait for map to be ready, then render
+ waitForMapAndRender(data.conflicts);
+ } else {
+ console.log("⚠️ No conflicts in response");
}
} catch (err) {
@@ -24,14 +30,49 @@ async function fetchConflicts() {
}
}
+/**
+ * Wait for map to be ready, then render conflicts
+ */
+function waitForMapAndRender(conflicts) {
+ const maxAttempts = 10;
+ let attempts = 0;
+
+ function tryRender() {
+ attempts++;
+
+ // Check if map globals and conflict layer are available
+ const { map, conflictLayer } = window.FlashPointMap || {};
+
+ if (map && conflictLayer) {
+ console.log("🔴 Map and conflict layer ready, rendering conflicts...");
+ renderConflictMarkers(conflicts);
+ return;
+ }
+
+ if (attempts < maxAttempts) {
+ console.log(`🔴 Map not ready, attempt ${attempts}/${maxAttempts}, retrying...`);
+ setTimeout(tryRender, 500);
+ } else {
+ console.error("❌ Failed to render conflicts - map not ready after all attempts");
+ }
+ }
+
+ tryRender();
+}
+
/**
* Initialize conflicts module
*/
export function initConflicts() {
- fetchConflicts();
-
+ console.log("⚔️ Initializing conflicts tracker...");
+
+ // Wait a bit longer for map initialization
+ setTimeout(() => {
+ fetchConflicts();
+ }, 1500);
+
// Refresh every 12 hours
setInterval(fetchConflicts, 12 * 60 * 60 * 1000);
-
- console.log("⚔️ Conflicts tracker initialized");
+
+ console.log("⚔️ Conflicts tracker initialized");
}
diff --git a/frontend/web/js/debug.js b/frontend/web/js/debug.js
new file mode 100644
index 0000000..d463fcc
--- /dev/null
+++ b/frontend/web/js/debug.js
@@ -0,0 +1,238 @@
+/**
+ * debug.js - Browser console debugging commands
+ */
+
+// Make debugging functions globally available
+window.FlashPointDebug = {
+
+ // Check map status
+ checkMap() {
+ console.log("🔍 MAP DEBUG INFO:");
+ console.log("- Map object:", window.FlashPointMap?.map);
+ console.log("- Main marker layer:", window.FlashPointMap?.markerLayer);
+ console.log("- Military aircraft layer:", window.FlashPointMap?.militaryAircraftLayer);
+ console.log("- Civilian aircraft layer:", window.FlashPointMap?.civilianAircraftLayer);
+ console.log("- Oil tanker layer:", window.FlashPointMap?.oilTankerLayer);
+ console.log("- Conflict layer:", window.FlashPointMap?.conflictLayer);
+ console.log("- Hotspot layer:", window.FlashPointMap?.hotspotLayer);
+ console.log("- Container element:", document.getElementById("map"));
+
+ const container = document.getElementById("map");
+ if (container) {
+ console.log("- Container dimensions:", {
+ width: container.offsetWidth,
+ height: container.offsetHeight,
+ display: getComputedStyle(container).display,
+ visibility: getComputedStyle(container).visibility
+ });
+ }
+
+ // Layer statistics
+ if (window.FlashPointMap) {
+ const layers = window.FlashPointMap;
+ console.log("- Layer counts:", {
+ military: layers.militaryAircraftLayer?.getLayers().length || 0,
+ civilian: layers.civilianAircraftLayer?.getLayers().length || 0,
+ tankers: layers.oilTankerLayer?.getLayers().length || 0,
+ conflicts: layers.conflictLayer?.getLayers().length || 0,
+ hotspots: layers.hotspotLayer?.getLayers().length || 0
+ });
+ }
+ },
+
+ // Add test marker manually
+ addTestMarker() {
+ if (window.FlashPointMap?.addTestMarker) {
+ return window.FlashPointMap.addTestMarker();
+ } else {
+ console.error("❌ Test marker function not available");
+ }
+ },
+
+ // Test military aircraft marker
+ addTestMilitary() {
+ const { militaryAircraftLayer } = window.FlashPointMap || {};
+ if (!militaryAircraftLayer) {
+ console.error("❌ Military layer not available");
+ return;
+ }
+
+ const marker = L.circleMarker([39.0, -77.0], {
+ color: '#ff3333',
+ fillColor: '#ff3333',
+ fillOpacity: 0.8,
+ radius: 10,
+ weight: 3
+ });
+ marker.bindPopup("🎯 TEST MILITARY
Washington DC Area");
+ marker.addTo(militaryAircraftLayer);
+ console.log("✅ Added test military aircraft marker");
+ return marker;
+ },
+
+ // Test civilian aircraft marker
+ addTestCivilian() {
+ const { civilianAircraftLayer } = window.FlashPointMap || {};
+ if (!civilianAircraftLayer) {
+ console.error("❌ Civilian layer not available");
+ return;
+ }
+
+ const marker = L.circleMarker([40.7, -74.0], {
+ color: '#0099ff',
+ fillColor: '#0099ff',
+ fillOpacity: 0.8,
+ radius: 6,
+ weight: 3
+ });
+ marker.bindPopup("✈️ TEST CIVILIAN
New York Area");
+ marker.addTo(civilianAircraftLayer);
+ console.log("✅ Added test civilian aircraft marker");
+ return marker;
+ },
+
+ // Test oil tanker marker
+ addTestTanker() {
+ const { oilTankerLayer } = window.FlashPointMap || {};
+ if (!oilTankerLayer) {
+ console.error("❌ Oil tanker layer not available");
+ return;
+ }
+
+ const marker = L.circleMarker([29.7, -95.3], {
+ color: '#ff8800',
+ fillColor: '#ff8800',
+ fillOpacity: 0.8,
+ radius: 9,
+ weight: 3
+ });
+ marker.bindPopup("🛢️ TEST TANKER
Houston Area");
+ marker.addTo(oilTankerLayer);
+ console.log("✅ Added test oil tanker marker");
+ return marker;
+ },
+
+ // Add marker at specific location
+ addMarker(lat, lon, color = '#ff0000', label = 'Debug Marker') {
+ const { map, markerLayer } = window.FlashPointMap || {};
+
+ if (!map || !markerLayer) {
+ console.error("❌ Map not available");
+ return null;
+ }
+
+ const marker = L.circle([lat, lon], {
+ color: color,
+ fillColor: color,
+ fillOpacity: 0.7,
+ radius: 100000,
+ weight: 3
+ });
+
+ marker.bindPopup(`${label}
Lat: ${lat}
Lon: ${lon}`);
+ marker.addTo(markerLayer);
+
+ console.log(`✅ Added marker at [${lat}, ${lon}]`);
+ return marker;
+ },
+
+ // Clear all markers
+ clearAllMarkers() {
+ const layers = window.FlashPointMap || {};
+ let totalCleared = 0;
+
+ ['markerLayer', 'militaryAircraftLayer', 'civilianAircraftLayer',
+ 'oilTankerLayer', 'conflictLayer', 'hotspotLayer'].forEach(layerName => {
+ const layer = layers[layerName];
+ if (layer) {
+ const count = layer.getLayers().length;
+ layer.clearLayers();
+ totalCleared += count;
+ console.log(`🗑️ Cleared ${count} markers from ${layerName}`);
+ }
+ });
+
+ console.log(`🗑️ Total cleared: ${totalCleared} markers`);
+ },
+
+ // Clear specific layer
+ clearLayer(layerType) {
+ const layers = window.FlashPointMap || {};
+ const layerMap = {
+ 'military': 'militaryAircraftLayer',
+ 'civilian': 'civilianAircraftLayer',
+ 'tankers': 'oilTankerLayer',
+ 'conflicts': 'conflictLayer',
+ 'hotspots': 'hotspotLayer'
+ };
+
+ const layerName = layerMap[layerType];
+ if (!layerName || !layers[layerName]) {
+ console.error(`❌ Layer '${layerType}' not found`);
+ return;
+ }
+
+ const count = layers[layerName].getLayers().length;
+ layers[layerName].clearLayers();
+ console.log(`🗑️ Cleared ${count} markers from ${layerType} layer`);
+ },
+
+ // Test tracking refresh
+ refreshTracking() {
+ import('./tracking.js').then(module => {
+ console.log("🔄 Refreshing tracking data...");
+ module.refreshFlights();
+ setTimeout(() => module.refreshShips(), 1000);
+ }).catch(err => {
+ console.error("❌ Failed to refresh tracking:", err);
+ });
+ },
+
+ // Test Leaflet basics
+ testLeaflet() {
+ console.log("🔍 LEAFLET TEST:");
+ console.log("- Leaflet loaded:", typeof L !== 'undefined');
+ console.log("- Leaflet version:", L?.version);
+
+ if (typeof L !== 'undefined') {
+ console.log("✅ Leaflet is available");
+
+ // Test creating a simple marker
+ try {
+ const testCircle = L.circle([0, 0], { radius: 1000 });
+ console.log("✅ Can create Leaflet circle:", testCircle);
+ } catch (e) {
+ console.error("❌ Failed to create Leaflet circle:", e);
+ }
+ } else {
+ console.error("❌ Leaflet not loaded!");
+ }
+ },
+
+ // Force map resize (sometimes fixes display issues)
+ resizeMap() {
+ const { map } = window.FlashPointMap || {};
+ if (map) {
+ map.invalidateSize();
+ console.log("🔄 Map resized");
+ }
+ }
+};
+
+// Show debug help
+console.log(`
+🔧 FlashPoint Debug Commands:
+- FlashPointDebug.checkMap() - Check map status and layer counts
+- FlashPointDebug.addTestMarker() - Add basic test marker
+- FlashPointDebug.addTestMilitary() - Add test military aircraft
+- FlashPointDebug.addTestCivilian() - Add test civilian aircraft
+- FlashPointDebug.addTestTanker() - Add test oil tanker
+- FlashPointDebug.addMarker(lat, lon, color, label) - Add custom marker
+- FlashPointDebug.clearAllMarkers() - Clear all markers from all layers
+- FlashPointDebug.clearLayer(type) - Clear specific layer (military/civilian/tankers/conflicts/hotspots)
+- FlashPointDebug.refreshTracking() - Refresh flight and ship data
+- FlashPointDebug.testLeaflet() - Test Leaflet library
+- FlashPointDebug.resizeMap() - Force map resize
+`);
+
+export default window.FlashPointDebug;
\ No newline at end of file
diff --git a/frontend/web/js/feed.js b/frontend/web/js/feed.js
index 2ad05d9..e57e14b 100644
--- a/frontend/web/js/feed.js
+++ b/frontend/web/js/feed.js
@@ -1,5 +1,5 @@
/**
- * feed.js - Real-time event feed via SSE
+ * feed.js - Optimized real-time event feed via SSE
*/
import { API_BASE, ENDPOINTS, biasClass, escapeHTML } from './utils.js';
@@ -7,6 +7,8 @@ import { updateMapHotspot } from './map.js';
let feedItems = [];
let eventSource = null;
+const pendingCards = [];
+let renderScheduled = false;
/**
* Build a feed card DOM element
@@ -28,29 +30,96 @@ function buildFeedCard(item, isNew = false) {
}
/**
- * Prepend new event card to feed (newest at top)
+ * Batch render pending cards using requestAnimationFrame
*/
-function prependFeedCard(item) {
+function scheduleBatchRender() {
+ if (renderScheduled || pendingCards.length === 0) return;
+
+ renderScheduled = true;
+ requestAnimationFrame(() => {
+ renderPendingCards();
+ renderScheduled = false;
+ });
+}
+
+/**
+ * Render all pending cards in one batch
+ */
+function renderPendingCards() {
+ if (pendingCards.length === 0) return;
+
const container = document.getElementById("feed-container");
if (!container) return;
- // Remove placeholder if present
- const ph = container.querySelector(".feed-placeholder");
- if (ph) ph.remove();
+ const fragment = document.createDocumentFragment();
+ const batch = pendingCards.splice(0, 10); // Process max 10 at a time
- const card = buildFeedCard(item, true);
- container.insertBefore(card, container.firstChild);
+ batch.forEach(item => {
+ const card = buildFeedCard(item, true);
+ fragment.appendChild(card);
+ feedItems.unshift(item);
- // Trigger slide-in animation
- setTimeout(() => card.classList.remove("feed-card--new"), 10);
+ // Update map with retry logic
+ if (item.lat && item.lon) {
+ waitForMapAndRenderHotspot(item);
+ }
+ });
+
+ // Add all cards at once
+ if (container.firstChild) {
+ container.insertBefore(fragment, container.firstChild);
+ } else {
+ container.appendChild(fragment);
+ }
// Limit displayed cards to 100
const cards = container.querySelectorAll(".cyber-card");
if (cards.length > 100) {
- cards[cards.length - 1].remove();
+ for (let i = 100; i < cards.length; i++) {
+ cards[i].remove();
+ }
+ }
+
+ // If more pending, schedule next batch
+ if (pendingCards.length > 0) {
+ scheduleBatchRender();
+ }
+}
+
+/**
+ * Queue new event card for batch rendering
+ */
+function queueFeedCard(item) {
+ pendingCards.push(item);
+ scheduleBatchRender();
+}
+
+/**
+ * Wait for map and render hotspot with retry logic
+ */
+function waitForMapAndRenderHotspot(item) {
+ const maxAttempts = 10;
+ let attempts = 0;
+
+ function tryRender() {
+ attempts++;
+ const { map, hotspotLayer } = window.FlashPointMap || {};
+
+ if (map && hotspotLayer) {
+ console.log("📍 Map and hotspot layer ready, rendering hotspot marker...");
+ updateMapHotspot(item);
+ return;
+ }
+
+ if (attempts < maxAttempts) {
+ console.log(`📍 Map/hotspot layer not ready, attempt ${attempts}/${maxAttempts}`);
+ setTimeout(tryRender, 500);
+ } else {
+ console.error("❌ Failed to render hotspot - map/hotspot layer not ready");
+ }
}
- feedItems.unshift(item);
+ tryRender();
}
/**
@@ -74,6 +143,11 @@ async function loadInitialEvents() {
const card = buildFeedCard(event);
container.appendChild(card);
feedItems.push(event);
+
+ // Update map hotspot for initial events with retry logic
+ if (event.lat && event.lon) {
+ waitForMapAndRenderHotspot(event);
+ }
});
console.log(`✅ Loaded ${data.count} initial events`);
@@ -107,19 +181,17 @@ function connectSSE() {
eventSource.onmessage = (e) => {
try {
const data = JSON.parse(e.data);
-
- // Ignore initial snapshot (already loaded via REST)
+
+ // Ignore duplicates
if (feedItems.some(item => item.id === data.id)) {
return;
}
- prependFeedCard(data);
-
- // Update map hotspot
- updateMapHotspot(data);
-
+ // Queue for batch rendering
+ queueFeedCard(data);
+
} catch (err) {
- console.error("SSE parse error:", err, e.data);
+ console.error("SSE parse error:", err);
}
};
diff --git a/frontend/web/js/map.js b/frontend/web/js/map.js
index 03b7002..9de235b 100644
--- a/frontend/web/js/map.js
+++ b/frontend/web/js/map.js
@@ -1,123 +1,318 @@
/**
- * map.js - Leaflet map with hotspots and conflict markers
+ * map.js - Ultra-simplified map with debugging
*/
import { escapeHTML } from './utils.js';
export let map, markerLayer;
-const locationFreq = {};
+const locationData = new Map();
+const markerCache = new Map();
+let updateQueue = [];
+let isProcessing = false;
+
+// Separate layers for different marker types
+let militaryAircraftLayer, civilianAircraftLayer, oilTankerLayer, conflictLayer, hotspotLayer;
+let layerControl;
+
+// Make map and layers globally accessible for debugging
+window.FlashPointMap = {
+ map: null,
+ markerLayer: null,
+ militaryAircraftLayer: null,
+ civilianAircraftLayer: null,
+ oilTankerLayer: null,
+ conflictLayer: null,
+ hotspotLayer: null,
+ addTestMarker: null
+};
+
const HOTSPOT_COLORS = {
- 1: "#00FF00",
- 5: "#FFFF00",
- 10: "#FF8800",
- 20: "#FF0000"
+ 1: "#10b981", // Green
+ 5: "#f59e0b", // Amber
+ 10: "#ef4444", // Red
+ 20: "#dc2626" // Dark Red
};
/**
- * Initialize Leaflet map
+ * Initialize map with maximum debugging
*/
export function initMap() {
- map = L.map("map", {
- center: [30, 20],
- zoom: 2,
- zoomControl: true,
- scrollWheelZoom: true
+ const container = document.getElementById("map");
+ if (!container) {
+ console.error("❌ Map container '#map' not found");
+ return false;
+ }
+
+ console.log("🗺️ Map container found:", container);
+ console.log("🗺️ Container dimensions:", {
+ width: container.offsetWidth,
+ height: container.offsetHeight
});
- // Dark tile layer
- L.tileLayer("https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png", {
- attribution: '© OpenStreetMap contributors © CARTO',
- subdomains: 'abcd',
- maxZoom: 19
- }).addTo(map);
+ try {
+ // Clear existing map
+ if (map) {
+ console.log("🗑️ Removing existing map");
+ map.remove();
+ }
- markerLayer = L.layerGroup().addTo(map);
-
- console.log("🗺️ Map initialized");
-}
+ // Create map
+ console.log("🗺️ Creating Leaflet map...");
+ map = L.map("map", {
+ center: [40, -95], // Center on USA
+ zoom: 4,
+ zoomControl: true,
+ scrollWheelZoom: true,
+ preferCanvas: true,
+ renderer: L.canvas({ padding: 0.5 })
+ });
-/**
- * Get hotspot color based on frequency
- */
-function getHotspotColor(count) {
- if (count >= 20) return HOTSPOT_COLORS[20];
- if (count >= 10) return HOTSPOT_COLORS[10];
- if (count >= 5) return HOTSPOT_COLORS[5];
- return HOTSPOT_COLORS[1];
+ // Add tile layer
+ console.log("🗺️ Adding OSM tiles...");
+ L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
+ attribution: '© OpenStreetMap',
+ subdomains: ['a', 'b', 'c'],
+ maxZoom: 18
+ }).addTo(map);
+
+ // Create marker layer (main container)
+ console.log("🗺️ Creating marker layer...");
+ markerLayer = L.layerGroup().addTo(map);
+
+ // Create separate layers for different marker types
+ militaryAircraftLayer = L.layerGroup().addTo(map);
+ civilianAircraftLayer = L.layerGroup().addTo(map);
+ oilTankerLayer = L.layerGroup().addTo(map);
+ conflictLayer = L.layerGroup().addTo(map);
+ hotspotLayer = L.layerGroup().addTo(map);
+
+ // Create layer control
+ const overlayMaps = {
+ "🎯 Military Aircraft": militaryAircraftLayer,
+ "✈️ Civilian Aircraft": civilianAircraftLayer,
+ "🛢️ Oil Tankers": oilTankerLayer,
+ "⚔️ Conflicts": conflictLayer,
+ "📍 Geo Hotspots": hotspotLayer
+ };
+
+ layerControl = L.control.layers(null, overlayMaps, {
+ collapsed: false,
+ position: 'topright'
+ }).addTo(map);
+
+ // Make all layers globally accessible
+ window.FlashPointMap.map = map;
+ window.FlashPointMap.markerLayer = markerLayer;
+ window.FlashPointMap.militaryAircraftLayer = militaryAircraftLayer;
+ window.FlashPointMap.civilianAircraftLayer = civilianAircraftLayer;
+ window.FlashPointMap.oilTankerLayer = oilTankerLayer;
+ window.FlashPointMap.conflictLayer = conflictLayer;
+ window.FlashPointMap.hotspotLayer = hotspotLayer;
+
+ // Add test marker function to global scope
+ window.FlashPointMap.addTestMarker = function() {
+ console.log("🧪 Adding manual test marker...");
+ const testMarker = L.circle([40.7128, -74.0060], {
+ color: '#ff0000',
+ fillColor: '#ff0000',
+ fillOpacity: 0.6,
+ radius: 100000,
+ weight: 3
+ });
+
+ testMarker.bindPopup("🧪 Manual Test Marker
New York City");
+ testMarker.addTo(markerLayer);
+
+ console.log("✅ Manual test marker added");
+ return testMarker;
+ };
+
+ // Immediately add a test marker
+ console.log("🧪 Adding immediate test marker...");
+ setTimeout(() => {
+ const immediateTest = L.circle([51.5074, -0.1278], {
+ color: '#00ff00',
+ fillColor: '#00ff00',
+ fillOpacity: 0.8,
+ radius: 50000,
+ weight: 4
+ });
+
+ immediateTest.bindPopup("✅ IMMEDIATE TEST MARKER
London, UK
If you see this, map rendering works!");
+ immediateTest.addTo(markerLayer);
+
+ console.log("✅ Immediate test marker added to London");
+ console.log("🗺️ Marker layer has", markerLayer.getLayers().length, "markers");
+ }, 500);
+
+ // Log successful initialization
+ console.log("✅ Map initialized successfully");
+ console.log("🗺️ Map object:", map);
+ console.log("🗺️ Marker layer object:", markerLayer);
+
+ return true;
+
+ } catch (error) {
+ console.error("❌ Map initialization failed:", error);
+ return false;
+ }
}
/**
- * Update map hotspot for an event
+ * Simplified marker update - uses hotspot layer
*/
export function updateMapHotspot(item) {
- if (!item.lat || !item.lon) return;
+ if (!item?.lat || !item?.lon) {
+ console.log("⚠️ Missing coordinates:", item);
+ return;
+ }
- const place = item.place || "Unknown";
- const key = `${place}|${item.lat.toFixed(2)}|${item.lon.toFixed(2)}`;
+ if (!map || !hotspotLayer) {
+ console.log("⚠️ Map or hotspot layer not initialized");
+ return;
+ }
- locationFreq[key] = (locationFreq[key] || 0) + 1;
- const count = locationFreq[key];
-
- // Remove old marker
- markerLayer.eachLayer(layer => {
- if (layer.options.locationKey === key) {
- markerLayer.removeLayer(layer);
- }
+ console.log("📍 Adding hotspot marker:", {
+ lat: item.lat,
+ lon: item.lon,
+ place: item.place
});
- // Add new circle with updated radius
- const radius = Math.sqrt(count) * 50000; // Scale radius
- const color = getHotspotColor(count);
-
- const circle = L.circle([item.lat, item.lon], {
- color: color,
- fillColor: color,
- fillOpacity: 0.4,
- radius: radius,
- locationKey: key
- }).addTo(markerLayer);
-
- circle.bindPopup(`
- ${escapeHTML(place)}
- Events: ${count}
- Latest: ${escapeHTML(item.text.substring(0, 100))}...
- `);
+ try {
+ const circle = L.circle([item.lat, item.lon], {
+ color: '#0066ff',
+ fillColor: '#0066ff',
+ fillOpacity: 0.6,
+ radius: 75000,
+ weight: 2
+ });
+
+ circle.bindPopup(`
+ ${escapeHTML(item.place || 'Unknown')}
+ ${escapeHTML((item.text || '').substring(0, 100))}...
+ `);
+
+ circle.addTo(hotspotLayer);
+
+ console.log("✅ Hotspot marker added successfully");
+ console.log("🗺️ Total hotspot markers:", hotspotLayer.getLayers().length);
+
+ } catch (error) {
+ console.error("❌ Failed to add hotspot marker:", error);
+ }
}
/**
- * Render conflict markers from API
+ * Simplified conflict markers - uses conflict layer
*/
export async function renderConflictMarkers(conflicts) {
- if (!conflicts || !Array.isArray(conflicts)) return;
+ if (!conflicts?.length) {
+ console.log("⚠️ No conflicts to render");
+ return;
+ }
- conflicts.forEach(conflict => {
- if (!conflict.lat || !conflict.lon) return;
+ if (!map || !conflictLayer) {
+ console.log("⚠️ Map or conflict layer not initialized for conflicts");
+ return;
+ }
- const severityColors = {
- critical: "#FF0000",
- high: "#FF8800",
- medium: "#FFFF00",
- low: "#00FF00"
- };
+ console.log("🔴 Rendering conflict markers:", conflicts.length);
- const color = severityColors[conflict.severity?.toLowerCase()] || "#FFFFFF";
- const radius = 30000; // Fixed size for conflicts
+ // Clear existing conflict markers
+ conflictLayer.clearLayers();
- const circle = L.circle([conflict.lat, conflict.lon], {
- color: color,
- fillColor: color,
- fillOpacity: 0.6,
- radius: radius,
- weight: 2
- }).addTo(markerLayer);
+ conflicts.forEach((conflict, index) => {
+ if (!conflict.lat || !conflict.lon) {
+ console.log("⚠️ Conflict missing coordinates:", conflict);
+ return;
+ }
- circle.bindPopup(`
- ${escapeHTML(conflict.name)}
- ${escapeHTML(conflict.status || "Active")}
- Severity: ${escapeHTML(conflict.severity || "Unknown")}
- ${conflict.description ? escapeHTML(conflict.description.substring(0, 150)) + "..." : ""}
- `);
+ try {
+ const colors = {
+ critical: "#dc2626",
+ high: "#ea580c",
+ medium: "#f59e0b",
+ low: "#16a34a"
+ };
+
+ const color = colors[conflict.severity?.toLowerCase()] || "#6b7280";
+
+ const marker = L.circleMarker([conflict.lat, conflict.lon], {
+ color: color,
+ fillColor: color,
+ fillOpacity: 0.8,
+ radius: 8,
+ weight: 2
+ });
+
+ marker.bindPopup(`
+ ${escapeHTML(conflict.name)}
+ Status: ${escapeHTML(conflict.status || 'Active')}
+ Severity: ${escapeHTML(conflict.severity || 'Unknown')}
+ `);
+
+ marker.addTo(conflictLayer);
+ console.log(`✅ Added conflict marker ${index + 1}:`, conflict.name);
+
+ } catch (error) {
+ console.error("❌ Failed to add conflict marker:", error, conflict);
+ }
});
- console.log(`✅ Rendered ${conflicts.length} conflict markers`);
+ console.log("✅ Conflict markers rendering complete");
+ console.log("🗺️ Total conflict markers:", conflictLayer.getLayers().length);
+}
+
+/**
+ * Clear all markers from all layers
+ */
+export function clearMap() {
+ let totalCleared = 0;
+
+ if (markerLayer) {
+ totalCleared += markerLayer.getLayers().length;
+ markerLayer.clearLayers();
+ }
+ if (militaryAircraftLayer) {
+ totalCleared += militaryAircraftLayer.getLayers().length;
+ militaryAircraftLayer.clearLayers();
+ }
+ if (civilianAircraftLayer) {
+ totalCleared += civilianAircraftLayer.getLayers().length;
+ civilianAircraftLayer.clearLayers();
+ }
+ if (oilTankerLayer) {
+ totalCleared += oilTankerLayer.getLayers().length;
+ oilTankerLayer.clearLayers();
+ }
+ if (conflictLayer) {
+ totalCleared += conflictLayer.getLayers().length;
+ conflictLayer.clearLayers();
+ }
+ if (hotspotLayer) {
+ totalCleared += hotspotLayer.getLayers().length;
+ hotspotLayer.clearLayers();
+ }
+
+ console.log(`🗑️ Cleared ${totalCleared} markers from all layers`);
+ locationData.clear();
+ markerCache.clear();
+ updateQueue = [];
}
+
+/**
+ * Get map statistics for all layers
+ */
+export function getMapStats() {
+ return {
+ hasMap: !!map,
+ hasMarkerLayer: !!markerLayer,
+ markerCount: markerLayer ? markerLayer.getLayers().length : 0,
+ militaryAircraft: militaryAircraftLayer ? militaryAircraftLayer.getLayers().length : 0,
+ civilianAircraft: civilianAircraftLayer ? civilianAircraftLayer.getLayers().length : 0,
+ oilTankers: oilTankerLayer ? oilTankerLayer.getLayers().length : 0,
+ conflicts: conflictLayer ? conflictLayer.getLayers().length : 0,
+ hotspots: hotspotLayer ? hotspotLayer.getLayers().length : 0,
+ queueSize: updateQueue.length
+ };
+}
\ No newline at end of file
diff --git a/frontend/web/js/reports.js b/frontend/web/js/reports.js
index ea44c3a..f81dbee 100644
--- a/frontend/web/js/reports.js
+++ b/frontend/web/js/reports.js
@@ -24,10 +24,12 @@ async function generateReport() {
const data = await resp.json();
latestReport = data.report;
- const outputDiv = document.getElementById("report-output");
- if (outputDiv) {
- outputDiv.innerHTML = `${latestReport}`;
- }
+ // Show report section and populate preview
+ const reportSection = document.getElementById("report-section");
+ const reportPreview = document.getElementById("report-preview");
+
+ if (reportSection) reportSection.classList.remove("hidden");
+ if (reportPreview) reportPreview.value = latestReport;
showNotification("SITREP generated successfully", "success");
console.log("✅ Report generated");
@@ -35,15 +37,10 @@ async function generateReport() {
} catch (err) {
console.error("Report generation failed:", err);
showNotification(`Failed to generate report: ${err.message}`, "error");
-
- const outputDiv = document.getElementById("report-output");
- if (outputDiv) {
- outputDiv.innerHTML = `❌ ${err.message}
`;
- }
} finally {
btn.disabled = false;
- btn.textContent = "🔄 Generate Report";
+ btn.textContent = "GENERATE REPORT";
}
}
@@ -80,7 +77,7 @@ async function downloadPDF() {
} finally {
btn.disabled = false;
- btn.textContent = "📄 Download PDF";
+ btn.textContent = "⬇ DOWNLOAD PDF";
}
}
diff --git a/frontend/web/js/test-data.js b/frontend/web/js/test-data.js
new file mode 100644
index 0000000..cb915dd
--- /dev/null
+++ b/frontend/web/js/test-data.js
@@ -0,0 +1,76 @@
+/**
+ * test-data.js - Simple test markers for debugging
+ */
+
+/**
+ * Add simple test markers directly
+ */
+export function initTestData() {
+ console.log("🧪 Test data initializing...");
+
+ setTimeout(() => {
+ // Access global map objects
+ const { map, markerLayer } = window.FlashPointMap || {};
+
+ if (!map || !markerLayer) {
+ console.error("❌ Test data: Map not available globally");
+ console.log("Available:", window.FlashPointMap);
+ return;
+ }
+
+ console.log("🧪 Adding test markers...");
+
+ // Test marker 1: Paris (Green)
+ const parisMarker = L.circle([48.8566, 2.3522], {
+ color: '#00ff00',
+ fillColor: '#00ff00',
+ fillOpacity: 0.7,
+ radius: 80000,
+ weight: 3
+ });
+ parisMarker.bindPopup("🧪 TEST: Paris, France
Green Circle");
+ parisMarker.addTo(markerLayer);
+
+ // Test marker 2: Tokyo (Red)
+ const tokyoMarker = L.circle([35.6762, 139.6503], {
+ color: '#ff0000',
+ fillColor: '#ff0000',
+ fillOpacity: 0.7,
+ radius: 80000,
+ weight: 3
+ });
+ tokyoMarker.bindPopup("🧪 TEST: Tokyo, Japan
Red Circle");
+ tokyoMarker.addTo(markerLayer);
+
+ // Test marker 3: New York (Blue)
+ const nyMarker = L.circle([40.7128, -74.0060], {
+ color: '#0000ff',
+ fillColor: '#0000ff',
+ fillOpacity: 0.7,
+ radius: 80000,
+ weight: 3
+ });
+ nyMarker.bindPopup("🧪 TEST: New York, USA
Blue Circle");
+ nyMarker.addTo(markerLayer);
+
+ // Test conflict marker: Los Angeles (Purple)
+ const laConflict = L.circleMarker([34.0522, -118.2437], {
+ color: '#800080',
+ fillColor: '#800080',
+ fillOpacity: 0.8,
+ radius: 10,
+ weight: 3
+ });
+ laConflict.bindPopup("⚔️ TEST CONFLICT
Los Angeles
Critical Status");
+ laConflict.addTo(markerLayer);
+
+ console.log("✅ Added 4 test markers");
+ console.log("🗺️ Total markers:", markerLayer.getLayers().length);
+
+ // Center map on USA to see markers
+ map.setView([39.8283, -98.5795], 4);
+
+ console.log("✅ Test data complete - check map for colored circles!");
+
+ }, 2000); // Wait 2 seconds for map to be ready
+}
\ No newline at end of file
diff --git a/frontend/web/js/tracking.js b/frontend/web/js/tracking.js
new file mode 100644
index 0000000..2bbeed5
--- /dev/null
+++ b/frontend/web/js/tracking.js
@@ -0,0 +1,448 @@
+/**
+ * tracking.js - Flight and Ship tracking integration with better timing
+ */
+
+import { API_BASE } from './utils.js';
+
+let flightMarkers = [];
+let shipMarkers = [];
+let lastPositions = new Map(); // Track previous positions for movement calculation
+
+/**
+ * Fetch and render flight tracking data
+ */
+async function fetchFlights() {
+ try {
+ console.log("✈️ Fetching flights...");
+ const resp = await fetch(`${API_BASE}/api/tracking/flights?limit=50`);
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
+
+ const data = await resp.json();
+
+ if (data.success && data.flights) {
+ console.log(`✈️ Received ${data.flights.length} flights`);
+ waitForMapAndRenderFlights(data.flights);
+ }
+
+ } catch (err) {
+ console.error("Failed to fetch flights:", err);
+ }
+}
+
+/**
+ * Fetch and render ship tracking data
+ */
+async function fetchShips() {
+ try {
+ console.log("🚢 Fetching ships...");
+ const resp = await fetch(`${API_BASE}/api/tracking/ships?limit=50`);
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
+
+ const data = await resp.json();
+
+ if (data.success && data.ships) {
+ console.log(`🚢 Received ${data.ships.length} ships`);
+ waitForMapAndRenderShips(data.ships);
+ }
+
+ } catch (err) {
+ console.error("Failed to fetch ships:", err);
+ }
+}
+
+/**
+ * Wait for map and render flights
+ */
+function waitForMapAndRenderFlights(flights) {
+ const maxAttempts = 10;
+ let attempts = 0;
+
+ function tryRender() {
+ attempts++;
+ const { map, militaryAircraftLayer, civilianAircraftLayer } = window.FlashPointMap || {};
+
+ if (map && militaryAircraftLayer && civilianAircraftLayer) {
+ console.log("✈️ Map and aircraft layers ready, rendering flights...");
+ renderFlightMarkers(flights);
+ return;
+ }
+
+ if (attempts < maxAttempts) {
+ console.log(`✈️ Aircraft layers not ready, attempt ${attempts}/${maxAttempts}`);
+ setTimeout(tryRender, 500);
+ } else {
+ console.error("❌ Failed to render flights - aircraft layers not ready");
+ }
+ }
+
+ tryRender();
+}
+
+/**
+ * Wait for map and render ships
+ */
+function waitForMapAndRenderShips(ships) {
+ const maxAttempts = 10;
+ let attempts = 0;
+
+ function tryRender() {
+ attempts++;
+ const { map, oilTankerLayer } = window.FlashPointMap || {};
+
+ if (map && oilTankerLayer) {
+ console.log("🚢 Map and tanker layer ready, rendering ships...");
+ renderShipMarkers(ships);
+ return;
+ }
+
+ if (attempts < maxAttempts) {
+ console.log(`🚢 Tanker layer not ready, attempt ${attempts}/${maxAttempts}`);
+ setTimeout(tryRender, 500);
+ } else {
+ console.error("❌ Failed to render ships - tanker layer not ready");
+ }
+ }
+
+ tryRender();
+}
+
+/**
+ * Render flight markers with filtering and dynamic movement
+ */
+function renderFlightMarkers(flights) {
+ const { map, militaryAircraftLayer, civilianAircraftLayer } = window.FlashPointMap || {};
+ if (!map || !militaryAircraftLayer || !civilianAircraftLayer) {
+ console.error("❌ No map layers available for flight rendering");
+ return;
+ }
+
+ console.log(`✈️ Rendering ${flights.length} flight markers...`);
+
+ // Clear old flight markers
+ flightMarkers.forEach(marker => {
+ if (militaryAircraftLayer.hasLayer(marker) || civilianAircraftLayer.hasLayer(marker)) {
+ militaryAircraftLayer.removeLayer(marker);
+ civilianAircraftLayer.removeLayer(marker);
+ }
+ });
+ flightMarkers = [];
+
+ // Filter: Only show military and civilian aircraft (exclude cargo, private, etc.)
+ const filteredFlights = flights.filter(flight => {
+ if (!flight.lat || !flight.lon) return false;
+
+ // Only show aircraft that are definitely military or civilian
+ const callsign = (flight.callsign || '').toUpperCase();
+ const icao = (flight.icao24 || '').toUpperCase();
+
+ // Military aircraft indicators
+ const isMilitary = flight.military === true ||
+ callsign.includes('AF') || // Air Force
+ callsign.includes('ARMY') ||
+ callsign.includes('NAVY') ||
+ callsign.match(/^[A-Z]{1,3}\d{2,4}$/) || // Military pattern like AF1, ARMY01
+ flight.squawk === '7700'; // Emergency military
+
+ // Civilian aircraft indicators
+ const isCivilian = flight.military === false ||
+ callsign.match(/^[A-Z]{2,3}\d{1,4}[A-Z]?$/) || // Airline format like UAL123, BA456A
+ flight.altitude > 20000; // High altitude civilian
+
+ return isMilitary || isCivilian;
+ });
+
+ console.log(`✈️ Filtered to ${filteredFlights.length} military/civilian aircraft`);
+
+ let militaryRendered = 0, civilianRendered = 0;
+
+ filteredFlights.forEach(flight => {
+ try {
+ const isMilitary = flight.military === true ||
+ (flight.callsign || '').toUpperCase().includes('AF') ||
+ (flight.callsign || '').toUpperCase().includes('ARMY') ||
+ (flight.callsign || '').toUpperCase().includes('NAVY');
+
+ const color = isMilitary ? "#ff3333" : "#0099ff";
+ const icon = isMilitary ? "🎯" : "✈️";
+ const layer = isMilitary ? militaryAircraftLayer : civilianAircraftLayer;
+
+ // Debug: Check if layer is available
+ if (!layer) {
+ console.error(`❌ Layer not available for ${isMilitary ? 'military' : 'civilian'} aircraft`);
+ return;
+ }
+
+ // Debug: Log first few aircraft
+ if (filteredFlights.indexOf(flight) < 3) {
+ console.log(`🔍 Aircraft sample:`, {
+ callsign: flight.callsign,
+ icao24: flight.icao24,
+ military: flight.military,
+ isMilitary: isMilitary,
+ lat: flight.lat,
+ lon: flight.lon,
+ layerType: isMilitary ? 'military' : 'civilian'
+ });
+ }
+
+ // Calculate movement vector if we have previous position
+ const flightId = flight.icao24 || flight.callsign;
+ const currentPos = [flight.lat, flight.lon];
+ const lastPos = lastPositions.get(flightId);
+ let movementArrow = '';
+
+ if (lastPos) {
+ const deltaLat = flight.lat - lastPos[0];
+ const deltaLon = flight.lon - lastPos[1];
+ const distance = Math.sqrt(deltaLat * deltaLat + deltaLon * deltaLon);
+
+ if (distance > 0.01) { // Significant movement
+ const heading = Math.atan2(deltaLon, deltaLat) * 180 / Math.PI;
+ movementArrow = getArrowForHeading(heading);
+ }
+ }
+
+ // Store current position for next update
+ lastPositions.set(flightId, currentPos);
+
+ const marker = L.circleMarker(currentPos, {
+ color: color,
+ fillColor: color,
+ fillOpacity: 0.8,
+ radius: isMilitary ? 10 : 6,
+ weight: 3
+ });
+
+ marker.bindPopup(`
+
+
+ ${icon} ${flight.callsign || flight.icao24} ${movementArrow}
+
+
+ ${isMilitary ? '🎯 MILITARY' : '✈️ CIVILIAN'}
+ Alt: ${flight.altitude || 'N/A'}ft
+ Speed: ${flight.velocity || 'N/A'} m/s
+ ${flight.emergency ? '⚠️ EMERGENCY' : ''}
+ ${movementArrow ? `
Moving: ${movementArrow}` : ''}
+
+
+ `);
+
+ marker.addTo(layer);
+ flightMarkers.push(marker);
+
+ if (isMilitary) militaryRendered++;
+ else civilianRendered++;
+
+ // Debug first few additions
+ if (filteredFlights.indexOf(flight) < 3) {
+ console.log(`✅ Added ${isMilitary ? 'military' : 'civilian'} marker for ${flight.callsign || flight.icao24}`);
+ }
+
+ } catch (error) {
+ console.error("❌ Failed to render flight marker:", error, flight);
+ }
+ });
+
+ console.log(`✅ Rendered ${militaryRendered} military + ${civilianRendered} civilian aircraft`);
+}
+
+/**
+ * Get arrow symbol for movement direction
+ */
+function getArrowForHeading(heading) {
+ const directions = ['↑', '↗', '→', '↘', '↓', '↙', '←', '↖'];
+ const index = Math.round(((heading + 360) % 360) / 45) % 8;
+ return directions[index];
+}
+
+/**
+ * Render ship markers - ONLY oil tankers with dynamic movement
+ */
+function renderShipMarkers(ships) {
+ const { map, oilTankerLayer } = window.FlashPointMap || {};
+ if (!map || !oilTankerLayer) {
+ console.error("❌ No map or oil tanker layer available for ship rendering");
+ return;
+ }
+
+ console.log(`🚢 Rendering ${ships.length} ship markers...`);
+
+ // Clear old ship markers
+ shipMarkers.forEach(marker => {
+ if (oilTankerLayer.hasLayer(marker)) {
+ oilTankerLayer.removeLayer(marker);
+ }
+ });
+ shipMarkers = [];
+
+ // Filter: ONLY show oil tankers
+ const oilTankers = ships.filter(ship => {
+ if (!ship.lat || !ship.lon) return false;
+
+ // Debug: Log first few ships to see data structure
+ if (ships.indexOf(ship) < 3) {
+ console.log("🔍 Ship data sample:", {
+ name: ship.name,
+ is_tanker: ship.is_tanker,
+ vessel_type: ship.vessel_type,
+ ship_type: ship.ship_type,
+ mmsi: ship.mmsi
+ });
+ }
+
+ // Multiple ways to detect oil tankers - made less strict
+ const isTanker = ship.is_tanker === true ||
+ ship.is_tanker === 'true' ||
+ (ship.vessel_type && ship.vessel_type.toLowerCase().includes('tanker')) ||
+ (ship.name && ship.name.toLowerCase().includes('tanker')) ||
+ (ship.name && ship.name.toLowerCase().includes('crude')) ||
+ (ship.name && ship.name.toLowerCase().includes('oil')) ||
+ ship.ship_type === 80 || // IMO tanker code
+ ship.ship_type === 81 || // Chemical tanker
+ ship.ship_type === 82 || // LNG tanker
+ // Add more lenient detection
+ (ship.flag && ship.flag.toLowerCase() === 'liberia') || // Common tanker flag
+ (ship.speed !== undefined && ship.speed < 8); // Slow ships often tankers
+
+ return isTanker;
+ });
+
+ console.log(`🛢️ Filtered to ${oilTankers.length} oil tankers out of ${ships.length} ships`);
+
+ let rendered = 0;
+ oilTankers.forEach(ship => {
+ try {
+ // Calculate movement vector if we have previous position
+ const shipId = ship.mmsi || ship.name;
+ const currentPos = [ship.lat, ship.lon];
+ const lastPos = lastPositions.get(shipId);
+ let movementArrow = '';
+
+ if (lastPos) {
+ const deltaLat = ship.lat - lastPos[0];
+ const deltaLon = ship.lon - lastPos[1];
+ const distance = Math.sqrt(deltaLat * deltaLat + deltaLon * deltaLon);
+
+ if (distance > 0.005) { // Significant movement for ships (slower than aircraft)
+ const heading = Math.atan2(deltaLon, deltaLat) * 180 / Math.PI;
+ movementArrow = getArrowForHeading(heading);
+ }
+ }
+
+ // Store current position for next update
+ lastPositions.set(shipId, currentPos);
+
+ // Determine tanker type and color
+ let tankerType = 'OIL TANKER';
+ let color = '#ff8800';
+
+ if ((ship.name || '').toLowerCase().includes('crude')) {
+ tankerType = 'CRUDE TANKER';
+ color = '#cc4400';
+ } else if ((ship.name || '').toLowerCase().includes('lng') || ship.ship_type === 82) {
+ tankerType = 'LNG TANKER';
+ color = '#00cc88';
+ } else if (ship.ship_type === 81) {
+ tankerType = 'CHEMICAL TANKER';
+ color = '#cc0088';
+ }
+
+ const marker = L.circleMarker(currentPos, {
+ color: color,
+ fillColor: color,
+ fillOpacity: 0.8,
+ radius: 9,
+ weight: 3
+ });
+
+ marker.bindPopup(`
+
+
+ 🛢️ ${ship.name || ship.mmsi} ${movementArrow}
+
+
+ ${tankerType}
+ Speed: ${ship.speed || 'N/A'} knots
+ Course: ${ship.course || 'N/A'}°
+ Flag: ${ship.flag || 'N/A'}
+ ${movementArrow ? `Moving: ${movementArrow}` : 'Stationary'}
+
+
+ `);
+
+ marker.addTo(oilTankerLayer);
+ shipMarkers.push(marker);
+ rendered++;
+
+ } catch (error) {
+ console.error("❌ Failed to render tanker marker:", error, ship);
+ }
+ });
+
+ console.log(`✅ Rendered ${rendered} oil tanker markers`);
+}
+
+/**
+ * Initialize tracking module with dynamic updates
+ */
+export function initTracking() {
+ console.log("📡 Initializing enhanced flight & ship tracking...");
+
+ // Wait longer for map layers to be ready
+ setTimeout(() => {
+ fetchFlights();
+ fetchShips();
+ }, 2000);
+
+ // More frequent updates for dynamic tracking
+ // Aircraft: every 30 seconds (they move fast)
+ setInterval(fetchFlights, 30 * 1000);
+
+ // Ships: every 2 minutes (they move slower)
+ setInterval(fetchShips, 2 * 60 * 1000);
+
+ console.log("✅ Dynamic tracking initialized:");
+ console.log(" - Aircraft: 30s updates");
+ console.log(" - Ships: 2min updates");
+ console.log(" - Movement vectors enabled");
+}
+
+/**
+ * Manual refresh functions for UI controls
+ */
+export function refreshFlights() {
+ console.log("🔄 Manual flight refresh...");
+ fetchFlights();
+}
+
+export function refreshShips() {
+ console.log("🔄 Manual ship refresh...");
+ fetchShips();
+}
+
+/**
+ * Clear position history (useful for debugging)
+ */
+export function clearTrackingHistory() {
+ lastPositions.clear();
+ console.log("🗑️ Cleared movement tracking history");
+}
+
+/**
+ * Get tracking statistics
+ */
+export function getTrackingStats() {
+ const stats = window.FlashPointMap ? {
+ militaryAircraft: window.FlashPointMap.militaryAircraftLayer ?
+ window.FlashPointMap.militaryAircraftLayer.getLayers().length : 0,
+ civilianAircraft: window.FlashPointMap.civilianAircraftLayer ?
+ window.FlashPointMap.civilianAircraftLayer.getLayers().length : 0,
+ oilTankers: window.FlashPointMap.oilTankerLayer ?
+ window.FlashPointMap.oilTankerLayer.getLayers().length : 0,
+ trackedPositions: lastPositions.size
+ } : {};
+
+ console.log("📊 Tracking Stats:", stats);
+ return stats;
+}
\ No newline at end of file
diff --git a/frontend/web/styles.css b/frontend/web/styles.css
index 7a33cd6..67582dd 100644
--- a/frontend/web/styles.css
+++ b/frontend/web/styles.css
@@ -215,14 +215,148 @@ svg path { fill: currentColor !important; }
.report-preview:focus { outline: 2px solid #1a1a1a; }
-/* ── MAP ── */
+/* ── MAP OPTIMIZATION ── */
.map-container {
- flex: 1; min-height: 0;
- border-radius: 0;
- overflow: hidden;
+ flex: 1;
+ min-height: 500px;
+ max-height: calc(100vh - 120px);
+ width: 100%;
+ position: relative;
border: 2px solid #1a1a1a;
- background: #d8d3c3;
- filter: grayscale(40%) sepia(20%);
+ background: #f5f5f5;
+ overflow: hidden;
+ /* GPU acceleration for smooth rendering */
+ transform: translateZ(0);
+ -webkit-backface-visibility: hidden;
+ backface-visibility: hidden;
+ will-change: transform;
+}
+
+#map {
+ width: 100% !important;
+ height: 100% !important;
+ min-height: 500px;
+ position: relative;
+ z-index: 1;
+ /* Optimize tile rendering */
+ image-rendering: optimizeSpeed;
+ image-rendering: -webkit-optimize-contrast;
+}
+
+/* Custom Leaflet popup styles */
+.leaflet-popup-content-wrapper {
+ background: #1a1a1a !important;
+ color: #ffffff !important;
+ border: 2px solid #cc0000 !important;
+ border-radius: 0 !important;
+ font-family: 'JetBrains Mono', monospace !important;
+ font-size: 12px !important;
+ box-shadow: 4px 4px 8px rgba(0,0,0,0.5) !important;
+}
+
+.leaflet-popup-tip {
+ background: #1a1a1a !important;
+ border: 2px solid #cc0000 !important;
+ border-top: none !important;
+ border-radius: 0 !important;
+}
+
+.map-popup {
+ padding: 8px;
+ min-width: 200px;
+}
+
+.popup-title {
+ font-weight: bold;
+ font-size: 14px;
+ margin-bottom: 4px;
+ color: #ffffff;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.popup-stats {
+ color: #00ff88;
+ font-size: 12px;
+ margin-bottom: 6px;
+ font-weight: bold;
+}
+
+.popup-latest {
+ font-size: 11px;
+ color: #cccccc;
+ line-height: 1.4;
+}
+
+.conflict-popup {
+ padding: 6px;
+}
+
+.conflict-title {
+ font-weight: bold;
+ font-size: 13px;
+ margin-bottom: 4px;
+ text-transform: uppercase;
+}
+
+.conflict-status, .conflict-severity {
+ font-size: 11px;
+ margin-bottom: 2px;
+}
+
+/* Leaflet controls cyber styling */
+.leaflet-control-zoom {
+ border: 2px solid #1a1a1a !important;
+ border-radius: 0 !important;
+ box-shadow: 2px 2px 4px rgba(0,0,0,0.3) !important;
+}
+
+.leaflet-control-zoom a {
+ background: #1a1a1a !important;
+ color: #ffffff !important;
+ border: none !important;
+ font-family: 'JetBrains Mono', monospace !important;
+ font-weight: bold !important;
+ width: 30px !important;
+ height: 30px !important;
+ line-height: 28px !important;
+}
+
+.leaflet-control-zoom a:hover {
+ background: #cc0000 !important;
+ color: #ffffff !important;
+}
+
+/* CRITICAL: Ensure Leaflet markers are visible */
+.leaflet-marker-pane,
+.leaflet-marker-pane * {
+ visibility: visible !important;
+ opacity: 1 !important;
+ z-index: 1000 !important;
+}
+
+.leaflet-overlay-pane {
+ z-index: 400 !important;
+}
+
+.leaflet-overlay-pane svg,
+.leaflet-overlay-pane canvas {
+ visibility: visible !important;
+ opacity: 1 !important;
+ pointer-events: auto !important;
+}
+
+/* Force Leaflet circles/paths to be visible */
+.leaflet-overlay-pane path {
+ visibility: visible !important;
+ opacity: 1 !important;
+ fill-opacity: 0.6 !important;
+ stroke-opacity: 1 !important;
+}
+
+.leaflet-interactive {
+ cursor: pointer !important;
+ visibility: visible !important;
}
.balance-labels {
@@ -272,6 +406,7 @@ svg path { fill: currentColor !important; }
.chat-history::-webkit-scrollbar-track { background: #ddd8cc; }
.chat-history::-webkit-scrollbar-thumb { background: #999; border-radius: 0; }
+.chat-msg,
.chat-bubble {
max-width: 90%;
padding: 9px 13px;
@@ -281,6 +416,7 @@ svg path { fill: currentColor !important; }
word-break: break-word;
}
+.chat-msg--user,
.chat-bubble.user {
align-self: flex-end;
background: #1a1a1a;
@@ -288,6 +424,7 @@ svg path { fill: currentColor !important; }
border: none;
}
+.chat-msg--assistant,
.chat-bubble.assistant {
align-self: flex-start;
background: #f5f2e8;
@@ -296,6 +433,16 @@ svg path { fill: currentColor !important; }
color: #1a1a1a;
}
+.chat-msg--system,
+.chat-bubble.system {
+ align-self: center;
+ background: #e8e4d8;
+ border: 1px solid #999;
+ color: #555;
+ font-size: 0.75rem;
+ text-align: center;
+}
+
.chat-bubble.thinking {
align-self: flex-start;
background: transparent;
@@ -306,6 +453,20 @@ svg path { fill: currentColor !important; }
border-radius: 0;
}
+.chat-role {
+ display: block;
+ font-weight: 700;
+ font-size: 0.7rem;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ margin-bottom: 4px;
+ opacity: 0.8;
+}
+
+.chat-content {
+ display: block;
+}
+
.chat-form { display: flex; gap: 0; flex-shrink: 0; }
.chat-input {
diff --git a/logs/celery_beat.log b/logs/celery_beat.log
index 67191fd..0d42616 100644
--- a/logs/celery_beat.log
+++ b/logs/celery_beat.log
@@ -3,3 +3,9 @@
[2026-03-22 15:57:07,846: INFO/MainProcess] Scheduler: Sending due task fetch-news-api (tasks.news_worker.fetch_news)
[2026-03-22 15:57:07,869: INFO/MainProcess] Scheduler: Sending due task fetch-rss-feeds (tasks.rss_worker.fetch_all_rss)
[2026-03-22 15:57:14,084: INFO/MainProcess] Scheduler: Sending due task fetch-reddit-posts (tasks.reddit_worker.fetch_reddit)
+[2026-03-22 15:58:14,084: INFO/MainProcess] Scheduler: Sending due task fetch-reddit-posts (tasks.reddit_worker.fetch_reddit)
+[2026-03-22 15:59:14,084: INFO/MainProcess] Scheduler: Sending due task fetch-reddit-posts (tasks.reddit_worker.fetch_reddit)
+[2026-03-22 16:00:14,084: INFO/MainProcess] Scheduler: Sending due task fetch-reddit-posts (tasks.reddit_worker.fetch_reddit)
+[2026-03-22 16:01:14,084: INFO/MainProcess] Scheduler: Sending due task fetch-reddit-posts (tasks.reddit_worker.fetch_reddit)
+[2026-03-22 16:02:07,869: INFO/MainProcess] Scheduler: Sending due task fetch-rss-feeds (tasks.rss_worker.fetch_all_rss)
+[2026-03-22 16:02:14,084: INFO/MainProcess] Scheduler: Sending due task fetch-reddit-posts (tasks.reddit_worker.fetch_reddit)
diff --git a/logs/celery_worker.log b/logs/celery_worker.log
index 3d29371..3b27465 100644
--- a/logs/celery_worker.log
+++ b/logs/celery_worker.log
@@ -70,3 +70,87 @@
[2026-03-22 15:57:14,095: INFO/MainProcess] Task tasks.reddit_worker.fetch_reddit[0faa2a75-ba9f-47c0-88de-615e2e302768] received
[2026-03-22 15:57:15,432: WARNING/ForkPoolWorker-10] ✅ Reddit: 0 new posts
[2026-03-22 15:57:15,443: INFO/ForkPoolWorker-10] Task tasks.reddit_worker.fetch_reddit[0faa2a75-ba9f-47c0-88de-615e2e302768] succeeded in 1.3461838380007976s: {'status': 'success', 'new_posts': 0}
+[2026-03-22 15:58:14,095: INFO/MainProcess] Task tasks.reddit_worker.fetch_reddit[f6507d4e-2b93-48a0-87fc-7998ca82c50e] received
+[2026-03-22 15:58:20,303: WARNING/ForkPoolWorker-10] ✅ Reddit: 0 new posts
+[2026-03-22 15:58:20,315: INFO/ForkPoolWorker-10] Task tasks.reddit_worker.fetch_reddit[f6507d4e-2b93-48a0-87fc-7998ca82c50e] succeeded in 6.21937701600109s: {'status': 'success', 'new_posts': 0}
+[2026-03-22 15:59:14,095: INFO/MainProcess] Task tasks.reddit_worker.fetch_reddit[2ea02c7c-5b81-48a8-8e03-b484bc3a9f1e] received
+[2026-03-22 15:59:15,334: INFO/MainProcess] Task tasks.processor.process_event[a7d67f06-60a1-4330-ab04-150adf53a762] received
+[2026-03-22 15:59:15,362: WARNING/ForkPoolWorker-11] ⚠️ Event None not found
+[2026-03-22 15:59:15,363: INFO/ForkPoolWorker-11] Task tasks.processor.process_event[a7d67f06-60a1-4330-ab04-150adf53a762] succeeded in 0.028530712001156644s: {'status': 'error', 'message': 'Event not found'}
+[2026-03-22 15:59:15,365: WARNING/ForkPoolWorker-10] ✅ Reddit: 1 new posts
+[2026-03-22 15:59:15,367: INFO/ForkPoolWorker-10] Task tasks.reddit_worker.fetch_reddit[2ea02c7c-5b81-48a8-8e03-b484bc3a9f1e] succeeded in 1.2706945210011327s: {'status': 'success', 'new_posts': 1}
+[2026-03-22 16:00:14,096: INFO/MainProcess] Task tasks.reddit_worker.fetch_reddit[66394475-bda6-43e9-a0f3-41c9f67f7a63] received
+[2026-03-22 16:00:15,850: WARNING/ForkPoolWorker-10] ✅ Reddit: 0 new posts
+[2026-03-22 16:00:15,862: INFO/ForkPoolWorker-10] Task tasks.reddit_worker.fetch_reddit[66394475-bda6-43e9-a0f3-41c9f67f7a63] succeeded in 1.7640483289997064s: {'status': 'success', 'new_posts': 0}
+[2026-03-22 16:01:14,095: INFO/MainProcess] Task tasks.reddit_worker.fetch_reddit[ddf42550-f33e-45ed-9de9-f4df0596da3a] received
+[2026-03-22 16:01:15,483: INFO/MainProcess] Task tasks.processor.process_event[0d467055-78d1-4061-b963-b11536f655f3] received
+[2026-03-22 16:01:15,500: WARNING/ForkPoolWorker-11] ⚠️ Event None not found
+[2026-03-22 16:01:15,501: INFO/ForkPoolWorker-11] Task tasks.processor.process_event[0d467055-78d1-4061-b963-b11536f655f3] succeeded in 0.017689016000076663s: {'status': 'error', 'message': 'Event not found'}
+[2026-03-22 16:01:15,504: WARNING/ForkPoolWorker-10] ✅ Reddit: 1 new posts
+[2026-03-22 16:01:15,505: INFO/ForkPoolWorker-10] Task tasks.reddit_worker.fetch_reddit[ddf42550-f33e-45ed-9de9-f4df0596da3a] succeeded in 1.4087705629990523s: {'status': 'success', 'new_posts': 1}
+[2026-03-22 16:02:07,882: INFO/MainProcess] Task tasks.rss_worker.fetch_all_rss[d5596343-ee30-407a-8d1e-296a99067796] received
+[2026-03-22 16:02:07,884: WARNING/ForkPoolWorker-10] 📡 Fetching 18 RSS feeds...
+[2026-03-22 16:02:07,887: INFO/MainProcess] Task tasks.rss_worker.fetch_single_rss[f8671780-6ad4-4d06-b438-4c63a91164d8] received
+[2026-03-22 16:02:07,888: INFO/MainProcess] Task tasks.rss_worker.fetch_single_rss[125fa2a1-12f3-405d-a229-4843dc76e0af] received
+[2026-03-22 16:02:07,892: INFO/MainProcess] Task tasks.rss_worker.fetch_single_rss[9c700290-10d2-40a6-b1d0-40eaea72be51] received
+[2026-03-22 16:02:07,893: INFO/MainProcess] Task tasks.rss_worker.fetch_single_rss[f1af8ac9-7fd2-4560-8f59-a2936234da98] received
+[2026-03-22 16:02:07,895: INFO/MainProcess] Task tasks.rss_worker.fetch_single_rss[530ba5ed-a248-454a-b0fa-1152c4513fde] received
+[2026-03-22 16:02:07,896: INFO/MainProcess] Task tasks.rss_worker.fetch_single_rss[c8091289-8973-438d-9987-899b13b05272] received
+[2026-03-22 16:02:07,899: INFO/MainProcess] Task tasks.rss_worker.fetch_single_rss[2e4fb441-d22c-4468-a9ea-da155af3c016] received
+[2026-03-22 16:02:07,901: INFO/MainProcess] Task tasks.rss_worker.fetch_single_rss[53308142-0d9e-4fc9-aeb4-17e47334775d] received
+[2026-03-22 16:02:07,903: INFO/MainProcess] Task tasks.rss_worker.fetch_single_rss[e0709ade-0e61-441d-91c5-ddd5c080e584] received
+[2026-03-22 16:02:07,905: INFO/MainProcess] Task tasks.rss_worker.fetch_single_rss[dd4057bb-c0b3-4f7d-b083-4c7100392a16] received
+[2026-03-22 16:02:07,906: INFO/MainProcess] Task tasks.rss_worker.fetch_single_rss[9b834431-1ed5-48fd-9823-f5ffa96ef749] received
+[2026-03-22 16:02:07,907: INFO/ForkPoolWorker-10] Task tasks.rss_worker.fetch_all_rss[d5596343-ee30-407a-8d1e-296a99067796] succeeded in 0.024227644000347937s: {'status': 'queued', 'count': 18}
+[2026-03-22 16:02:07,907: INFO/MainProcess] Task tasks.rss_worker.fetch_single_rss[a844c9dc-a15c-4465-9482-9ca72d32092c] received
+[2026-03-22 16:02:07,908: INFO/MainProcess] Task tasks.rss_worker.fetch_single_rss[e308cc02-5219-4db3-a84a-c97d7def3c37] received
+[2026-03-22 16:02:07,909: INFO/MainProcess] Task tasks.rss_worker.fetch_single_rss[8b34b7cd-93bb-4ce8-9eb9-0d180ff19a11] received
+[2026-03-22 16:02:07,910: INFO/MainProcess] Task tasks.rss_worker.fetch_single_rss[0dacba00-e0e4-4f1f-a6e4-98d5ded12e76] received
+[2026-03-22 16:02:07,911: INFO/MainProcess] Task tasks.rss_worker.fetch_single_rss[9a39a511-8c1e-45c4-ab63-115c2e44f0a7] received
+[2026-03-22 16:02:07,911: INFO/MainProcess] Task tasks.rss_worker.fetch_single_rss[c2290128-1dd7-4caa-b197-73aac4b5aba6] received
+[2026-03-22 16:02:07,912: INFO/MainProcess] Task tasks.rss_worker.fetch_single_rss[944ba725-401c-4925-b8b7-b35e752690ae] received
+[2026-03-22 16:02:08,170: WARNING/ForkPoolWorker-15] ✅ RSS Al Jazeera: 0 new items
+[2026-03-22 16:02:08,171: INFO/ForkPoolWorker-15] Task tasks.rss_worker.fetch_single_rss[530ba5ed-a248-454a-b0fa-1152c4513fde] succeeded in 0.27491214300061984s: {'status': 'success', 'feed': 'Al Jazeera', 'new_items': 0}
+[2026-03-22 16:02:08,235: WARNING/ForkPoolWorker-8] ✅ RSS Times of Israel: 0 new items
+[2026-03-22 16:02:08,235: INFO/ForkPoolWorker-8] Task tasks.rss_worker.fetch_single_rss[0dacba00-e0e4-4f1f-a6e4-98d5ded12e76] succeeded in 0.32451037900136726s: {'status': 'success', 'feed': 'Times of Israel', 'new_items': 0}
+[2026-03-22 16:02:08,292: WARNING/ForkPoolWorker-6] ⚠️ RSS parse error for Ukraine Pravda:
+[2026-03-22 16:02:08,294: INFO/ForkPoolWorker-6] Task tasks.rss_worker.fetch_single_rss[e308cc02-5219-4db3-a84a-c97d7def3c37] succeeded in 0.38420458899963705s: {'status': 'error', 'message': ''}
+[2026-03-22 16:02:08,306: WARNING/ForkPoolWorker-4] ⚠️ RSS parse error for DW News: :2:0: syntax error
+[2026-03-22 16:02:08,307: INFO/ForkPoolWorker-4] Task tasks.rss_worker.fetch_single_rss[dd4057bb-c0b3-4f7d-b083-4c7100392a16] succeeded in 0.4004385740008729s: {'status': 'error', 'message': ':2:0: syntax error'}
+[2026-03-22 16:02:08,309: WARNING/ForkPoolWorker-12] ✅ RSS SCMP: 0 new items
+[2026-03-22 16:02:08,309: INFO/ForkPoolWorker-12] Task tasks.rss_worker.fetch_single_rss[125fa2a1-12f3-405d-a229-4843dc76e0af] succeeded in 0.42058950399950845s: {'status': 'success', 'feed': 'SCMP', 'new_items': 0}
+[2026-03-22 16:02:08,355: WARNING/ForkPoolWorker-7] ⚠️ RSS parse error for Jerusalem Post: text/html; charset=utf-8 is not an XML media type
+[2026-03-22 16:02:08,356: INFO/ForkPoolWorker-7] Task tasks.rss_worker.fetch_single_rss[8b34b7cd-93bb-4ce8-9eb9-0d180ff19a11] succeeded in 0.4469912149997981s: {'status': 'error', 'message': 'text/html; charset=utf-8 is not an XML media type'}
+[2026-03-22 16:02:08,412: WARNING/ForkPoolWorker-5] ✅ RSS The Guardian World: 0 new items
+[2026-03-22 16:02:08,412: INFO/ForkPoolWorker-5] Task tasks.rss_worker.fetch_single_rss[9b834431-1ed5-48fd-9823-f5ffa96ef749] succeeded in 0.5061851789996581s: {'status': 'success', 'feed': 'The Guardian World', 'new_items': 0}
+[2026-03-22 16:02:08,430: WARNING/ForkPoolWorker-1] ⚠️ RSS parse error for Reuters World:
+[2026-03-22 16:02:08,431: INFO/ForkPoolWorker-1] Task tasks.rss_worker.fetch_single_rss[2e4fb441-d22c-4468-a9ea-da155af3c016] succeeded in 0.5314619180007867s: {'status': 'error', 'message': ''}
+[2026-03-22 16:02:08,452: INFO/MainProcess] Task tasks.processor.process_event[1fa4fca1-6174-4012-9f60-b8e05756901b] received
+[2026-03-22 16:02:08,466: WARNING/ForkPoolWorker-12] ⚠️ Event None not found
+[2026-03-22 16:02:08,467: INFO/ForkPoolWorker-12] Task tasks.processor.process_event[1fa4fca1-6174-4012-9f60-b8e05756901b] succeeded in 0.014233669000532245s: {'status': 'error', 'message': 'Event not found'}
+[2026-03-22 16:02:08,473: WARNING/ForkPoolWorker-13] ✅ RSS NYTimes: 1 new items
+[2026-03-22 16:02:08,474: INFO/ForkPoolWorker-13] Task tasks.rss_worker.fetch_single_rss[9c700290-10d2-40a6-b1d0-40eaea72be51] succeeded in 0.5814898710013949s: {'status': 'success', 'feed': 'NYTimes', 'new_items': 1}
+[2026-03-22 16:02:08,566: WARNING/ForkPoolWorker-3] ✅ RSS France 24: 0 new items
+[2026-03-22 16:02:08,567: INFO/ForkPoolWorker-3] Task tasks.rss_worker.fetch_single_rss[e0709ade-0e61-441d-91c5-ddd5c080e584] succeeded in 0.662431505999848s: {'status': 'success', 'feed': 'France 24', 'new_items': 0}
+[2026-03-22 16:02:08,596: WARNING/ForkPoolWorker-14] ✅ RSS BBC World: 0 new items
+[2026-03-22 16:02:08,596: INFO/ForkPoolWorker-14] Task tasks.rss_worker.fetch_single_rss[f1af8ac9-7fd2-4560-8f59-a2936234da98] succeeded in 0.7006060650001018s: {'status': 'success', 'feed': 'BBC World', 'new_items': 0}
+[2026-03-22 16:02:08,839: WARNING/ForkPoolWorker-15] ⚠️ RSS parse error for Military Times: :34:189: not well-formed (invalid token)
+[2026-03-22 16:02:08,840: INFO/ForkPoolWorker-15] Task tasks.rss_worker.fetch_single_rss[c2290128-1dd7-4caa-b197-73aac4b5aba6] succeeded in 0.6685227710004256s: {'status': 'error', 'message': ':34:189: not well-formed (invalid token)'}
+[2026-03-22 16:02:08,963: WARNING/ForkPoolWorker-9] ✅ RSS Bellingcat: 0 new items
+[2026-03-22 16:02:08,964: INFO/ForkPoolWorker-9] Task tasks.rss_worker.fetch_single_rss[9a39a511-8c1e-45c4-ab63-115c2e44f0a7] succeeded in 1.0530578589987272s: {'status': 'success', 'feed': 'Bellingcat', 'new_items': 0}
+[2026-03-22 16:02:09,150: WARNING/ForkPoolWorker-2] ⚠️ RSS parse error for AP News: :351:32: not well-formed (invalid token)
+[2026-03-22 16:02:09,152: INFO/ForkPoolWorker-2] Task tasks.rss_worker.fetch_single_rss[53308142-0d9e-4fc9-aeb4-17e47334775d] succeeded in 1.2493687480000517s: {'status': 'error', 'message': ':351:32: not well-formed (invalid token)'}
+[2026-03-22 16:02:09,702: WARNING/ForkPoolWorker-8] ⚠️ RSS parse error for Defense One: :20:42: not well-formed (invalid token)
+[2026-03-22 16:02:09,703: INFO/ForkPoolWorker-8] Task tasks.rss_worker.fetch_single_rss[944ba725-401c-4925-b8b7-b35e752690ae] succeeded in 1.4673395330009953s: {'status': 'error', 'message': ':20:42: not well-formed (invalid token)'}
+[2026-03-22 16:02:09,783: WARNING/ForkPoolWorker-10] ⚠️ RSS parse error for Kyiv Independent: :2:729247: not well-formed (invalid token)
+[2026-03-22 16:02:09,784: INFO/ForkPoolWorker-10] Task tasks.rss_worker.fetch_single_rss[a844c9dc-a15c-4465-9482-9ca72d32092c] succeeded in 1.8764025610016688s: {'status': 'error', 'message': ':2:729247: not well-formed (invalid token)'}
+[2026-03-22 16:02:09,912: WARNING/ForkPoolWorker-16] ✅ RSS TASS: 0 new items
+[2026-03-22 16:02:09,913: INFO/ForkPoolWorker-16] Task tasks.rss_worker.fetch_single_rss[c8091289-8973-438d-9987-899b13b05272] succeeded in 2.0139641869991465s: {'status': 'success', 'feed': 'TASS', 'new_items': 0}
+[2026-03-22 16:02:11,368: WARNING/ForkPoolWorker-11] ✅ RSS Russia Today: 0 new items
+[2026-03-22 16:02:11,378: INFO/ForkPoolWorker-11] Task tasks.rss_worker.fetch_single_rss[f8671780-6ad4-4d06-b438-4c63a91164d8] succeeded in 3.4892667650001385s: {'status': 'success', 'feed': 'Russia Today', 'new_items': 0}
+[2026-03-22 16:02:14,094: INFO/MainProcess] Task tasks.reddit_worker.fetch_reddit[d71d27e1-734d-401d-8ae5-eaaef848c24a] received
+[2026-03-22 16:02:16,680: INFO/MainProcess] Task tasks.processor.process_event[3a181ec7-c191-4b49-bf1b-0b1c439d538a] received
+[2026-03-22 16:02:16,688: WARNING/ForkPoolWorker-11] ⚠️ Event None not found
+[2026-03-22 16:02:16,689: INFO/ForkPoolWorker-11] Task tasks.processor.process_event[3a181ec7-c191-4b49-bf1b-0b1c439d538a] succeeded in 0.0075494269985938445s: {'status': 'error', 'message': 'Event not found'}
+[2026-03-22 16:02:16,719: WARNING/ForkPoolWorker-10] ✅ Reddit: 1 new posts
+[2026-03-22 16:02:16,721: INFO/ForkPoolWorker-10] Task tasks.reddit_worker.fetch_reddit[d71d27e1-734d-401d-8ae5-eaaef848c24a] succeeded in 2.626639743999476s: {'status': 'success', 'new_posts': 1}
diff --git a/test-layers.html b/test-layers.html
new file mode 100644
index 0000000..5709129
--- /dev/null
+++ b/test-layers.html
@@ -0,0 +1,255 @@
+
+
+
+
+
+ FlashPoint - Enhanced Layer Test
+
+
+
+
+
+
+
🚀 FlashPoint Enhanced Layer Test
+
+
+
📊 Layer Statistics (Auto-updating)
+
Initializing...
+
+
+
+
🗺️ Map with Layer Controls
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test-timing.html b/test-timing.html
new file mode 100644
index 0000000..2b096a7
--- /dev/null
+++ b/test-timing.html
@@ -0,0 +1,232 @@
+
+
+
+
+
+ FlashPoint - Timing Test
+
+
+
+
+
+
+
🔧 FlashPoint Timing Test
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file