From 7ecf6ceb7f5ce87c60bc02b84437874086c1f79b Mon Sep 17 00:00:00 2001 From: Reaper-ai Date: Sun, 22 Mar 2026 16:02:15 +0530 Subject: [PATCH 1/3] done --- logs/celery_beat.log | 6 ++++ logs/celery_worker.log | 79 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) 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..5b41bd7 100644 --- a/logs/celery_worker.log +++ b/logs/celery_worker.log @@ -70,3 +70,82 @@ [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 From d17f0cec532605e524705976c0cd80f50a784970 Mon Sep 17 00:00:00 2001 From: Reaper-ai Date: Sun, 22 Mar 2026 16:03:38 +0530 Subject: [PATCH 2/3] jkbjkg --- logs/celery_worker.log | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/logs/celery_worker.log b/logs/celery_worker.log index 5b41bd7..3b27465 100644 --- a/logs/celery_worker.log +++ b/logs/celery_worker.log @@ -149,3 +149,8 @@ [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} From 3c7c6eaf6509e3ffd03eed6854c86dbd18b4dc52 Mon Sep 17 00:00:00 2001 From: Reaper-ai Date: Sun, 22 Mar 2026 17:09:48 +0530 Subject: [PATCH 3/3] uga budga --- backend/api.py | 30 +- backend/config/auth_telegram.py | 2 +- backend/init_infra.py | 1 - .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 150 bytes .../__pycache__/database.cpython-310.pyc | Bin 0 -> 4334 bytes .../__pycache__/database.cpython-311.pyc | Bin 6903 -> 7305 bytes backend/models/database.py | 83 +++- .../conflict_service.cpython-311.pyc | Bin 14216 -> 11670 bytes .../__pycache__/rag_service.cpython-311.pyc | Bin 7636 -> 7647 bytes .../tracking_service.cpython-311.pyc | Bin 0 -> 11210 bytes backend/services/rag_service.py | 8 +- backend/services/tracking_service.py | 49 +- .../__pycache__/news_worker.cpython-311.pyc | Bin 5555 -> 6094 bytes .../__pycache__/processor.cpython-311.pyc | Bin 5292 -> 5250 bytes .../__pycache__/reddit_worker.cpython-311.pyc | Bin 5782 -> 6321 bytes .../__pycache__/rss_worker.cpython-311.pyc | Bin 6811 -> 7336 bytes .../telegram_worker.cpython-311.pyc | Bin 7052 -> 7584 bytes backend/workers/tasks/news_worker.py | 27 +- backend/workers/tasks/processor.py | 23 +- backend/workers/tasks/reddit_worker.py | 27 +- backend/workers/tasks/rss_worker.py | 27 +- backend/workers/tasks/telegram_worker.py | 29 +- data/commodity_cache.json | 10 +- data/conflicts.json | 200 +++++++- debug-layers.html | 292 ++++++++++++ diagnostic_map.html | 124 +++++ frontend/web/app.js | 35 +- frontend/web/index.html | 4 +- frontend/web/js/conflicts.js | 57 ++- frontend/web/js/debug.js | 238 ++++++++++ frontend/web/js/feed.js | 112 ++++- frontend/web/js/map.js | 365 ++++++++++---- frontend/web/js/reports.js | 19 +- frontend/web/js/test-data.js | 76 +++ frontend/web/js/tracking.js | 448 ++++++++++++++++++ frontend/web/styles.css | 173 ++++++- test-layers.html | 255 ++++++++++ test-timing.html | 232 +++++++++ 38 files changed, 2722 insertions(+), 224 deletions(-) create mode 100644 backend/models/__pycache__/__init__.cpython-310.pyc create mode 100644 backend/models/__pycache__/database.cpython-310.pyc create mode 100644 backend/services/__pycache__/tracking_service.cpython-311.pyc create mode 100644 debug-layers.html create mode 100644 diagnostic_map.html create mode 100644 frontend/web/js/debug.js create mode 100644 frontend/web/js/test-data.js create mode 100644 frontend/web/js/tracking.js create mode 100644 test-layers.html create mode 100644 test-timing.html 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 0000000000000000000000000000000000000000..9d924d865e85f3d419c19660636fa02f60099304 GIT binary patch literal 150 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 literal 0 HcmV?d00001 diff --git a/backend/models/__pycache__/database.cpython-311.pyc b/backend/models/__pycache__/database.cpython-311.pyc index b81653b8bdb87fdcf63871cb254ff2d4db8963ac..2968280b88cb93324f149b06fc095f550904ec67 100644 GIT binary patch delta 956 zcma)3PiWI{6n?)XP1>4tYn#rtwyRYU$}XaIpcQp)BDfWVtw=AE+C(k%kEW4UX`Kg^ zZHPPk4i2VLFY_=|#H-+;o;%oIBtz)bV+)Liem+yV=%S*BqUN~cW zCrMVoSo!`nHQ@MUd*e7V!~nbmg(yrKGNcRyETD6Ss23HMjyaula&W*LhB7IU3TF@k zFi--8r9qRACxEo+pU%$pX}Eo8OnYcXb!mGE0EJ&A0Be}NAfO1wv5M=jVeYW>mD3X! zrr{{)wrw8Z7-;@7v&+$3VpiZa)9)Kc=Q8t%QP87@3Zwu+&tKh3%*P^cel+dU4&{wC3-n6Y1m~ zHS?frs3p*~DI{~*teVuDf?8CQ_jR?&Cv%xhO4n{0ZOi3iQoXCEa@i2uooaH%K$EC> z7W;?k`yuaK8qq_{{4@M)k70ug)OMdO#%GF>AChxvc5${Y`5ThI1s=<}io78XE=?^? zEz8StZFsuwnQ3@t>SC-R#%f}09h;MjlMB)3(fMeL2g$yzZ2=z`Yy^IhgI5e!C j#2=xlSm>1dBT3UgZa>jJdp%!BFhHWD1$$R*3`hO~yz=jz delta 764 zcmZ`#&ubGw82!HNZqvlw{N98FB9Yia8jvc~sw+wY;-zf_PXTFZvn!^gso7mfmBxtR z!5*wI2MS#pR)j2zfxdZN;1%c>%719o8^?F#}_usb-urG8QmC|ad@J!oyL4HTS;W~LAi(}=-LF^}9iQ*z!aDLY_ G^!h&?EVAPO 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 43462be31de792c5248149a90f17f1896ed388e8..80962ffea8ed323e52d7567e2229bfc25f78f1fd 100644 GIT binary patch literal 11670 zcmd5iZA@HOcJIx+`GVooU_Q*p^9RO*VK5jR+rc(tKKz9-0UXC{;@QXC2Rt($NmwndvROx2sjQl+R;5<)R+Unzm1fXHOkaf>ZMD_%rzLV$vR3`k zbMBjg84Rhl>W^M#&b@Qbz31M0?m6e4doDlE$;sj%{q?{7opS3S$NfA0NTrfw;c2&t z;~sK^8{!C0On!cdXSr#}#B%eH8FI5<2na(0k8K3MC14%0va-c*3)qM30mqOdkTsOW zb0$u<%Gn?D9QP@Fjda`jZ6#ujWOE#`Asosf_Mu$jAXy}vI7tr4C3z&D6fBxZA=$HN zCq<-~>`h`yNGZdVF-$qxM=AhQNvcRS5gBGb!yF(7q0b?})R4oZmSK)CjEmHfqtK@w zFb%}bFpUgzj5Gn}5X_*4G?U{D)50({q%}E%DsqCn#4slrriz?mGpHxW$Z7I2!?ZC> zE;+--L275oIfiLx7!Nt0>{CNJNGHQ|F-$kP0GLBCHy2R5$S{`}#!fCL=XQ)-Aw3Lp zm0^0xH8!_Om_Z+Tg<<*`W`GQm>x?@!8?4tbqn-ayP~-cX*2 zyTFlKZ*#=^F-Xa$@HNs1vV3qys(G{Cur!@mK1M|Zz?!5t%id2F#ZUYRU{4F=#<#hERKc3RCQ;q zBO`iUyl#iN%bF69p)B-C+L+6%3!SiLUFdGo?Ani2|1eqF#-MshR9SV!GhM^%Xw z_J>tfks7X#Q%MQR;th%F)~+xOO~|_AMu@6%5ICXRJE)@J8rL1rYZSg5*M70xD9n1= zKQZZMzoY>Ojz&7V!+w8*3aw?)ARiQ!;2lYYDT$#GF*Q~wI2r=tqRO_QTOOui3D(t-A&Zy-#hpxB|@QTzr=JtH74lAb-F zj0TkvCLv|H8R)T9&uj` zg|#sywHOh$s~^O0K$b*xOqrO#3BZgrB^ZXHd))&4BL!Edq+mdzX{!}HAX2)mM+qpP zG@~P!{-ZUyMDDfrNXu2lkJFzLZ-;{#uvHly1FEAS`>+&ELY4G|Bxr*oGZLac8CYw@ z;r0$n$|S6MKm&mV9hZYBR;t0rzkDgh$6K(Pz8;SlNDU<4m8nlb6%WcV*(8;;)kKOf zQJ~u!_W7~F0L+4fMn;kw)ZRY&!TXT@<#KzZku@|70I*mCeu*cJ)zpiZJY619^1)

