feat: enhance East Asia coverage with adversary bases, news sources, ICAO ranges, and PLAN vessel DB

- Add 68 military bases (PLA, Russia, DPRK, ROC, Philippines, Australia)
  with data-driven color coding (red/blue/green) on the map
- Add 6 news RSS feeds (Yonhap, Nikkei Asia, Taipei Times, Asia Times,
  Defense News, Japan Times) and 15 geocoding keywords for islands,
  straits, and disputed areas
- Extend ICAO country ranges for Russia, Australia, Philippines,
  Singapore, DPRK and add Russian aircraft classification (fighters,
  bombers, cargo, recon)
- Create PLAN/CCG vessel enrichment module (90+ ships) following
  yacht_alert pattern for automatic MMSI-based identification
- Update frontend types and popup styling for adversary/allied/ROC
  color distinction

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
adust09
2026-03-16 12:46:40 +09:00
co-authored by Claude Opus 4.6
parent 95da3015d9
commit 910d1fd633
14 changed files with 1467 additions and 11 deletions
+5
View File
@@ -39,6 +39,11 @@ def fetch_ships():
for ship in ships:
enrich_with_yacht_alert(ship)
# Enrich ships with PLAN/CCG vessel data
from services.fetchers.plan_vessel_alert import enrich_with_plan_vessel
for ship in ships:
enrich_with_plan_vessel(ship)
logger.info(f"Ships: {len(carriers)} carriers + {len(ais_vessels)} AIS vessels")
with _data_lock:
latest_data['ships'] = ships
+12 -1
View File
@@ -38,6 +38,11 @@ _ICAO_COUNTRY_RANGES = [
(0x840000, 0x87FFFF, "Japan", "JSDF"),
(0x700000, 0x71FFFF, "South Korea", "ROK"),
(0xE80000, 0xE80FFF, "Taiwan", "ROC"),
(0x150000, 0x157FFF, "Russia", "VKS"),
(0x7C0000, 0x7FFFFF, "Australia", "RAAF"),
(0x758000, 0x75FFFF, "Philippines", "PAF"),
(0x768000, 0x76FFFF, "Singapore", "RSAF"),
(0x720000, 0x727FFF, "North Korea", "KPAF"),
]
@@ -66,18 +71,24 @@ def _classify_military_type(raw_model: str) -> str:
if any(k in model for k in [
"F16", "F35", "F22", "F15", "F18", "T38", "T6", "A10",
"J10", "J11", "J15", "J16", "J20", "JF17",
"SU27", "SU30", "SU35",
"SU27", "SU30", "SU35", "SU57", "MIG29", "MIG31",
"F15J", "F2", "IDF", "FA50", "KF21",
]):
return "fighter"
if any(k in model for k in [
"TU95", "TU160", "TU22",
]):
return "bomber"
if any(k in model for k in [
"C17", "C5", "C130", "C30", "A400", "V22",
"Y20", "Y9", "Y8", "C2",
"IL76", "AN124", "AN12",
]):
return "cargo"
if any(k in model for k in [
"P8", "E3", "E8", "U2",
"KJ500", "KJ200", "GX11", "P1", "E767", "E2K", "E2C",
"A50", "TU214R", "IL20",
]):
return "recon"
return "default"
+16
View File
@@ -111,6 +111,22 @@ _KEYWORD_COORDS = {
"singapore": (1.352, 103.819),
"bangkok": (13.756, 100.501),
"jakarta": (-6.208, 106.845),
# East Asia — islands, straits, and disputed areas
"pratas": (20.71, 116.72),
"dongsha": (20.71, 116.72),
"kinmen": (24.45, 118.38),
"matsu": (26.16, 119.94),
"scarborough": (15.14, 117.77),
"paracel": (16.50, 112.00),
"spratly": (10.00, 114.00),
"miyako strait": (24.78, 125.30),
"bashi channel": (21.00, 121.50),
"luzon strait": (20.50, 121.50),
" dmz ": (38.00, 127.00),
"yalu": (40.00, 124.40),
"yongbyon": (39.80, 125.76),
"wonsan": (39.18, 127.48),
"busan": (35.18, 129.07),
}
# Immutable after module load — sort by descending keyword length so
@@ -0,0 +1,42 @@
"""PLAN/CCG Vessel Alert DB — load and enrich AIS vessels with Chinese navy/coast guard metadata."""
import os
import json
import logging
logger = logging.getLogger("services.data_fetcher")
_PLAN_CCG_DB: dict = {}
def _load_plan_ccg_db():
"""Load plan_ccg_vessels.json into memory at import time."""
global _PLAN_CCG_DB
json_path = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
"data", "plan_ccg_vessels.json"
)
if not os.path.exists(json_path):
logger.warning(f"PLAN/CCG vessel DB not found at {json_path}")
return
try:
with open(json_path, "r", encoding="utf-8") as fh:
_PLAN_CCG_DB.update(json.load(fh))
logger.info(f"PLAN/CCG vessel DB loaded: {len(_PLAN_CCG_DB)} vessels")
except (IOError, OSError, json.JSONDecodeError, ValueError, KeyError) as e:
logger.error(f"Failed to load PLAN/CCG vessel DB: {e}")
_load_plan_ccg_db()
def enrich_with_plan_vessel(ship: dict) -> dict:
"""If ship's MMSI is in the PLAN/CCG DB, attach enrichment metadata."""
mmsi = str(ship.get("mmsi", "")).strip()
if mmsi and mmsi in _PLAN_CCG_DB:
info = _PLAN_CCG_DB[mmsi]
ship["plan_name"] = info.get("name", "")
ship["plan_class"] = info.get("class", "")
ship["plan_force"] = info.get("force", "")
ship["plan_hull"] = info.get("hull_number", "")
ship["plan_wiki"] = info.get("wiki", "")
return ship
+7 -1
View File
@@ -9,7 +9,7 @@ from pathlib import Path
logger = logging.getLogger(__name__)
CONFIG_PATH = Path(__file__).parent.parent / "config" / "news_feeds.json"
MAX_FEEDS = 20
MAX_FEEDS = 25
DEFAULT_FEEDS = [
{"name": "NPR", "url": "https://feeds.npr.org/1004/rss.xml", "weight": 4},
@@ -25,6 +25,12 @@ DEFAULT_FEEDS = [
{"name": "SCMP", "url": "https://www.scmp.com/rss/91/feed", "weight": 4},
{"name": "The Diplomat", "url": "https://thediplomat.com/feed/", "weight": 4},
{"name": "Stars and Stripes", "url": "https://www.stripes.com/feeds/pacific.rss", "weight": 4},
{"name": "Yonhap", "url": "https://en.yna.co.kr/RSS/news.xml", "weight": 4},
{"name": "Nikkei Asia", "url": "https://asia.nikkei.com/rss", "weight": 3},
{"name": "Taipei Times", "url": "https://www.taipeitimes.com/xml/pda.rss", "weight": 4},
{"name": "Asia Times", "url": "https://asiatimes.com/feed/", "weight": 3},
{"name": "Defense News", "url": "https://www.defensenews.com/arc/outboundfeeds/rss/", "weight": 3},
{"name": "Japan Times", "url": "https://www.japantimes.co.jp/feed/", "weight": 3},
]