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...
+
+ +
+ + + + + +
+ +
+ + + +
+ +
+ +
+

📝 Debug Log

+
+
+
+ + + + + \ 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

+
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + +
+ +
+

📝 Test Log

+
+
+
+ + + + + \ 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

+ +
+

🗺️ Map Container

+
+
+ +
+ + + + + +
+ +
+

📝 Test Log

+
+
+
+ + + + + \ No newline at end of file