M5Dx)%WM@oA`6fE175QRk%$ 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 literal 14216 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 delta 128 zcmca_eZ`t@IWI340}!a+*_U};Y$IP3lK_8yL26!6erZW+k$zfHYU<`zrf)3jYPqQ; ziMlyCiMfgTU{cpu&sf*oAW1hfuehYBG`YkItauW;2Maq}ysy8DtIy_l+!Bl|R(EBT ZHgoaT^E3KP?vh!{!YS3jHrYmYF#yf+DWCuV 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 0000000000000000000000000000000000000000..835efb69f69b2e799bc6cf68ab7a0ebb9498dac2 GIT binary patch literal 11210 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 Q0YE?Yfll!5NCf%%ANE;93IG5A literal 0 HcmV?d00001 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 425038fa5fe1c2c59364e4cc5e893b71a4c8ac13..e6550a918cedb2bfea1b902493a59fecb1092559 100644 GIT binary patch delta 1911 zcmZ`)T}%{L6uvXV{_oB%vwws9$geQ|tXj%8Re}P=0>auzQX|@Imc1ja9d_xRSx_@8 zCbbC~5*Np&rfSO*O`{K{rb(aLKp&bm)ks3p&Gf}IZIiw&(LS_Elb*YP3QbSuetYhB z&YXMBJ@?K&*!5^H|Cz_*L?GM`-=u!^&G91hY_9pd6EoPM2;=@?|J)~)rvlW23U)3q z2;GITCc{DOx{QWHERtPk!y7Di-}7wt!~A8m0Oq?JE@)W9>=2T?Mu7P)ATg#-KBtjH zQj05@WI{`2(#FqB6GIG!?R#*T{fl9I#(}c2y)_VkuA|x@hfC-;A@t$7xyX&kiqO0w z0Jg0N?O#nU3q6l}Pb~|lj7Md^_rD4L_kCSFT13IlI&LxOrg~jjXOnHQiKBW8Lv^Vh z^d&b#k#}}Qxh2uxwVzws#ZY~}r7LRuV|mrZLpTn`(_gF)+O3NW$G#K%!frB+I={K- zoPeuOgJ;g43*>^jKo04G@x&ntTipCW@3V*f+qdw6HNLKP3m?>)Vfl5zbvkcE>=mu` zIJCgR4#QXzy&N~_Ufr(;^x(b51qaw*tVu4U+vWpC!s-trJ+wJKgTNM~He=f6^}*DE zn=W{u4u@`^4^e|3;T?MDrH0pO`wil3mYQ+-2W-X0DhRh+06opjj#~)tzQb*;67PY+BDdibHX!Zr^PEv5B9WY^*%sQV&QZ+z)C0Q)@0J0va^i&4&14DK%QSj>I*e~U^~cEpNQoSz&<^ki zy{o20o|MyC>;e~wL?kEppP!ITHigMy>eor3gF+VtTGPu>j3D0$`JLeRy;e`?1tcK= z(;e$MF%*Za;zxT<5|L7NBEOfFRZTTrs+Q2QYCM_2G6@)8^QU*Z=$rIgV{!?MP0JZ2 ztB#qriR_4yQpZSd$)d<RLU71mO-BjhM0Yzd;Jl8Q`PsZSe)DCHezvWgPd zrs{$`^%ybuOB3F*hbm#qf zx!@Iwi1SE=P{l{1*GF&4bLkuDJhyc!R(@*x*fwul7F$-tmb}HN6`F<$fiQhSl|{cZ z$gd)RXB1|7iX!rOuN}F1B;R&raCUHJu;Ave^vns*In6M*{;Gg`Xdz z&cO)x5H$=++`|Tj>XHResxWQwIL;*FaWV+|M2=B_y@D=IB(yOlHA2cL6);x)XNN=3 z_{mgKRwK}<`1=1!h7fuHP3x$v$>~WlK>g`qhRcvp-!1jvbMzDlRBF)Q_V)1%&MI;T v(SW13NNqB12JQ<5)cSbyWAp~Qq)~>677@i&4zbY{rme`n;MOceg)sjDa6r{@ delta 1417 zcmZWoU1%It6ux(7cQ(7(*`L{;Wb32i|$JN+hXhN;!5~cnbTZ|V;Lj#0M@RI`ib)z z8Vy*GQa%yfXG!^r>q$EzT|>*T{b1h&jfRM21V!B}g1=*raWfug^_GEE-SQ{K9!9L= z9kpN^f~8p@+}r3EIrQPoV*G>nmfX4}1NM9=ZpytsB+hNh=h$QGuLJ!&fH&ztUk4L4 z!kX6w>ROF85wWjD1gGiUdfDS~(e>_d{6!QWZD1K}r+V>>EeOiP0^5;~wN% zv*#y(YxguC`^+U>ZZ#aY5oxU6}&PrEtJ^Nl_fEoA8ZKi!#nbs0# z2TY+rRa1N=O|uG#il(UL3pAscR$ZCV=`ptBZs_`*kFRM{t|&Qucut7N<6vmjXQvd( zbEhXb9OKZ-;Uv4{NkE+Yp04L3lusq)lWw|(`_7M~AZBW??*gshR2fy?EGU|;ndO?E z)(cuHlP3!GGq3lxFdryWNE2e(rwU_gRvR}3Rmnk8A9>r}dV%lSh@z%vO-q)TVkSR1 znbl2OM$Kyqt^*(q5UV~+N16k}h>KeIpl4M&TTJ?P#>0Do~<=qt)d^4#pd z-hZp-y`dXJ3qy}wl^-WIU9}5miXxKyH;1neKdP)_Zv;+O!{Hit5dnY)$paoF5BvfI z*tda)X9f{k51wv^&jxZ19u+p6jf01TjZVzCl(7 zKCWiRsFhQG_ET`;gm_8OmE0BjBByxsVN%lC<>qa7d+EP3IG5A delta 194 zcmZqDT%*aioR^o20SE;6)?_jWZsaRuWV|`Kfl+JnIc|Z;I~fgC@=7vGDiu;IN{SMb zOEUBG6g2!?gA_`OGxO3FiVG5*D>b={CqHM@XEfO?&h(3sQEsyk%LFDywats!%GmiV zfkqdrGchnUFnnOM-K@@6$H@9YhJllN^9ug=jEps#X9(sna%me_#Mo2;w6%1C#j`pa>I4*oA4bt%xfC1OPS@ diff --git a/backend/workers/tasks/__pycache__/reddit_worker.cpython-311.pyc b/backend/workers/tasks/__pycache__/reddit_worker.cpython-311.pyc index 6a64e79af8dc8f4dafbc10bc8dd64af4d9377b03..91139ef582d4f136e6e1a194715601b28cc619c2 100644 GIT binary patch delta 1708 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 delta 1508 zcmZWpU1(fI6rQ;#%#KO z%)M(&*tH@NViB|sD6|#!DYdpBvJVQPs82R;f=?Gjd=UzQiTdPAJ!g|NZ89+5Ju~NL z&YAPg{cP;|sQ;#_dI${m>vx5Bw5$FYeeLR@`H)l$%mj?Uf`&RA&7k;F3#TPw=r9!{ zv_=WJ4RdQOgm)$JTQDl_EB?!ru#Ocn6Jc&cXC6bJb1?6UV?Qp(Qs@5zco3E5Dqikj8bLKqn165D!YuBt43xSxCY-#8?#{x_VAO+51 z#4|z%gyA_s)8d-v96c!h@T3(yibsUz?N#uCcu1V|W)8*C)6w=sUl43{&am?H*<7)h z<)+C;VK3cU^$ye22@vxS?im)?E*a)Or=5g+Lc}pO-ul~Dru5ZT&flQoisp^piLNPd ze=xU3L#sQR0q9_)TYL1bDXF_y=Q7+?!&O(r7g|(du{yQ-L{A`bgvBpmo`kD)FY~eZ zyAtNUHCSR9gfgvr>%MxRsUttcUz)$aF0BZJKdCL?PLQD95b zc@fg<+zt?woz2*`tci2FdcM0(uM_5}y8sV->f9H(r%s`W)D3x>^bu=hCqcME!iO8W z<=)+4k8Q`&#Sdy9)y2u8A}4?~e}=nhM0g@c29b*#`^u36wob41DB`JbQrZ#f!PZ1yHv1jXTDf54UU}5gg8=+A4I@n+7<8@L9!(_ zYby}Xtj+S5aKi~D@Fji*&7MMNU%@jdJqKXBw+}biRvnY$sA}iXCOtl%)c(K%W-hN8eTk-G?{HpqS|VR=g4m_im`Jh<}p+{EM*q?1oO%zy}joCO#Z} zZ|eQ2)hW@8Ma4I<@g$gb`8Nmv*sB4sR|BvF2#a|9_p#?wP&}R}&dTPJX*;SpYu;9J)v8fu{5)#?2myrl z4*+`x&CZ#Z+7H`yutMHyS9*)^G@9W*;J=R3r3$MRjWfIqOC*h16$g^n#Cr1PlarJ_ Z(M)d;{637EGLcUGLdQ3xy_$C9{{gRLOhEtu diff --git a/backend/workers/tasks/__pycache__/telegram_worker.cpython-311.pyc b/backend/workers/tasks/__pycache__/telegram_worker.cpython-311.pyc index 1c9e64f604616eb31cb1c58c46a65e9f468a70f0..d9f7a87a9e47c95cbd233afb018411a91cb84d7f 100644 GIT binary patch delta 2245 zcmZuzTW{1x6dv1qv-jq`#MxbPzp$*xtpq{{0g`fwLLn8U60mAn%-Tsd>?Iu=0+E;S zK&4jd!-heH0ulQ_Q>pOOR7jO7D5_Kh>I0GT5cvln9x8QFsam0`>Y2?25_M#MKJ(4- znKNh38T*Uv#hspS+-?Culb$|I9SKZ(LQKPSbmvq?hzkr-7{!)$#+~rAr(GFW+_lWP z<8I&_X-~!*_bzi{Tx5~zodsiesl{?*5OD4>-%4NMXBilWd#T0!>&AuhIC*H}r{aNi zW6pK&24S2Xt5~t6xD@w2&nye};a+Nr_cB`RQCw2Q%P3x{*p8x@Z}PJb)wDW->bt`feAw7N6S^_IXPm@ zkZ-NR+ymYLJzZ*Xu@@7BXK!vq)nizQJ3oKa2Ou1gloB0iJp?ml@x^ zjbP_{ObdD7Y!%#~L*V}7`P`q*W`_L5iov{9;XdLJ%G>hxyu+~KJ%*KZ*ee5;wL!_- zmU-(s-oDJ+*6|L#l8El`VP2=MEjPt(!1fxJn@HgeG)5gNx0`6jqF1l<5E}EY$e<;z z$VDGH!Znbr%fo)|1t+C8!=ealH^yKMn_)K`_nfmlY#7{2EiV{65j_%X2n0L2f($Z* zH}mJ6244(<^(G4l6;xpLR_ediWw?r!Kmu;eB}OYSS;t)+}z&1k3wpW;tfWvcUT zaNb?4p-!DL)+H~4^gS!45K;o=SO;ggwYx)0>nLhOdfyw}CWNc=o;7b*7#{MgHyrj{ z^iX|;$%8b(OrZ1J2{ggPB=WC!(~cN7G&E##X;}xzWligNT288_W2FKn-J`lbuJyFH z>unje{Wa5$J7C%vPX@)>`gRaaS2CB?)vTTvm99g9XX@l~~q;xEi8DHfToyp`=T5dRpGqPTu&#as%m$ufX%i0;OZ5-#4s;1>I z-cPlyikg-$Ks0bFtDF4(^GS7_mU&FX{dC^)3i_y~O;fXqit!=Z;YQTl+bf|QOKSaf zv9eW0HTlyiSxe~hh^Eofs9W1E;V@DBTZ7cRHV3D}-!IS$bP1Jg?=s}Bzc5r8E$Z`9 z%YxKW;1(OUOnVDl;pp||#eh^ooUe5;QhhsmBU)_!s_j-=Vek5GRqgG88w16G`Kp$M zs+Pij(j7SQ{$gkYy&|=xfKcs!i2zzGa8CmM!Oh&0&DOyV;;0y98p$UW;Ybz42Girl zw2WzxvG#FUA5EuD;~H|Oq9Ikzl4DfINx?-GSx^&()QQ8e!}5q~S~Xq9I;JNWK1vlN zd^Vdno7Ge4gp$%ApR;OGR~3AmY8(KFx$)|}c$l&^q+N=JTIt)iWI6@;k7ddyD4Zf$ zNvfx(2Bu%4GBkpq-G6IOEyLt%>5ykZ>iel{UK)IMC^0W3=KhgRGtqqv-%~>C@g;|Z zt_6Nui7k%|vE)+d*+JKVZuH0}^>=cQqGh?$(!bsQXgf#w9)`+0`yJ5v*|K*FJbvjG z20FOMY~#RY?r}3i<;_;WScu#RpPVu!O~vO@Pz-I5%!%di9Mna*1_$VB=(!9r)l8d` zgTgCXbD=b~vFQOnF~?8`xh`iCOJ^y|13k0_N|D9{+acJyX)%GtEK8(*Zh>9MJG cl4Rt#*!c{txyTQZ@9IYxrmKYhAInz%1GKOWGynhq delta 1796 zcmZuyO>7%Q6rS<^*!$zZb?j`Km^yBgVoFVww1K8AX%(ijjpLBLh z8f4Rg$^~&qIu%7~gM2{Y&Y-Zoij)&_1ws`k-q>+M0#BN6-hA)P zoA;h)*0rNwyd3z}?^h8>C~!aD5nc$GIJU6wD9I`W6S<%k)vSsU=u$z;YA|~XdeNWt zZ*YNZz=JIR5(K!%qcj8~;Iye=YfXGh80;{bk_|QO>?heI+3*f$b%(P7cF$C#wV7x{ zzwcl2KtLExA%PocdkWb$61;)3Q6i0^REUk>wdH=#Hy)`2Hng)R;w}7yU6zW=>(ZQv zO*XC$;(oTMeuaD3fcBh!AGp~(d?G*2Wk62wl)Y&yK#NQ3I? z6XlX)m7KB3oIT0zh2wGA6`i>mi@pM8dW6Fu2c9fVvuEK8zTK{1JCq(_7mQOu-cBk3 zxSl+rKK7&0VK#gZa!$w*;>r`{VlnT~DEr-LzsRGxO5QGCEYo7np^Y@!XUlZjqITNJ z+0%A zEz;A|*o-Y#8PDhJF(-G)wsr7IY{_+a9KS>kMfhfW2;R%Hb+nGIqq^M3qHXi+ueP&C z>V9wBc!~i2&jERtKfb z5c@4QiIZ%ky<=A=WJ(w@xouG9B8@HbBRyLCO_rjA&% z??6j)mn>JZog8&2Zv`FU4l#PAGN; z?tAw|P~qn-dNOGs|{?&5vEc?`aTl?Y6TI}S* z)92P==a!$C<2ZRp8LXpi@KiwRz$4{w-NWe;X4bBUZ+g%4qZK_ioEBD+I@iShdnLeBj9u%zaE+eiiTVJbf3_== zGCY$HslwS1XXQ&)gW2qJqoMG4;09CveQ1{2%eDO+c=><(jup$~O2K-S&cc%aq1fGk hbpc~s_lQ`mBMv&7>w1m-+Vy?+IgAJD=>M@1_aDyMiSGaa 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 + + + + +

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